├── LICENSE
├── Makefile
├── README.md
├── autoload
├── sneak.vim
└── sneak
│ ├── label.vim
│ ├── search.vim
│ └── util.vim
├── doc
├── .gitignore
└── sneak.txt
├── lua
└── sneak.lua
├── plugin
└── sneak.vim
└── tests
├── .gitignore
└── test.vader
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Justin M. Keyes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VIM = vim -N -u NORC -i NONE --cmd 'set rtp+=tests/vim-vader rtp+=tests/vim-repeat rtp+=tests/vim-surround rtp+=$$PWD'
2 |
3 | test: tests/vim-vader tests/vim-repeat tests/vim-surround
4 | $(VIM) '+Vader! tests/*.vader'
5 |
6 | # https://github.com/junegunn/vader.vim/pull/75
7 | testnvim: tests/vim-vader tests/vim-repeat tests/vim-surround
8 | VADER_OUTPUT_FILE=/dev/stderr n$(VIM) --headless '+Vader! tests/*.vader'
9 |
10 | testinteractive: tests/vim-vader tests/vim-repeat tests/vim-surround
11 | $(VIM) '+Vader tests/*.vader'
12 |
13 | tests/vim-vader:
14 | git clone https://github.com/junegunn/vader.vim tests/vim-vader || ( cd tests/vim-vader && git pull --rebase; )
15 |
16 | tests/vim-repeat:
17 | git clone https://github.com/tpope/vim-repeat tests/vim-repeat || ( cd tests/vim-repeat && git pull --rebase; )
18 |
19 | tests/vim-surround:
20 | git clone https://github.com/tpope/vim-surround tests/vim-surround || ( cd tests/vim-surround && git pull --rebase; )
21 |
22 | .PHONY: test testnvim testinteractive
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | sneak.vim 👟
2 | ================
3 |
4 | Jump to any location specified by two characters.
5 |
6 | Sneak is a powerful, reliable, yet minimal _motion_ plugin for Vim. It works with **multiple
7 | lines**, **operators** (including repeat `.` and [surround]), motion-repeat
8 | (`;` and `,`), **[keymaps]**, **visual** mode, **[multibyte]** text, and
9 | **macros**.
10 |
11 | Try *label-mode* for a minimalist alternative to
12 | [EasyMotion](https://github.com/easymotion/vim-easymotion):
13 |
14 | ```vim
15 | let g:sneak#label = 1
16 | ```
17 |
18 | Usage
19 | -----
20 |
21 |
22 |
23 | Sneak is invoked with `s` followed by exactly two characters:
24 |
25 | s{char}{char}
26 |
27 | * Type `sab` to **move the cursor** immediately to the next instance of the text "ab".
28 | * Additional matches, if any, are highlighted until the cursor is moved.
29 | * Type `;` to go to the next match (or `s` again, if `s_next` is enabled; see [`:help sneak`](doc/sneak.txt)).
30 | * Type `3;` to skip to the third match from the current position.
31 | * Type `ctrl-o` or ``` `` ``` to go back to the starting point.
32 | * This is a built-in Vim motion; Sneak adds to Vim's [jumplist](http://vimdoc.sourceforge.net/htmldoc/motion.html#jumplist)
33 | *only* on `s` invocation—not repeats—so you can
34 | abandon a trail of `;` or `,` by a single `ctrl-o` or ``` `` ```.
35 | * Type `s` at any time to repeat the last Sneak-search.
36 | * Type `S` to search backwards.
37 |
38 | Sneak can be limited to a **vertical scope** by prefixing `s` with a [count].
39 |
40 | * Type `5sxy` to go immediately to the next instance of "xy" within 5 columns
41 | of the cursor.
42 |
43 | Sneak is invoked with [**operators**](http://vimdoc.sourceforge.net/htmldoc/motion.html#operator)
44 | via `z` (because `s` is taken by surround.vim).
45 |
46 | * Type `3dzqt` to delete up to the *third* instance of "qt".
47 | * Type `.` to repeat the `3dzqt` operation.
48 | * Type `2.` to repeat twice.
49 | * Type `d;` to delete up to the next match.
50 | * Type `4d;` to delete up to the *fourth* next match.
51 | * Type `yszxy]` to [surround] in brackets up to `xy`.
52 | * Type `.` to repeat the surround operation.
53 | * Type `gUz\}` to upper-case the text from the cursor until the next instance
54 | of the literal text `\}`
55 | * Type `.` to repeat the `gUz\}` operation.
56 |
57 | Install
58 | -------
59 |
60 | Requires Vim 7.3+ or [Nvim](https://neovim.io/). Label-mode requires Vim
61 | 7.4.792+. With Nvim 0.5+ label-mode is driven by [virtual text](https://neovim.io/doc/user/api.html#nvim_buf_set_extmark())
62 | instead of the legacy "conceal" feature.
63 |
64 | - [vim-plug](https://github.com/junegunn/vim-plug)
65 | - `Plug 'justinmk/vim-sneak'`
66 | - [Pathogen](https://github.com/tpope/vim-pathogen)
67 | - `git clone git://github.com/justinmk/vim-sneak.git ~/.vim/bundle/vim-sneak`
68 | - Manual installation:
69 | - Copy the files to your `.vim` directory.
70 |
71 | To repeat Sneak *operations* (like `dzab`) with dot `.`,
72 | [repeat.vim](https://github.com/tpope/vim-repeat) is required.
73 |
74 | FAQ
75 | ---
76 |
77 | ### Why not use `/`?
78 |
79 | For the same reason that Vim has [motions](http://vimdoc.sourceforge.net/htmldoc/motion.html#left-right-motions)
80 | like `f` and `t`: common operations should use the fewest keystrokes.
81 |
82 | * `/ab` requires 33% more keystrokes than `sab`
83 | * Sets *only* the initial position in the Vim jumplist—so you can explore a
84 | trail of matches via `;`, then return to the start with a single `ctrl-o` or ``` `` ```
85 | * Doesn't clutter your search history
86 | * Input is always literal (don't need to escape special characters)
87 | * Ignores accents ("equivalence class") when matching
88 | ([#183](https://github.com/justinmk/vim-sneak/issues/183))
89 | * Smarter, subtler highlighting
90 |
91 | ### Why not use `f`?
92 |
93 | * 50x more precise than `f` or `t`
94 | * Moves vertically
95 | * Highlights matches in the direction of your search
96 |
97 | ### How dare you remap `s`?
98 |
99 | You can specify any mapping for Sneak (see [`:help sneak`](doc/sneak.txt)).
100 | By the way: `cl` is equivalent to `s`, and `cc` is equivalent to `S`.
101 |
102 | ### How can I replace `f` with Sneak?
103 |
104 | ```vim
105 | map f Sneak_s
106 | map F Sneak_S
107 | ```
108 |
109 | ### How can I replace `f` and/or `t` with *one-character* Sneak?
110 |
111 | Sneak has `` mappings for `f` and `t` 1-character-sneak.
112 | These mappings do *not* invoke label-mode, even if you have it enabled.
113 |
114 | ```vim
115 | map f Sneak_f
116 | map F Sneak_F
117 | map t Sneak_t
118 | map T Sneak_T
119 | ```
120 |
121 | Related
122 | -------
123 |
124 | * [Seek](https://github.com/goldfeld/vim-seek)
125 | * [EasyMotion](https://github.com/Lokaltog/vim-easymotion)
126 | * [smalls](https://github.com/t9md/vim-smalls)
127 | * [improvedft](https://github.com/chrisbra/improvedft)
128 | * [clever-f](https://github.com/rhysd/clever-f.vim)
129 | * [vim-extended-ft](https://github.com/svermeulen/vim-extended-ft)
130 | * [Fanf,ingTastic;](https://github.com/dahu/vim-fanfingtastic)
131 | * [IdeaVim-Sneak](https://plugins.jetbrains.com/plugin/15348-ideavim-sneak)
132 | * [leap.nvim](https://github.com/ggandor/leap.nvim)
133 | * [flash.nvim](https://github.com/folke/flash.nvim)
134 |
135 | License
136 | -------
137 |
138 | Copyright © Justin M. Keyes. Distributed under the MIT license.
139 |
140 | [multibyte]: http://vimdoc.sourceforge.net/htmldoc/mbyte.html#UTF-8
141 | [keymaps]: http://vimdoc.sourceforge.net/htmldoc/mbyte.html#mbyte-keymap
142 | [surround]: https://github.com/tpope/vim-surround
143 | [count]: http://vimdoc.sourceforge.net/htmldoc/intro.html#[count]
144 |
--------------------------------------------------------------------------------
/autoload/sneak.vim:
--------------------------------------------------------------------------------
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('', 'n') =~# "'s'\\.'neak#cancel'" " Remove temporary mapping.
28 | silent! unmap
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 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 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 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 during change-operation should return to previous mode.
83 | call feedkeys((col('.') > 1 && col('.') < col('$') ? "\" : '') . "\\", '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_scope_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 | if a:inputlen > 1
193 | let w:sneak_cur_hl = matchadd('SneakCurrent', '\%#.\{'.a:inputlen.'}')
194 | endif
195 |
196 | " Clear with . Use a funny mapping to avoid false positives. #287
197 | if (has('nvim') || has('gui_running')) && maparg('', 'n') ==# ""
198 | nnoremap call('s'.'neak#cancel',[]) . "\"
199 | endif
200 |
201 | " Operators always invoke label-mode.
202 | " If a:label is a string set it as the target, without prompting.
203 | let label = a:label !~# '[012]' ? a:label : ''
204 | let target = (2 == a:label || !empty(label) || (a:label && g:sneak_opt.label && (is_op || s.hasmatches(1)))) && !max(bounds)
205 | \ ? sneak#label#to(s, is_v, label) : ""
206 |
207 | if nudge
208 | call sneak#util#nudge(a:reverse) "undo nudge for t
209 | endif
210 |
211 | if is_op && 2 != a:inclusive && !a:reverse
212 | " f/t operations do not apply to the current character; nudge the cursor.
213 | call sneak#util#nudge(1)
214 | endif
215 |
216 | if is_op || '' != target
217 | call sneak#util#removehl()
218 | endif
219 |
220 | if is_op && a:op !=# 'y'
221 | let change = a:op !=? "c" ? "" : "\.\"
222 | let args = sneak#util#strlen(a:input) . a:reverse . a:inclusive . (2*!empty(target))
223 | if a:op !=# 'g@'
224 | let args .= a:input . target . change
225 | endif
226 | let seq = a:op . "\SneakRepeat" . args
227 | silent! call repeat#setreg(seq, a:register)
228 | silent! call repeat#set(seq, a:count)
229 |
230 | let s:st.label = target
231 | if empty(s:st.opfunc_st)
232 | let s:st.opfunc_st = filter(deepcopy(s:st), 'v:key !=# "opfunc_st"')
233 | endif
234 | endif
235 | endf "}}}
236 |
237 | func! s:attach_autocmds() abort
238 | augroup sneak
239 | autocmd!
240 | autocmd InsertEnter,WinLeave,BufLeave * call sneak#cancel()
241 | "_nested_ autocmd to skip the _first_ CursorMoved event.
242 | "NOTE: CursorMoved is _not_ triggered if there is typeahead during a macro/script...
243 | autocmd CursorMoved * autocmd sneak CursorMoved * call sneak#cancel()
244 | augroup END
245 | endf
246 |
247 | func! sneak#reset(key) abort
248 | let c = sneak#util#getchar()
249 |
250 | let s:st.rst = 1
251 | let s:st.reverse = 0
252 | for k in ['f', 't'] "unmap the temp mappings
253 | if g:sneak_opt[k.'_reset']
254 | silent! exec 'unmap '.k
255 | silent! exec 'unmap '.toupper(k)
256 | endif
257 | endfor
258 |
259 | "count is prepended implicitly by the mapping
260 | return a:key.c
261 | endf
262 |
263 | func! s:map_reset_key(key, mode) abort
264 | exec printf("%snoremap %s sneak#reset('%s')", a:mode, a:key, a:key)
265 | endf
266 |
267 | " Sets temporary mappings to 'hook' into f/F/t/T.
268 | func! s:ft_hook() abort
269 | for k in ['f', 't']
270 | for m in ['n', 'x']
271 | "if user mapped anything to f or t, do not map over it; unfortunately this
272 | "also means we cannot reset ; or , when f or t is invoked.
273 | if g:sneak_opt[k.'_reset'] && maparg(k, m) ==# ''
274 | call s:map_reset_key(k, m) | call s:map_reset_key(toupper(k), m)
275 | endif
276 | endfor
277 | endfor
278 | endf
279 |
280 | func! s:getnchars(n, mode) abort
281 | let s = ''
282 | echo g:sneak_opt.prompt | redraw
283 | for i in range(1, a:n)
284 | if sneak#util#isvisualop(a:mode) | exe 'norm! gv' | endif "preserve selection
285 | let c = sneak#util#getchar()
286 | if -1 != index(["\", "\", "\", "\", "\"], c)
287 | return ""
288 | endif
289 | if c == "\"
290 | if i > 1 "special case: accept the current input (#15)
291 | break
292 | else "special case: repeat the last search (useful for label-mode).
293 | return s:st.input
294 | endif
295 | else
296 | let s .= c
297 | if 1 == &iminsert && sneak#util#strlen(s) >= a:n
298 | "HACK: this can happen if the user entered multiple characters while we
299 | "were waiting to resolve a multi-char keymap.
300 | "example for keymap 'bulgarian-phonetic':
301 | " e:: => ё | resolved, strwidth=1
302 | " eo => eo | unresolved, strwidth=2
303 | break
304 | endif
305 | endif
306 | redraw | echo g:sneak_opt.prompt . s
307 | endfor
308 | return s
309 | endf
310 |
311 |
--------------------------------------------------------------------------------
/autoload/sneak/label.vim:
--------------------------------------------------------------------------------
1 | " NOTES:
2 | " problem: cchar cannot be more than 1 character.
3 | " strategy: make fg/bg the same color, then conceal the other char.
4 |
5 | let g:sneak#target_labels = get(g:, 'sneak#target_labels', ";sftunq/SFGHLTUNRMQZ?0")
6 |
7 | let s:matchmap = {}
8 | let s:orig_conceal_matches = []
9 |
10 | let s:use_virt_text = has('nvim-0.5')
11 | if s:use_virt_text
12 | call luaeval('require("sneak").init()')
13 | else
14 | let s:match_ids = []
15 | endif
16 |
17 | if exists('*strcharpart')
18 | func! s:strchar(s, i) abort
19 | return strcharpart(a:s, a:i, 1)
20 | endf
21 | else
22 | func! s:strchar(s, i) abort
23 | return matchstr(a:s, '.\{'.a:i.'\}\zs.')
24 | endf
25 | endif
26 |
27 | func! s:placematch(c, pos) abort
28 | let s:matchmap[a:c] = a:pos
29 | if s:use_virt_text
30 | call luaeval('require("sneak").placematch(_A[1], _A[2], _A[3])', [a:c, a:pos[0] - 1, a:pos[1] - 1])
31 | else
32 | let pat = '\%'.a:pos[0].'l\%'.a:pos[1].'c.'
33 | let id = matchadd('Conceal', pat, 999, -1, { 'conceal': a:c })
34 | call add(s:match_ids, id)
35 | endif
36 | endf
37 |
38 | func! s:save_conceal_matches() abort
39 | for m in getmatches()
40 | if m.group ==# 'Conceal'
41 | call add(s:orig_conceal_matches, m)
42 | silent! call matchdelete(m.id)
43 | endif
44 | endfor
45 | endf
46 |
47 | func! s:restore_conceal_matches() abort
48 | for m in s:orig_conceal_matches
49 | let d = {}
50 | if has_key(m, 'conceal') | let d.conceal = m.conceal | endif
51 | if has_key(m, 'window') | let d.window = m.window | endif
52 | silent! call matchadd(m.group, m.pattern, m.priority, m.id, d)
53 | endfor
54 | let s:orig_conceal_matches = []
55 | endf
56 |
57 | func! sneak#label#to(s, v, label) abort
58 | let seq = ""
59 | while 1
60 | let choice = s:do_label(a:s, a:v, a:s._reverse, a:label)
61 | let seq .= choice
62 | if choice =~# "^\\\|\$"
63 | call a:s.init(a:s._input, a:s._repeatmotion, 1)
64 | elseif choice ==# "\"
65 | call a:s.init(a:s._input, a:s._repeatmotion, 0)
66 | else
67 | return seq
68 | endif
69 | endwhile
70 | endf
71 |
72 | func! s:do_label(s, v, reverse, label) abort "{{{
73 | let w = winsaveview()
74 | call s:before()
75 | let search_pattern = (a:s.prefix).(a:s.search).(a:s.get_onscreen_searchpattern(w))
76 |
77 | let i = 0
78 | let overflow = [0, 0] " Position of the next match (if any) after we have run out of target labels.
79 | while 1
80 | " searchpos() is faster than 'norm! /'
81 | let p = searchpos(search_pattern, a:s.search_options_no_s, a:s.get_stopline())
82 | let skippedfold = sneak#util#skipfold(p[0], a:reverse) " Note: 'set foldopen-=search' does not affect search().
83 |
84 | if 0 == p[0] || -1 == skippedfold
85 | break
86 | elseif 1 == skippedfold
87 | continue
88 | endif
89 |
90 | if i < s:maxmarks
91 | let c = s:strchar(g:sneak#target_labels, i)
92 | call s:placematch(c, p)
93 | else " We have exhausted the target labels; grab the first non-labeled match.
94 | let overflow = p
95 | break
96 | endif
97 |
98 | let i += 1
99 | endwhile
100 |
101 | call winrestview(w) | redraw
102 | let choice = empty(a:label) ? sneak#util#getchar() : a:label
103 | call s:after()
104 |
105 | let mappedto = maparg(choice, a:v ? 'x' : 'n')
106 | let mappedtoNext = (g:sneak_opt.absolute_dir && a:reverse)
107 | \ ? mappedto =~# 'Sneak\(_,\|Previous\)'
108 | \ : mappedto =~# 'Sneak\(_;\|Next\)'
109 |
110 | if choice =~# "\\v^\|\|\$" " Decorate next N matches.
111 | if (!a:reverse && choice ==# "\") || (a:reverse && choice =~# "^\\\|\$")
112 | call cursor(overflow[0], overflow[1])
113 | endif " ...else we just switched directions, do not overflow.
114 | elseif (strlen(g:sneak_opt.label_esc) && choice ==# g:sneak_opt.label_esc)
115 | \ || -1 != index(["\", "\"], choice)
116 | return "\" " Exit label-mode.
117 | elseif !mappedtoNext && !has_key(s:matchmap, choice) " Fallthrough: press _any_ invalid key to escape.
118 | call sneak#util#removehl()
119 | call feedkeys(choice) " Exit label-mode, fall through to Vim.
120 | return ""
121 | else " Valid target was selected.
122 | let p = mappedtoNext ? s:matchmap[s:strchar(g:sneak#target_labels, 0)] : s:matchmap[choice]
123 | call cursor(p[0], p[1])
124 | endif
125 |
126 | return choice
127 | endf "}}}
128 |
129 | func! s:after() abort
130 | autocmd! sneak_label_cleanup
131 | try | call matchdelete(s:sneak_cursor_hl) | catch | endtry
132 | if s:use_virt_text
133 | call luaeval('require("sneak").after()')
134 | else
135 | call map(s:match_ids, 'matchdelete(v:val)')
136 | let s:match_ids = []
137 | " Remove temporary highlight links.
138 | exec 'hi! link Conceal '.s:orig_hl_conceal
139 | call s:restore_conceal_matches()
140 | let [&l:concealcursor,&l:conceallevel]=[s:o_cocu,s:o_cole]
141 | endif
142 | exec 'hi! link Sneak '.s:orig_hl_sneak
143 | endf
144 |
145 | func! s:disable_conceal_in_other_windows() abort
146 | for w in range(1, winnr('$'))
147 | if 'help' !=# getwinvar(w, '&buftype') && w != winnr()
148 | \ && empty(getbufvar(winbufnr(w), 'dirvish'))
149 | call setwinvar(w, 'sneak_orig_cl', getwinvar(w, '&conceallevel'))
150 | call setwinvar(w, '&conceallevel', 0)
151 | endif
152 | endfor
153 | endf
154 | func! s:restore_conceal_in_other_windows() abort
155 | for w in range(1, winnr('$'))
156 | if 'help' !=# getwinvar(w, '&buftype') && w != winnr()
157 | \ && empty(getbufvar(winbufnr(w), 'dirvish'))
158 | call setwinvar(w, '&conceallevel', getwinvar(w, 'sneak_orig_cl'))
159 | endif
160 | endfor
161 | endf
162 |
163 | func! s:before() abort
164 | let s:matchmap = {}
165 |
166 | " Highlight the cursor location (because cursor is hidden during getchar()).
167 | let s:sneak_cursor_hl = matchadd("SneakScope", '\%#', 11, -1)
168 |
169 | if s:use_virt_text
170 | call luaeval('require("sneak").before()')
171 | else
172 | for o in ['cocu', 'cole']
173 | exe 'let s:o_'.o.'=&l:'.o
174 | endfor
175 | setlocal concealcursor=ncv conceallevel=2
176 |
177 | let s:orig_hl_conceal = sneak#util#links_to('Conceal')
178 | call s:save_conceal_matches()
179 | " Set temporary link to our custom 'conceal' highlight.
180 | hi! link Conceal SneakLabel
181 | endif
182 |
183 | let s:orig_hl_sneak = sneak#util#links_to('Sneak')
184 | " Set temporary link to hide the sneak search targets.
185 | hi! link Sneak SneakLabelMask
186 |
187 | augroup sneak_label_cleanup
188 | autocmd!
189 | autocmd CursorMoved * call after()
190 | augroup END
191 | endf
192 |
193 | " Returns 1 if a:key is invisible or special.
194 | func! s:is_special_key(key) abort
195 | return -1 != index(["\", "\", "\", "\", "\"], a:key)
196 | \ || maparg(a:key, 'n') =~# 'Sneak\(_;\|_,\|Next\|Previous\)'
197 | \ || (g:sneak_opt.s_next && maparg(a:key, 'n') =~# 'Sneak\(_s\|Forward\)')
198 | endf
199 |
200 | " We must do this because:
201 | " - Don't know which keys the user assigned to Sneak_;/Sneak_,
202 | " - Must reserve special keys like and
203 | func! sneak#label#sanitize_target_labels() abort
204 | let nrbytes = len(g:sneak#target_labels)
205 | let i = 0
206 | while i < nrbytes
207 | " Intentionally using byte-index for use with substitute().
208 | let k = strpart(g:sneak#target_labels, i, 1)
209 | if s:is_special_key(k) " Remove the char.
210 | let g:sneak#target_labels = substitute(g:sneak#target_labels, '\%'.(i+1).'c.', '', '')
211 | " 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') =~# 'Sneak\(_;\|Next\)')
214 | \ || (maparg(k, 'n') =~# 'Sneak\(_s\|Forward\)'))
215 | let g:sneak#target_labels = k . g:sneak#target_labels
216 | else
217 | let nrbytes -= 1
218 | continue
219 | endif
220 | endif
221 | let i += 1
222 | endwhile
223 | endf
224 |
225 | call sneak#label#sanitize_target_labels()
226 | let s:maxmarks = sneak#util#strlen(g:sneak#target_labels)
227 |
--------------------------------------------------------------------------------
/autoload/sneak/search.vim:
--------------------------------------------------------------------------------
1 | func! sneak#search#new() abort
2 | let s = {}
3 |
4 | func! s.init(input, repeatmotion, reverse) abort
5 | let self._input = a:input
6 | let self._repeatmotion = a:repeatmotion
7 | let self._reverse = a:reverse
8 | " search pattern modifiers (case-sensitivity, magic)
9 | let self.prefix = sneak#search#get_cs(a:input, g:sneak_opt.use_ic_scs).'\V'
10 | " the escaped user input to search for
11 | let self.search = substitute(escape(a:input, '"\'), '\a', '\\[[=\0=]]', 'g')
12 | " example: highlight string 'ab' after line 42, column 5
13 | " matchadd('foo', 'ab\%>42l\%5c', 1)
14 | let self.match_pattern = ''
15 | " do not wrap search backwards
16 | let self._search_options = 'W' . (a:reverse ? 'b' : '')
17 | let self.search_options_no_s = self._search_options
18 | " save the jump on the initial invocation, _not_ repeats or consecutive invocations.
19 | if !a:repeatmotion && !sneak#is_sneaking() | let self._search_options .= 's' | endif
20 | endf
21 |
22 | func! s.initpattern() abort
23 | let self._searchpattern = (self.prefix).(self.match_pattern).'\zs'.(self.search)
24 | endf
25 |
26 | func! s.dosearch(...) abort " a:1 : extra search options
27 | return searchpos(self._searchpattern
28 | \, self._search_options.(a:0 ? a:1 : '')
29 | \, 0
30 | \)
31 | endf
32 |
33 | func! s.get_onscreen_searchpattern(w) abort
34 | if &wrap
35 | return ''
36 | endif
37 | let wincol_lhs = a:w.leftcol "this is actually just to the _left_ of the first onscreen column.
38 | let wincol_rhs = 2 + (winwidth(0) - sneak#util#wincol1()) + wincol_lhs
39 | "restrict search to window
40 | return '\%>'.(wincol_lhs).'v'.'\%<'.(wincol_rhs+1).'v'
41 | endf
42 |
43 | func! s.get_stopline() abort
44 | return self._reverse ? line("w0") : line("w$")
45 | endf
46 |
47 | " returns 1 if there are n _on-screen_ matches in the search direction.
48 | func! s.hasmatches(n) abort
49 | let w = winsaveview()
50 | let searchpattern = (self._searchpattern).(self.get_onscreen_searchpattern(w))
51 | let visiblematches = 0
52 |
53 | while 1
54 | let matchpos = searchpos(searchpattern, self.search_options_no_s, self.get_stopline())
55 | if 0 == matchpos[0] "no more matches
56 | break
57 | elseif 0 != sneak#util#skipfold(matchpos[0], self._reverse)
58 | continue
59 | endif
60 | let visiblematches += 1
61 | if visiblematches == a:n
62 | break
63 | endif
64 | endwhile
65 |
66 | call winrestview(w)
67 | return visiblematches >= a:n
68 | endf
69 |
70 | return s
71 | endf
72 |
73 | " gets the case sensitivity modifier for the search
74 | func! sneak#search#get_cs(input, use_ic_scs) abort
75 | if !a:use_ic_scs || !&ignorecase || (&smartcase && sneak#util#has_upper(a:input))
76 | return '\C'
77 | endif
78 | return '\c'
79 | endf
80 |
81 | "search object singleton
82 | let g:sneak#search#instance = sneak#search#new()
83 |
--------------------------------------------------------------------------------
/autoload/sneak/util.vim:
--------------------------------------------------------------------------------
1 | if v:version >= 703
2 | func! sneak#util#strlen(s) abort
3 | return strwidth(a:s)
4 | "return call('strdisplaywidth', a:000)
5 | endf
6 | else
7 | func! sneak#util#strlen(s) abort
8 | return strlen(substitute(a:s, ".", "x", "g"))
9 | endf
10 | endif
11 |
12 | func! sneak#util#isvisualop(op) abort
13 | return a:op =~# "^[vV\]"
14 | endf
15 |
16 | func! sneak#util#getc() abort
17 | sleep 1m
18 | let c = (has('patch-9.1.1070') || has('nvim-0.11')) ? getchar(-1,{'cursor':'keep'}) : getchar()
19 | return type(c) == type(0) ? nr2char(c) : c
20 | endf
21 |
22 | func! sneak#util#getchar() abort
23 | let input = sneak#util#getc()
24 | if 1 != &iminsert
25 | return input
26 | endif
27 | "a language keymap is activated, so input must be resolved to the mapped values.
28 | let partial_keymap_seq = mapcheck(input, "l")
29 | while partial_keymap_seq !=# ""
30 | let full_keymap = maparg(input, "l")
31 | if full_keymap ==# "" && len(input) >= 3 "HACK: assume there are no keymaps longer than 3.
32 | return input
33 | elseif full_keymap ==# partial_keymap_seq
34 | return full_keymap
35 | endif
36 | let c = sneak#util#getc()
37 | if c == "\" || c == "\"
38 | "if the short sequence has a valid mapping, return that.
39 | if !empty(full_keymap)
40 | return full_keymap
41 | endif
42 | return input
43 | endif
44 | let input .= c
45 | let partial_keymap_seq = mapcheck(input, "l")
46 | endwhile
47 | return input
48 | endf
49 |
50 | "returns 1 if the string contains an uppercase char. [unicode-compatible]
51 | func! sneak#util#has_upper(s) abort
52 | return -1 != match(a:s, '\C[[:upper:]]')
53 | endf
54 |
55 | "displays a message that will dissipate at the next opportunity.
56 | func! sneak#util#echo(msg) abort
57 | redraw | echo a:msg
58 | augroup sneak_echo
59 | autocmd!
60 | autocmd CursorMoved,InsertEnter,WinLeave,BufLeave * redraw | echo '' | autocmd! sneak_echo
61 | augroup END
62 | endf
63 |
64 | "returns the least possible 'wincol'
65 | " - if 'sign' column is displayed, the least 'wincol' is 3
66 | " - there is (apparently) no clean way to detect if 'sign' column is visible
67 | func! sneak#util#wincol1() abort
68 | let w = winsaveview()
69 | norm! 0
70 | let c = wincol()
71 | call winrestview(w)
72 | return c
73 | endf
74 |
75 | "Moves the cursor to the outmost position in the current folded area.
76 | "Returns:
77 | " 1 if the cursor was moved
78 | " 0 if the cursor is not in a fold
79 | " -1 if the start/end of the fold is at/above/below the edge of the window
80 | func! sneak#util#skipfold(current_line, reverse) abort
81 | let foldedge = a:reverse ? foldclosed(a:current_line) : foldclosedend(a:current_line)
82 | if -1 != foldedge
83 | if (a:reverse && foldedge <= line("w0")) "fold starts at/above top of window.
84 | \ || foldedge >= line("w$") "fold ends at/below bottom of window.
85 | return -1
86 | endif
87 | call cursor(foldedge, 0)
88 | call cursor(0, a:reverse ? 1 : col('$'))
89 | return 1
90 | endif
91 | return 0
92 | endf
93 |
94 | " Moves the cursor 1 char to the left or right; wraps at EOL, but _not_ EOF.
95 | func! sneak#util#nudge(right) abort
96 | let nextchar = searchpos('\_.', 'nW'.(a:right ? '' : 'b'))
97 | if [0, 0] == nextchar
98 | return 0
99 | endif
100 | call cursor(nextchar)
101 | return 1
102 | endf
103 |
104 | " Removes highlighting.
105 | func! sneak#util#removehl() abort
106 | silent! call matchdelete(w:sneak_hl_id)
107 | silent! call matchdelete(w:sneak_cur_hl)
108 | silent! call matchdelete(w:sneak_scope_hl)
109 | endf
110 |
111 | " Gets the 'links to' value of the specified highlight group, if any.
112 | func! sneak#util#links_to(hlgroup) abort
113 | redir => hl | exec 'silent highlight '.a:hlgroup | redir END
114 | let s = substitute(matchstr(hl, 'links to \zs.*'), '\s', '', 'g')
115 | return empty(s) ? 'NONE' : s
116 | endf
117 |
118 | func! s:default_color(hlgroup, what, mode) abort
119 | let c = synIDattr(synIDtrans(hlID(a:hlgroup)), a:what, a:mode)
120 | return !empty(c) && c != -1 ? c : (a:what ==# 'bg' ? 'magenta' : 'white')
121 | endfunc
122 |
123 | func! s:init_hl() abort
124 | exec "highlight default Sneak guifg=white guibg=magenta ctermfg=white ctermbg=".(&t_Co < 256 ? "magenta" : "201")
125 | exec "highlight default SneakCurrent guifg=black guibg=LightMagenta ctermfg=0 ctermbg=LightMagenta"
126 |
127 | if &background ==# 'dark'
128 | highlight default SneakScope guifg=black guibg=white ctermfg=0 ctermbg=255
129 | else
130 | highlight default SneakScope guifg=white guibg=black ctermfg=255 ctermbg=0
131 | endif
132 |
133 | let guibg = s:default_color('Sneak', 'bg', 'gui')
134 | let guifg = s:default_color('Sneak', 'fg', 'gui')
135 | let ctermbg = s:default_color('Sneak', 'bg', 'cterm')
136 | let ctermfg = s:default_color('Sneak', 'fg', 'cterm')
137 | exec 'highlight default SneakLabel gui=bold cterm=bold guifg='.guifg.' guibg='.guibg.' ctermfg='.ctermfg.' ctermbg='.ctermbg
138 |
139 | let guibg = s:default_color('SneakLabel', 'bg', 'gui')
140 | let ctermbg = s:default_color('SneakLabel', 'bg', 'cterm')
141 | " fg same as bg
142 | exec 'highlight default SneakLabelMask guifg='.guibg.' guibg='.guibg.' ctermfg='.ctermbg.' ctermbg='.ctermbg
143 | endf
144 |
145 | augroup sneak_colorscheme " Re-init on :colorscheme change at runtime. #108
146 | autocmd!
147 | autocmd ColorScheme * call init_hl()
148 | augroup END
149 |
150 | call s:init_hl()
151 |
--------------------------------------------------------------------------------
/doc/.gitignore:
--------------------------------------------------------------------------------
1 | /tags
2 |
--------------------------------------------------------------------------------
/doc/sneak.txt:
--------------------------------------------------------------------------------
1 | *sneak.txt* motion improved
2 |
3 | Sneak - the missing motion for Vim
4 |
5 | ==============================================================================
6 | OVERVIEW *sneak*
7 |
8 | Sneak provides a way to move quickly and precisely to locations that would be
9 | awkward to reach with built-in Vim motions.
10 |
11 | To use Sneak, type "s" followed by exactly two characters:
12 |
13 | s{char}{char}
14 |
15 | Thus you can often reach a target with 3 keystrokes. Sneak always moves
16 | immediately to the first {char}{char} match. Additional matches are
17 | highlighted, you can reach them by pressing ; (just like |f| and |t|).
18 |
19 | Above all, the goal is to get out of your way. See |sneak-usage| for
20 | a quick-start, and |sneak-features| for full description.
21 |
22 | Sneak works with Vim 7.2+ (|sneak-label-mode| requires Vim 7.3+).
23 |
24 | ==============================================================================
25 | USAGE *sneak-usage*
26 |
27 | Example (cursor position indicated with brackets []): >
28 | [L]orem ipsum dolor sit amet, consectetur adipisicing elit
29 | <
30 | Type ssi to go to the beginning of the word "sit": >
31 | Lorem ipsum dolor [s]it amet, consectetur adipisicing elit
32 | <
33 | Type ; (or s again, if |sneak-clever-s| is enabled) to go to the next match: >
34 | Lorem ipsum dolor sit amet, consectetur adipi[s]icing elit
35 | <
36 | Type Sdo to go backwards to the beginning of the word "dolor": >
37 | Lorem ipsum [d]olor sit amet, consectetur adipisicing elit
38 | <
39 | Type dzad to delete from the cursor to the first instance of "ad": >
40 | Lorem ipsum [a]dipisicing elit
41 | <
42 | ------------------------------------------------------------------------------
43 | DEFAULT MAPPINGS *sneak-mappings*
44 |
45 | Full list of default mappings (see |sneak-custom-mappings| to change them):
46 |
47 | NORMAL-MODE~
48 | Key Sequence | Description
49 | -------------------------|----------------------------------------------
50 | s{char}{char} | Go to the next occurrence of {char}{char}
51 | S{char}{char} | Go to the previous occurrence of {char}{char}
52 | s{char} | Go to the next occurrence of {char}
53 | S{char} | Go to the previous occurrence of {char}
54 | s | Repeat the last Sneak.
55 | S | Repeat the last Sneak, in reverse direction.
56 | ; | Go to the [count]th next match
57 | , or \ | Go to the [count]th previous match
58 | s | Go to the [count]th next match (see NOTE)
59 | S | Go to the [count]th previous match (see NOTE)
60 | [count]s{char}{char} | Invoke |sneak-vertical-scope|
61 | [count]S{char}{char} | Invoke backwards |sneak-vertical-scope|
62 | {operator}z{char}{char} | Perform {operator} from the cursor to the next
63 | | occurrence of {char}{char}
64 | {operator}Z{char}{char} | Perform {operator} from the cursor to the
65 | | previous occurrence of {char}{char}
66 |
67 | NOTE: s and S go to the next/previous match immediately after invoking
68 | Sneak, if |sneak-clever-s| is enabled.
69 |
70 | VISUAL-MODE~
71 | Key Sequence | Description
72 | -------------------------|----------------------------------------------
73 | s{char}{char} | Go to the next occurrence of {char}{char}
74 | Z{char}{char} | Go to the previous occurrence of {char}{char}
75 | s{char} | Go to the next occurrence of {char}
76 | Z{char} | Go to the previous occurrence of {char}
77 | s | Repeat the last Sneak.
78 | Z | Repeat the last Sneak, in reverse direction.
79 | ; | Go to the [count]th next match
80 | , or \ | Go to the [count]th previous match
81 | s | Go to the [count]th next match (NOTE above)
82 | S | Go to the [count]th previous match (NOTE above)
83 |
84 | NOTE: Z goes backwards in visual-mode because S is used by |surround|.
85 |
86 | LABEL-MODE~
87 | Key Sequence | Description
88 | -------------------------|----------------------------------------------
89 | or | Exit |sneak-label-mode| where the cursor is.
90 | | Label the next set of matches.
91 | or | Label the previous set of matches.
92 |
93 | ------------------------------------------------------------------------------
94 | CUSTOM MAPPINGS *sneak-custom-mappings*
95 |
96 | Sneak defines keys so you can choose alternative mappings. But keep in
97 | mind that motions should be the least-friction mappings, because motion is the
98 | most common editor task.
99 | *sneak-disable-mappings*
100 | To "disable" Sneak default mappings, simply define any other mapping to the
101 | relevant key. For example to prevent Sneak from remapping "s" and "S",
102 | just map to Sneak_s and Sneak_S as shown below.
103 |
104 | Available keys: >
105 |
106 | " 2-character Sneak (default)
107 | nmap ? Sneak_s
108 | nmap ? Sneak_S
109 | " visual-mode
110 | xmap ? Sneak_s
111 | xmap ? Sneak_S
112 | " operator-pending-mode
113 | omap ? Sneak_s
114 | omap ? Sneak_S
115 |
116 | " repeat motion
117 | map ? Sneak_;
118 | map ? Sneak_,
119 |
120 | " 1-character enhanced 'f'
121 | nmap ? Sneak_f
122 | nmap ? Sneak_F
123 | " visual-mode
124 | xmap ? Sneak_f
125 | xmap ? Sneak_F
126 | " operator-pending-mode
127 | omap ? Sneak_f
128 | omap ? Sneak_F
129 |
130 | " 1-character enhanced 't'
131 | nmap ? Sneak_t
132 | nmap ? Sneak_T
133 | " visual-mode
134 | xmap ? Sneak_t
135 | xmap ? Sneak_T
136 | " operator-pending-mode
137 | omap ? Sneak_t
138 | omap ? Sneak_T
139 |
140 | " label-mode
141 | nmap ? SneakLabel_s
142 | nmap ? SneakLabel_S
143 |
144 | ==============================================================================
145 | FEATURES *sneak-features*
146 |
147 | ------------------------------------------------------------------------------
148 | NORMAL-MODE
149 |
150 | `s` (and `S`) waits for two characters, then immediately moves to the next
151 | (previous) match. Additional matches are highlighted until the cursor is
152 | moved. Works across multiple lines.
153 |
154 | If a |language-mapping| ('keymap') is active Sneak waits for keymapped
155 | sequences as needed and searches for the keymapped result as expected. You
156 | probably want to set |g:sneak#target_labels| to labels that make sense for
157 | your preferred 'keymap'.
158 |
159 | [count]s enters |sneak-vertical-scope| mode.
160 |
161 | `;` and `,` repeat the last `s` and `S`. They also work correctly with `f` and `t`
162 | (unless you or another plugin have mapped `f` or `t` to a custom mapping).
163 | [count]; and [count], skip to the [count]th match, similar to the
164 | behavior of [count]f and [count]t.
165 |
166 | Note: If your mapleader is |,| then Sneak maps |\| instead of |,|. You can
167 | override this by specifying some other mapping, e.g.: >
168 |
169 | map gS Sneak_,
170 | <
171 | *sneak-clever-s*
172 | Similar to clever-f[3], immediately after invoking Sneak you can move to the
173 | next match by hitting `s` (or `S`) again. If you instead move the cursor or do
174 | something else, then `s` starts a new Sneak. In that case, you can repeat the
175 | most recent Sneak search by pressing "s" (or whatever key, if any, you
176 | mapped to Sneak_;). To enable "clever s": >
177 | let g:sneak#s_next = 1
178 | <
179 | Sneak adds to the |jumplist| only on the initial invocation; so after moving
180 | around with ; and , (or s/S) you can easily go back via |CTRL-O| or |``|.
181 | - Repeat invocations (;/,) do not add to the jumplist.
182 | - Consecutive invocations ("s" immediately after s/S/;/,)
183 | do not add to the jumplist.
184 |
185 | s ("s" followed by Enter) always repeats the last search, even if |;|
186 | and |,| were reset by |f| or |t|. This is especially useful for re-invoking
187 | |sneak-label-mode| (because |;| and |,| never invoke label-mode).
188 |
189 | ------------------------------------------------------------------------------
190 | OPERATIONS
191 |
192 | Use `z` for operations (or map to something else: |sneak-custom-mappings|).
193 | For example, `dzab` deletes from the cursor to the next instance of "ab".
194 | `dZab` deletes backwards to the previous instance. `czab` `cZab` `yzab` and
195 | `yZab` also work as expected.
196 |
197 | Repeat the operation with dot |.| (requires repeat.vim
198 | https://github.com/tpope/vim-repeat).
199 |
200 | Note: |.| repeat works correctly with vim-surround (requires Vim 7.4.786), but
201 | if you undo followed by dot-repeat again, it waits for input. This bug is not
202 | related to Sneak, it happens with builtin motions also.
203 |
204 | ------------------------------------------------------------------------------
205 | VERTICAL SCOPE *sneak-vertical-scope*
206 |
207 | Sneak has a unique feature called "vertical scope" search. This is invoked by
208 | prefixing `s` or `S` with a [count]. Then the search is limited to a column of
209 | width of 2×[count]. Vertical-scope never invokes |sneak-label-mode|.
210 |
211 | Use-cases:
212 | - CSV file
213 | - man page (example: search for "-e" in `man bash`)
214 | - hex editing :%!xxd
215 | - any columnar layout
216 |
217 | ------------------------------------------------------------------------------
218 | LABEL-MODE *sneak-label-mode* *sneak-streak-mode*
219 |
220 | *g:sneak#label*
221 | Label-mode minimizes the steps to jump to a location, using a clever interface
222 | similar to EasyMotion. If enabled, Sneak overlays text with "labels" which can
223 | be jumped-to by typing the label character. To enable label-mode: >
224 |
225 | let g:sneak#label = 1
226 |
227 | Label-mode features:
228 | - Jump to a label by typing its character. *sneak-fallthrough*
229 | - Non-label keys "fall through" to normal-mode, so you don't need to
230 | explicitly exit label-mode if the first match is correct.
231 | - skips to the next set of matches (, go the other way).
232 | These actions are preserved with dot-repeat.
233 | - or exits label-mode. |g:sneak#label_esc|
234 | - |folds| are skipped (not labeled). Use |;| and |,| to reach folded or
235 | off-screen matches.
236 |
237 | Standard features behave as usual:
238 | - If `s` is prefixed with [count] then |sneak-vertical-scope| is invoked, not
239 | label-mode.
240 | - Skip to the next or previous match with |;| or |,|
241 | - Return to your original location via |CTRL-O| or |``|
242 | - Use any |operator|, including |surround|.
243 | - Repeat operations with |.| (dot).
244 |
245 | Sneak compared to EasyMotion:
246 | - is faster and more predictable
247 | - never modifies your data (EasyMotion uses edit-and-undo hacks)
248 | - never adds extra "stages" or "groups", so it is predictable (common case is
249 | the highest priority)
250 | - always visually separates target labels
251 |
252 | ==============================================================================
253 | CONFIGURATION *sneak-config*
254 |
255 | Sneak is designed with sane, effective defaults, to avoid configuration as
256 | much as possible. But you can change them as follows.
257 |
258 | ------------------------------------------------------------------------------
259 | OPTIONS *sneak-options*
260 |
261 | To change an option, add a |let| statement in your |vimrc|. Example: >
262 | let g:sneak#use_ic_scs = 0
263 | <
264 | g:sneak#f_reset = 1
265 | g:sneak#t_reset = 1
266 |
267 | Note: Defaults to 0 if you mapped f (or t) to any |sneak-mappings|.
268 |
269 | 0: Pressing |f| (or |t|) will NOT clear the last Sneak search. So you could
270 | define different maps for Sneak_; or Sneak_, and still
271 | use ; and , for |f| and |t| as usual.
272 |
273 | 1: Pressing |f| (or |t|) causes Sneak to "reset" so that |;| and |,| will apply
274 | to the last f (or t) motion instead of the last s or S. This makes it
275 | possible to use ; and , for all three motions (f, t, and s).
276 |
277 | Note: if f (or t) was already mapped by you or a plugin, Sneak will
278 | not override the existing mapping unless you explicitly set f_reset
279 | (or t_reset). This means Sneak will not be able to reset ; or , when
280 | you invoke f or t. See the README.md FAQ regarding "f-enhancement"
281 | plugins.
282 |
283 | g:sneak#s_next = 0
284 |
285 | 0: Disable |sneak-clever-s|.
286 |
287 | 1: Enable |sneak-clever-s|. It works like this: immediately after
288 | invoking Sneak, press "s" _again_ to go to the next match, or "S" for
289 | the previous match. This behavior persists until you move the cursor.
290 | Note: You can still use |;| and |,| as usual.
291 |
292 | g:sneak#absolute_dir = 0
293 |
294 | 0: Relative direction. |;| goes to the next match in the direction of the
295 | initial s/S invocation and |,| goes in the opposite direction.
296 |
297 | 1: Absolute direction. Repeat via |;| or |,| always goes forwards or
298 | backwards respectively.
299 | Note: With |sneak-clever-s|, s and S behave the same as |;| and |,|.
300 |
301 | g:sneak#use_ic_scs = 0
302 |
303 | 0: Always case-sensitive
304 |
305 | 1: Case sensitivity is determined by 'ignorecase' and 'smartcase'.
306 |
307 | g:sneak#map_netrw = 1
308 |
309 | 0: Don't do any special handling of "file manager" buffers (such as
310 | |netrw| or filebeagle).
311 |
312 | 1: Set up Sneak mappings (s and S) in "file manager" buffers (|:Ex|,
313 | |:Vex|, filebeagle) and replace the default buffer-local s and
314 | S mappings with s and S.
315 |
316 | g:sneak#label = 0
317 |
318 | 0: Disable |sneak-label-mode|.
319 |
320 | 1: Enable |sneak-label-mode| if the Vim |+conceal| feature is available.
321 |
322 | g:sneak#label_esc = "\" *g:sneak#label_esc*
323 |
324 | Exit |sneak-label-mode| as if were pressed.
325 | https://github.com/justinmk/vim-sneak/issues/122
326 |
327 | g:sneak#target_labels = ";sftunq/SFGHLTUNRMQZ?0" *g:sneak#target_labels*
328 |
329 | List of characters used to label the target locations. |sneak-label-mode|
330 |
331 | Labels should be keys that you never use after searching. E.g. it is
332 | unlikely that you will use "F" immediately after a Sneak, so "F" is in the
333 | label list. This removes a step if the first match is correct: instead,
334 | just type your next command. |sneak-fallthrough|
335 |
336 | Note: Any key mapped to Sneak_; is moved to the first position
337 | (because it's semantically equivalent).
338 |
339 | Note: Invalid target labels are removed:
340 | - Any key mapped to Sneak_,
341 | - Special keys:
342 | - Invisible keys:
343 |
344 | g:sneak#prompt = '>'
345 |
346 | Message displayed at the Sneak input prompt.
347 |
348 | ------------------------------------------------------------------------------
349 | HIGHLIGHTING *sneak-highlight*
350 |
351 | Sneak uses these highlight groups:
352 |
353 | Sneak
354 | Highlights Sneak matches. Default color is hideous magenta :)
355 | Also used for |sneak-label-mode| unless "SneakLabel" group exists.
356 |
357 | SneakCurrent
358 | Highlights the current Sneak match at cursor position.
359 | Analogous to the |CurSearch| highlight group for the |/| command.
360 |
361 | SneakScope
362 | Highlights the "scope" for |sneak-vertical-scope|.
363 | Default color is white (or black if 'background' is "light").
364 | Also used to show the cursor placement during |sneak-label-mode|.
365 |
366 | SneakLabel
367 | Highlights |sneak-label-mode|.
368 |
369 | To customize colors: >
370 | highlight! Sneak guifg=black guibg=red ctermfg=black ctermbg=red
371 | highlight! SneakCurrent guifg=black guibg=cyan ctermfg=black ctermbg=cyan
372 | highlight! SneakScope guifg=red guibg=yellow ctermfg=red ctermbg=yellow
373 |
374 | " Highlight current match the same way as other matches
375 | highlight! link SneakCurrent Sneak
376 |
377 | " Highlight matches the same way as the / command
378 | highlight! link Sneak Search
379 | highlight! link SneakCurrent CurSearch
380 |
381 | To disable highlighting: >
382 | highlight! link Sneak None
383 | highlight! link SneakCurrent None
384 |
385 | " Needed if a plugin sets the colorscheme dynamically:
386 | autocmd User SneakLeave highlight clear Sneak |
387 | \ highlight clear SneakCurrent
388 |
389 | ------------------------------------------------------------------------------
390 | FUNCTIONS *sneak-functions*
391 |
392 | These functions are provided if you want more control.
393 |
394 | sneak#wrap(op, inputlen, reverse, inclusive, label) *sneak#wrap()*
395 | Sneaks with exactly {inputlen} characters. For example with {inputlen}
396 | of 2 the search is performed when the second character is typed,
397 | without waiting for .
398 |
399 | Parameters:
400 | {op}: operator name: empty string "" for normal-mode,
401 | otherwise |v:operator|, |visualmode()|, or custom.
402 | {inputlen}: wait for this many input keys
403 | {reverse}: 0: forward motion
404 | 1: backward motion
405 | {inclusive}: 0: |t|-like motion
406 | 1: |f|-like motion
407 | 2: |exclusive| motion like |/|
408 | {label}: 0: never invoke |sneak-label-mode|
409 | 1: label-mode for >=2 matches
410 | 2: label-mode always
411 |
412 | Example: Configure "f" to trigger label-mode: >
413 | nnoremap f :call sneak#wrap('', 1, 0, 1, 1)
414 | nnoremap F :call sneak#wrap('', 1, 1, 1, 1)
415 | xnoremap f :call sneak#wrap(visualmode(), 1, 0, 1, 1)
416 | xnoremap F :call sneak#wrap(visualmode(), 1, 1, 1, 1)
417 | onoremap f :call sneak#wrap(v:operator, 1, 0, 1, 1)
418 | onoremap F :call sneak#wrap(v:operator, 1, 1, 1, 1)
419 | <
420 | Example: Configure "s" to wait for 3 characters: >
421 | nnoremap s :call sneak#wrap('', 3, 0, 2, 1)
422 | nnoremap S :call sneak#wrap('', 3, 1, 2, 1)
423 | xnoremap s :call sneak#wrap(visualmode(), 3, 0, 2, 1)
424 | xnoremap S :call sneak#wrap(visualmode(), 3, 1, 2, 1)
425 | onoremap s :call sneak#wrap(v:operator, 3, 0, 2, 1)
426 | onoremap S :call sneak#wrap(v:operator, 3, 1, 2, 1)
427 | <
428 | sneak#is_sneaking() *sneak#is_sneaking()*
429 | Returns 1 if Sneak is active, else 0. Sneak deactivates when the user
430 | chooses a target or moves the cursor. Useful for changing the behavior
431 | of a mapping.
432 |
433 | Example: goes to the next match _only_ while Sneak is active: >
434 | nmap sneak#is_sneaking() ? 'Sneak_;' : ''
435 | <
436 | Example: change the statusline: >
437 | set statusline=%{sneak#is_sneaking()?'SNEAKING':'RELAXING'}
438 | <
439 | https://github.com/justinmk/vim-sneak/pull/93
440 |
441 | sneak#cancel() *sneak#cancel()*
442 | Deactivates Sneak: removes autocmds/highlighting and causes
443 | |sneak#is_sneaking()| to return 0.
444 | https://github.com/justinmk/vim-sneak/issues/106
445 |
446 | sneak#reset(key) *sneak#reset()*
447 | Prevents Sneak from hijacking |;| and |,| until the next invocation of
448 | Sneak. This is useful if you have remapped the Vim built-in |f| or
449 | |t| to another key and you still want to use |;| and |,| for both Sneak
450 | and your custom "f" mapping.
451 | https://github.com/justinmk/vim-sneak/issues/114
452 |
453 | For example, to use "a" as your "f":
454 | >
455 | nnoremap a sneak#reset('f')
456 | nnoremap A sneak#reset('F')
457 | xnoremap a sneak#reset('f')
458 | xnoremap A sneak#reset('F')
459 | onoremap a sneak#reset('f')
460 | onoremap A sneak#reset('F')
461 | <
462 | Note: The modifier is required!
463 |
464 | sneak#state() *sneak#state()*
465 | Returns a read-only dictionary representing the current behavior.
466 |
467 | Values set at invocation (_not_ for repeat):
468 | KEY VALUE~
469 | bounds [left,right] integer pair representing the current
470 | |sneak-vertical-scope| or [0,0] if not scoped
471 | inclusive 0: |t|-like motion
472 | 1: |f|-like motion
473 | 2: |exclusive| motion like |/|
474 | input current search string
475 | inputlen length of the current search string
476 | reverse 0: invoked as forward motion `s`
477 | 1: invoked as backward motion `S`
478 | rst 0: |;| and |,| should repeat the most recent Sneak
479 | 1: after calling |sneak#reset()|
480 |
481 | Values updated on each repeat (|;| |,|):
482 | KEY VALUE~
483 | rptreverse 0: repeat was effectively forward
484 | 1: repeat was effectively backward
485 |
486 | For example, to create a mapping that behaves differently depending
487 | on the current Sneak direction (`rptreverse` key):
488 | >
489 | nmap sneak#is_sneaking()
490 | \ ? (sneak#state().rptreverse ? 'SneakLabel_S' : 'SneakLabel_s')
491 | \ : 'Sneak_s'
492 | <
493 | ==============================================================================
494 | EVENTS *sneak-events*
495 |
496 | The |User| *SneakEnter* event is triggered when Sneak is invoked (including
497 | dot-repeat invocations, but not motion-repeat).
498 |
499 | Example: >
500 | autocmd User SneakEnter set nocursorcolumn nocursorline
501 |
502 | The |User| *SneakLeave* event is triggered when Sneak is finished (including
503 | dot-repeat invocations, but not motion-repeat).
504 |
505 | Example: >
506 | autocmd User SneakLeave set cursorcolumn cursorline
507 |
508 | ==============================================================================
509 | CONTRIBUTING *sneak-contributing*
510 |
511 | Bug reports, feature requests, and patches are welcome:
512 | https://github.com/justinmk/vim-sneak
513 |
514 | To narrow down the issue, run Vim with a minimal configuration: >
515 | vim -u NORC -N +"let g:sneak#label=1" +":set runtimepath+=~/.vim/bundle/vim-sneak/" +":runtime plugin/sneak.vim"
516 |
517 | or Nvim: >
518 | nvim -u NORC --cmd "let g:sneak#label=1" +":set runtimepath+=~/.local/share/nvim/bundle/vim-sneak/" +":runtime plugin/sneak.vim"
519 |
520 | ==============================================================================
521 | CREDITS *sneak-credits*
522 |
523 | Author: Justin M. Keyes
524 |
525 | Sneak was inspired by vim-seek, vim-easymotion, clever-f, and Tim Pope's
526 | plugins.
527 |
528 | https://github.com/goldfeld/vim-seek
529 | https://github.com/Lokaltog/vim-easymotion
530 | https://github.com/rhysd/clever-f.vim
531 |
532 | ==============================================================================
533 | vim:tw=78:sw=4:ts=8:ft=help:norl:
534 |
--------------------------------------------------------------------------------
/lua/sneak.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | local ns = vim.api.nvim_create_namespace('sneak')
4 |
5 | local matches
6 |
7 | function M.before()
8 | matches = {}
9 | end
10 |
11 | function M.placematch(c, row, col)
12 | matches[#matches+1] = { c, row, col }
13 | end
14 |
15 | function M.after()
16 | matches = nil
17 | end
18 |
19 | function M.init()
20 | vim.api.nvim_set_decoration_provider(ns, {
21 | on_start = function(_, _)
22 | if not matches then
23 | return false
24 | end
25 | end,
26 | on_win = function(_, win, _, _, _)
27 | if win ~= vim.api.nvim_get_current_win() then
28 | return false
29 | end
30 | for _, m in ipairs(matches) do
31 | local c, row, col = unpack(m)
32 | vim.api.nvim_buf_set_extmark(0, ns, row, col, {
33 | priority = 1000,
34 | virt_text = { {c, 'SneakLabel'} },
35 | virt_text_pos = 'overlay',
36 | ephemeral = true,
37 | })
38 | end
39 | end
40 | })
41 | end
42 |
43 | return M
44 |
--------------------------------------------------------------------------------
/plugin/sneak.vim:
--------------------------------------------------------------------------------
1 | " sneak.vim - The missing motion
2 | " Author: Justin M. Keyes
3 | " Version: 1.8
4 | " License: MIT
5 |
6 | if exists('g:loaded_sneak_plugin') || &compatible || v:version < 700
7 | finish
8 | endif
9 | let g:loaded_sneak_plugin = 1
10 |
11 | let s:cpo_save = &cpo
12 | set cpo&vim
13 |
14 | func! s:sneak_init() abort
15 | unlockvar g:sneak_opt
16 | "options v-- for backwards-compatibility
17 | let g:sneak_opt = { 'f_reset' : get(g:, 'sneak#nextprev_f', get(g:, 'sneak#f_reset', 1))
18 | \ ,'t_reset' : get(g:, 'sneak#nextprev_t', get(g:, 'sneak#t_reset', 1))
19 | \ ,'s_next' : get(g:, 'sneak#s_next', 0)
20 | \ ,'absolute_dir' : get(g:, 'sneak#absolute_dir', 0)
21 | \ ,'use_ic_scs' : get(g:, 'sneak#use_ic_scs', 0)
22 | \ ,'map_netrw' : get(g:, 'sneak#map_netrw', 1)
23 | \ ,'label' : get(g:, 'sneak#label', get(g:, 'sneak#streak', 0)) && (v:version >= 703) && has("conceal")
24 | \ ,'label_esc' : get(g:, 'sneak#label_esc', get(g:, 'sneak#streak_esc', "\"))
25 | \ ,'prompt' : get(g:, 'sneak#prompt', '>')
26 | \ }
27 |
28 | for k in ['f', 't'] "if user mapped f/t to Sneak, then disable f/t reset.
29 | if maparg(k, 'n') =~# 'Sneak'
30 | let g:sneak_opt[k.'_reset'] = 0
31 | endif
32 | endfor
33 | lockvar g:sneak_opt
34 | endf
35 |
36 | call s:sneak_init()
37 |
38 | " 2-char sneak
39 | nnoremap Sneak_s :call sneak#wrap('', 2, 0, 2, 1)
40 | nnoremap Sneak_S :call sneak#wrap('', 2, 1, 2, 1)
41 | xnoremap Sneak_s :call sneak#wrap(visualmode(), 2, 0, 2, 1)
42 | xnoremap Sneak_S :call sneak#wrap(visualmode(), 2, 1, 2, 1)
43 | onoremap Sneak_s :call sneak#wrap(v:operator, 2, 0, 2, 1)
44 | onoremap Sneak_S :call sneak#wrap(v:operator, 2, 1, 2, 1)
45 |
46 | onoremap SneakRepeat :call sneak#wrap(v:operator, sneak#util#getc(), sneak#util#getc(), sneak#util#getc(), sneak#util#getc())
47 |
48 | " repeat motion (explicit--as opposed to implicit 'clever-s')
49 | nnoremap Sneak_; :call sneak#rpt('', 0)
50 | nnoremap Sneak_, :call sneak#rpt('', 1)
51 | xnoremap Sneak_; :call sneak#rpt(visualmode(), 0)
52 | xnoremap Sneak_, :call sneak#rpt(visualmode(), 1)
53 | onoremap Sneak_; :call sneak#rpt(v:operator, 0)
54 | onoremap Sneak_, :call sneak#rpt(v:operator, 1)
55 |
56 | " 1-char 'enhanced f' sneak
57 | nnoremap Sneak_f :call sneak#wrap('', 1, 0, 1, 0)
58 | nnoremap Sneak_F :call sneak#wrap('', 1, 1, 1, 0)
59 | xnoremap Sneak_f :call sneak#wrap(visualmode(), 1, 0, 1, 0)
60 | xnoremap Sneak_F :call sneak#wrap(visualmode(), 1, 1, 1, 0)