1 " EasyMotion - Vim motions on speed!
3 " Author: Kim Silkebækken <kim.silkebaekken+vim@gmail.com>
4 " Source repository: https://github.com/Lokaltog/vim-easymotion
6 " Default configuration functions {{{
7 function! EasyMotion#InitOptions(options) " {{{
8 for [key, value] in items(a:options)
9 if ! exists('g:EasyMotion_' . key)
10 exec 'let g:EasyMotion_' . key . ' = ' . string(value)
14 function! EasyMotion#InitHL(group, colors) " {{{
15 let group_default = a:group . 'Default'
17 " Prepare highlighting variables
18 let guihl = printf('guibg=%s guifg=%s gui=%s', a:colors.gui[0], a:colors.gui[1], a:colors.gui[2])
19 if !exists('g:CSApprox_loaded')
20 let ctermhl = &t_Co == 256
21 \ ? printf('ctermbg=%s ctermfg=%s cterm=%s', a:colors.cterm256[0], a:colors.cterm256[1], a:colors.cterm256[2])
22 \ : printf('ctermbg=%s ctermfg=%s cterm=%s', a:colors.cterm[0], a:colors.cterm[1], a:colors.cterm[2])
27 " Create default highlighting group
28 execute printf('hi default %s %s %s', group_default, guihl, ctermhl)
30 " Check if the hl group exists
32 redir => hlstatus | exec 'silent hi ' . a:group | redir END
34 " Return if the group isn't cleared
35 if hlstatus !~ 'cleared'
40 " No colors are defined for this group, link to defaults
41 execute printf('hi default link %s %s', a:group, group_default)
43 function! EasyMotion#InitMappings(motions) "{{{
44 for motion in keys(a:motions)
45 call EasyMotion#InitOptions({ 'mapping_' . motion : g:EasyMotion_leader_key . motion })
48 if g:EasyMotion_do_mapping
49 for [motion, fn] in items(a:motions)
50 if empty(g:EasyMotion_mapping_{motion})
54 silent exec 'nnoremap <silent> ' . g:EasyMotion_mapping_{motion} . ' :call EasyMotion#' . fn.name . '(0, ' . fn.dir . ')<CR>'
55 silent exec 'onoremap <silent> ' . g:EasyMotion_mapping_{motion} . ' :call EasyMotion#' . fn.name . '(0, ' . fn.dir . ')<CR>'
56 silent exec 'vnoremap <silent> ' . g:EasyMotion_mapping_{motion} . ' :<C-U>call EasyMotion#' . fn.name . '(1, ' . fn.dir . ')<CR>'
61 " Motion functions {{{
62 function! EasyMotion#F(visualmode, direction) " {{{
63 let char = s:GetSearchChar(a:visualmode)
69 let re = '\C' . escape(char, '.$^~')
71 call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', mode(1))
73 function! EasyMotion#T(visualmode, direction) " {{{
74 let char = s:GetSearchChar(a:visualmode)
81 let re = '\C' . escape(char, '.$^~') . '\zs.'
83 let re = '\C.' . escape(char, '.$^~')
86 call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', mode(1))
88 function! EasyMotion#WB(visualmode, direction) " {{{
89 call s:EasyMotion('\(\<.\|^$\)', a:direction, a:visualmode ? visualmode() : '', '')
91 function! EasyMotion#WBW(visualmode, direction) " {{{
92 call s:EasyMotion('\(\(^\|\s\)\@<=\S\|^$\)', a:direction, a:visualmode ? visualmode() : '', '')
94 function! EasyMotion#E(visualmode, direction) " {{{
95 call s:EasyMotion('\(.\>\|^$\)', a:direction, a:visualmode ? visualmode() : '', mode(1))
97 function! EasyMotion#EW(visualmode, direction) " {{{
98 call s:EasyMotion('\(\S\(\s\|$\)\|^$\)', a:direction, a:visualmode ? visualmode() : '', mode(1))
100 function! EasyMotion#JK(visualmode, direction) " {{{
101 call s:EasyMotion('^\(\w\|\s*\zs\|$\)', a:direction, a:visualmode ? visualmode() : '', '')
103 function! EasyMotion#Search(visualmode, direction) " {{{
104 call s:EasyMotion(@/, a:direction, a:visualmode ? visualmode() : '', '')
107 " Helper functions {{{
108 function! s:Message(message) " {{{
109 echo 'EasyMotion: ' . a:message
111 function! s:Prompt(message) " {{{
113 echo a:message . ': '
116 function! s:VarReset(var, ...) " {{{
117 if ! exists('s:var_reset')
121 let buf = bufname("")
123 if a:0 == 0 && has_key(s:var_reset, a:var)
124 " Reset var to original value
125 call setbufvar(buf, a:var, s:var_reset[a:var])
127 let new_value = a:0 == 1 ? a:1 : ''
129 " Store original value
130 let s:var_reset[a:var] = getbufvar(buf, a:var)
133 call setbufvar(buf, a:var, new_value)
136 function! s:SetLines(lines, key) " {{{
138 " Try to join changes with previous undo block
143 for [line_num, line] in a:lines
144 call setline(line_num, line[a:key])
147 function! s:GetChar() " {{{
154 call s:Message('Cancelled')
161 function! s:GetSearchChar(visualmode) " {{{
162 call s:Prompt('Search for character')
164 let char = s:GetChar()
166 " Check that we have an input char
169 if ! empty(a:visualmode)
170 silent exec 'normal! gv'
179 " Grouping algorithms {{{
180 let s:grouping_algorithms = {
184 " Single-key/closest target priority tree {{{
185 " This algorithm tries to assign one-key jumps to all the targets closest to the cursor.
186 " It works recursively and will work correctly with as few keys as two.
187 function! s:GroupingAlgorithmSCTree(targets, keys)
188 " Prepare variables for working
189 let targets_len = len(a:targets)
190 let keys_len = len(a:keys)
194 let keys = reverse(copy(a:keys))
196 " Semi-recursively count targets {{{
197 " We need to know exactly how many child nodes (targets) this branch will have
198 " in order to pass the correct amount of targets to the recursive function.
200 " Prepare sorted target count list {{{
201 " This is horrible, I know. But dicts aren't sorted in vim, so we need to
202 " work around that. That is done by having one sorted list with key counts,
203 " and a dict which connects the key with the keys_count list.
206 let keys_count_keys = {}
210 call add(keys_count, 0)
212 let keys_count_keys[key] = i
218 let targets_left = targets_len
222 while targets_left > 0
223 " Calculate the amount of child nodes based on the current level
224 let childs_len = (level == 0 ? 1 : (keys_len - 1) )
227 " Add child node count to the keys_count array
228 let keys_count[keys_count_keys[key]] += childs_len
230 " Subtract the child node count
231 let targets_left -= childs_len
234 " Subtract the targets left if we added too many too
235 " many child nodes to the key count
236 let keys_count[keys_count_keys[key]] += targets_left
247 " Create group tree {{{
251 call reverse(keys_count)
253 for key_count in keys_count
255 " We need to create a subgroup
256 " Recurse one level deeper
257 let groups[a:keys[key]] = s:GroupingAlgorithmSCTree(a:targets[i : i + key_count - 1], a:keys)
258 elseif key_count == 1
259 " Assign single target key
260 let groups[a:keys[key]] = a:targets[i]
276 function! s:GroupingAlgorithmOriginal(targets, keys)
277 " Split targets into groups (1 level)
278 let targets_len = len(a:targets)
279 let keys_len = len(a:keys)
286 while root_group < targets_len
287 let groups[a:keys[root_group]] = {}
290 let groups[a:keys[root_group]][key] = a:targets[i]
299 " Flatten the group array
301 let groups = groups[a:keys[0]]
307 " Coord/key dictionary creation {{{
308 function! s:CreateCoordKeyDict(groups, ...)
314 let group_key = a:0 == 1 ? a:1 : ''
316 for [key, item] in items(a:groups)
317 let key = ( ! empty(group_key) ? group_key : key)
322 " The key needs to be zero-padded in order to
324 let dict_key = printf('%05d,%05d', item[0], item[1])
325 let coord_keys[dict_key] = key
327 " We need a sorting list to loop correctly in
328 " PromptUser, dicts are unsorted
329 call add(sort_list, dict_key)
331 " Item is a dict (has children)
332 let coord_key_dict = s:CreateCoordKeyDict(item, key)
334 " Make sure to extend both the sort list and the
336 call extend(sort_list, coord_key_dict[0])
337 call extend(coord_keys, coord_key_dict[1])
343 return [sort_list, coord_keys]
348 function! s:PromptUser(groups) "{{{
349 " If only one possible match, jump directly to it {{{
350 let group_values = values(a:groups)
352 if len(group_values) == 1
355 return group_values[0]
358 " Prepare marker lines {{{
361 let coord_key_dict = s:CreateCoordKeyDict(a:groups)
363 for dict_key in sort(coord_key_dict[0])
364 let target_key = coord_key_dict[1][dict_key]
365 let [line_num, col_num] = split(dict_key, ',')
367 let line_num = str2nr(line_num)
368 let col_num = str2nr(col_num)
370 " Add original line and marker line
371 if ! has_key(lines, line_num)
372 let current_line = getline(line_num)
374 let lines[line_num] = { 'orig': current_line, 'marker': current_line, 'mb_compensation': 0 }
377 " Compensate for byte difference between marker
378 " character and target character
380 " This has to be done in order to match the correct
381 " column; \%c matches the byte column and not display
383 let target_char_len = strlen(matchstr(lines[line_num]['marker'], '\%' . col_num . 'c.'))
384 let target_key_len = strlen(target_key)
386 " Solve multibyte issues by matching the byte column
387 " number instead of the visual column
388 let col_num -= lines[line_num]['mb_compensation']
390 if strlen(lines[line_num]['marker']) > 0
391 " Substitute marker character if line length > 0
392 let lines[line_num]['marker'] = substitute(lines[line_num]['marker'], '\%' . col_num . 'c.', target_key, '')
394 " Set the line to the marker character if the line is empty
395 let lines[line_num]['marker'] = target_key
398 " Add highlighting coordinates
399 call add(hl_coords, '\%' . line_num . 'l\%' . col_num . 'c')
401 " Add marker/target lenght difference for multibyte
403 let lines[line_num]['mb_compensation'] += (target_char_len - target_key_len)
406 let lines_items = items(lines)
408 " Highlight targets {{{
409 let target_hl_id = matchadd(g:EasyMotion_hl_group_target, join(hl_coords, '\|'), 1)
413 " Set lines with markers
414 call s:SetLines(lines_items, 'marker')
418 " Get target character {{{
419 call s:Prompt('Target key')
421 let char = s:GetChar()
424 " Restore original lines
425 call s:SetLines(lines_items, 'orig')
427 " Un-highlight targets {{{
428 if exists('target_hl_id')
429 call matchdelete(target_hl_id)
436 " Check if we have an input char {{{
441 " Check if the input char is valid {{{
442 if ! has_key(a:groups, char)
443 throw 'Invalid target'
447 let target = a:groups[char]
450 " Return target coordinates
453 " Prompt for new target character
454 return s:PromptUser(target)
457 function! s:EasyMotion(regexp, direction, visualmode, mode) " {{{
458 let orig_pos = [line('.'), col('.')]
462 " Reset properties {{{
463 call s:VarReset('&scrolloff', 0)
464 call s:VarReset('&modified', 0)
465 call s:VarReset('&modifiable', 1)
466 call s:VarReset('&readonly', 0)
467 call s:VarReset('&spell', 0)
468 call s:VarReset('&virtualedit', '')
470 " Find motion targets {{{
471 let search_direction = (a:direction == 1 ? 'b' : '')
472 let search_stopline = line(a:direction == 1 ? 'w0' : 'w$')
475 let pos = searchpos(a:regexp, search_direction, search_stopline)
477 " Reached end of search range
483 if foldclosed(pos[0]) != -1
487 call add(targets, pos)
490 let targets_len = len(targets)
496 let GroupingFn = function('s:GroupingAlgorithm' . s:grouping_algorithms[g:EasyMotion_grouping])
497 let groups = GroupingFn(targets, split(g:EasyMotion_keys, '\zs'))
499 " Shade inactive source {{{
500 if g:EasyMotion_do_shade
501 let shade_hl_pos = '\%' . orig_pos[0] . 'l\%'. orig_pos[1] .'c'
505 let shade_hl_re = '\%'. line('w0') .'l\_.*' . shade_hl_pos
508 let shade_hl_re = shade_hl_pos . '\_.*\%'. line('w$') .'l'
511 let shade_hl_id = matchadd(g:EasyMotion_hl_group_shade, shade_hl_re, 0)
515 " Prompt user for target group/character
516 let coords = s:PromptUser(groups)
518 " Update selection {{{
519 if ! empty(a:visualmode)
520 keepjumps call cursor(orig_pos[0], orig_pos[1])
522 exec 'normal! ' . a:visualmode
525 " Handle operator-pending mode {{{
527 " This mode requires that we eat one more
528 " character to the right if we're using
536 " Update cursor position
537 call cursor(orig_pos[0], orig_pos[1])
539 call cursor(coords[0], coords[1])
541 call s:Message('Jumping to [' . coords[0] . ', ' . coords[1] . ']')
545 " Show exception message
546 call s:Message(v:exception)
548 " Restore original cursor position/selection {{{
549 if ! empty(a:visualmode)
550 silent exec 'normal! gv'
552 keepjumps call cursor(orig_pos[0], orig_pos[1])
556 " Restore properties {{{
557 call s:VarReset('&scrolloff')
558 call s:VarReset('&modified')
559 call s:VarReset('&modifiable')
560 call s:VarReset('&readonly')
561 call s:VarReset('&spell')
562 call s:VarReset('&virtualedit')
565 if g:EasyMotion_do_shade && exists('shade_hl_id')
566 call matchdelete(shade_hl_id)
573 " vim: fdm=marker:noet:ts=4:sw=4:sts=4