vim-easymotion/plugin/EasyMotion.vim
Kim Silkebækken a5119b9981 Wrap the main function in a try..catch..finally loop
The script throws exceptions when cancelled or when there's no matches.
The catch block displays the message and restores the cursor position.
The finally block restores buffer properties.
2011-03-28 17:48:09 +02:00

376 lines
9.5 KiB
VimL

" EasyMotion - Vim motions on speed!
"
" Author: Kim Silkebækken <kim.silkebaekken+github@gmail.com>
" Source: https://github.com/Lokaltog/EasyMotion
" Prevent double loading {{{
if exists('g:EasyMotion_loaded')
finish
endif
let g:EasyMotion_loaded = 1
" }}}
" Default configuration {{{
if ! exists('g:EasyMotion_keys') " {{{
let g:EasyMotion_keys = ''
let g:EasyMotion_keys .= 'abcdefghijklmnopqrstuvwxyz'
let g:EasyMotion_keys .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
endif " }}}
if ! exists('g:EasyMotion_target_hl') " {{{
let g:EasyMotion_target_hl = 'EasyMotionTarget'
endif " }}}
if ! exists('g:EasyMotion_shade_hl') " {{{
let g:EasyMotion_shade_hl = 'EasyMotionShade'
endif " }}}
if ! exists('g:EasyMotion_do_shade') " {{{
let g:EasyMotion_shade = 1
endif " }}}
if ! exists('g:EasyMotion_do_mapping') " {{{
let g:EasyMotion_do_mapping = 1
endif " }}}
" Create default highlighting {{{
if ! hlexists(g:EasyMotion_target_hl) " {{{
execute 'hi ' . g:EasyMotion_target_hl . ' ctermfg=red ctermbg=none cterm=bold gui=bold guibg=Red guifg=yellow'
endif " }}}
if ! hlexists(g:EasyMotion_shade_hl) " {{{
execute 'hi ' . g:EasyMotion_shade_hl . ' ctermfg=black ctermbg=none cterm=bold gui=bold guibg=none guifg=black'
endif " }}}
" }}}
" }}}
" Default key mapping {{{
if g:EasyMotion_do_mapping
nnoremap <silent> <Leader>f :call EasyMotionF(0)<CR>
vnoremap <silent> <Leader>f :<C-U>call EasyMotionF(0, visualmode())<CR>
nnoremap <silent> <Leader>F :call EasyMotionF(1)<CR>
vnoremap <silent> <Leader>F :<C-U>call EasyMotionF(1, visualmode())<CR>
nnoremap <silent> <Leader>t :call EasyMotionT(0)<CR>
vnoremap <silent> <Leader>t :<C-U>call EasyMotionT(0, visualmode())<CR>
nnoremap <silent> <Leader>T :call EasyMotionT(1)<CR>
vnoremap <silent> <Leader>T :<C-U>call EasyMotionT(1, visualmode())<CR>
nnoremap <silent> <Leader>w :call EasyMotionW()<CR>
vnoremap <silent> <Leader>w :<C-U>call EasyMotionW(visualmode())<CR>
nnoremap <silent> <Leader>e :call EasyMotionE()<CR>
vnoremap <silent> <Leader>e :<C-U>call EasyMotionE(visualmode())<CR>
nnoremap <silent> <Leader>b :call EasyMotionB()<CR>
vnoremap <silent> <Leader>b :<C-U>call EasyMotionB(visualmode())<CR>
endif
" }}}
" Initialize variables {{{
let s:index_to_key = split(g:EasyMotion_keys, '\zs')
let s:key_to_index = {}
let index = 0
for i in s:index_to_key
let s:key_to_index[i] = index
let index += 1
endfor
let s:var_reset = {}
" }}}
" Motion functions {{{
" F key motions {{{
" Go to {char} to the right or the left
function! EasyMotionF(direction, ...)
call <SID>Prompt('Search for character')
let char = getchar()
let re = '\C' . escape(nr2char(char), '.$^~')
call <SID>EasyMotion(re, a:direction, a:0 > 0 ? a:1 : '')
endfunction
" }}}
" T key motions {{{
" Go to {char} to the right (before) or the left (after)
function! EasyMotionT(direction, ...)
call <SID>Prompt('Search for character')
let char = getchar()
if a:direction == 1
let re = '\C' . escape(nr2char(char), '.$^~') . '\zs.'
else
let re = '\C.' . escape(nr2char(char), '.$^~')
endif
call <SID>EasyMotion(re, a:direction, a:0 > 0 ? a:1 : '')
endfunction
" }}}
" W key motions {{{
" Beginning of word forward
function! EasyMotionW(...)
call <SID>EasyMotion('\<.', 0, a:0 > 0 ? a:1 : '')
endfunction
" }}}
" E key motions {{{
" End of word forward
function! EasyMotionE(...)
call <SID>EasyMotion('.\>', 0, a:0 > 0 ? a:1 : '')
endfunction
" }}}
" B key motions {{{
" Beginning of word backward
function! EasyMotionB(...)
call <SID>EasyMotion('\<.', 1, a:0 > 0 ? a:1 : '')
endfunction
" }}}
" }}}
" Helper functions {{{
function! s:Message(message) " {{{
echo 'EasyMotion: ' . a:message
endfunction " }}}
function! s:Prompt(message) " {{{
echohl Question
echo a:message . ': '
echohl None
endfunction " }}}
function! s:VarReset(var, ...) " {{{
if a:0 == 0 && has_key(s:var_reset, a:var)
" Reset var to original value
call setbufvar(bufname(0), a:var, s:var_reset[a:var])
elseif a:0 == 1
let new_value = a:0 == 1 ? a:1 : ''
" Store original value
let s:var_reset[a:var] = getbufvar(bufname(0), a:var)
" Set new var value
call setbufvar(bufname(0), a:var, new_value)
endif
endfunction " }}}
" }}}
" Core functions {{{
function! s:PromptUser(groups) "{{{
let single_group = len(a:groups) == 1
let targets_len = single_group ? len(a:groups[0]) : len(a:groups)
" Only one possible match {{{
if single_group && targets_len == 1
redraw
return a:groups[0][0]
endif
" }}}
" Prepare marker lines {{{
let lines = {}
let hl_coords = []
let current_group = 0
for group in a:groups
let element = 0
for [line_num, col_num] in group
" Add original line and marker line
if ! has_key(lines, line_num)
let current_line = getline(line_num)
let lines[line_num] = { 'orig': current_line, 'marker': current_line }
endif
" Substitute marker character
let lines[line_num]['marker'] = substitute(lines[line_num]['marker'], '\%' . col_num . 'c.', s:index_to_key[single_group ? element : current_group], '')
" Add highlighting coordinates
call add(hl_coords, '\%' . line_num . 'l\%' . col_num . 'c')
let element += 1
endfor
let current_group += 1
endfor
let lines_items = items(lines)
" }}}
let input_char = ''
try
" Highlight source
let target_hl_id = matchadd(g:EasyMotion_target_hl, join(hl_coords, '\|'), 1)
" Set lines with markers
for [line_num, line] in lines_items
try
undojoin
catch
endtry
call setline(line_num, line['marker'])
endfor
redraw
" Get target/group character
if single_group
call <SID>Prompt('Target character')
else
call <SID>Prompt('Group character')
endif
let input_char = nr2char(getchar())
redraw
finally
" Restore original lines
for [line_num, line] in lines_items
try
undojoin
catch
endtry
call setline(line_num, line['orig'])
endfor
call matchdelete(target_hl_id)
redraw
" Check if the input char is valid
if ! has_key(s:key_to_index, input_char) || s:key_to_index[input_char] >= targets_len
" Invalid input char
return []
else
if single_group
" Return target coordinates
return a:groups[0][s:key_to_index[input_char]]
else
" Prompt for target character
return s:PromptUser([a:groups[s:key_to_index[input_char]]])
endif
endif
endtry
endfunction "}}}
function! s:EasyMotion(regexp, direction, ...) " {{{
let orig_pos = [line('.'), col('.')]
let targets = []
let visualmode = a:0 > 0 ? a:1 : ''
try
" Reset properties
call <SID>VarReset('&scrolloff', 0)
call <SID>VarReset('&modified', 0)
call <SID>VarReset('&modifiable', 1)
call <SID>VarReset('&readonly', 0)
" Find motion targets
while 1
let search_direction = (a:direction == 1 ? 'b' : '')
let search_stopline = line(a:direction == 1 ? 'w0' : 'w$')
let pos = searchpos(a:regexp, search_direction, search_stopline)
" Reached end of search range
if pos == [0, 0]
break
endif
" Skip folded lines
if foldclosed(pos[0]) != -1
continue
endif
call add(targets, pos)
endwhile
let targets_len = len(targets)
let groups_len = len(s:index_to_key)
if targets_len == 0
throw 'No matches'
endif
" Restore cursor position
call setpos('.', [0, orig_pos[0], orig_pos[1]])
" Split targets into key groups {{{
let groups = []
let i = 0
while i < targets_len
call add(groups, targets[i : i + groups_len - 1])
let i += groups_len
endwhile
" }}}
" Too many groups; only display the first ones {{{
if len(groups) > groups_len
call <SID>Message('Only displaying the first matches')
let groups = groups[0 : groups_len - 1]
endif
" }}}
" Shade inactive source
if g:EasyMotion_shade
let shade_hl_pos = '\%' . orig_pos[0] . 'l\%'. orig_pos[1] .'c'
if a:direction == 1
" Backward
let shade_hl_re = '\%'. line('w0') .'l\_.*' . shade_hl_pos
else
" Forward
let shade_hl_re = shade_hl_pos . '\_.*\%'. line('w$') .'l'
endif
let shade_hl_id = matchadd(g:EasyMotion_shade_hl, shade_hl_re, 0)
endif
" Prompt user for target group/character
let coords = <SID>PromptUser(groups)
" Remove shading
if g:EasyMotion_shade
call matchdelete(shade_hl_id)
endif
if len(coords) != 2
throw 'Cancelled'
else
if ! empty(visualmode)
" Store original marks
let m_a = getpos("'a")
let m_b = getpos("'b")
" Store start/end positions
call setpos("'a", [0, orig_pos[0], orig_pos[1]])
call setpos("'b", [0, coords[0], coords[1]])
" Update selection
silent exec 'normal! `a' . visualmode . '`b'
" Restore original marks
call setpos("'a", m_a)
call setpos("'b", m_b)
else
" Update cursor position
call setpos('.', [0, coords[0], coords[1]])
endif
call <SID>Message('Jumping to [' . coords[0] . ', ' . coords[1] . ']')
endif
catch /.*/
redraw
" Show exception message
call <SID>Message(v:exception)
" Restore cursor position/selection
if ! empty(visualmode)
silent exec 'normal! `<' . visualmode . '`>'
else
call setpos('.', [0, orig_pos[0], orig_pos[1]])
endif
finally
redraw
" Restore properties
call <SID>VarReset('&scrolloff')
call <SID>VarReset('&modified')
call <SID>VarReset('&modifiable')
call <SID>VarReset('&readonly')
endtry
endfunction " }}}
" }}}