|
| 1 | +" Persist state for repeat. |
| 2 | +" opfunc : &operatorfunc at g@ invocation. |
| 3 | +" opfunc_st : State during last 'operatorfunc' (g@) invocation. |
| 4 | +let s:st = { 'rst':1, 'input':'', 'inputlen':0, 'reverse':0, 'bounds':[0,0], |
| 5 | + \'inclusive':0, 'label':'', 'opfunc':'', 'opfunc_st':{} } |
| 6 | + |
| 7 | +if exists('##OptionSet') |
| 8 | + augroup sneak_optionset |
| 9 | + autocmd! |
| 10 | + autocmd OptionSet operatorfunc let s:st.opfunc = &operatorfunc | let s:st.opfunc_st = {} |
| 11 | + augroup END |
| 12 | +endif |
| 13 | + |
| 14 | +func! sneak#state() abort |
| 15 | + return deepcopy(s:st) |
| 16 | +endf |
| 17 | + |
| 18 | +func! sneak#is_sneaking() abort |
| 19 | + return exists("#sneak#CursorMoved") |
| 20 | +endf |
| 21 | + |
| 22 | +func! sneak#cancel() abort |
| 23 | + call sneak#util#removehl() |
| 24 | + augroup sneak |
| 25 | + autocmd! |
| 26 | + augroup END |
| 27 | + if maparg('<esc>', 'n') =~# "'s'\\.'neak#cancel'" " Remove temporary mapping. |
| 28 | + silent! unmap <esc> |
| 29 | + endif |
| 30 | + return '' |
| 31 | +endf |
| 32 | + |
| 33 | +" Entrypoint for `s`. |
| 34 | +func! sneak#wrap(op, inputlen, reverse, inclusive, label) abort |
| 35 | + let save_cmdheight = &cmdheight |
| 36 | + try |
| 37 | + if &cmdheight < 1 |
| 38 | + set cmdheight=1 |
| 39 | + endif |
| 40 | + |
| 41 | + let [cnt, reg] = [v:count1, v:register] "get count and register before doing _anything_, else they get overwritten. |
| 42 | + let is_similar_invocation = a:inputlen == s:st.inputlen && a:inclusive == s:st.inclusive |
| 43 | + |
| 44 | + if g:sneak_opt.s_next && is_similar_invocation && (sneak#util#isvisualop(a:op) || empty(a:op)) && sneak#is_sneaking() |
| 45 | + " Repeat motion (clever-s). |
| 46 | + call sneak#rpt(a:op, a:reverse) |
| 47 | + elseif a:op ==# 'g@' && !empty(s:st.opfunc_st) && !empty(s:st.opfunc) && s:st.opfunc ==# &operatorfunc |
| 48 | + " Replay state from the last 'operatorfunc'. |
| 49 | + call sneak#to(a:op, s:st.opfunc_st.input, s:st.opfunc_st.inputlen, cnt, reg, 1, s:st.opfunc_st.reverse, s:st.opfunc_st.inclusive, s:st.opfunc_st.label) |
| 50 | + else |
| 51 | + if exists('#User#SneakEnter') |
| 52 | + doautocmd <nomodeline> User SneakEnter |
| 53 | + redraw |
| 54 | + endif |
| 55 | + " Prompt for input. |
| 56 | + call sneak#to(a:op, s:getnchars(a:inputlen, a:op), a:inputlen, cnt, reg, 0, a:reverse, a:inclusive, a:label) |
| 57 | + if exists('#User#SneakLeave') |
| 58 | + doautocmd <nomodeline> User SneakLeave |
| 59 | + endif |
| 60 | + endif |
| 61 | + finally |
| 62 | + let &cmdheight = save_cmdheight |
| 63 | + endtry |
| 64 | +endf |
| 65 | + |
| 66 | +" Repeats the last motion. |
| 67 | +func! sneak#rpt(op, reverse) abort |
| 68 | + if s:st.rst "reset by f/F/t/T |
| 69 | + exec "norm! ".(sneak#util#isvisualop(a:op) ? "gv" : "").v:count1.(a:reverse ? "," : ";") |
| 70 | + return |
| 71 | + endif |
| 72 | + |
| 73 | + let l:relative_reverse = (a:reverse && !s:st.reverse) || (!a:reverse && s:st.reverse) |
| 74 | + call sneak#to(a:op, s:st.input, s:st.inputlen, v:count1, v:register, 1, |
| 75 | + \ (g:sneak_opt.absolute_dir ? a:reverse : l:relative_reverse), s:st.inclusive, 0) |
| 76 | +endf |
| 77 | + |
| 78 | +" input: may be shorter than inputlen if the user pressed <enter> at the prompt. |
| 79 | +" inclusive: 0: t-like, 1: f-like, 2: /-like |
| 80 | +func! sneak#to(op, input, inputlen, count, register, repeatmotion, reverse, inclusive, label) abort "{{{ |
| 81 | + if empty(a:input) "user canceled |
| 82 | + if a:op ==# 'c' " user <esc> during change-operation should return to previous mode. |
| 83 | + call feedkeys((col('.') > 1 && col('.') < col('$') ? "\<RIGHT>" : '') . "\<C-\>\<C-G>", 'n') |
| 84 | + endif |
| 85 | + redraw | echo '' | return |
| 86 | + endif |
| 87 | + |
| 88 | + let is_v = sneak#util#isvisualop(a:op) |
| 89 | + let [curlin, curcol] = [line('.'), virtcol('.')] "initial position |
| 90 | + let is_op = !empty(a:op) && !is_v "operator-pending invocation |
| 91 | + let s = g:sneak#search#instance |
| 92 | + call s.init(a:input, a:repeatmotion, a:reverse) |
| 93 | + |
| 94 | + if is_v && a:repeatmotion |
| 95 | + norm! gv |
| 96 | + endif |
| 97 | + |
| 98 | + " [count] means 'skip to this match' _only_ for operators/repeat-motion/1-char-search |
| 99 | + " sanity check: max out at 999, to avoid searchpos() OOM. |
| 100 | + let skip = (is_op || a:repeatmotion || a:inputlen < 2) ? min([999, a:count]) : 0 |
| 101 | + |
| 102 | + let l:gt_lt = a:reverse ? '<' : '>' |
| 103 | + let bounds = a:repeatmotion ? s:st.bounds : [0,0] " [left_bound, right_bound] |
| 104 | + let l:scope_pattern = '' " pattern used to highlight the vertical 'scope' |
| 105 | + let l:match_bounds = '' |
| 106 | + |
| 107 | + "scope to a column of width 2*(v:count1)+1 _except_ for operators/repeat-motion/1-char-search |
| 108 | + if ((!skip && a:count > 1) || max(bounds)) && !is_op |
| 109 | + if !max(bounds) "derive bounds from count (_logical_ bounds highlighted in 'scope') |
| 110 | + let bounds[0] = max([0, (virtcol('.') - a:count - 1)]) |
| 111 | + let bounds[1] = a:count + virtcol('.') + 1 |
| 112 | + endif |
| 113 | + "Match *all* chars in scope. Use \%<42v (virtual column) instead of \%<42c (byte column). |
| 114 | + let l:scope_pattern .= '\%>'.bounds[0].'v\%<'.bounds[1].'v' |
| 115 | + endif |
| 116 | + |
| 117 | + if max(bounds) |
| 118 | + "adjust logical left-bound for the _match_ pattern by -length(s) so that if _any_ |
| 119 | + "char is within the logical bounds, it is considered a match. |
| 120 | + let l:leftbound = max([0, (bounds[0] - a:inputlen) + 1]) |
| 121 | + let l:match_bounds = '\%>'.l:leftbound.'v\%<'.bounds[1].'v' |
| 122 | + let s.match_pattern .= l:match_bounds |
| 123 | + endif |
| 124 | + |
| 125 | + "TODO: refactor vertical scope calculation into search.vim, |
| 126 | + " so this can be done in s.init() instead of here. |
| 127 | + call s.initpattern() |
| 128 | + |
| 129 | + let s:st.rptreverse = a:reverse |
| 130 | + if !a:repeatmotion "this is a new (not repeat) invocation |
| 131 | + "persist even if the search fails, because the _reverse_ direction might have a match. |
| 132 | + let s:st.rst = 0 | let s:st.input = a:input | let s:st.inputlen = a:inputlen |
| 133 | + let s:st.reverse = a:reverse | let s:st.bounds = bounds | let s:st.inclusive = a:inclusive |
| 134 | + |
| 135 | + " Set temporary hooks on f/F/t/T so that we know when to reset Sneak. |
| 136 | + call s:ft_hook() |
| 137 | + endif |
| 138 | + |
| 139 | + let nextchar = searchpos('\_.', 'n'.(s.search_options_no_s)) |
| 140 | + let nudge = !a:inclusive && a:repeatmotion && nextchar == s.dosearch('n') |
| 141 | + if nudge |
| 142 | + let nudge = sneak#util#nudge(!a:reverse) "special case for t |
| 143 | + endif |
| 144 | + |
| 145 | + for i in range(1, max([1, skip])) "jump to the [count]th match |
| 146 | + let matchpos = s.dosearch() |
| 147 | + if 0 == max(matchpos) |
| 148 | + break |
| 149 | + else |
| 150 | + let nudge = !a:inclusive |
| 151 | + endif |
| 152 | + endfor |
| 153 | + |
| 154 | + if 0 == max(matchpos) |
| 155 | + if nudge |
| 156 | + call sneak#util#nudge(a:reverse) "undo nudge for t |
| 157 | + endif |
| 158 | + |
| 159 | + let km = empty(&keymap) ? '' : ' ('.&keymap.' keymap)' |
| 160 | + call sneak#util#echo('not found'.(max(bounds) ? printf(km.' (in columns %d-%d): %s', bounds[0], bounds[1], a:input) : km.': '.a:input)) |
| 161 | + return |
| 162 | + endif |
| 163 | + "search succeeded |
| 164 | + |
| 165 | + call sneak#util#removehl() |
| 166 | + |
| 167 | + if (!is_op || a:op ==# 'y') "position _after_ search |
| 168 | + let curlin = string(line('.')) |
| 169 | + let curcol = string(virtcol('.') + (a:reverse ? -1 : 1)) |
| 170 | + endif |
| 171 | + |
| 172 | + "Might as well scope to window height (+/- 99). |
| 173 | + let l:top = max([0, line('w0')-99]) |
| 174 | + let l:bot = line('w$')+99 |
| 175 | + let l:restrict_top_bot = '\%'.l:gt_lt.curlin.'l\%>'.l:top.'l\%<'.l:bot.'l' |
| 176 | + let l:scope_pattern .= l:restrict_top_bot |
| 177 | + let s.match_pattern .= l:restrict_top_bot |
| 178 | + let curln_pattern = l:match_bounds.'\%'.curlin.'l\%'.l:gt_lt.curcol.'v' |
| 179 | + |
| 180 | + "highlight the vertical 'tunnel' that the search is scoped-to |
| 181 | + if max(bounds) "perform the scoped highlight... |
| 182 | + let w:sneak_sc_hl = matchadd('SneakScope', l:scope_pattern) |
| 183 | + endif |
| 184 | + |
| 185 | + call s:attach_autocmds() |
| 186 | + |
| 187 | + "highlight actual matches at or beyond the cursor position |
| 188 | + " - store in w: because matchadd() highlight is per-window. |
| 189 | + let w:sneak_hl_id = matchadd('Sneak', |
| 190 | + \ (s.prefix).(s.match_pattern).(s.search).'\|'.curln_pattern.(s.search)) |
| 191 | + |
| 192 | + " Clear with <esc>. Use a funny mapping to avoid false positives. #287 |
| 193 | + if (has('nvim') || has('gui_running')) && maparg('<esc>', 'n') ==# "" |
| 194 | + nnoremap <expr> <silent> <esc> call('s'.'neak#cancel',[]) . "\<esc>" |
| 195 | + endif |
| 196 | + |
| 197 | + " Operators always invoke label-mode. |
| 198 | + " If a:label is a string set it as the target, without prompting. |
| 199 | + let label = a:label !~# '[012]' ? a:label : '' |
| 200 | + let target = (2 == a:label || !empty(label) || (a:label && g:sneak_opt.label && (is_op || s.hasmatches(1)))) && !max(bounds) |
| 201 | + \ ? sneak#label#to(s, is_v, label) : "" |
| 202 | + |
| 203 | + if nudge |
| 204 | + call sneak#util#nudge(a:reverse) "undo nudge for t |
| 205 | + endif |
| 206 | + |
| 207 | + if is_op && 2 != a:inclusive && !a:reverse |
| 208 | + " f/t operations do not apply to the current character; nudge the cursor. |
| 209 | + call sneak#util#nudge(1) |
| 210 | + endif |
| 211 | + |
| 212 | + if is_op || '' != target |
| 213 | + call sneak#util#removehl() |
| 214 | + endif |
| 215 | + |
| 216 | + if is_op && a:op !=# 'y' |
| 217 | + let change = a:op !=? "c" ? "" : "\<c-r>.\<esc>" |
| 218 | + let args = sneak#util#strlen(a:input) . a:reverse . a:inclusive . (2*!empty(target)) |
| 219 | + if a:op !=# 'g@' |
| 220 | + let args .= a:input . target . change |
| 221 | + endif |
| 222 | + let seq = a:op . "\<Plug>SneakRepeat" . args |
| 223 | + silent! call repeat#setreg(seq, a:register) |
| 224 | + silent! call repeat#set(seq, a:count) |
| 225 | + |
| 226 | + let s:st.label = target |
| 227 | + if empty(s:st.opfunc_st) |
| 228 | + let s:st.opfunc_st = filter(deepcopy(s:st), 'v:key !=# "opfunc_st"') |
| 229 | + endif |
| 230 | + endif |
| 231 | +endf "}}} |
| 232 | + |
| 233 | +func! s:attach_autocmds() abort |
| 234 | + augroup sneak |
| 235 | + autocmd! |
| 236 | + autocmd InsertEnter,WinLeave,BufLeave * call sneak#cancel() |
| 237 | + "_nested_ autocmd to skip the _first_ CursorMoved event. |
| 238 | + "NOTE: CursorMoved is _not_ triggered if there is typeahead during a macro/script... |
| 239 | + autocmd CursorMoved * autocmd sneak CursorMoved * call sneak#cancel() |
| 240 | + augroup END |
| 241 | +endf |
| 242 | + |
| 243 | +func! sneak#reset(key) abort |
| 244 | + let c = sneak#util#getchar() |
| 245 | + |
| 246 | + let s:st.rst = 1 |
| 247 | + let s:st.reverse = 0 |
| 248 | + for k in ['f', 't'] "unmap the temp mappings |
| 249 | + if g:sneak_opt[k.'_reset'] |
| 250 | + silent! exec 'unmap '.k |
| 251 | + silent! exec 'unmap '.toupper(k) |
| 252 | + endif |
| 253 | + endfor |
| 254 | + |
| 255 | + "count is prepended implicitly by the <expr> mapping |
| 256 | + return a:key.c |
| 257 | +endf |
| 258 | + |
| 259 | +func! s:map_reset_key(key, mode) abort |
| 260 | + exec printf("%snoremap <silent> <expr> %s sneak#reset('%s')", a:mode, a:key, a:key) |
| 261 | +endf |
| 262 | + |
| 263 | +" Sets temporary mappings to 'hook' into f/F/t/T. |
| 264 | +func! s:ft_hook() abort |
| 265 | + for k in ['f', 't'] |
| 266 | + for m in ['n', 'x'] |
| 267 | + "if user mapped anything to f or t, do not map over it; unfortunately this |
| 268 | + "also means we cannot reset ; or , when f or t is invoked. |
| 269 | + if g:sneak_opt[k.'_reset'] && maparg(k, m) ==# '' |
| 270 | + call s:map_reset_key(k, m) | call s:map_reset_key(toupper(k), m) |
| 271 | + endif |
| 272 | + endfor |
| 273 | + endfor |
| 274 | +endf |
| 275 | + |
| 276 | +func! s:getnchars(n, mode) abort |
| 277 | + let s = '' |
| 278 | + echo g:sneak_opt.prompt | redraw |
| 279 | + for i in range(1, a:n) |
| 280 | + if sneak#util#isvisualop(a:mode) | exe 'norm! gv' | endif "preserve selection |
| 281 | + let c = sneak#util#getchar() |
| 282 | + if -1 != index(["\<esc>", "\<c-c>", "\<c-g>", "\<backspace>", "\<del>"], c) |
| 283 | + return "" |
| 284 | + endif |
| 285 | + if c == "\<CR>" |
| 286 | + if i > 1 "special case: accept the current input (#15) |
| 287 | + break |
| 288 | + else "special case: repeat the last search (useful for label-mode). |
| 289 | + return s:st.input |
| 290 | + endif |
| 291 | + else |
| 292 | + let s .= c |
| 293 | + if 1 == &iminsert && sneak#util#strlen(s) >= a:n |
| 294 | + "HACK: this can happen if the user entered multiple characters while we |
| 295 | + "were waiting to resolve a multi-char keymap. |
| 296 | + "example for keymap 'bulgarian-phonetic': |
| 297 | + " e:: => ё | resolved, strwidth=1 |
| 298 | + " eo => eo | unresolved, strwidth=2 |
| 299 | + break |
| 300 | + endif |
| 301 | + endif |
| 302 | + redraw | echo g:sneak_opt.prompt . s |
| 303 | + endfor |
| 304 | + return s |
| 305 | +endf |
| 306 | + |
0 commit comments