├── MIT-LICENSE.txt ├── README.md ├── autoload └── expand_region.vim ├── doc └── expand_region.txt ├── expand-region.gif └── plugin └── expand_region.vim /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Terry Ma 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-expand-region 2 | 3 | ## About 4 | [vim-expand-region] is a Vim plugin that allows you to visually select increasingly larger regions of text using the same key combination. It is similar to features from other editors: 5 | 6 | - Emac's [expand region](https://github.com/magnars/expand-region.el) 7 | - IntelliJ's [syntax aware selection](http://www.jetbrains.com/idea/documentation/tips/#tips_code_editing) 8 | - Eclipse's [select enclosing element](http://stackoverflow.com/questions/4264047/intellij-ctrlw-equivalent-shortcut-in-eclipse) 9 | 10 |

11 | vim-expand-region 12 |

13 | 14 | ## Installation 15 | Install using [Pathogen], [Vundle], [Neobundle], or your favorite Vim package manager. 16 | 17 | ## Quick Start 18 | Press ```+``` to expand the visual selection and ```_``` to shrink it. 19 | 20 | ## Mapping 21 | Customize the key mapping if you don't like the default. 22 | 23 | ``` 24 | map K (expand_region_expand) 25 | map J (expand_region_shrink) 26 | ``` 27 | 28 | ## Setting 29 | ### Customize selected regions 30 | The plugin uses __your own__ text objects to determine the expansion. You can customize the text objects the plugin knows about with ```g:expand_region_text_objects```. 31 | 32 | ```vim 33 | " Default settings. (NOTE: Remove comments in dictionary before sourcing) 34 | let g:expand_region_text_objects = { 35 | \ 'iw' :0, 36 | \ 'iW' :0, 37 | \ 'i"' :0, 38 | \ 'i''' :0, 39 | \ 'i]' :1, " Support nesting of square brackets 40 | \ 'ib' :1, " Support nesting of parentheses 41 | \ 'iB' :1, " Support nesting of braces 42 | \ 'il' :0, " 'inside line'. Available through https://github.com/kana/vim-textobj-line 43 | \ 'ip' :0, 44 | \ 'ie' :0, " 'entire file'. Available through https://github.com/kana/vim-textobj-entire 45 | \ } 46 | ``` 47 | 48 | You can extend the global default dictionary by calling ```expand_region#custom_text_objects```: 49 | 50 | ```vim 51 | " Extend the global default (NOTE: Remove comments in dictionary before sourcing) 52 | call expand_region#custom_text_objects({ 53 | \ "\/\\n\\n\": 1, " Motions are supported as well. Here's a search motion that finds a blank line 54 | \ 'a]' :1, " Support nesting of 'around' brackets 55 | \ 'ab' :1, " Support nesting of 'around' parentheses 56 | \ 'aB' :1, " Support nesting of 'around' braces 57 | \ 'ii' :0, " 'inside indent'. Available through https://github.com/kana/vim-textobj-indent 58 | \ 'ai' :0, " 'around indent'. Available through https://github.com/kana/vim-textobj-indent 59 | \ }) 60 | ``` 61 | 62 | You can further customize the text objects dictionary on a per filetype basis by defining global variables like ```g:expand_region_text_objects_{ft}```. 63 | 64 | ```vim 65 | " Use the following setting for ruby. (NOTE: Remove comments in dictionary before sourcing) 66 | let g:expand_region_text_objects_ruby = { 67 | \ 'im' :0, " 'inner method'. Available through https://github.com/vim-ruby/vim-ruby 68 | \ 'am' :0, " 'around method'. Available through https://github.com/vim-ruby/vim-ruby 69 | \ } 70 | ``` 71 | 72 | Note that this completely replaces the default dictionary. To extend the default on a per filetype basis, you can call ```expand_region#custom_text_objects``` by passing in the filetype in the first argument: 73 | 74 | ```vim 75 | " Use the global default + the following for ruby 76 | call expand_region#custom_text_objects('ruby', { 77 | \ 'im' :0, 78 | \ 'am' :0, 79 | \ }) 80 | ``` 81 | 82 | ### Customize selection mode 83 | By default, after an expansion, the plugin leaves you in visual mode. If your ```selectmode```(h:selectmode)) contains ```cmd```, then the plugin will respect that setting and leave you in select mode. If you don't have ```selectmode``` set, but would like to default the expansion in select mode, you can use the global setting below: 84 | 85 | ```vim 86 | let g:expand_region_use_select_mode = 1 87 | ``` 88 | 89 | [vim-expand-region]:http://github.com/terryma/vim-expand-region 90 | [Pathogen]:http://github.com/tpope/vim-pathogen 91 | [Vundle]:http://github.com/gmarik/vundle 92 | [Neobundle]:http://github.com/Shougo/neobundle.vim 93 | 94 | 95 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/terryma/vim-expand-region/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 96 | 97 | -------------------------------------------------------------------------------- /autoload/expand_region.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================== 2 | " File: expand_region.vim 3 | " Author: Terry Ma 4 | " Last Modified: March 30, 2013 5 | " ============================================================================== 6 | 7 | let s:save_cpo = &cpo 8 | set cpo&vim 9 | 10 | " ============================================================================== 11 | " Settings 12 | " ============================================================================== 13 | 14 | " Init global vars 15 | function! expand_region#init() 16 | if exists('g:expand_region_init') && g:expand_region_init 17 | return 18 | endif 19 | let g:expand_region_init = 1 20 | 21 | " Dictionary of text objects that are supported by default. Note that some of 22 | " the text objects are not available in vanilla vim. '1' indicates that the 23 | " text object is recursive (think of nested parens or brackets) 24 | let g:expand_region_text_objects = get(g:, 'expand_region_text_objects', { 25 | \ 'iw' :0, 26 | \ 'iW' :0, 27 | \ 'i"' :0, 28 | \ 'i''' :0, 29 | \ 'i]' :1, 30 | \ 'ib' :1, 31 | \ 'iB' :1, 32 | \ 'il' :0, 33 | \ 'ip' :0, 34 | \ 'ie' :0, 35 | \}) 36 | 37 | " Option to default to the select mode when selecting a new region 38 | let g:expand_region_use_select_mode = get(g:, 'expand_region_use_select_mode', 0) 39 | endfunction 40 | call expand_region#init() 41 | 42 | " ============================================================================== 43 | " Global Functions 44 | " ============================================================================== 45 | 46 | " Allow user to customize the global dictionary, or the per file type dictionary 47 | function! expand_region#custom_text_objects(...) 48 | if a:0 == 1 49 | call extend(g:expand_region_text_objects, a:1) 50 | elseif a:0 == 2 51 | if !exists("g:expand_region_text_objects_".a:1) 52 | let g:expand_region_text_objects_{a:1} = {} 53 | call extend(g:expand_region_text_objects_{a:1}, g:expand_region_text_objects) 54 | endif 55 | call extend(g:expand_region_text_objects_{a:1}, a:2) 56 | endif 57 | endfunction 58 | 59 | " Returns whether we should perform the region highlighting use visual mode or 60 | " select mode 61 | function! expand_region#use_select_mode() 62 | return g:expand_region_use_select_mode || index(split(s:saved_selectmode, ','), 'cmd') != -1 63 | endfunction 64 | 65 | " Main function 66 | function! expand_region#next(mode, direction) 67 | call s:expand_region(a:mode, a:direction) 68 | endfunction 69 | 70 | " ============================================================================== 71 | " Variables 72 | " ============================================================================== 73 | 74 | " The saved cursor position when user initiates expand. This is the position we 75 | " use to calcuate the region for all of our text objects. This is also used to 76 | " restore the original cursor position when the region is completely shrinked. 77 | let s:saved_pos = [] 78 | 79 | " Index into the list of filtered text objects(s:candidates), the text object 80 | " this points to is the currently selected region. 81 | let s:cur_index = -1 82 | 83 | " The list of filtered text objects used to expand/shrink the visual selection. 84 | " This is computed when expand-region is called the first time. 85 | " Each item is a dictionary containing the following: 86 | " text_object: The actual text object string 87 | " start_pos: The result of getpos() on the starting position of the text object 88 | " end_pos: The result of getpos() on the ending position of the text object 89 | " length: The number of characters for the text object 90 | let s:candidates = [] 91 | 92 | " This is used to save the user's selectmode setting. If the user's selectmode 93 | " contains 'cmd', then our expansion should result in the region selected under 94 | " select mode. 95 | let s:saved_selectmode = &selectmode 96 | 97 | " ============================================================================== 98 | " Functions 99 | " ============================================================================== 100 | 101 | " Sort the text object by length in ascending order 102 | function! s:sort_text_object(l, r) 103 | return a:l.length - a:r.length 104 | endfunction 105 | 106 | " Compare two position arrays. Each input is the result of getpos(). Return a 107 | " negative value if lhs occurs before rhs, positive value if after, and 0 if 108 | " they are the same. 109 | function! s:compare_pos(l, r) 110 | " If number lines are the same, compare columns 111 | return a:l[1] ==# a:r[1] ? a:l[2] - a:r[2] : a:l[1] - a:r[1] 112 | endfunction 113 | 114 | " Boundary check on the cursor position to make sure it's inside the text object 115 | " region. Return 1 if the cursor is within range, 0 otherwise. 116 | function! s:is_cursor_inside(pos, region) 117 | if s:compare_pos(a:pos, a:region.start_pos) < 0 118 | return 0 119 | endif 120 | if s:compare_pos(a:pos, a:region.end_pos) > 0 121 | return 0 122 | endif 123 | return 1 124 | endfunction 125 | 126 | " Remove duplicates from the candidate list. Two candidates are duplicates if 127 | " they cover the exact same region (same length and same starting position) 128 | function! s:remove_duplicate(input) 129 | let i = len(a:input) - 1 130 | while i >= 1 131 | if a:input[i].length ==# a:input[i-1].length && 132 | \ a:input[i].start_pos ==# a:input[i-1].start_pos 133 | call remove(a:input, i) 134 | endif 135 | let i-=1 136 | endwhile 137 | endfunction 138 | 139 | " Return a single candidate dictionary. Each dictionary contains the following: 140 | " text_object: The actual text object string 141 | " start_pos: The result of getpos() on the starting position of the text object 142 | " end_pos: The result of getpos() on the ending position of the text object 143 | " length: The number of characters for the text object 144 | function! s:get_candidate_dict(text_object) 145 | " Store the current view so we can restore it at the end 146 | let winview = winsaveview() 147 | 148 | " Use ! as much as possible 149 | exec 'normal! v' 150 | exec 'silent! normal '.a:text_object 151 | " The double quote is important 152 | exec "normal! \" 153 | 154 | let selection = s:get_visual_selection() 155 | let ret = { 156 | \ "text_object": a:text_object, 157 | \ "start_pos": selection.start_pos, 158 | \ "end_pos": selection.end_pos, 159 | \ "length": selection.length, 160 | \} 161 | 162 | " Restore peace 163 | call winrestview(winview) 164 | return ret 165 | endfunction 166 | 167 | 168 | " Return dictionary of text objects that are to be used for the current 169 | " filetype. Filetype-specific dictionaries will be loaded if they exist 170 | " and the global dictionary will be used as a fallback. 171 | function! s:get_configuration() 172 | let configuration = {} 173 | for ft in split(&ft, '\.') 174 | if exists("g:expand_region_text_objects_".ft) 175 | call extend(configuration, g:expand_region_text_objects_{ft}) 176 | endif 177 | endfor 178 | 179 | if empty(configuration) 180 | call extend(configuration, g:expand_region_text_objects) 181 | endif 182 | 183 | return configuration 184 | endfunction 185 | 186 | " Return list of candidate dictionary. Each dictionary contains the following: 187 | " text_object: The actual text object string 188 | " start_pos: The result of getpos() on the starting position of the text object 189 | " length: The number of characters for the text object 190 | function! s:get_candidate_list() 191 | " Turn off wrap to allow recursive search to work without triggering errors 192 | let save_wrapscan = &wrapscan 193 | set nowrapscan 194 | 195 | let config = s:get_configuration() 196 | 197 | " Generate the candidate list for every defined text object 198 | let candidates = keys(config) 199 | call map(candidates, "s:get_candidate_dict(v:val)") 200 | 201 | " For the ones that are recursive, generate them until they no longer match 202 | " any region 203 | let recursive_candidates = [] 204 | for i in candidates 205 | " Continue if not recursive 206 | if !config[i.text_object] 207 | continue 208 | endif 209 | " If the first level is already empty, no point in going any further 210 | if i.length ==# 0 211 | continue 212 | endif 213 | let l:count = 2 214 | let previous = i.length 215 | while 1 216 | let test = l:count.i.text_object 217 | let candidate = s:get_candidate_dict(test) 218 | if candidate.length ==# 0 219 | break 220 | endif 221 | " If we're not producing larger regions, end early 222 | if candidate.length ==# previous 223 | break 224 | endif 225 | call add(recursive_candidates, candidate) 226 | let l:count+=1 227 | let previous = candidate.length 228 | endwhile 229 | endfor 230 | 231 | " Restore wrapscan 232 | let &wrapscan = save_wrapscan 233 | 234 | return extend(candidates, recursive_candidates) 235 | endfunction 236 | 237 | " Return a dictionary containing the start position, end position and length of 238 | " the current visual selection. 239 | function! s:get_visual_selection() 240 | let start_pos = getpos("'<") 241 | let end_pos = getpos("'>") 242 | let [lnum1, col1] = start_pos[1:2] 243 | let [lnum2, col2] = end_pos[1:2] 244 | let lines = getline(lnum1, lnum2) 245 | let lines[-1] = lines[-1][: col2 - 1] 246 | let lines[0] = lines[0][col1 - 1:] 247 | return { 248 | \ 'start_pos': start_pos, 249 | \ 'end_pos': end_pos, 250 | \ 'length': len(join(lines, "\n")) 251 | \} 252 | endfunction 253 | 254 | " Figure out whether we should compute the candidate text objects, or we're in 255 | " the middle of an expand/shrink. 256 | function! s:should_compute_candidates(mode) 257 | if a:mode ==# 'v' 258 | " Check that current visual selection is idential to our last expanded 259 | " region 260 | if s:cur_index >= 0 261 | let selection = s:get_visual_selection() 262 | if s:candidates[s:cur_index].start_pos ==# selection.start_pos 263 | \ && s:candidates[s:cur_index].length ==# selection.length 264 | return 0 265 | endif 266 | endif 267 | endif 268 | return 1 269 | endfunction 270 | 271 | " Computes the list of text object candidates to be used given the current 272 | " cursor position. 273 | function! s:compute_candidates(cursor_pos) 274 | " Reset index into the candidates list 275 | let s:cur_index = -1 276 | 277 | " Save the current cursor position so we can restore it later 278 | let s:saved_pos = a:cursor_pos 279 | 280 | " Compute a list of candidate regions 281 | let s:candidates = s:get_candidate_list() 282 | 283 | " Sort them and remove the ones with 0 or 1 length 284 | call filter(sort(s:candidates, "s:sort_text_object"), 'v:val.length > 1') 285 | 286 | " Filter out the ones where the cursor falls outside of its region. i" and i' 287 | " can start after the cursor position, and ib can start before, so both checks 288 | " are needed 289 | call filter(s:candidates, 's:is_cursor_inside(s:saved_pos, v:val)') 290 | 291 | " Remove duplicates 292 | call s:remove_duplicate(s:candidates) 293 | endfunction 294 | 295 | " Perform the visual selection at the end. If the user wants to be left in 296 | " select mode, do so 297 | function! s:select_region() 298 | exec 'normal! v' 299 | exec 'normal '.s:candidates[s:cur_index].text_object 300 | if expand_region#use_select_mode() 301 | exec "normal! \" 302 | endif 303 | endfunction 304 | 305 | " Expand or shrink the visual selection to the next candidate in the text object 306 | " list. 307 | function! s:expand_region(mode, direction) 308 | " Save the selectmode setting, and remove the setting so our 'v' command do 309 | " not get interfered 310 | let s:saved_selectmode = &selectmode 311 | let &selectmode="" 312 | 313 | if s:should_compute_candidates(a:mode) 314 | call s:compute_candidates(getpos('.')) 315 | else 316 | call setpos('.', s:saved_pos) 317 | endif 318 | 319 | if a:direction ==# '+' 320 | " Expanding 321 | if s:cur_index ==# len(s:candidates) - 1 322 | normal! gv 323 | else 324 | let s:cur_index+=1 325 | " Associate the window view with the text object 326 | let s:candidates[s:cur_index].prev_winview = winsaveview() 327 | call s:select_region() 328 | endif 329 | else 330 | "Shrinking 331 | if s:cur_index <=# 0 332 | " In visual mode, doing nothing here will return us to normal mode. For 333 | " select mode, the following is needed. 334 | if expand_region#use_select_mode() 335 | exec "normal! gV" 336 | endif 337 | else 338 | " Restore the window view 339 | call winrestview(s:candidates[s:cur_index].prev_winview) 340 | let s:cur_index-=1 341 | call s:select_region() 342 | endif 343 | endif 344 | 345 | " Restore the selectmode setting 346 | let &selectmode = s:saved_selectmode 347 | endfunction 348 | 349 | let &cpo = s:save_cpo 350 | unlet s:save_cpo 351 | -------------------------------------------------------------------------------- /doc/expand_region.txt: -------------------------------------------------------------------------------- 1 | *vim-expand-region.txt* Incremental visual selection 2 | 3 | __ _ 4 | ___ _ ______ ____ _____ ____/ / ________ ____ _(_)___ ____ 5 | / _ \| |/_/ __ \/ __ `/ __ \/ __ / / ___/ _ \/ __ `/ / __ \/ __ \ 6 | / __/> 43 | 44 | map K (expand_region_expand) 45 | map J (expand_region_shrink) 46 | < 47 | 48 | ============================================================================== 49 | 4. Global Options *expand-region-global-options* 50 | 51 | *expand_region_text_objects* 52 | Default: See below 53 | Dictionary containing the text objects the plugin uses to search for the 54 | available regions to expand/shrink to. The value corresponding to each plugin 55 | indicates whether text object is recursive. A recursive text object is 56 | continually expanded until the region no longer gets larger. > 57 | 58 | " Default settings. (NOTE: Remove comments in dictionary before sourcing) 59 | let g:expand_region_text_objects = { 60 | \ 'iw' :0, 61 | \ 'iW' :0, 62 | \ 'i"' :0, 63 | \ 'i''' :0, 64 | \ 'i]' :1, " Support nesting of square brackets 65 | \ 'ib' :1, " Support nesting of parentheses 66 | \ 'iB' :1, " Support nesting of braces 67 | \ 'il' :0, " 'inside line'. Available through https://github.com/kana/vim-textobj-line 68 | \ 'ip' :0, 69 | \ 'ie' :0, " 'entire file'. Available through https://github.com/kana/vim-textobj-entire 70 | \ } 71 | < 72 | 73 | You can extend the global default dictionary by calling 74 | 'expand_region#custom_text_objects'. > 75 | 76 | " Extend the global default (NOTE: Remove comments in dictionary before sourcing) 77 | call expand_region#custom_text_objects({ 78 | \ "\/\\n\\n\": 1, " Motions are supported as well. Here's a search motion that finds a blank line 79 | \ 'a]' :1, " Support nesting of 'around' brackets 80 | \ 'ab' :1, " Support nesting of 'around' parentheses 81 | \ 'aB' :1, " Support nesting of 'around' braces 82 | \ 'ii' :0, " 'inside indent'. Available through https://github.com/kana/vim-textobj-indent 83 | \ 'ai' :0, " 'around indent'. Available through https://github.com/kana/vim-textobj-indent 84 | \ }) 85 | < 86 | 87 | You can further customize the text objects dictionary on a per filetype basis 88 | by defining global variables like 'g:expand_region_text_objects_{ft}'. > 89 | 90 | " Use the following setting for ruby. (NOTE: Remove comments in dictionary before sourcing) 91 | let g:expand_region_text_objects_ruby = { 92 | \ 'im' :0, " 'inner method'. Available through https://github.com/vim-ruby/vim-ruby 93 | \ 'am' :0, " 'around method'. Available through https://github.com/vim-ruby/vim-ruby 94 | \ } 95 | < 96 | 97 | Note that this completely replaces the default dictionary. To extend the 98 | default on a per filetype basis, you can call 99 | 'expand_region#custom_text_objects' by passing in the filetype in the first 100 | argument: > 101 | 102 | " Use the global default + the following for ruby 103 | call expand_region#custom_text_objects('ruby', { 104 | \ 'im' :0, 105 | \ 'am' :0, 106 | \ }) 107 | < 108 | *expand_region_use_select_mode* 109 | Default: 0 110 | By default, after an expansion, the plugin leaves you in visual mode. If your 111 | 'selectmode' contains "cmd", then the plugin will respect that setting and 112 | leave you in select mode. If you don't have 'selectmode' set, but would 113 | like to default the expansion in select mode, you can use the global setting 114 | below: > 115 | 116 | let g:expand_region_use_select_mode = 1 117 | < 118 | 119 | ============================================================================== 120 | 5. About *expand-region-about* 121 | ============================================================================== 122 | 123 | Find the latest version of the plugin here: 124 | http://github.com/terryma/vim-expand-region 125 | 126 | vim:tw=78:sw=4:ft=help:norl: 127 | -------------------------------------------------------------------------------- /expand-region.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terryma/vim-expand-region/966513543de0ddc2d673b5528a056269e7917276/expand-region.gif -------------------------------------------------------------------------------- /plugin/expand_region.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================== 2 | " File: expand_region.vim 3 | " Author: Terry Ma 4 | " Description: Incrementally select larger regions of text in visual mode by 5 | " repeating the same key combination 6 | " Last Modified: March 30, 2013 7 | " ============================================================================== 8 | 9 | let s:save_cpo = &cpo 10 | set cpo&vim 11 | 12 | " Init global vars 13 | call expand_region#init() 14 | 15 | " ============================================================================== 16 | " Mappings 17 | " ============================================================================== 18 | if !hasmapto('(expand_region_expand)') 19 | nmap + (expand_region_expand) 20 | vmap + (expand_region_expand) 21 | endif 22 | if !hasmapto('(expand_region_shrink)') 23 | vmap _ (expand_region_shrink) 24 | nmap _ (expand_region_shrink) 25 | endif 26 | 27 | nnoremap (expand_region_expand) 28 | \ :call expand_region#next('n', '+') 29 | " Map keys differently depending on which mode is desired 30 | if expand_region#use_select_mode() 31 | snoremap (expand_region_expand) 32 | \ :call expand_region#next('v', '+') 33 | snoremap (expand_region_shrink) 34 | \ :call expand_region#next('v', '-') 35 | else 36 | xnoremap (expand_region_expand) 37 | \ :call expand_region#next('v', '+') 38 | xnoremap (expand_region_shrink) 39 | \ :call expand_region#next('v', '-') 40 | endif 41 | 42 | let &cpo = s:save_cpo 43 | unlet s:save_cpo 44 | --------------------------------------------------------------------------------