├── 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) 61 | onoremap Sneak_f :call sneak#wrap(v:operator, 1, 0, 1, 0) 62 | onoremap Sneak_F :call sneak#wrap(v:operator, 1, 1, 1, 0) 63 | 64 | " 1-char 'enhanced t' sneak 65 | nnoremap Sneak_t :call sneak#wrap('', 1, 0, 0, 0) 66 | nnoremap Sneak_T :call sneak#wrap('', 1, 1, 0, 0) 67 | xnoremap Sneak_t :call sneak#wrap(visualmode(), 1, 0, 0, 0) 68 | xnoremap Sneak_T :call sneak#wrap(visualmode(), 1, 1, 0, 0) 69 | onoremap Sneak_t :call sneak#wrap(v:operator, 1, 0, 0, 0) 70 | onoremap Sneak_T :call sneak#wrap(v:operator, 1, 1, 0, 0) 71 | 72 | nnoremap SneakLabel_s :call sneak#wrap('', 2, 0, 2, 2) 73 | nnoremap SneakLabel_S :call sneak#wrap('', 2, 1, 2, 2) 74 | xnoremap SneakLabel_s :call sneak#wrap(visualmode(), 2, 0, 2, 2) 75 | xnoremap SneakLabel_S :call sneak#wrap(visualmode(), 2, 1, 2, 2) 76 | onoremap SneakLabel_s :call sneak#wrap(v:operator, 2, 0, 2, 2) 77 | onoremap SneakLabel_S :call sneak#wrap(v:operator, 2, 1, 2, 2) 78 | 79 | if !hasmapto('SneakForward') && !hasmapto('Sneak_s', 'n') && mapcheck('s', 'n') ==# '' 80 | nmap s Sneak_s 81 | endif 82 | if !hasmapto('SneakBackward') && !hasmapto('Sneak_S', 'n') && mapcheck('S', 'n') ==# '' 83 | nmap S Sneak_S 84 | endif 85 | if !hasmapto('Sneak_s', 'o') && mapcheck('z', 'o') ==# '' 86 | omap z Sneak_s 87 | endif 88 | if !hasmapto('Sneak_S', 'o') && mapcheck('Z', 'o') ==# '' 89 | omap Z Sneak_S 90 | endif 91 | 92 | if !hasmapto('Sneak_;', 'n') && !hasmapto('SneakNext', 'n') && mapcheck(';', 'n') ==# '' 93 | nmap ; Sneak_; 94 | omap ; Sneak_; 95 | xmap ; Sneak_; 96 | endif 97 | if !hasmapto('Sneak_,', 'n') && !hasmapto('SneakPrevious', 'n') 98 | if mapcheck(',', 'n') ==# '' 99 | nmap , Sneak_, 100 | omap , Sneak_, 101 | xmap , Sneak_, 102 | elseif mapcheck('\', 'n') ==# '' || mapcheck('\', 'n') ==# ',' 103 | nmap \ Sneak_, 104 | omap \ Sneak_, 105 | xmap \ Sneak_, 106 | endif 107 | endif 108 | 109 | if !hasmapto('VSneakForward') && !hasmapto('Sneak_s', 'v') && mapcheck('s', 'x') ==# '' 110 | xmap s Sneak_s 111 | endif 112 | if !hasmapto('VSneakBackward') && !hasmapto('Sneak_S', 'v') && mapcheck('Z', 'x') ==# '' 113 | xmap Z Sneak_S 114 | endif 115 | 116 | " redundant legacy mappings for backwards compatibility (must come _after_ the hasmapto('Sneak_S') checks above) 117 | nmap SneakForward Sneak_s 118 | nmap SneakBackward Sneak_S 119 | xmap VSneakForward Sneak_s 120 | xmap VSneakBackward Sneak_S 121 | xmap VSneakNext Sneak_; 122 | xmap VSneakPrevious Sneak_, 123 | nmap (SneakStreak) SneakLabel_s 124 | nmap (SneakStreakBackward) SneakLabel_S 125 | xmap (SneakStreak) SneakLabel_s 126 | xmap (SneakStreakBackward) SneakLabel_S 127 | omap (SneakStreak) SneakLabel_s 128 | omap (SneakStreakBackward) SneakLabel_S 129 | nmap SneakNext Sneak_; 130 | nmap SneakPrevious Sneak_, 131 | xmap SneakNext Sneak_; 132 | xmap SneakPrevious Sneak_, 133 | omap SneakNext Sneak_; 134 | omap SneakPrevious Sneak_, 135 | 136 | if g:sneak_opt.map_netrw && -1 != stridx(maparg("s", "n"), "Sneak") 137 | func! s:map_netrw_key(key) abort 138 | let expanded_map = maparg(a:key,'n') 139 | if !strlen(expanded_map) || expanded_map =~# '_Net\|FileBeagle' 140 | if strlen(expanded_map) > 0 "else, mapped to 141 | silent exe (expanded_map =~# '' ? 'nmap' : 'nnoremap').' '.a:key.' '.expanded_map 142 | endif 143 | "unmap the default buffer-local mapping to allow Sneak's global mapping. 144 | silent! exe 'nunmap '.a:key 145 | endif 146 | endf 147 | 148 | augroup sneak_netrw 149 | autocmd! 150 | autocmd FileType netrw,filebeagle autocmd sneak_netrw CursorMoved 151 | \ call map_netrw_key('s') | call map_netrw_key('S') | autocmd! sneak_netrw * 152 | augroup END 153 | endif 154 | 155 | 156 | let &cpo = s:cpo_save 157 | unlet s:cpo_save 158 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | vim-vader 2 | vim-repeat 3 | vim-surround 4 | -------------------------------------------------------------------------------- /tests/test.vader: -------------------------------------------------------------------------------- 1 | # vim -N -u NONE -c "set rtp+=~/.vim/bundle/vim-sneak,~/.vim/bundle/vader.vim,~/.vim/bundle/vim-repeat" -c "runtime! plugin/sneak.vim plugin/vader.vim plugin/repeat.vim" -c "Vader ~/.vim/bundle/vim-sneak/tests/test.vader" 2 | # 3 | # BUG: highlight targets are bogus if &iminsert=1 4 | # BUG: sneak cannot find ёе: in this text 5 | # BUG: sneak cannot find е:а in this text 6 | # 7 | # ёе:а 8 | # ёе:а 9 | # ёе:а 10 | # ёе:а 11 | # ёе:а 12 | # ёе:ёе:ёе:ёе:ёе:ёе:ёе:аааааааёе:а 13 | # ёе:ёе:ёе:ёе:ёе:ёе:ёе:аааааааёе:а 14 | # ёе:ёе:ёе:ёе:ёе:ёе:ёе:аааааааёе:а 15 | # ёе:ёе:ёе:ёе:ёе:ёе:ёе:аааааааёе:а 16 | # ёе:ёе:ёе:ёе:ёе:ёе:ёе:аааааааёе:а 17 | # ёе:ёе:ёе:ёе:ёе:ёе:ёе:аааааааёе:а 18 | 19 | Execute (Clean up test environment): 20 | if !has('nvim') 21 | set encoding=utf8 22 | scriptencoding utf8 23 | endif 24 | nmap ; Sneak_; 25 | nmap \ Sneak_, 26 | omap \ Sneak_, 27 | xmap \ Sneak_, 28 | silent! unmap f 29 | silent! unmap t 30 | let mapleader = ',' 31 | let g:sneak#label = 0 32 | let g:sneak#use_ic_scs = 0 33 | func! SneakReset() abort 34 | unlet g:loaded_sneak_plugin 35 | runtime! plugin/sneak.vim 36 | endfunc 37 | call SneakReset() 38 | 39 | ########################################################### 40 | Execute (autoloaded functions should not exist): 41 | Assert !exists("*sneak#label#to") 42 | Assert !exists("*sneak#search#new") 43 | Assert !exists("*sneak#util#strlen") 44 | 45 | # this must be the first test, because it tests built-in f-repeat 46 | # for the case where s has not ever been invoked. 47 | Given: 48 | ababAbABababaB 49 | Do (f-repeat without having ever invoked s. #31): 50 | fa;ix 51 | Expect: 52 | ababAbABxababaB 53 | 54 | ########################################################### 55 | # unit tests 56 | 57 | Do (tickle the autoloads): 58 | sab 59 | 60 | Execute ('label' functions should not exist after vanilla-sneak): 61 | Assert !exists("*sneak#label#to") 62 | 63 | Execute (hl#links_to() returns linked-to highlight group): 64 | hi! link Conceal SneakConceal 65 | Assert 'SneakConceal' ==# sneak#util#links_to('Conceal') 66 | 67 | Execute (hl#links_to() returns NONE if not linked): 68 | hi! link Conceal NONE 69 | Assert "NONE" ==# sneak#util#links_to('Conceal') 70 | 71 | Execute (sneak#is_sneaking() returns 1 when activated): 72 | norm sab 73 | Assert sneak#is_sneaking() 74 | 75 | Execute (sneak#cancel() cancels sneak#is_sneaking()): 76 | norm sab 77 | Assert sneak#is_sneaking() 78 | call sneak#cancel() 79 | Assert !sneak#is_sneaking() 80 | 81 | Execute (label#sanitize_target_labels()): 82 | runtime autoload/sneak/label.vim 83 | let orig = g:sneak#target_labels 84 | let g:sneak#target_labels = " \\ as d\fo\ob\a;\\rASD" 85 | call sneak#label#sanitize_target_labels() 86 | Assert g:sneak#target_labels ==# ";asdfoobarASD" 87 | "cleanup 88 | let g:sneak#target_labels = orig 89 | 90 | Given: 91 | abab 92 | abab 93 | 94 | Execute (s.hasmatches()): 95 | norm! jll 96 | let s = g:sneak#search#instance 97 | call s.init("ab", 0, 0) 98 | Assert !s.hasmatches(2) 99 | Assert !s.hasmatches(1) 100 | norm! k 101 | Assert s.hasmatches(1) 102 | Assert s.hasmatches(2) 103 | Assert !s.hasmatches(3) 104 | 105 | Execute (autoloaded functions should exist): 106 | Assert exists("*sneak#label#to") 107 | Assert exists("*sneak#search#new") 108 | Assert exists("*sneak#util#strlen") 109 | 110 | ########################################################### 111 | 112 | Given: 113 | 1 22222 33 444 555 6666 7 888 114 | 11 222 3333 4 55 6666 77 888 115 | 111 22 333 444 42555 6666 7 88888 116 | 1111 2 33 444 555 66 77a 8 117 | 118 | Do (move to 22): 119 | s22x 120 | Expect: 121 | 1 2222 33 444 555 6666 7 888 122 | 11 222 3333 4 55 6666 77 888 123 | 111 22 333 444 42555 6666 7 88888 124 | 1111 2 33 444 555 66 77a 8 125 | 126 | Do (next match): 127 | s22;x 128 | Expect: 129 | 1 2222 33 444 555 6666 7 888 130 | 11 222 3333 4 55 6666 77 888 131 | 111 22 333 444 42555 6666 7 88888 132 | 1111 2 33 444 555 66 77a 8 133 | 134 | Do (skip to third match): 135 | s\43;x 136 | Expect: 137 | 1 22222 33 444 555 6666 7 888 138 | 11 222 3333 4 55 6666 77 888 139 | 111 22 333 44442555 6666 7 88888 140 | 1111 2 33 444 555 66 77a 8 141 | 142 | # CursorMoved is not triggered in scripts/macros, so we must force it. 143 | # https://github.com/junegunn/vader.vim/issues/2 144 | Do (visual mode forwards): 145 | vwjs42lo 146 | :\doautocmd CursorMoved\ 147 | :\doautocmd CursorMoved\ 148 | gvs55;d 149 | Expect: 150 | 1 22222 33 444 555 6666 7 888 151 | 11 222 3333 4 555 6666 7 88888 152 | 1111 2 33 444 555 66 77a 8 153 | 154 | Do (visual mode backwards): 155 | jjjvwwoZ33;;\d 156 | Expect: 157 | 1 22222 33 444 555 6666 7 888 158 | 11 222 333 444 555 66 77a 8 159 | 160 | Given: 161 | a,,bbb 162 | aa,,bb 163 | aaa,,b 164 | aaaa,, 165 | aaa,b, 166 | aa,bb, 167 | a,bbb, 168 | 169 | Do (search backwards): 170 | jjSbbx 171 | Expect: 172 | a,,bbb 173 | aa,,b 174 | aaa,,b 175 | aaaa,, 176 | aaa,b, 177 | aa,bb, 178 | a,bbb, 179 | 180 | ########################################################### 181 | # delete, change, yank 182 | 183 | Given: 184 | Paul McCartney 1942 185 | George Harrison 1943mmdd 186 | Ringo Starr 1940mm 187 | Pete Best 1941 188 | 189 | Do (delete): 190 | dzar 191 | Expect: 192 | artney 1942 193 | George Harrison 1943mmdd 194 | Ringo Starr 1940mm 195 | Pete Best 1941 196 | 197 | Do (delete backwards): 198 | jwdZar 199 | Expect: 200 | Paul McCHarrison 1943mmdd 201 | Ringo Starr 1940mm 202 | Pete Best 1941 203 | 204 | Do (delete [count]): 205 | d3zar 206 | Expect: 207 | arr 1940mm 208 | Pete Best 1941 209 | 210 | Do (delete ;): 211 | dzar 212 | :\doautocmd CursorMoved\ 213 | d; 214 | Expect: 215 | arrison 1943mmdd 216 | Ringo Starr 1940mm 217 | Pete Best 1941 218 | 219 | Do (change): 220 | llczar 221 | Expect: 222 | Paartney 1942 223 | George Harrison 1943mmdd 224 | Ringo Starr 1940mm 225 | Pete Best 1941 226 | 227 | Do (change backwards): 228 | jjllcZar 229 | Expect: 230 | Paul McCartney 1942 231 | George Hngo Starr 1940mm 232 | Pete Best 1941 233 | 234 | Do (change [count]): 235 | c3zar 236 | Expect: 237 | arr 1940mm 238 | Pete Best 1941 239 | 240 | Do (change ;): 241 | czarfoo\l 242 | :\doautocmd CursorMoved\ 243 | c;bar 244 | Expect: 245 | foobararrison 1943mmdd 246 | Ringo Starr 1940mm 247 | Pete Best 1941 248 | 249 | Do (yank): 250 | jlyzarp 251 | Expect: 252 | Paul McCartney 1942 253 | Geeorge Horge Harrison 1943mmdd 254 | Ringo Starr 1940mm 255 | Pete Best 1941 256 | 257 | Do (yank backwards): 258 | jjllyZarp 259 | Expect: 260 | Paul McCartney 1942 261 | George Haarrison 1943mmdd 262 | Rirrison 1943mmdd 263 | Ringo Starr 1940mm 264 | Pete Best 1941 265 | 266 | Do (yank [count]): 267 | y3zarP 268 | Expect: 269 | Paul McCartney 1942 270 | George Harrison 1943mmdd 271 | Ringo StPaul McCartney 1942 272 | George Harrison 1943mmdd 273 | Ringo Starr 1940mm 274 | Pete Best 1941 275 | 276 | ########################################################### 277 | # repeat-operation (.) 278 | 279 | Given: 280 | Paul McCartney 1942 281 | George Harrison 1943mmdd 282 | Ringo Starr 1940mm 283 | Pete Best 1941 284 | 285 | Do (search for H): 286 | sH\x 287 | Expect: 288 | Paul McCartney 1942 289 | George arrison 1943mmdd 290 | Ringo Starr 1940mm 291 | Pete Best 1941 292 | 293 | Do (repeat change): 294 | jlczarfoo\l 295 | :\doautocmd CursorMoved\ 296 | . 297 | Expect: 298 | Paul McCartney 1942 299 | Gfoofooarr 1940mm 300 | Pete Best 1941 301 | 302 | Do (repeat backwards change): 303 | jjllcZarfoo\hh 304 | :\doautocmd CursorMoved\ 305 | . 306 | Expect: 307 | Paul McCfoofoongo Starr 1940mm 308 | Pete Best 1941 309 | 310 | Do (repeat delete operation): 311 | dzar 312 | :\doautocmd CursorMoved\ 313 | . 314 | Expect: 315 | arrison 1943mmdd 316 | Ringo Starr 1940mm 317 | Pete Best 1941 318 | 319 | Do (repeat delete operation twice): 320 | dzar 321 | :\doautocmd CursorMoved\ 322 | . 323 | :\doautocmd CursorMoved\ 324 | . 325 | Expect: 326 | arr 1940mm 327 | Pete Best 1941 328 | 329 | Do (repeat backwards delete operation. #7): 330 | 3jdZar 331 | :\doautocmd CursorMoved\ 332 | . 333 | Expect: 334 | Paul McCartney 1942 335 | George Ht 336 | Pete Best 1941 337 | 338 | Do (repeat backwards delete operation twice. #7): 339 | 3jdZar 340 | :\doautocmd CursorMoved\ 341 | . 342 | :\doautocmd CursorMoved\ 343 | . 344 | Expect: 345 | Paul McCt 346 | Pete Best 1941 347 | 348 | ########################################################### 349 | # [count]repeat and repeat [count]op 350 | 351 | Given: 352 | Paul McCartney 1942 353 | George Harrison 1943mmdd 354 | Ringo Starr 1940mm 355 | Pete Babbar 1941 356 | 357 | Do (repeat [count] delete operation): 358 | 2dzar 359 | :\doautocmd CursorMoved\ 360 | . 361 | Expect: 362 | ar 1941 363 | 364 | Do (repeat [count] delete operation): 365 | 2czarfoo\l 366 | :\doautocmd CursorMoved\ 367 | . 368 | Expect: 369 | foofooar 1941 370 | 371 | Do ([count]repeat delete): 372 | dzar 373 | :\doautocmd CursorMoved\ 374 | 2. 375 | Expect: 376 | arr 1940mm 377 | Pete Babbar 1941 378 | 379 | Do ([count]repeat change): 380 | czarfoo\l 381 | :\doautocmd CursorMoved\ 382 | 2. 383 | Expect: 384 | foofooarr 1940mm 385 | Pete Babbar 1941 386 | 387 | ########################################################### 388 | 389 | Given: 390 | xyz="abc=def" 391 | a=b#=c 392 | \\\\ \ \ ? !--``.ago_ *$&()[]{} 393 | 394 | 395 | Do (search for backticks, then backslashes): 396 | llls`` 397 | :\doautocmd CursorMoved\ 398 | :\doautocmd CursorMoved\ 399 | S\\i:) 400 | Expect: 401 | xyz="abc=def" 402 | a=b#=c 403 | \\:)\\ \ \ ? !--``.ago_ *$&()[]{} 404 | 405 | ########################################################### 406 | 407 | Given: 408 | var jdbc = { 409 | // JDBC driver for MySQL database: 410 | driver: "com.mysql.jdbc.Driver", 411 | /* JDBC URL for the connection (jdbc:mysql://HOSTNAME/DATABASE) */ 412 | url: 'jdbc:mysql://localhost/test', 413 | abpa: "pass", 414 | "pass:pass":"r00t:pa55" 415 | }; 416 | 417 | Do (vertical scope and count-prefixed repeat-motion): 418 | wwjl3spa2;i: 419 | Expect: 420 | var jdbc = { 421 | // JDBC driver for MySQL database: 422 | driver: "com.mysql.jdbc.Driver", 423 | /* JDBC URL for the connection (jdbc:mysql://HOSTNAME/DATABASE) */ 424 | url: 'jdbc:mysql://localhost/test', 425 | abpa: "pass", 426 | "pass::pass":"r00t:pa55" 427 | }; 428 | 429 | ########################################################### 430 | 431 | Given: 432 | | Option | Type | Default | Description | 433 | | -- | -- | -- | -- | 434 | | threads | Fixnum | 1 | number of threads in the thread pool | 435 | | queues | Fixnum | 1 | number of concurrent queues | 436 | | queue_size | Fixnum | 1000 | size of each queue | 437 | | interval | Numeric | 0 | dispatcher interval for batch processing | 438 | | batch | Boolean | false | enables batch processing mode | 439 | | batch_size | Fixnum | nil | number of maximum items to be assigned at once | 440 | | logger | Logger | nil | logger instance for debug logs | 441 | 442 | Do (crazy sequence with s, f, counts, and repeats): 443 | fDs1\;\2fe2;3j4Ti\~ 444 | Expect: 445 | | Option | Type | Default | Description | 446 | | -- | -- | -- | -- | 447 | | threads | Fixnum | 1 | number of threads in the thread pool | 448 | | queues | Fixnum | 1 | number of concurrent queues | 449 | | queue_size | Fixnum | 1000 | size of each queue | 450 | | interval | NumeRic | 0 | dispatcher interval for batch processing | 451 | | batch | Boolean | false | enables batch processing mode | 452 | | batch_size | Fixnum | nil | number of maximum items to be assigned at once | 453 | | logger | Logger | nil | logger instance for debug logs | 454 | 455 | ########################################################### 456 | # clever-s _not_ in label-mode 457 | 458 | Execute (init clever-s): 459 | let g:sneak#s_next = 1 460 | call SneakReset() 461 | 462 | Given: 463 | abcdef abcdef abcdef 464 | abcdef abcdef abcdef 465 | abcdef abcdef abcdef 466 | 467 | Do (dummy step to work around Vim (or Vader) macro bug): 468 | sab\u 469 | 470 | Do (clever-s): 471 | sabsax 472 | Expect: 473 | abcdef abcdef axbcdef 474 | abcdef abcdef abcdef 475 | abcdef abcdef abcdef 476 | 477 | Do (clever-s): 478 | sab3sax 479 | Expect: 480 | abcdef abcdef abcdef 481 | abcdef axbcdef abcdef 482 | abcdef abcdef abcdef 483 | 484 | Do (clever-s): 485 | sabsssssssSSax 486 | Expect: 487 | abcdef abcdef abcdef 488 | abcdef abcdef abcdef 489 | axbcdef abcdef abcdef 490 | 491 | Do (clever-s with input truncated by should still clever-repeat): 492 | se\sssix 493 | Expect: 494 | abcdef abcdef abcdef 495 | abcdxef abcdef abcdef 496 | abcdef abcdef abcdef 497 | 498 | Execute (unset clever-s): 499 | let g:sneak#s_next = 0 500 | call SneakReset() 501 | 502 | ########################################################### 503 | # g:sneak#absolute_dir 504 | 505 | Given: 506 | abcdef abcdef abcdef 507 | abcdef abcdef abcdef 508 | abcdef abcdef abcdef 509 | 510 | Execute (absolute_dir = 0): 511 | let g:sneak#absolute_dir = 0 512 | call SneakReset() 513 | 514 | norm sab 515 | Assert sneak#state()['reverse'] == 0 516 | Assert sneak#state()['rptreverse'] == 0 517 | norm ; 518 | Assert sneak#state()['reverse'] == 0 519 | Assert sneak#state()['rptreverse'] == 0 520 | norm , 521 | Assert sneak#state()['reverse'] == 0 522 | Assert sneak#state()['rptreverse'] == 1 523 | norm Sab 524 | Assert sneak#state()['reverse'] == 1 525 | Assert sneak#state()['rptreverse'] == 1 526 | norm ; 527 | Assert sneak#state()['reverse'] == 1 528 | Assert sneak#state()['rptreverse'] == 1 529 | norm , 530 | Assert sneak#state()['reverse'] == 1 531 | Assert sneak#state()['rptreverse'] == 0 532 | 533 | Execute (absolute_dir = 1): 534 | let g:sneak#absolute_dir = 1 535 | call SneakReset() 536 | 537 | norm sab 538 | Assert sneak#state()['reverse'] == 0 539 | Assert sneak#state()['rptreverse'] == 0 540 | norm ; 541 | Assert sneak#state()['reverse'] == 0 542 | Assert sneak#state()['rptreverse'] == 0 543 | norm , 544 | Assert sneak#state()['reverse'] == 0 545 | Assert sneak#state()['rptreverse'] == 1 546 | norm Sab 547 | Assert sneak#state()['reverse'] == 1 548 | Assert sneak#state()['rptreverse'] == 1 549 | norm ; 550 | Assert sneak#state()['reverse'] == 1 551 | Assert sneak#state()['rptreverse'] == 0 552 | norm , 553 | Assert sneak#state()['reverse'] == 1 554 | Assert sneak#state()['rptreverse'] == 1 555 | 556 | Execute (restore g:sneak#absolute_dir default): 557 | let g:sneak#absolute_dir = 0 558 | call SneakReset() 559 | 560 | ########################################################### 561 | 562 | Given: 563 | abcdef abcdef abcdef 564 | abcdef abcdef abcdef 565 | abcdef abcdef abcdef 566 | 567 | Execute (create mapping with f as a prefix / do s ; fe): 568 | nnoremap foobar :echo 'hi' 569 | normal sbc2;;fe;iX 570 | nunmap foobar 571 | Expect: 572 | abcdef abcdef abcdef 573 | abcdef abcdXef abcdef 574 | abcdef abcdef abcdef 575 | 576 | ########################################################### 577 | # label-mode 578 | 579 | Execute (label-mode): 580 | let g:sneak#label = 1 581 | call SneakReset() 582 | 583 | Given: 584 | abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 585 | `=QWEOIJ/+ ~~~!!! ! { } abc AAaab 586 | abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 587 | 588 | Do (basic label motion): 589 | sabsD 590 | Expect: 591 | abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 592 | `=QWEOIJ/+ ~~~!!! ! { } abc AAa 593 | abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 594 | 595 | Do (backwards label motion): 596 | jj$Seffix 597 | Expect: 598 | abcdxef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 599 | `=QWEOIJ/+ ~~~!!! ! { } abc AAaab 600 | abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 601 | 602 | #TODO: feedkeys() in do_label() breaks macros/Vader 603 | #Do (Sneak_; in label-mode): 604 | # sab;x 605 | #Expect: 606 | # abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 607 | # `=QWEOIJ/+ ~~~!!! ! { } bc AAaab 608 | # abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 609 | #Do (Sneak_, in label-mode): 610 | # jsab\x 611 | #Expect: 612 | # abcdef ``=2Bd-a3/+ bCDef ``=2Bd-a3/+ 613 | # `=QWEOIJ/+ ~~~!!! ! { } abc AAaab 614 | # abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 615 | #Do (Sneak_; in visual-mode + label-mode): 616 | # vsab;x 617 | #Expect: 618 | # bc AAaab 619 | # abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 620 | #Do (Sneak_, in visual-mode + label-mode): 621 | # jvsab\x 622 | #Expect: 623 | # abcdef ``=2Bd-a3/+ =QWEOIJ/+ ~~~!!! ! { } abc AAaab 624 | # abcdef ``=2Bd-a3/+ abCDef ``=2Bd-a3/+ 625 | 626 | Given: 627 | | Option | Type | Default | Description | 628 | | -- | -- | -- | -- | 629 | | threads | Fixnum | 1 | number of threads in the thread pool | 630 | | queues | Fixnum | 1 | number of concurrent queues | 631 | | queue_size | Fixnum | 1000 | size of each queue | 632 | | interval | Numeric | 0 | dispatcher interval for batch processing | 633 | | batch | Boolean | false | enables batch processing mode | 634 | | batch_size | Fixnum | nil | number of maximum items to be assigned at once | 635 | | logger | Logger | nil | logger instance for debug logs | 636 | 637 | Do (delete + label): 638 | dznun 639 | Expect: 640 | number of maximum items to be assigned at once | 641 | | logger | Logger | nil | logger instance for debug logs | 642 | 643 | Given: 644 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 645 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 646 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 647 | 648 | Do (label + delete): 649 | dzab; 650 | Expect: 651 | ab3cdef ab4cdef ab5cdef ab6cdef 652 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 653 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 654 | 655 | Do (label + delete + repeat): 656 | dzabs 657 | :\doautocmd CursorMoved\ 658 | . 659 | Expect: 660 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 661 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 662 | 663 | Do (label + 2-delete + repeat): 664 | 2dzab\ 665 | :\doautocmd CursorMoved\ 666 | . 667 | Expect: 668 | ab5cdef ab6cdef 669 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 670 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 671 | 672 | Do (label/target/backwards/delete/repeat): 673 | 2j2wdZeff 674 | :\doautocmd CursorMoved\ 675 | . 676 | Expect: 677 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 678 | ab7cdab3cdef ab4cdef ab5cdef ab6cdef 679 | 680 | Do (label/target/change/repeat): 681 | jwczefsFOO\ 682 | :\doautocmd CursorMoved\ 683 | jb. 684 | Expect: 685 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 686 | ab7cdef FOOef ab1cdef ab2cdef 687 | ab1cdef FOOef ab5cdef ab6cdef 688 | 689 | ########################################################### 690 | # Label-mode with exclusive (t-like) sneak 691 | 692 | Execute (mappings for exclusive streak mode): 693 | nnoremap t :call sneak#wrap('', 1, 0, 0, 2) 694 | nnoremap T :call sneak#wrap('', 1, 1, 0, 2) 695 | onoremap t :call sneak#wrap(v:operator, 1, 0, 0, 2) 696 | onoremap T :call sneak#wrap(v:operator, 1, 1, 0, 2) 697 | 698 | Do (label-mode, t-like sneak, forwards, normal [issue #176]): 699 | tcsx 700 | Expect: 701 | ab1cdef ab2cdef abcdef ab4cdef ab5cdef ab6cdef 702 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 703 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 704 | 705 | Do (label-mode, t-like sneak, backwards, normal [issue #176]): 706 | $Tcfx 707 | Expect: 708 | ab1cdef ab2cdef ab3cef ab4cdef ab5cdef ab6cdef 709 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 710 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 711 | 712 | Do (label-mode, t-like sneak, forwards, op-pending [issue #176]): 713 | wwdtfs 714 | Expect: 715 | ab1cdef ab2cdef f ab6cdef 716 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 717 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 718 | 719 | Do (label-mode, t-like sneak, backwards, op-pending [issue #176]): 720 | 5wcTb;X 721 | Expect: 722 | ab1cdef ab2cdef ab3cdef abXab6cdef 723 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 724 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 725 | 726 | Execute (cleanup): 727 | silent! unmap f 728 | silent! unmap F 729 | silent! unmap t 730 | silent! unmap T 731 | silent! xunmap f 732 | silent! xunmap F 733 | silent! xunmap t 734 | silent! xunmap T 735 | 736 | Given: 737 | A11111111111111111111111111111111111111111111111111111K 738 | B22222222222222222222222222222222222222222222222222222L 739 | C11111111111111111111111111111111111111111111111111111M 740 | D22222222222222222222222222222222222222222222222222222N 741 | E11111111111111111111111111111111111111111111111111111O 742 | F22222222222222222222222222222222222222222222222222222P 743 | G11111111111111111111111111111111111111111111111111111Q 744 | H22222222222222222222222222222222222222222222222222222R 745 | I11111111111111111111111111111111111111111111111111111S 746 | J22222222222222222222222222222222222222222222222222222T 747 | 748 | Do (label/change-direction/delete): 749 | 4j9ls22\\\\\\Rr- 750 | Expect: 751 | A11111111111111111111111111111111111111111111111111111K 752 | B222222222222222222222222-2222222222222222222222222222L 753 | C11111111111111111111111111111111111111111111111111111M 754 | D22222222222222222222222222222222222222222222222222222N 755 | E11111111111111111111111111111111111111111111111111111O 756 | F22222222222222222222222222222222222222222222222222222P 757 | G11111111111111111111111111111111111111111111111111111Q 758 | H22222222222222222222222222222222222222222222222222222R 759 | I11111111111111111111111111111111111111111111111111111S 760 | J22222222222222222222222222222222222222222222222222222T 761 | 762 | Do (label/change-direction/delete/repeat): 763 | 4j9ldz22\\\N 764 | :\doautocmd CursorMoved\ 765 | . 766 | Expect: 767 | A11111111111111111111111111111111111111111111111111111K 768 | B2222222222222222222222222222222222222222111111111111111111111111111111111111111111111O 769 | F22222222222222222222222222222222222222222222222222222P 770 | G11111111111111111111111111111111111111111111111111111Q 771 | H22222222222222222222222222222222222222222222222222222R 772 | I11111111111111111111111111111111111111111111111111111S 773 | J22222222222222222222222222222222222222222222222222222T 774 | 775 | Do (label/backwards/): 776 | 4j9lS22\\fr- 777 | Expect: 778 | A11111111111111111111111111111111111111111111111111111K 779 | B2222-222222222222222222222222222222222222222222222222L 780 | C11111111111111111111111111111111111111111111111111111M 781 | D22222222222222222222222222222222222222222222222222222N 782 | E11111111111111111111111111111111111111111111111111111O 783 | F22222222222222222222222222222222222222222222222222222P 784 | G11111111111111111111111111111111111111111111111111111Q 785 | H22222222222222222222222222222222222222222222222222222R 786 | I11111111111111111111111111111111111111111111111111111S 787 | J22222222222222222222222222222222222222222222222222222T 788 | 789 | Execute (cleanup): 790 | let g:sneak#label = 0 791 | call SneakReset() 792 | 793 | ########################################################### 794 | # unicode, multibyte 795 | 796 | Execute (ASCII has_upper): 797 | Assert sneak#util#has_upper("Ab") 798 | 799 | Execute (ASCII !has_upper): 800 | Assert !sneak#util#has_upper("ab") 801 | 802 | Execute (cyrillic has_upper): 803 | Assert sneak#util#has_upper("АБРАЗиЯ") 804 | 805 | Execute (cyrillic !has_upper): 806 | Assert !sneak#util#has_upper("абразия") 807 | 808 | Execute (set ignorecase): 809 | set ignorecase 810 | set nosmartcase 811 | let g:sneak#use_ic_scs = 1 812 | call SneakReset() 813 | 814 | Execute (sneak#search#get_cs): 815 | Assert '\c' == sneak#search#get_cs('AB', 1) 816 | 817 | Given (ASCII): 818 | fooabAB 819 | Do (ASCII + ignorecase): 820 | sABix 821 | Expect: 822 | fooxabAB 823 | Do (ASCII + ignorecase + repeat): 824 | sAB;ix 825 | Expect: 826 | fooabxAB 827 | 828 | Given (cyrillic): 829 | fooабАБаб 830 | Do: 831 | sабix 832 | Expect: 833 | fooxабАБаб 834 | Do: 835 | sАБix 836 | Expect: 837 | fooxабАБаб 838 | Do (cyrillic + ignorecase + repeat): 839 | sАБ;ix 840 | Expect: 841 | fooабxАБаб 842 | Do (cyrillic + ignorecase + repeat x2): 843 | sАБ;;ix 844 | Expect: 845 | fooабАБxаб 846 | 847 | Execute (set smartcase): 848 | set smartcase 849 | call SneakReset() 850 | 851 | Given (ASCII): 852 | fooabABaB 853 | Do: 854 | sABix 855 | Expect: 856 | fooabxABaB 857 | Given (cyrillic): 858 | fooабАБаБ 859 | Do: 860 | sабix 861 | Expect: 862 | fooxабАБаБ 863 | Do: 864 | sАБix 865 | Expect: 866 | fooабxАБаБ 867 | Do: 868 | sаБix 869 | Expect: 870 | fooабАБxаБ 871 | 872 | Execute (set case-sensitive): 873 | let g:sneak#use_ic_scs = 0 874 | call SneakReset() 875 | 876 | Given (ASCII): 877 | fooAbabaB 878 | Do: 879 | sabix 880 | Expect: 881 | fooAbxabaB 882 | Given (cyrillic): 883 | fooаБАБаб 884 | Do: 885 | sабix 886 | Expect: 887 | fooаБАБxаб 888 | Do: 889 | sАБix 890 | Expect: 891 | fooаБxАБаб 892 | Do: 893 | sаБix 894 | Expect: 895 | fooxаБАБаб 896 | Do: 897 | sабix 898 | Expect: 899 | fooаБАБxаб 900 | 901 | Given (kanji): 902 | この機能を使用する場合、コマンfooド 903 | ラインではなくてコマンドラインウィンドウから 904 | Do: 905 | s機能ix 906 | Expect: 907 | このx機能を使用する場合、コマンfooド 908 | ラインではなくてコマンドラインウィンドウから 909 | Do: 910 | sooix 911 | Expect: 912 | この機能を使用する場合、コマンfxooド 913 | ラインではなくてコマンドラインウィンドウから 914 | Do: 915 | sド\ix 916 | Expect: 917 | この機能を使用する場合、コマンfooxド 918 | ラインではなくてコマンドラインウィンドウから 919 | 920 | # https://github.com/Lokaltog/vim-easymotion/issues/28 921 | # "If a word starts with æ, ø or å the highlighting is disturbed." 922 | Given (Norwegian): 923 | foo æfooæ æfooø øfoo 924 | Do: 925 | søfix 926 | Expect: 927 | foo æfooæ æfooø xøfoo 928 | 929 | Given (random unicode): 930 | ´µµ¶·¸¹°°¡À£¨ª¦©¨ª¦¦Þ¨¦©ß©¨ 931 | foo æfïooææ ïoòôõïo ïofoo fooïo ïoïo 932 | Do: 933 | sææix 934 | Expect: 935 | ´µµ¶·¸¹°°¡À£¨ª¦©¨ª¦¦Þ¨¦©ß©¨ 936 | foo æfïooxææ ïoòôõïo ïofoo fooïo ïoïo 937 | 938 | Given (multibyte chars in various arrangements): 939 | foo æfïooæ ïoòôõïo ïofoo fooïo ïoïo 940 | Do: 941 | sïo;;;;ix 942 | Expect: 943 | foo æfïooæ ïoòôõïo ïofoo fooxïo ïoïo 944 | 945 | Given (adjacent multibyte chars): 946 | foo æfïïooæ ïoòôõïo ïïofoo fïïooïo 947 | Do: 948 | sïï;;ax 949 | Expect: 950 | foo æfïïooæ ïoòôõïo ïïofoo fïxïooïo 951 | 952 | ########################################################### 953 | # 'keymap' 954 | 955 | Execute (SETUP): 956 | set keymap=bulgarian-phonetic 957 | 958 | Given: 959 | abcdef abcdef abcdef 960 | абцдеф АБЦДЕФ абцдеф 961 | абцдёё еёееёП ЯВЕРПО 962 | абцдёё еёееёП ЯВЕРПО 963 | 964 | Do (search for multi-keymap): 965 | se::e::x 966 | Expect: 967 | abcdef abcdef abcdef 968 | абцдеф АБЦДЕФ абцдеф 969 | абцдё еёееёП ЯВЕРПО 970 | абцдёё еёееёП ЯВЕРПО 971 | 972 | Do: 973 | se::e::;x 974 | Expect (repeat search for multi-keymap): 975 | abcdef abcdef abcdef 976 | абцдеф АБЦДЕФ абцдеф 977 | абцдёё еёееёП ЯВЕРПО 978 | абцдё еёееёП ЯВЕРПО 979 | 980 | Execute (SETUP): 981 | let g:sneak#target_labels = 'адсфгхйкл;' 982 | let g:sneak#label = 1 983 | call SneakReset() 984 | call sneak#label#sanitize_target_labels() 985 | Assert ';адсфгхйкл' ==# g:sneak#target_labels 986 | Given: 987 | яверуипоо пасдфкййл; 988 | яверуипоо пасдфкййл; 989 | яверуипоо пасдфкййл; 990 | яверуипоо пасдфкййл; 991 | яверуипоо пасдфкййл; 992 | Do: 993 | dzсдд 994 | Expect (keymap + label-mode + multibyte labels): 995 | сдфкййл; 996 | яверуипоо пасдфкййл; 997 | 998 | Execute (TEARDOWN): 999 | set iminsert=0 1000 | set keymap= 1001 | let g:sneak#label = 0 1002 | call SneakReset() 1003 | 1004 | ########################################################### 1005 | # Sneak_f and Sneak_t 1006 | 1007 | Execute (map to Sneak_f): 1008 | silent! unmap t 1009 | silent! unmap T 1010 | silent! unmap f 1011 | silent! unmap F 1012 | silent! xunmap t 1013 | silent! xunmap T 1014 | silent! xunmap f 1015 | silent! xunmap F 1016 | 1017 | nmap f Sneak_f 1018 | nmap F Sneak_F 1019 | nmap t Sneak_t 1020 | nmap T Sneak_T 1021 | 1022 | xmap f Sneak_f 1023 | xmap F Sneak_F 1024 | xmap t Sneak_t 1025 | xmap T Sneak_T 1026 | 1027 | omap f Sneak_f 1028 | omap F Sneak_F 1029 | omap t Sneak_t 1030 | omap T Sneak_T 1031 | 1032 | let g:sneak#f_reset = 0 1033 | 1034 | call SneakReset() 1035 | 1036 | Given: 1037 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1038 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1039 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1040 | 1041 | Do ([count]f): 1042 | 3feix 1043 | Expect ([count]f should NOT invoke vertical-scope): 1044 | ab1cdef ab2cdef ab3cdxef ab4cdef ab5cdef ab6cdef 1045 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1046 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1047 | 1048 | Do ([count]f): 1049 | jj3Feix 1050 | Expect ([count]F should NOT invoke vertical-scope): 1051 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1052 | ab7cdef ab8cdef ab9cdef ab0cdxef ab1cdef ab2cdef 1053 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1054 | 1055 | Do ([count]s): 1056 | 3sab;ax 1057 | Expect ([count]s SHOULD invoke vertical-scope): 1058 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1059 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1060 | axb1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1061 | 1062 | Do (f-delete forward): 1063 | dfe 1064 | Expect: 1065 | f ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1066 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1067 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1068 | 1069 | Do (f-delete invalid target): 1070 | dfZ 1071 | Expect (should not delete current char. #177): 1072 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1073 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1074 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1075 | 1076 | Do (t-delete forward): 1077 | dte 1078 | Expect: 1079 | ef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1080 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1081 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1082 | 1083 | Do (f-delete-3 forward): 1084 | 3dfe 1085 | Expect: 1086 | f ab4cdef ab5cdef ab6cdef 1087 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1088 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1089 | 1090 | Do (t-delete-3 forward): 1091 | 3dte 1092 | Expect: 1093 | ef ab4cdef ab5cdef ab6cdef 1094 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1095 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1096 | 1097 | Do: 1098 | dvfe 1099 | Expect (FIXME inclusive f-delete forward): 1100 | ef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1101 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1102 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1103 | 1104 | Do: 1105 | dvte 1106 | Expect (FIXME inclusive t-delete forward): 1107 | def ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1108 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1109 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1110 | 1111 | Do (exclusive f-delete backward. #121): 1112 | eedFa 1113 | Expect: 1114 | ab1cdef f ab3cdef ab4cdef ab5cdef ab6cdef 1115 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1116 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1117 | 1118 | Do (exclusive t-delete backward. #121): 1119 | eedTa 1120 | Expect: 1121 | ab1cdef af ab3cdef ab4cdef ab5cdef ab6cdef 1122 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1123 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1124 | 1125 | Do (inclusive f-delete backward. #121): 1126 | eedvFa 1127 | Expect: 1128 | ab1cdef ab3cdef ab4cdef ab5cdef ab6cdef 1129 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1130 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1131 | 1132 | Do (inclusive t-delete backward. #121): 1133 | eedvTa 1134 | Expect: 1135 | ab1cdef a ab3cdef ab4cdef ab5cdef ab6cdef 1136 | ab7cdef ab8cdef ab9cdef ab0cdef ab1cdef ab2cdef 1137 | ab1cdef ab2cdef ab3cdef ab4cdef ab5cdef ab6cdef 1138 | 1139 | ########################################################### 1140 | # test sneak mapped to 'f' (instead of default 's') 1141 | 1142 | Execute (map to f): 1143 | unmap s 1144 | unmap S 1145 | nmap f Sneak_s 1146 | nmap F Sneak_S 1147 | let g:sneak#f_reset = 1 1148 | call SneakReset() 1149 | 1150 | Given: 1151 | abcdef abcdef abcdef 1152 | 1153 | # issue #35 1154 | Do (sneak f, then Vim default t, then sneak f): 1155 | fderxtcrx 1156 | :\doautocmd CursorMoved\ 1157 | :\doautocmd CursorMoved\ 1158 | fderx;rx 1159 | Expect: 1160 | abcxef axcxef abcxef 1161 | 1162 | ########################################################### 1163 | Execute (cleanup): 1164 | unmap f 1165 | unmap F 1166 | nmap s Sneak_s 1167 | nmap S Sneak_S 1168 | call SneakReset() 1169 | 1170 | --------------------------------------------------------------------------------