From bdd7fdd82001821635916651108a4963d111dd97 Mon Sep 17 00:00:00 2001 From: "Andy K. Massimino" Date: Tue, 31 Oct 2017 18:06:07 -0400 Subject: [PATCH] Implement text object repetition and count --- README.md | 52 +++++++++--- autoload/matchup.vim | 2 +- autoload/matchup/delim.vim | 36 +++++++-- autoload/matchup/text_obj.vim | 144 ++++++++++++++++++++-------------- 4 files changed, 156 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 9018830..078ca6a 100644 --- a/README.md +++ b/README.md @@ -131,15 +131,18 @@ words are `else` and `elseif`. The `if`/`endif` pair is called an is an [inclusive] motion. #### (b.1) full set of text objects - - `i%` the inside of an open to close block - - `1i%` the inside of an any block - - `{count}i%` If count is not 1, the inside open-to-close block +- `i%` the inside of an any block +- `1i%` the inside of an open-to-close block +- `{count}i%` If count is greater than 1, the inside of the `{count}`th + surrounding open-to-close block - - `a%` an open-to-close block. - - `1a%` an any block. Includes mids but does not include open and close. - - `{count}a%` if `{count}` is greater than 1, the `{count}` surrounding open-to-close block. +- `a%` an any block. +- `1a%` an open-to-close block. Includes mids but does not include open + and close words. +- `{count}a%` if `{count}` is greater than 1, the `{count}`th surrounding + open-to-close block. - Note: by default objects involving `matchpairs` such as `(){}[]` are +Note: by default objects involving `matchpairs` such as `(){}[]` are performed character-wise, while `matchwords` such as `if`/`endif` are performed line-wise. The -wise can be forced using "v", "V", or `^V` @@ -289,7 +292,7 @@ let g:matchup_text_obj_enabled = 0 ``` defaults: 1 -To enable the experimental [transmute](#d3-parallel-transmutation) +To enable the experimental [transmute](#d1-parallel-transmutation) module, ```vim let g:matchup_transmute_enabled = 1 @@ -335,9 +338,37 @@ let g:matchup_matchparen_status_offscreen = 0 ``` default: 1 +Adjust timeouts for matchparen highlighting +``` +let g:matchup_matchparen_timeout = 300 +let g:matchparen_insert_timeout = 60 +``` +default: 300, 60 + ### motion -### text_obj +To allow `{count}%`, +```vim +g:matchup_motion_override_Npercent = 1 +``` +default: 0 + +If enabled, cursor will land on the end of mid and close words while +moving downwards (`%`/`]%`). While moving upwards (`g%`, `[%`) the cursor +will land on the beginning. To disable, +```vim +let g:matchup_motion_cursor_end = 0 +``` +default: 1 + +### Module text_obj + +Modify the set of operators which may operate +[line-wise](#line-wise-operator-text-object-combinations) +```vim +let g:matchup_text_obj_linewise_operators' = ['d', 'y'] +``` +default: `['d', 'y']` ### transmute @@ -523,6 +554,9 @@ Convert between single-line and multi-line blocks. Mappings undecided. - write proper vim doc - thoroughly test with unicode, tabs - add screenshots and animations +- add file type `quirks` module +- investigate whether `&selection`/`&virtualedit` options are important - can match-up be integrated with [vim-surround](https://github.com/tpope/vim-surround)? +- complete parallel transmutation in an efficient way. diff --git a/autoload/matchup.vim b/autoload/matchup.vim index 42371f5..a49b85e 100644 --- a/autoload/matchup.vim +++ b/autoload/matchup.vim @@ -26,7 +26,7 @@ function! s:init_options() call s:init_option('matchup_text_obj_enabled', 1) call s:init_option('matchup_text_obj_linewise_operators', ['d', 'y']) - call s:init_option('matchup_transmute_enabled', 1) + call s:init_option('matchup_transmute_enabled', 0) call s:init_option('matchup_imap_enabled', 1) diff --git a/autoload/matchup/delim.vim b/autoload/matchup/delim.vim index ef9aa7f..a34293d 100644 --- a/autoload/matchup/delim.vim +++ b/autoload/matchup/delim.vim @@ -204,23 +204,39 @@ function! matchup#delim#get_matching(delim, ...) " {{{1 endfunction " }}}1 -function! matchup#delim#get_surrounding(type) " {{{1 +function! matchup#delim#get_surrounding(type, ...) " {{{1 + call matchup#perf#tic('delim#get_surrounding') + let l:save_pos = matchup#pos#get_cursor() let l:pos_val_cursor = matchup#pos#val(l:save_pos) let l:pos_val_last = l:pos_val_cursor let l:pos_val_open = l:pos_val_cursor - 1 + let l:count = a:0 >= 1 ? a:1 : 1 + let l:counter = l:count + + " provided count == 0 refers to local any block + let l:local = l:count == 0 ? 1 : 0 + while l:pos_val_open < l:pos_val_last - let l:open = matchup#delim#get_prev(a:type, 'open') + let l:open = matchup#delim#get_prev(a:type, + \ l:local ? 'open_mid' : 'open') if empty(l:open) | break | endif - " echo l:open.lnum l:open.cnum | sleep 1 - let l:close = matchup#delim#get_matching(l:open) - " echo l:close.lnum l:close.cnum | sleep 1 + + let l:match = matchup#delim#get_matching(l:open, 1) + let l:close = l:local ? l:open.links.next : l:open.links.close + let l:pos_val_try = matchup#pos#val(l:close) \ + strdisplaywidth(l:close.match) - 1 if l:pos_val_try >= l:pos_val_cursor - call matchup#pos#set_cursor(l:save_pos) - return [l:open, l:close] + if l:counter <= 1 + " restore cursor and accept + call matchup#pos#set_cursor(l:save_pos) + call matchup#perf#toc('delim#get_surrounding', 'accept') + return [l:open, l:close] + endif + call matchup#pos#set_cursor(matchup#pos#prev(l:open)) + let l:counter -= 1 else call matchup#pos#set_cursor(matchup#pos#prev(l:open)) let l:pos_val_last = l:pos_val_open @@ -228,7 +244,9 @@ function! matchup#delim#get_surrounding(type) " {{{1 endif endwhile + " restore cursor and return failure call matchup#pos#set_cursor(l:save_pos) + call matchup#perf#toc('delim#get_surrounding', 'fail') return [{}, {}] endfunction @@ -1332,7 +1350,8 @@ function! s:init_delim_regexes() " {{{1 let l:re.delim_tex = s:init_delim_regexes_generator('delim_tex') - for l:k in ['open', 'close', 'both', 'mid', 'both_all'] + " XXX use keys(sidedict) + for l:k in ['open', 'close', 'both', 'mid', 'both_all', 'open_mid'] let l:re.delim_all[l:k] = l:re.delim_tex[l:k] let l:re.all[l:k] = l:re.delim_all[l:k] endfor @@ -1546,6 +1565,7 @@ let s:sidedict = { \ 'close' : ['close'], \ 'both' : ['close', 'open'], \ 'both_all' : ['close', 'mid', 'open'], + \ 'open_mid' : ['mid', 'open'], \} let s:basetypes = { diff --git a/autoload/matchup/text_obj.vim b/autoload/matchup/text_obj.vim index d5a7a8b..3334338 100644 --- a/autoload/matchup/text_obj.vim +++ b/autoload/matchup/text_obj.vim @@ -23,82 +23,106 @@ function! matchup#text_obj#init_module() " {{{1 endfor endfunction +" MAXCOL is probably a lot bigger in actuality, but we don't want +" to support such long lines +let s:MAXCOL = 0x7fff + " }}}1 function! matchup#text_obj#delimited(is_inner, visual, type) " {{{1 + " get the current selection, move to end of range if a:visual let l:selection = getpos("'<")[1:2] + getpos("'>")[1:2] call matchup#pos#set_cursor(getpos("'>")) endif - let [l:open, l:close] = matchup#delim#get_surrounding(a:type) + " determine if operator is able to act line-wise (i.e., for inner) + let l:linewise = index(g:matchup_text_obj_linewise_operators, + \ v:operator) >= 0 - if empty(l:open) - if a:visual - normal! gv - endif - return - endif + " try up to three times (rarely) + for l:try_again in range(3) + " on the first try, we use v:count which may be zero + " on the next tries, use the previous count plus one + let [l:open, l:close] = matchup#delim#get_surrounding( + \ a:type, l:try_again ? (v:count1 + l:try_again) : v:count) - let [l:l1, l:c1, l:l2, l:c2] = [l:open.lnum, l:open.cnum, - \ l:close.lnum, l:close.cnum] - - " Determine if operator is linewise - let l:linewise = index(g:matchup_text_obj_linewise_operators, - \ v:operator) >= 0 - let l:linewise = 1 - - " Adjust the borders - if a:is_inner - let c1 += len(l:open.match) - let c2 -= 1 - - let l:is_inline = (l:l2 - l:l1) > 1 ? 1 : 0 - - " \ && match(strpart(getline(l1), c1 ), '^\s*$') >= 0 - " \ && match(strpart(getline(l2), 0, c2), '^\s*$') >= 0 - - if l:is_inline - let l1 += 1 - let c1 = strlen(matchstr(getline(l1), '^\s*')) + 1 - let l2 -= 1 - let c2 = strlen(getline(l2)) - if c2 == 0 && !l:linewise - let l2 -= 1 - let c2 = len(getline(l2)) + 1 - endif - elseif c2 == 0 - let l2 -= 1 - let c2 = len(getline(l2)) + 1 - endif - else - let c2 += len(l:close.match) - 1 - endif - - " select next pair if we reached the same selection - if a:visual && l:selection == [l1, c1, l2, c2] - echo 'foobar' | sleep 1 - call matchup#pos#set_cursor(matchup#pos#next([l2, c2])) - let [l:open, l:close] = matchup#delim#get_surrounding(a:type) - if empty(l:open) + if empty(l:open) + if a:visual normal! gv - return endif - let [l1, c1, l2, c2] = [l:open.lnum, l:open.cnum, - \ l:close.lnum, l:close.cnum + len(l:close.match) - 1] + return endif - let l:is_inline = (l2 - l1) > 1 - \ && match(strpart(getline(l1), 0, c1-1), '^\s*$') >= 0 - \ && match(strpart(getline(l2), 0, c2), '^\s*$') >= 0 + " heuristic to handle overlapping any-blocks; + " if the start delimiter is inside our already visually selected + " area, try again but this time find open instead of open_mid + if a:visual && !l:try_again + \ && (l:open.lnum > l:selection[0] + \ || l:open.lnum == l:selection[0] + \ && l:open.cnum >= l:selection[1]) + let [l:open, l:close] = matchup#delim#get_surrounding( + \ a:type, v:count1 + l:try_again) + endif - " Determine the select mode - let l:select_mode = l:is_inline && l:linewise ? 'V' + let [l:l1, l:c1, l:l2, l:c2] = [l:open.lnum, l:open.cnum, + \ l:close.lnum, l:close.cnum] + + " special case: if inner, and the current selection coincides + " with the open and close positions, try for a second time + " this allows vi% in [[ ]] to work + if l:selection[1] == l:c1 && l:selection[3] == l:c2 + continue + endif + + let l:is_multiline = (l:l2 - l:l1) > 1 ? 1 : 0 + + " adjust the borders + if a:is_inner + let l:c1 += len(l:open.match) + let l:c2 -= 1 + + if l:is_multiline + let l:l1 += 1 + let l:c1 = strlen(matchstr(getline(l:l1), '^\s*')) + 1 + let l:l2 -= 1 + let l:c2 = strlen(getline(l:l2)) + if l:c2 == 0 && !l:linewise + let l:l2 -= 1 + let l:c2 = len(getline(l:l2)) + 1 + endif + elseif l:c2 == 0 + let l:l2 -= 1 + let l:c2 = len(getline(l2)) + 1 + endif + else + let l:c2 += len(l:close.match) - 1 + endif + + " in visual line mode, force new selection to be larger + if a:visual && visualmode() ==# 'V' + \ && (l:l1 > l:selection[0] || l:l2 < l:selection[2]) + continue + endif + + " try again if we reached the same selection + " for visual line mode, only check line numbers + " workaround for cases where the cursor might get fooled + " into going into one of the inner blocks + if a:visual && (l:selection == [l:l1, l:c1, l:l2, l:c2] + \ || visualmode() ==# 'V' + \ && [l:selection[0], l:selection[2]] == [l:l1, l:l2]) + continue + else + break + endif + + endfor + + " determine the proper select mode + let l:select_mode = l:is_multiline && l:linewise ? 'V' \ : (v:operator ==# ':') ? visualmode() : 'v' - echo l:is_inline l:linewise v:operator visualmode() - \ l:l2 l:l1 l:l2-l:l1 | sleep 1 - - " Apply selection + " apply selection execute 'normal!' l:select_mode call matchup#pos#set_cursor(l1, c1) normal! o