Skip to content

Commit c13d049

Browse files
committed
perf: autoload (avoid eager-load)
Sneak is now structured better for Vim "autoload". So its plugin file is small, and its autoload code is not sourced unless you call a `sneak#xx()` function.
1 parent 1f8702b commit c13d049

File tree

5 files changed

+345
-341
lines changed

5 files changed

+345
-341
lines changed

autoload/sneak.vim

+306
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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+

autoload/sneak/label.vim

+5-5
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,15 @@ func! s:do_label(s, v, reverse, label) abort "{{{
103103
call s:after()
104104

105105
let mappedto = maparg(choice, a:v ? 'x' : 'n')
106-
let mappedtoNext = (g:sneak#opt.absolute_dir && a:reverse)
106+
let mappedtoNext = (g:sneak_opt.absolute_dir && a:reverse)
107107
\ ? mappedto =~# '<Plug>Sneak\(_,\|Previous\)'
108108
\ : mappedto =~# '<Plug>Sneak\(_;\|Next\)'
109109

110110
if choice =~# "\\v^\<Tab>|\<S-Tab>|\<BS>$" " Decorate next N matches.
111111
if (!a:reverse && choice ==# "\<Tab>") || (a:reverse && choice =~# "^\<S-Tab>\\|\<BS>$")
112112
call cursor(overflow[0], overflow[1])
113113
endif " ...else we just switched directions, do not overflow.
114-
elseif (strlen(g:sneak#opt.label_esc) && choice ==# g:sneak#opt.label_esc)
114+
elseif (strlen(g:sneak_opt.label_esc) && choice ==# g:sneak_opt.label_esc)
115115
\ || -1 != index(["\<Esc>", "\<C-c>"], choice)
116116
return "\<Esc>" " Exit label-mode.
117117
elseif !mappedtoNext && !has_key(s:matchmap, choice) " Fallthrough: press _any_ invalid key to escape.
@@ -194,7 +194,7 @@ endf
194194
func! s:is_special_key(key) abort
195195
return -1 != index(["\<Esc>", "\<C-c>", "\<Space>", "\<CR>", "\<Tab>"], a:key)
196196
\ || maparg(a:key, 'n') =~# '<Plug>Sneak\(_;\|_,\|Next\|Previous\)'
197-
\ || (g:sneak#opt.s_next && maparg(a:key, 'n') =~# '<Plug>Sneak\(_s\|Forward\)')
197+
\ || (g:sneak_opt.s_next && maparg(a:key, 'n') =~# '<Plug>Sneak\(_s\|Forward\)')
198198
endf
199199

200200
" We must do this because:
@@ -209,8 +209,8 @@ func! sneak#label#sanitize_target_labels() abort
209209
if s:is_special_key(k) " Remove the char.
210210
let g:sneak#target_labels = substitute(g:sneak#target_labels, '\%'.(i+1).'c.', '', '')
211211
" Move ; (or s if 'clever-s' is enabled) to the front.
212-
if !g:sneak#opt.absolute_dir
213-
\ && ((!g:sneak#opt.s_next && maparg(k, 'n') =~# '<Plug>Sneak\(_;\|Next\)')
212+
if !g:sneak_opt.absolute_dir
213+
\ && ((!g:sneak_opt.s_next && maparg(k, 'n') =~# '<Plug>Sneak\(_;\|Next\)')
214214
\ || (maparg(k, 'n') =~# '<Plug>Sneak\(_s\|Forward\)'))
215215
let g:sneak#target_labels = k . g:sneak#target_labels
216216
else

autoload/sneak/search.vim

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ func! sneak#search#new() abort
66
let self._repeatmotion = a:repeatmotion
77
let self._reverse = a:reverse
88
" search pattern modifiers (case-sensitivity, magic)
9-
let self.prefix = sneak#search#get_cs(a:input, g:sneak#opt.use_ic_scs).'\V'
9+
let self.prefix = sneak#search#get_cs(a:input, g:sneak_opt.use_ic_scs).'\V'
1010
" the escaped user input to search for
1111
let self.search = substitute(escape(a:input, '"\'), '\a', '\\[[=\0=]]', 'g')
1212
" example: highlight string 'ab' after line 42, column 5

0 commit comments

Comments
 (0)