]>
Commit | Line | Data |
---|---|---|
1 | " EasyMotion - Vim motions on speed! | |
2 | " | |
3 | " Author: Kim Silkebækken <kim.silkebaekken+vim@gmail.com> | |
4 | " Source repository: https://github.com/Lokaltog/vim-easymotion | |
5 | ||
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) | |
11 | endif | |
12 | endfor | |
13 | endfunction " }}} | |
14 | function! EasyMotion#InitHL(group, colors) " {{{ | |
15 | let group_default = a:group . 'Default' | |
16 | ||
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]) | |
23 | else | |
24 | let ctermhl = '' | |
25 | endif | |
26 | ||
27 | " Create default highlighting group | |
28 | execute printf('hi default %s %s %s', group_default, guihl, ctermhl) | |
29 | ||
30 | " Check if the hl group exists | |
31 | if hlexists(a:group) | |
32 | redir => hlstatus | exec 'silent hi ' . a:group | redir END | |
33 | ||
34 | " Return if the group isn't cleared | |
35 | if hlstatus !~ 'cleared' | |
36 | return | |
37 | endif | |
38 | endif | |
39 | ||
40 | " No colors are defined for this group, link to defaults | |
41 | execute printf('hi default link %s %s', a:group, group_default) | |
42 | endfunction " }}} | |
43 | function! EasyMotion#InitMappings(motions) "{{{ | |
44 | for motion in keys(a:motions) | |
45 | call EasyMotion#InitOptions({ 'mapping_' . motion : g:EasyMotion_leader_key . motion }) | |
46 | endfor | |
47 | ||
48 | if g:EasyMotion_do_mapping | |
49 | for [motion, fn] in items(a:motions) | |
50 | if empty(g:EasyMotion_mapping_{motion}) | |
51 | continue | |
52 | endif | |
53 | ||
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>' | |
57 | endfor | |
58 | endif | |
59 | endfunction "}}} | |
60 | " }}} | |
61 | " Motion functions {{{ | |
62 | function! EasyMotion#F(visualmode, direction) " {{{ | |
63 | let char = s:GetSearchChar(a:visualmode) | |
64 | ||
65 | if empty(char) | |
66 | return | |
67 | endif | |
68 | ||
69 | let re = '\C' . escape(char, '.$^~') | |
70 | ||
71 | call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', mode(1)) | |
72 | endfunction " }}} | |
73 | function! EasyMotion#T(visualmode, direction) " {{{ | |
74 | let char = s:GetSearchChar(a:visualmode) | |
75 | ||
76 | if empty(char) | |
77 | return | |
78 | endif | |
79 | ||
80 | if a:direction == 1 | |
81 | let re = '\C' . escape(char, '.$^~') . '\zs.' | |
82 | else | |
83 | let re = '\C.' . escape(char, '.$^~') | |
84 | endif | |
85 | ||
86 | call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', mode(1)) | |
87 | endfunction " }}} | |
88 | function! EasyMotion#WB(visualmode, direction) " {{{ | |
89 | call s:EasyMotion('\(\<.\|^$\)', a:direction, a:visualmode ? visualmode() : '', '') | |
90 | endfunction " }}} | |
91 | function! EasyMotion#WBW(visualmode, direction) " {{{ | |
92 | call s:EasyMotion('\(\(^\|\s\)\@<=\S\|^$\)', a:direction, a:visualmode ? visualmode() : '', '') | |
93 | endfunction " }}} | |
94 | function! EasyMotion#E(visualmode, direction) " {{{ | |
95 | call s:EasyMotion('\(.\>\|^$\)', a:direction, a:visualmode ? visualmode() : '', mode(1)) | |
96 | endfunction " }}} | |
97 | function! EasyMotion#EW(visualmode, direction) " {{{ | |
98 | call s:EasyMotion('\(\S\(\s\|$\)\|^$\)', a:direction, a:visualmode ? visualmode() : '', mode(1)) | |
99 | endfunction " }}} | |
100 | function! EasyMotion#JK(visualmode, direction) " {{{ | |
101 | call s:EasyMotion('^\(\w\|\s*\zs\|$\)', a:direction, a:visualmode ? visualmode() : '', '') | |
102 | endfunction " }}} | |
103 | function! EasyMotion#Search(visualmode, direction) " {{{ | |
104 | call s:EasyMotion(@/, a:direction, a:visualmode ? visualmode() : '', '') | |
105 | endfunction " }}} | |
106 | " }}} | |
107 | " Helper functions {{{ | |
108 | function! s:Message(message) " {{{ | |
109 | echo 'EasyMotion: ' . a:message | |
110 | endfunction " }}} | |
111 | function! s:Prompt(message) " {{{ | |
112 | echohl Question | |
113 | echo a:message . ': ' | |
114 | echohl None | |
115 | endfunction " }}} | |
116 | function! s:VarReset(var, ...) " {{{ | |
117 | if ! exists('s:var_reset') | |
118 | let s:var_reset = {} | |
119 | endif | |
120 | ||
121 | let buf = bufname("") | |
122 | ||
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]) | |
126 | elseif a:0 == 1 | |
127 | let new_value = a:0 == 1 ? a:1 : '' | |
128 | ||
129 | " Store original value | |
130 | let s:var_reset[a:var] = getbufvar(buf, a:var) | |
131 | ||
132 | " Set new var value | |
133 | call setbufvar(buf, a:var, new_value) | |
134 | endif | |
135 | endfunction " }}} | |
136 | function! s:SetLines(lines, key) " {{{ | |
137 | try | |
138 | " Try to join changes with previous undo block | |
139 | undojoin | |
140 | catch | |
141 | endtry | |
142 | ||
143 | for [line_num, line] in a:lines | |
144 | call setline(line_num, line[a:key]) | |
145 | endfor | |
146 | endfunction " }}} | |
147 | function! s:GetChar() " {{{ | |
148 | let char = getchar() | |
149 | ||
150 | if char == 27 | |
151 | " Escape key pressed | |
152 | redraw | |
153 | ||
154 | call s:Message('Cancelled') | |
155 | ||
156 | return '' | |
157 | endif | |
158 | ||
159 | return nr2char(char) | |
160 | endfunction " }}} | |
161 | function! s:GetSearchChar(visualmode) " {{{ | |
162 | call s:Prompt('Search for character') | |
163 | ||
164 | let char = s:GetChar() | |
165 | ||
166 | " Check that we have an input char | |
167 | if empty(char) | |
168 | " Restore selection | |
169 | if ! empty(a:visualmode) | |
170 | silent exec 'normal! gv' | |
171 | endif | |
172 | ||
173 | return '' | |
174 | endif | |
175 | ||
176 | return char | |
177 | endfunction " }}} | |
178 | " }}} | |
179 | " Grouping algorithms {{{ | |
180 | let s:grouping_algorithms = { | |
181 | \ 1: 'SCTree' | |
182 | \ , 2: 'Original' | |
183 | \ } | |
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) | |
191 | ||
192 | let groups = {} | |
193 | ||
194 | let keys = reverse(copy(a:keys)) | |
195 | ||
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. | |
199 | ||
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. | |
204 | ||
205 | let keys_count = [] | |
206 | let keys_count_keys = {} | |
207 | ||
208 | let i = 0 | |
209 | for key in keys | |
210 | call add(keys_count, 0) | |
211 | ||
212 | let keys_count_keys[key] = i | |
213 | ||
214 | let i += 1 | |
215 | endfor | |
216 | " }}} | |
217 | ||
218 | let targets_left = targets_len | |
219 | let level = 0 | |
220 | let i = 0 | |
221 | ||
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) ) | |
225 | ||
226 | for key in keys | |
227 | " Add child node count to the keys_count array | |
228 | let keys_count[keys_count_keys[key]] += childs_len | |
229 | ||
230 | " Subtract the child node count | |
231 | let targets_left -= childs_len | |
232 | ||
233 | if targets_left <= 0 | |
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 | |
237 | ||
238 | break | |
239 | endif | |
240 | ||
241 | let i += 1 | |
242 | endfor | |
243 | ||
244 | let level += 1 | |
245 | endwhile | |
246 | " }}} | |
247 | " Create group tree {{{ | |
248 | let i = 0 | |
249 | let key = 0 | |
250 | ||
251 | call reverse(keys_count) | |
252 | ||
253 | for key_count in keys_count | |
254 | if key_count > 1 | |
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] | |
261 | else | |
262 | " No target | |
263 | continue | |
264 | endif | |
265 | ||
266 | let key += 1 | |
267 | let i += key_count | |
268 | endfor | |
269 | " }}} | |
270 | ||
271 | " Finally! | |
272 | return groups | |
273 | endfunction | |
274 | " }}} | |
275 | " Original {{{ | |
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) | |
280 | ||
281 | let groups = {} | |
282 | ||
283 | let i = 0 | |
284 | let root_group = 0 | |
285 | try | |
286 | while root_group < targets_len | |
287 | let groups[a:keys[root_group]] = {} | |
288 | ||
289 | for key in a:keys | |
290 | let groups[a:keys[root_group]][key] = a:targets[i] | |
291 | ||
292 | let i += 1 | |
293 | endfor | |
294 | ||
295 | let root_group += 1 | |
296 | endwhile | |
297 | catch | endtry | |
298 | ||
299 | " Flatten the group array | |
300 | if len(groups) == 1 | |
301 | let groups = groups[a:keys[0]] | |
302 | endif | |
303 | ||
304 | return groups | |
305 | endfunction | |
306 | " }}} | |
307 | " Coord/key dictionary creation {{{ | |
308 | function! s:CreateCoordKeyDict(groups, ...) | |
309 | " Dict structure: | |
310 | " 1,2 : a | |
311 | " 2,3 : b | |
312 | let sort_list = [] | |
313 | let coord_keys = {} | |
314 | let group_key = a:0 == 1 ? a:1 : '' | |
315 | ||
316 | for [key, item] in items(a:groups) | |
317 | let key = ( ! empty(group_key) ? group_key : key) | |
318 | ||
319 | if type(item) == 3 | |
320 | " Destination coords | |
321 | ||
322 | " The key needs to be zero-padded in order to | |
323 | " sort correctly | |
324 | let dict_key = printf('%05d,%05d', item[0], item[1]) | |
325 | let coord_keys[dict_key] = key | |
326 | ||
327 | " We need a sorting list to loop correctly in | |
328 | " PromptUser, dicts are unsorted | |
329 | call add(sort_list, dict_key) | |
330 | else | |
331 | " Item is a dict (has children) | |
332 | let coord_key_dict = s:CreateCoordKeyDict(item, key) | |
333 | ||
334 | " Make sure to extend both the sort list and the | |
335 | " coord key dict | |
336 | call extend(sort_list, coord_key_dict[0]) | |
337 | call extend(coord_keys, coord_key_dict[1]) | |
338 | endif | |
339 | ||
340 | unlet item | |
341 | endfor | |
342 | ||
343 | return [sort_list, coord_keys] | |
344 | endfunction | |
345 | " }}} | |
346 | " }}} | |
347 | " Core functions {{{ | |
348 | function! s:PromptUser(groups) "{{{ | |
349 | " If only one possible match, jump directly to it {{{ | |
350 | let group_values = values(a:groups) | |
351 | ||
352 | if len(group_values) == 1 | |
353 | redraw | |
354 | ||
355 | return group_values[0] | |
356 | endif | |
357 | " }}} | |
358 | " Prepare marker lines {{{ | |
359 | let lines = {} | |
360 | let hl_coords = [] | |
361 | let coord_key_dict = s:CreateCoordKeyDict(a:groups) | |
362 | ||
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, ',') | |
366 | ||
367 | let line_num = str2nr(line_num) | |
368 | let col_num = str2nr(col_num) | |
369 | ||
370 | " Add original line and marker line | |
371 | if ! has_key(lines, line_num) | |
372 | let current_line = getline(line_num) | |
373 | ||
374 | let lines[line_num] = { 'orig': current_line, 'marker': current_line, 'mb_compensation': 0 } | |
375 | endif | |
376 | ||
377 | " Compensate for byte difference between marker | |
378 | " character and target character | |
379 | " | |
380 | " This has to be done in order to match the correct | |
381 | " column; \%c matches the byte column and not display | |
382 | " column. | |
383 | let target_char_len = strlen(matchstr(lines[line_num]['marker'], '\%' . col_num . 'c.')) | |
384 | let target_key_len = strlen(target_key) | |
385 | ||
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'] | |
389 | ||
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, '') | |
393 | else | |
394 | " Set the line to the marker character if the line is empty | |
395 | let lines[line_num]['marker'] = target_key | |
396 | endif | |
397 | ||
398 | " Add highlighting coordinates | |
399 | call add(hl_coords, '\%' . line_num . 'l\%' . col_num . 'c') | |
400 | ||
401 | " Add marker/target lenght difference for multibyte | |
402 | " compensation | |
403 | let lines[line_num]['mb_compensation'] += (target_char_len - target_key_len) | |
404 | endfor | |
405 | ||
406 | let lines_items = items(lines) | |
407 | " }}} | |
408 | " Highlight targets {{{ | |
409 | let target_hl_id = matchadd(g:EasyMotion_hl_group_target, join(hl_coords, '\|'), 1) | |
410 | " }}} | |
411 | ||
412 | try | |
413 | " Set lines with markers | |
414 | call s:SetLines(lines_items, 'marker') | |
415 | ||
416 | redraw | |
417 | ||
418 | " Get target character {{{ | |
419 | call s:Prompt('Target key') | |
420 | ||
421 | let char = s:GetChar() | |
422 | " }}} | |
423 | finally | |
424 | " Restore original lines | |
425 | call s:SetLines(lines_items, 'orig') | |
426 | ||
427 | " Un-highlight targets {{{ | |
428 | if exists('target_hl_id') | |
429 | call matchdelete(target_hl_id) | |
430 | endif | |
431 | " }}} | |
432 | ||
433 | redraw | |
434 | endtry | |
435 | ||
436 | " Check if we have an input char {{{ | |
437 | if empty(char) | |
438 | throw 'Cancelled' | |
439 | endif | |
440 | " }}} | |
441 | " Check if the input char is valid {{{ | |
442 | if ! has_key(a:groups, char) | |
443 | throw 'Invalid target' | |
444 | endif | |
445 | " }}} | |
446 | ||
447 | let target = a:groups[char] | |
448 | ||
449 | if type(target) == 3 | |
450 | " Return target coordinates | |
451 | return target | |
452 | else | |
453 | " Prompt for new target character | |
454 | return s:PromptUser(target) | |
455 | endif | |
456 | endfunction "}}} | |
457 | function! s:EasyMotion(regexp, direction, visualmode, mode) " {{{ | |
458 | let orig_pos = [line('.'), col('.')] | |
459 | let targets = [] | |
460 | ||
461 | try | |
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', '') | |
469 | " }}} | |
470 | " Find motion targets {{{ | |
471 | let search_direction = (a:direction == 1 ? 'b' : '') | |
472 | let search_stopline = line(a:direction == 1 ? 'w0' : 'w$') | |
473 | ||
474 | while 1 | |
475 | let pos = searchpos(a:regexp, search_direction, search_stopline) | |
476 | ||
477 | " Reached end of search range | |
478 | if pos == [0, 0] | |
479 | break | |
480 | endif | |
481 | ||
482 | " Skip folded lines | |
483 | if foldclosed(pos[0]) != -1 | |
484 | continue | |
485 | endif | |
486 | ||
487 | call add(targets, pos) | |
488 | endwhile | |
489 | ||
490 | let targets_len = len(targets) | |
491 | if targets_len == 0 | |
492 | throw 'No matches' | |
493 | endif | |
494 | " }}} | |
495 | ||
496 | let GroupingFn = function('s:GroupingAlgorithm' . s:grouping_algorithms[g:EasyMotion_grouping]) | |
497 | let groups = GroupingFn(targets, split(g:EasyMotion_keys, '\zs')) | |
498 | ||
499 | " Shade inactive source {{{ | |
500 | if g:EasyMotion_do_shade | |
501 | let shade_hl_pos = '\%' . orig_pos[0] . 'l\%'. orig_pos[1] .'c' | |
502 | ||
503 | if a:direction == 1 | |
504 | " Backward | |
505 | let shade_hl_re = '\%'. line('w0') .'l\_.*' . shade_hl_pos | |
506 | else | |
507 | " Forward | |
508 | let shade_hl_re = shade_hl_pos . '\_.*\%'. line('w$') .'l' | |
509 | endif | |
510 | ||
511 | let shade_hl_id = matchadd(g:EasyMotion_hl_group_shade, shade_hl_re, 0) | |
512 | endif | |
513 | " }}} | |
514 | ||
515 | " Prompt user for target group/character | |
516 | let coords = s:PromptUser(groups) | |
517 | ||
518 | " Update selection {{{ | |
519 | if ! empty(a:visualmode) | |
520 | keepjumps call cursor(orig_pos[0], orig_pos[1]) | |
521 | ||
522 | exec 'normal! ' . a:visualmode | |
523 | endif | |
524 | " }}} | |
525 | " Handle operator-pending mode {{{ | |
526 | if a:mode == 'no' | |
527 | " This mode requires that we eat one more | |
528 | " character to the right if we're using | |
529 | " a forward motion | |
530 | if a:direction != 1 | |
531 | let coords[1] += 1 | |
532 | endif | |
533 | endif | |
534 | " }}} | |
535 | ||
536 | " Update cursor position | |
537 | call cursor(orig_pos[0], orig_pos[1]) | |
538 | mark ' | |
539 | call cursor(coords[0], coords[1]) | |
540 | ||
541 | call s:Message('Jumping to [' . coords[0] . ', ' . coords[1] . ']') | |
542 | catch | |
543 | redraw | |
544 | ||
545 | " Show exception message | |
546 | call s:Message(v:exception) | |
547 | ||
548 | " Restore original cursor position/selection {{{ | |
549 | if ! empty(a:visualmode) | |
550 | silent exec 'normal! gv' | |
551 | else | |
552 | keepjumps call cursor(orig_pos[0], orig_pos[1]) | |
553 | endif | |
554 | " }}} | |
555 | finally | |
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') | |
563 | " }}} | |
564 | " Remove shading {{{ | |
565 | if g:EasyMotion_do_shade && exists('shade_hl_id') | |
566 | call matchdelete(shade_hl_id) | |
567 | endif | |
568 | " }}} | |
569 | endtry | |
570 | endfunction " }}} | |
571 | " }}} | |
572 | ||
573 | " vim: fdm=marker:noet:ts=4:sw=4:sts=4 |