├── .gitignore ├── README.markdown ├── doc └── cycle.txt └── plugin └── cycle.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Cycle.vim 2 | ========= 3 | 4 | A Vim plugin that allows you to toggle between pairs or lists of related words. 5 | Use by placing your cursor on a word, like `left`, and press `control-a`. 6 | The word will be replaced with `right`. 7 | The default mappings are the same as those with which you can increment and 8 | decrement a number under the cursor: `` and ``, respectively. 9 | 10 | Check out [the code](https://github.com/zef/vim-cycle/blob/master/plugin/cycle.vim) 11 | to see what cycle groups are included by default. 12 | 13 | Customization 14 | ------------- 15 | 16 | You can add your own word groups: 17 | 18 | call AddCycleGroup(['one', 'two', 'three']) 19 | 20 | To deal with conflicts, Cycle.vim also supports adding groups that are specific 21 | to a certain filetype: 22 | 23 | call AddCycleGroup('ruby', ['class', 'module']) 24 | call AddCycleGroup('python', ['else', 'elif']) 25 | 26 | When multiple groups define the same word, groups belonging to specific 27 | filetypes will be used instead of global groups. This is useful in the cases 28 | above, since in HTML we would want `class` to cycle with `id` and Python uses 29 | `elif` while some other languages use `else if` or `elsif`. 30 | 31 | Providing a list of filetypes is also supported: 32 | 33 | call AddCycleGroup(['ruby', 'eruby', 'perl'], ['else', 'elsif']) 34 | 35 | However, if there are no conflicting cases it is preferable to define all cycle 36 | groups in the global namespace, using filetype-specific groups only in case of 37 | conflict. 38 | 39 | Matches are evaluated in reverse order, so whatever has been defined most 40 | recently will take precedence over groups defined previously. Keep this in mind 41 | when defining new cycle groups to make sure broad definitions are not matched 42 | when they are not desired. 43 | 44 | You may also clear all the hard coded pairs and work entirely with your own pairs. 45 | Use the following code to initialize your own variables at startup in your vimrc file: 46 | 47 | let g:cycle_override_defaults = [ 48 | \ ['global', ['true', 'false']], 49 | \ ['ruby', ['class', 'module']], 50 | \] 51 | 52 | This will initialize vim-cycle with only 2 pairs of cycles: one will be set globally 53 | and the other one will apply only to the ruby filetype. 54 | 55 | Todo 56 | ---- 57 | 58 | - The ability to handle pairs: quotes, brackets, html tags, etc. 59 | - Operate on non-lowercase text and retain case 60 | - Put cursor back at beginning of word if it started there 61 | 62 | Bugs 63 | ---- 64 | 65 | - Does not work with speeddating.vim - currently won't work if speeddating is 66 | installed. 67 | 68 | -------------------------------------------------------------------------------- /doc/cycle.txt: -------------------------------------------------------------------------------- 1 | *cycle.txt* Quickly toggle between related words. 2 | 3 | ABOUT *cycle* 4 | 5 | Cycle.vim allows you to toggle between pairs or lists of related words. The 6 | default mappings are the same as those with which you can increment and 7 | decrement a number under the cursor —  and , respectively. 8 | 9 | CUSTOMIZATION 10 | 11 | You can add your own word groups: 12 | > 13 | call AddCycleGroup(['one', 'two', 'three']) 14 | < 15 | To deal with conflicts, Cycle.vim also supports adding groups that are specific 16 | to a certain filetype: 17 | > 18 | call AddCycleGroup('ruby', ['class', 'module']) 19 | call AddCycleGroup('python', ['else', 'elif']) 20 | < 21 | When multiple groups define the same word, groups belonging to specific 22 | filetypes will be used instead of global groups. This is useful in the cases 23 | above, since in HTML we would want 'class' to cycle with 'id' and Python uses 24 | 'elif' while some other languages use 'else if' or 'elsif'. 25 | 26 | Providing a list of filetypes is also supported: 27 | > 28 | call AddCycleGroup(['ruby', 'eruby', 'perl'], ['else', 'elsif']) 29 | < 30 | However, if there are no conflicting cases it is preferable to define all 31 | cycle groups in the global namespace, using filetype-specific groups only in 32 | case of conflict. 33 | 34 | Matches are evaluated in reverse order, so whatever has been defined most 35 | recently will take precedence over groups defined previously. Keep this in 36 | mind when defining new cycle groups to make sure broad definitions are not 37 | matched when they are not desired. 38 | 39 | vim:tw=78:ts=8:ft=help:norl: 40 | 41 | -------------------------------------------------------------------------------- /plugin/cycle.vim: -------------------------------------------------------------------------------- 1 | " cycle.vim - Toggle between related words 2 | 3 | " if exists("g:loaded_cycle") 4 | " finish 5 | " endif 6 | " let g:loaded_cycle = 1 7 | 8 | let s:options = {} 9 | 10 | let s:options['global'] = [] 11 | 12 | if !exists("g:cycle_override_defaults") 13 | let s:options['global'] = [ 14 | \ ['==', '!='], 15 | \ ['_', '-'], 16 | \ [' + ', ' - '], 17 | \ ['-=', '+='], 18 | \ ['&&', '||'], 19 | \ ['and', 'or'], 20 | \ ['if', 'unless'], 21 | \ ['true', 'false'], 22 | \ ['YES', 'NO'], 23 | \ ['yes', 'no'], 24 | \ ['on', 'off'], 25 | \ ['running', 'stopped'], 26 | \ ['enabled', 'disabled'], 27 | \ ['present', 'absent'], 28 | \ ['first', 'last'], 29 | \ ['else', 'else if'], 30 | \] 31 | 32 | " css/sass/javascript/html 33 | let s:options['global'] = s:options['global'] + [ 34 | \ ['div', 'p', 'span'], 35 | \ ['max', 'min'], 36 | \ ['ul', 'ol'], 37 | \ ['class', 'id'], 38 | \ ['px', '%', 'em'], 39 | \ ['left', 'right'], 40 | \ ['top', 'bottom'], 41 | \ ['margin', 'padding'], 42 | \ ['height', 'width'], 43 | \ ['absolute', 'relative'], 44 | \ ['h1', 'h2', 'h3'], 45 | \ ['png', 'jpg', 'gif'], 46 | \ ['linear', 'radial'], 47 | \ ['horizontal', 'vertical'], 48 | \ ['show', 'hide'], 49 | \ ['mouseover', 'mouseout'], 50 | \ ['mouseenter', 'mouseleave'], 51 | \ ['add', 'remove'], 52 | \ ['up', 'down'], 53 | \ ['before', 'after'], 54 | \ ['text', 'html'], 55 | \ ['slow', 'fast'], 56 | \ ['small', 'large'], 57 | \ ['even', 'odd'], 58 | \ ['inside', 'outside'], 59 | \ ['push', 'pull'], 60 | \ ['header', 'footer'], 61 | \] 62 | 63 | " ruby/eruby 64 | let s:options['global'] = s:options['global'] + [ 65 | \ ['include', 'require'], 66 | \ ['Time', 'Date'], 67 | \ ['present', 'blank'], 68 | \ ['while', 'until'], 69 | \ ['only', 'except'], 70 | \ ['create', 'update'], 71 | \ ['new', 'edit'], 72 | \ ['get', 'post', 'put', 'patch'] 73 | \] 74 | endif 75 | 76 | " Takes one or two arguments: 77 | " 78 | " group 79 | " - or - 80 | " filetype(s), group 81 | function! AddCycleGroup(filetypes_or_group, ...) 82 | if a:0 83 | let group = a:1 84 | 85 | " type(['list']) == 3 86 | " type('string') == 1 87 | if type(a:filetypes_or_group) == 1 88 | let filetypes = [a:filetypes_or_group] 89 | else 90 | let filetypes = a:filetypes_or_group 91 | end 92 | else 93 | let group = a:filetypes_or_group 94 | let filetypes = ['global'] 95 | endif 96 | 97 | for type in filetypes 98 | if !has_key(s:options, type) 99 | let s:options[type] = [] 100 | endif 101 | 102 | call add(s:options[type], group) 103 | endfor 104 | endfunction 105 | 106 | function! s:Cycle(direction) 107 | let filetype = &ft 108 | let match = [] 109 | 110 | if has_key(s:options, filetype) 111 | let match = s:matchInList(s:options[filetype]) 112 | endif 113 | 114 | if empty(match) 115 | let match = s:matchInList(s:options['global']) 116 | endif 117 | 118 | if empty(match) 119 | " if exists("g:loaded_speeddating") 120 | " echo 'speed dating!' 121 | " else 122 | " echo 'no speed dating' 123 | " endif 124 | 125 | if a:direction == 1 126 | exe "norm! " . v:count1 . "\" 127 | else 128 | exe "norm! " . v:count1 . "\" 129 | endif 130 | else 131 | let [group, start, end, string] = match 132 | 133 | let index = index(group, string) + a:direction 134 | let max_index = (len(group) - 1) 135 | 136 | if index > max_index 137 | let index = 0 138 | endif 139 | 140 | call s:replaceinline(start,end,group[index]) 141 | endif 142 | 143 | endfunction 144 | 145 | " returns the following list if a match is found: 146 | " [group, start, end, string] 147 | " 148 | " returns [] if no match is found 149 | function! s:matchInList(list) 150 | " reverse the list so the most recently defined matches are used 151 | for group in reverse(copy(a:list)) 152 | " We must iterate each group with the longest values first. 153 | " This covers a case like ['else', 'else if'] where the 154 | " first will match successfuly even if the second could 155 | " be matched. Checking for the longest values first 156 | " ensures that the most specific match will be returned 157 | for item in sort(copy(group), "s:sorterByLength") 158 | let match = s:findinline(item) 159 | if match[0] >= 0 160 | return [group] + match 161 | endif 162 | endfor 163 | endfor 164 | 165 | return [] 166 | endfunction 167 | 168 | function! s:sorterByLength(item, other) 169 | return len(a:other) - len(a:item) 170 | endfunction 171 | 172 | " pulled the following out of speeddating.vim 173 | " modified slightly 174 | function! s:findatoffset(string,pattern,offset) 175 | let line = a:string 176 | let curpos = 0 177 | let offset = a:offset 178 | while strpart(line,offset,1) == " " 179 | let offset += 1 180 | endwhile 181 | let [start,end,string] = s:match(line,a:pattern,curpos,0) 182 | while start >= 0 183 | if offset >= start && offset < end 184 | break 185 | endif 186 | let curpos = start + 1 187 | let [start,end,string] = s:match(line,a:pattern,curpos,0) 188 | endwhile 189 | return [start,end,string] 190 | endfunction 191 | 192 | function! s:findinline(pattern) 193 | return s:findatoffset(getline('.'),a:pattern,col('.')-1) 194 | endfunction 195 | 196 | function! s:replaceinline(start,end,new) 197 | let line = getline('.') 198 | let before_text = strpart(line,0,a:start) 199 | let after_text = strpart(line,a:end) 200 | " If this generates a warning it will be attached to an ugly backtrace. 201 | " No warning at all is preferable to that. 202 | silent call setline('.',before_text.a:new.after_text) 203 | call setpos("'[",[0,line('.'),strlen(before_text)+1,0]) 204 | call setpos("']",[0,line('.'),a:start+strlen(a:new),0]) 205 | endfunction 206 | 207 | function! s:match(...) 208 | let start = call("match",a:000) 209 | let end = call("matchend",a:000) 210 | let matches = call("matchlist",a:000) 211 | if empty(matches) 212 | let string = '' 213 | else 214 | let string = matches[0] 215 | endif 216 | return [start, end, string] 217 | endfunction 218 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 219 | 220 | if !exists("g:cycle_override_defaults") 221 | " language specific overrides: 222 | call AddCycleGroup('ruby', ['class', 'module']) 223 | call AddCycleGroup(['ruby', 'eruby', 'perl'], ['else', 'elsif']) 224 | call AddCycleGroup('python', ['else', 'elif']) 225 | 226 | " Swift 227 | call AddCycleGroup('swift', ['let', 'var']) 228 | call AddCycleGroup('swift', ['open', 'public', 'internal', 'fileprivate', 'private']) 229 | call AddCycleGroup('swift', ['class', 'struct', 'enum', 'protocol', 'extension']) 230 | call AddCycleGroup('swift', ['set', 'get']) 231 | else 232 | for group in g:cycle_override_defaults 233 | call AddCycleGroup(group[0], group[1]) 234 | endfor 235 | endif 236 | 237 | nnoremap CycleNext :call Cycle(1) 238 | nnoremap CyclePrevious :call Cycle(-1) 239 | 240 | if !exists("g:cycle_no_mappings") || !g:cycle_no_mappings 241 | nmap CycleNext 242 | nmap CyclePrevious 243 | endif 244 | 245 | --------------------------------------------------------------------------------