├── .gitignore ├── LICENSE.txt ├── README.md ├── autoload └── vru.vim ├── ftplugin └── javascript.vim ├── steps.txt └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mark Volkmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-react 2 | 3 | This plugin provides several functions. 4 | 5 | ### `ReactToggleComponent` 6 | This function toggles the implementation of a React component 7 | between class-based and functional. 8 | It is mapped to `rt` unless that key is already mapped. 9 | Just place the cursor on the first line of an existing 10 | React component definition and run the function to toggle it. 11 | 12 | Here is an example of a functional React component. 13 | ```jsx 14 | const Foo = ({bar, baz}) => { 15 | return ( 16 |
17 |
bar = {bar}
18 |
baz = {baz}
19 |
20 | ); 21 | }; 22 | 23 | Foo.displayName = 'Foo'; 24 | 25 | Foo.propTypes = { 26 | bar: number, 27 | baz: string 28 | }; 29 | ``` 30 | 31 | Here is an example of an equivalent class-based React component. 32 | ```jsx 33 | class Foo extends Component { 34 | static displayName = 'Foo'; 35 | 36 | static propTypes = { 37 | bar: number, 38 | baz: string 39 | }; 40 | 41 | render() { 42 | const {bar, baz} = this.props; 43 | return ( 44 |
45 |
bar = {bar}
46 |
baz = {baz}
47 |
48 | ); 49 | } 50 | } 51 | ``` 52 | 53 | `ReactToggleComponent` will convert either of these forms to the other. 54 | 55 | ### JSXCommentAdd 56 | This function adds a JSX comment `{/* ... */}` 57 | around the lines selected in visual mode. 58 | It is mapped to `jc` in visual mode 59 | unless that key is already mapped. 60 | 61 | ### JSXCommentRemove 62 | This function removes the JSX comment `{/* ... */}` 63 | surrounding the current line in normal mode. 64 | It is mapped to `jc` in normal mode 65 | unless that key is already mapped. 66 | 67 | These functions makes certain assumptions about the code 68 | and the result to be produced. 69 | If you are unhappy with the results, please open an issue and 70 | I will consider making this more configurable. 71 | -------------------------------------------------------------------------------- /autoload/vru.vim: -------------------------------------------------------------------------------- 1 | " This file name is an abbreviation for "Vim React Utilities". 2 | 3 | function! vru#DeleteLine(lineNum) 4 | call vru#DeleteLines(a:lineNum, a:lineNum) 5 | endf 6 | 7 | function! vru#DeleteLineIfBlank(lineNum) 8 | if (len(vru#Trim(getline(a:lineNum))) == 0) 9 | execute 'silent ' . a:lineNum . 'd' 10 | endif 11 | endf 12 | 13 | function! vru#DeleteLines(start, end) 14 | " Without silent the deletion is reported in the status bar. 15 | execute 'silent ' . a:start . ',' . a:end . 'd' 16 | endf 17 | 18 | " Returns the line number of the next line found starting 19 | " from lineNum that matches a given regular expression 20 | " or zero if none is found. 21 | function! vru#FindNextLine(startLineNum, pattern) 22 | let lineNum = a:startLineNum 23 | let lastLineNum = line('$') 24 | let found = 0 25 | while (!found && lineNum < lastLineNum) 26 | let line = getline(lineNum) 27 | let found = line =~# a:pattern 28 | let lineNum += 1 29 | endw 30 | return found ? lineNum - 1: 0 31 | endf 32 | 33 | " Returns the line number of the previous line found starting 34 | " from lineNum that matches a given regular expression 35 | " or zero of none is found. 36 | function! vru#FindPreviousLine(startLineNum, pattern) 37 | let lineNum = a:startLineNum 38 | let found = 0 39 | while (!found && lineNum > 0) 40 | let line = getline(lineNum) 41 | let found = line =~# a:pattern 42 | let lineNum -= 1 43 | endw 44 | return found ? lineNum + 1: 0 45 | endf 46 | 47 | function! vru#GetLinesTo(startLineNum, pattern) 48 | let lines = [] 49 | let lineNum = a:startLineNum 50 | let lastLineNum = line('$') 51 | let found = 0 52 | while (!found && lineNum <= lastLineNum) 53 | let line = getline(lineNum) 54 | let found = line =~ a:pattern 55 | call add(lines, line) 56 | let lineNum += 1 57 | endw 58 | return lines 59 | endf 60 | 61 | function! vru#LastToken(string) 62 | let tokens = split(a:string, ' ') 63 | return tokens[len(tokens) - 1] 64 | endf 65 | 66 | function! vru#LogList(label, list) 67 | echo a:label 68 | for item in a:list 69 | echo ' ' . len(item) . ': ' . item 70 | endfor 71 | endf 72 | 73 | " Returns the nth token in a string 74 | " where the first token is 1. 75 | function! vru#NthToken(n, string) 76 | let tokens = split(a:string, ' ') 77 | return tokens[a:n - 1] 78 | endf 79 | 80 | " Removes and returns the last element of a list. 81 | function! vru#PopList(list) 82 | let index = len(a:list) - 1 83 | let last = a:list[index] 84 | call remove(a:list, index) 85 | return last 86 | endf 87 | 88 | " Returns a copy of a string with leading and trailing whitespace trimmed. 89 | function! vru#Trim(string) 90 | return substitute(a:string, '^\s*\(.\{-}\)\s*$', '\1', '') 91 | endf 92 | 93 | " Returns a copy of a string with trailing whitespace trimmed. 94 | function! vru#TrimTrailing(string) 95 | return substitute(a:string, '^(\s*\.\{-}\)\s*$', '\1', '') 96 | endf 97 | -------------------------------------------------------------------------------- /ftplugin/javascript.vim: -------------------------------------------------------------------------------- 1 | " These lines are just for trying things. 2 | " To run this, press ",sb" which sources this buffer. 3 | let sample = "const Foo = ({\nbar,\nbaz\n}) => {\nmy body\n};" 4 | "let pattern = '\s*const (\w+) = \((\_.+)\) \=>( =>)?' 5 | let pattern = '\v\s*const (\w+) \= \((\_.+)\) \=\>( {})?' 6 | let result = matchlist(sample, pattern) 7 | "echo 'len(result) = ' . len(result) 8 | let className = result[1] 9 | "echo 'className = ' . className 10 | let props = result[2] 11 | "echo 'props = ' . props 12 | 13 | "echo 'foo' =~# 'f' 14 | "echo matchlist('foo bar baz', '\v\s*(\w+) (\w+) (\w+)') 15 | 16 | " This file depends on many functions defined in plugins/utilities.vim. 17 | 18 | function! JSXPropJoin() 19 | " If current line doesn't start with <, error. 20 | " Find next > 21 | " Join all the lines in between. 22 | " Delete the previous lines. 23 | " Append the new line. 24 | echo 'not implemented yet' 25 | endf 26 | 27 | function! JSXPropSplit() 28 | " If current line doesn't start with <, error. 29 | " Get a string from < to >. 30 | " Split the string using a regex. 31 | " Delete the current line. 32 | " Append the new lines. 33 | echo 'not implemented yet' 34 | endf 35 | 36 | function! JSXCommentAdd() 37 | " Get first and last line number selected in visual mode. 38 | let firstLineNum = line("'<") 39 | let lastLineNum = line("'>") 40 | 41 | let column = match(getline(firstLineNum), '\w') 42 | let indent = repeat(' ', column - 1) 43 | 44 | call append(lastLineNum, indent . '*/}') 45 | call append(firstLineNum - 1, indent . '{/*') 46 | endf 47 | 48 | function! JSXCommentRemove() 49 | let lineNum = line('.') 50 | let startLineNum = vru#FindPreviousLine(lineNum, '{/\*') 51 | if startLineNum == 0 52 | echo 'no JSX comment found' 53 | return 54 | endif 55 | 56 | let endLineNum = vru#FindNextLine(lineNum, '*/}') 57 | call vru#DeleteLine(endLineNum) 58 | call vru#DeleteLine(startLineNum) 59 | endf 60 | 61 | " Converts a React component definition from a class to an arrow function. 62 | function! ReactClassToFn() 63 | let line = getline('.') " gets entire current line 64 | let tokens = split(line, ' ') 65 | if tokens[0] !=# 'class' 66 | echo 'must start with "class"' 67 | return 68 | endif 69 | if line !~# ' extends Component {$' && 70 | \ line !~# ' extends React.Component {$' 71 | echo 'must extend Component' 72 | return 73 | endif 74 | 75 | let startLineNum = line('.') 76 | let endLineNum = vru#FindNextLine(startLineNum, '^}') 77 | if !endLineNum 78 | echo 'end of class definition not found' 79 | return 80 | endif 81 | 82 | let className = tokens[1] 83 | 84 | let displayNameLineNum = vru#FindNextLine(startLineNum, 'static displayName = ') 85 | if displayNameLineNum 86 | let line = getline(displayNameLineNum) 87 | let pattern = '\vstatic displayName \= ''(.+)'';' 88 | let result = matchlist(line, pattern) 89 | let displayName = result[1] " first capture group 90 | endif 91 | 92 | let propTypesLineNum = vru#FindNextLine(startLineNum, 'static propTypes = {$') 93 | let propTypesInsideClass = propTypesLineNum ? 1 : 0 94 | if !propTypesLineNum 95 | let propTypesLineNum = vru#FindNextLine(startLineNum, className . '.propTypes = {$') 96 | endif 97 | if propTypesLineNum 98 | let propTypesLines = vru#GetLinesTo(propTypesLineNum + 1, '};$') 99 | call vru#PopList(propTypesLines) 100 | let propNames = [] 101 | for line in propTypesLines 102 | call add(propNames, split(vru#Trim(line), ':')[0]) 103 | endfor 104 | let params = '{' . join(propNames, ', ') . '}' 105 | else 106 | let params = '' 107 | endif 108 | 109 | let renderLineNum = vru#FindNextLine(startLineNum, ' render() {') 110 | if renderLineNum 111 | let renderLines = vru#GetLinesTo(renderLineNum + 1, '^\s*}$') 112 | 113 | " Remove last line that closes the render method. 114 | call vru#PopList(renderLines) 115 | 116 | " Remove any lines that destructure this.props since 117 | " all are destructured in the arrow function parameter list. 118 | let length = len(renderLines) 119 | let index = 0 120 | while index < length 121 | let line = renderLines[index] 122 | if line =~# '} = this.props;' 123 | call remove(renderLines, index) 124 | let length -= 1 125 | endif 126 | let index += 1 127 | endw 128 | 129 | " If the first render line is empty, remove it. 130 | if len(vru#Trim(renderLines[0])) == 0 131 | call remove(renderLines, 0) 132 | endif 133 | else 134 | let renderLines = [] 135 | endif 136 | 137 | let lines = ['const ' . className . ' = (' . params . ') => {'] 138 | 139 | for line in renderLines 140 | call add(lines, line[2:]) 141 | endfor 142 | call add(lines, '};') 143 | 144 | if exists('displayName') 145 | let lines += [ 146 | \ '', 147 | \ className . ".displayName = '" . displayName . "';" 148 | \ ] 149 | endif 150 | 151 | if exists('propTypesLines') && propTypesInsideClass 152 | let lines += ['', className . '.propTypes = {'] 153 | for line in propTypesLines 154 | call add(lines, ' ' . vru#Trim(line)) 155 | endfor 156 | let lines += ['};'] 157 | endif 158 | 159 | call vru#DeleteLines(startLineNum, endLineNum) 160 | call append(startLineNum - 1, lines) 161 | endf 162 | 163 | " Converts a React component definition from an arrow function to a class. 164 | function! ReactFnToClass(matches) 165 | let className = a:matches[1] 166 | 167 | let lineNum = line('.') 168 | let arrowLineNum = vru#FindNextLine(lineNum, '=>') 169 | let arrowLine = getline(arrowLineNum) 170 | 171 | let hasBlock = arrowLine =~# '{$' 172 | if hasBlock 173 | " Find next line that only contains "};". 174 | let lastLineNum = vru#FindNextLine(lineNum, '^\w*};\w*$') 175 | if !lastLineNum 176 | echo 'arrow function end not found' 177 | return 178 | endif 179 | else 180 | " Find next line that ends with ";". 181 | let lastLineNum = vru#FindNextLine(lineNum, ';\w*$') 182 | if !lastLineNum 183 | echo 'arrow function end not found' 184 | return 185 | endif 186 | endif 187 | 188 | let renderLines = getline(arrowLineNum + 1, lastLineNum - (hasBlock ? 1 : 0)) 189 | 190 | if !hasBlock 191 | " Remove semicolon from end of last line if exists. 192 | let index = len(renderLines) - 1 193 | let lastRenderLine = renderLines[index] 194 | if lastRenderLine =~# ';$' 195 | let renderLines[index] = lastRenderLine[0:-2] 196 | endif 197 | endif 198 | 199 | let displayNameLineNum = vru#FindNextLine(lineNum, className . '.displayName =') 200 | if displayNameLineNum 201 | let displayName = vru#LastToken(getline(displayNameLineNum)) 202 | call vru#DeleteLine(displayNameLineNum) 203 | call vru#DeleteLineIfBlank(displayNameLineNum - 1) 204 | endif 205 | 206 | let propTypesLineNum = vru#FindNextLine(lineNum, className . '.propTypes =') 207 | if propTypesLineNum 208 | let propTypes = vru#GetLinesTo(propTypesLineNum + 1, '.*};') 209 | let propNames = [] 210 | for line in propTypes 211 | let propName = vru#Trim(split(line, ':')[0]) 212 | if propName !=# '};' 213 | call add(propNames, propName) 214 | endif 215 | endfor 216 | call vru#DeleteLines(propTypesLineNum, propTypesLineNum + len(propTypes)) 217 | call vru#DeleteLineIfBlank(propTypesLineNum - 1) 218 | endif 219 | 220 | " Delete lines from function body. 221 | call vru#DeleteLines(lineNum, lastLineNum) 222 | 223 | let lines = ['class ' . className . ' extends Component {'] 224 | 225 | if exists('displayName') 226 | let lines += [ 227 | \ ' static displayName = ' . displayName, 228 | \ '' 229 | \ ] 230 | endif 231 | 232 | if exists('propTypes') 233 | call add(lines, ' static propTypes = {') 234 | for line in propTypes 235 | call add(lines, ' ' . line) 236 | endfor 237 | call add(lines, '') 238 | endif 239 | 240 | call add(lines, ' render() {') 241 | 242 | if exists('propTypes') 243 | call add(lines, 244 | \ ' const {' . join(propNames, ', ') . '} = this.props;') 245 | endif 246 | 247 | if !hasBlock 248 | call add(lines, ' return (') 249 | endif 250 | let indent = hasBlock ? '' : ' ' 251 | 252 | for line in renderLines 253 | let output = len(line) ? indent . ' ' . line : line 254 | call add(lines, output) 255 | endfor 256 | 257 | if !hasBlock 258 | call add(lines, ' );') 259 | endif 260 | 261 | let lines += [' }', '}'] 262 | 263 | call append(lineNum - 1, lines) 264 | endf 265 | 266 | function! OnArrowFunction(startLineNum) 267 | let lineNum = a:startLineNum 268 | 269 | let line = getline(lineNum) 270 | if line =~# '\v\s*class ' 271 | return [] 272 | endif 273 | 274 | let arrowLineNum = vru#FindNextLine(lineNum, '=>') 275 | if !arrowLineNum 276 | return [] 277 | endif 278 | 279 | let result = '' 280 | while lineNum <= arrowLineNum 281 | let result = result . ' ' . vru#Trim(getline(lineNum)) 282 | let lineNum += 1 283 | endw 284 | 285 | " Using "Very Magic" regular expression mode, 286 | " match any # of spaces, "const ", a variable name, " = (", 287 | " a parameter list, ") =>" and an optional empty function body. 288 | " Capture the variable name and the parameter list. 289 | let pattern = '\v\s*const (\w+) \= \((\_.+)\) \=\>( {})?' 290 | let matches = matchlist(result, pattern) 291 | return matches 292 | endf 293 | 294 | function! ReactToggleComponent() 295 | let lineNum = line('.') 296 | let colNum = col('.') 297 | 298 | let line = getline('.') " gets entire current line 299 | let matches = OnArrowFunction(lineNum) 300 | "call vru#LogList('matches', matches) 301 | if len(matches) 302 | call ReactFnToClass(matches) 303 | elseif line =~# '^class ' || line =~# ' class ' 304 | call ReactClassToFn() 305 | else 306 | echo 'must be on first line of a React component' 307 | endif 308 | 309 | " Move cursor back to start. 310 | call cursor(lineNum, colNum) 311 | endf 312 | 313 | " removes the automatic range specification 314 | " when command mode is entered from visual mode, 315 | " changing the command line from :'<'> to just : 316 | 317 | " If jc for "JSX Comment" is not already mapped ... 318 | if mapcheck('\jc', 'N') ==# '' 319 | nnoremap jc :call JSXCommentRemove() 320 | vnoremap jc :call JSXCommentAdd() 321 | endif 322 | 323 | " If js for "JSX Split" is not already mapped ... 324 | if mapcheck('\js', 'N') ==# '' 325 | nnoremap js :call JSXPropSplit() 326 | endif 327 | 328 | " If rt for "React Toggle" is not already mapped ... 329 | if mapcheck('\rt', 'N') ==# '' 330 | nnoremap rt :call ReactToggleComponent() 331 | endif 332 | -------------------------------------------------------------------------------- /steps.txt: -------------------------------------------------------------------------------- 1 | Steps to convert class to function 2 | - if first token of current line is not "class" 3 | output an error message and exit 4 | - save current line number 5 | - get name of class from second token 6 | - find line number of end of class 7 | - capture static displayName if set inside class, 8 | - capture static propTypes if set inside class, 9 | - if anything other than render method remains inside class, 10 | output an error message and exit 11 | - determine if render method contains anything before the return statement 12 | - output this: 13 | const {className} = ({{prop-names}}) => 14 | - if has more than return, output this: 15 | { 16 | {body-of-render} 17 | }; 18 | - else output this: 19 | {value-of-return-without-wrapping-parens}; 20 | - close class with } 21 | - if static displayName was set inside class, 22 | output it at end as 23 | {className}.displayName = '{className}'; 24 | - if propTypes was set inside class, 25 | output it at end as 26 | {className}.propTypes = { 27 | {prop-types} 28 | }; 29 | 30 | Steps to convert function to class 31 | - if first token is not "const" 32 | or third token is not "=" 33 | or line does not end with "=>" or "=> {", 34 | output an error message and exit 35 | - get class name from second token 36 | - output this: 37 | class {class-name} extends Component { 38 | - capture code up to ";" 39 | - output this: 40 | static displayName = '{class-name}'; 41 | static propTypes = { 42 | {prop-types} 43 | }; 44 | render() { 45 | const {{prop-list}} = this.props; 46 | {code} 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes as t} from 'react'; 2 | 3 | // before 4 | class Foo1 extends Component { 5 | static displayName = 'Foo1'; 6 | 7 | static propTypes = { 8 | bar: t.number, 9 | baz: t.string 10 | }; 11 | 12 | render() { 13 | const {bar, baz} = this.props; 14 | return ( 15 |
16 |
bar = {bar}
17 |
baz = {baz}
18 |
19 | ); 20 | } 21 | } 22 | // after 23 | 24 | // before 25 | const Foo2 = ({bar, baz}) => { 26 | return ( 27 |
28 |
bar = {bar}
29 |
baz = {baz}
30 |
31 | ); 32 | }; 33 | 34 | Foo2.displayName = 'FooBar'; 35 | Foo2.propTypes = { 36 | bar: t.number, 37 | baz: t.string 38 | }; 39 | // after 40 | 41 | // before 42 | const Foo3 = ({ 43 | bar, 44 | baz 45 | }) => 46 |
47 |
bar = {bar}
48 |
baz = {baz}
49 |
; 50 | 51 | Foo3.displayName = 'FooBar'; 52 | Foo3.propTypes = { 53 | bar: t.number, 54 | baz: t.string 55 | }; 56 | // after 57 | 58 | export Foo1, Foo2, Foo3; 59 | --------------------------------------------------------------------------------