1630 lines
56 KiB
VimL
1630 lines
56 KiB
VimL
scriptencoding utf-8
|
|
" EasyMotion - Vim motions on speed!
|
|
"
|
|
" Author: Kim Silkebækken <kim.silkebaekken+vim@gmail.com>
|
|
" haya14busa <hayabusa1419@gmail.com>
|
|
" Source: https://github.com/easymotion/vim-easymotion
|
|
"=============================================================================
|
|
" Saving 'cpoptions' {{{
|
|
let s:save_cpo = &cpo
|
|
set cpo&vim
|
|
" }}}
|
|
|
|
let s:TRUE = !0
|
|
let s:FALSE = 0
|
|
let s:DIRECTION = { 'forward': 0, 'backward': 1, 'bidirection': 2}
|
|
|
|
|
|
" Init: {{{
|
|
let s:loaded = s:FALSE
|
|
function! EasyMotion#init()
|
|
if s:loaded
|
|
return
|
|
endif
|
|
let s:loaded = s:TRUE
|
|
call EasyMotion#highlight#load()
|
|
" Store previous motion info
|
|
let s:previous = {}
|
|
" Store previous operator-pending motion info for '.' repeat
|
|
let s:dot_repeat = {}
|
|
" Prepare 1-key Migemo Dictionary
|
|
let s:migemo_dicts = {}
|
|
let s:EasyMotion_is_active = 0
|
|
call EasyMotion#reset()
|
|
" Anywhere regular expression: {{{
|
|
let re = '\v' .
|
|
\ '(<.|^$)' . '|' .
|
|
\ '(.>|^$)' . '|' .
|
|
\ '(\l)\zs(\u)' . '|' .
|
|
\ '(_\zs.)' . '|' .
|
|
\ '(#\zs.)'
|
|
" 1. word
|
|
" 2. end of word
|
|
" 3. CamelCase
|
|
" 4. after '_' hoge_foo
|
|
" 5. after '#' hoge#foo
|
|
let g:EasyMotion_re_anywhere = get(g:, 'EasyMotion_re_anywhere', re)
|
|
|
|
" Anywhere regular expression within line:
|
|
let re = '\v' .
|
|
\ '(<.|^$)' . '|' .
|
|
\ '(.>|^$)' . '|' .
|
|
\ '(\l)\zs(\u)' . '|' .
|
|
\ '(_\zs.)' . '|' .
|
|
\ '(#\zs.)'
|
|
let g:EasyMotion_re_line_anywhere = get(g:, 'EasyMotion_re_line_anywhere', re)
|
|
"}}}
|
|
" For other plugin
|
|
let s:EasyMotion_is_cancelled = 0
|
|
" 0 -> Success
|
|
" 1 -> Cancel
|
|
let g:EasyMotion_ignore_exception = 0
|
|
return ""
|
|
endfunction
|
|
"}}}
|
|
" Reset: {{{
|
|
function! EasyMotion#reset()
|
|
let s:flag = {
|
|
\ 'within_line' : 0,
|
|
\ 'dot_repeat' : 0,
|
|
\ 'regexp' : 0,
|
|
\ 'bd_t' : 0,
|
|
\ 'find_bd' : 0,
|
|
\ 'linewise' : 0,
|
|
\ 'count_dot_repeat' : 0,
|
|
\ }
|
|
" regexp: -> regular expression
|
|
" This value is used when multi input find motion. If this values is
|
|
" 1, input text is treated as regexp.(Default: escaped)
|
|
" bd_t: -> bi-directional 't' motion
|
|
" This value is used to re-define regexp only for bi-directional 't'
|
|
" motion
|
|
" find_bd: -> bi-directional find motion
|
|
" This value is used to recheck the motion is inclusive or exclusive
|
|
" because 'f' & 't' forward find motion is inclusive, but 'F' & 'T'
|
|
" backward find motion is exclusive
|
|
" count_dot_repeat: -> dot repeat with count
|
|
" https://github.com/easymotion/vim-easymotion/issues/164
|
|
let s:current = {
|
|
\ 'is_operator' : 0,
|
|
\ 'is_search' : 0,
|
|
\ 'dot_repeat_target_cnt' : 0,
|
|
\ 'dot_prompt_user_cnt' : 0,
|
|
\ 'changedtick' : 0,
|
|
\ }
|
|
" \ 'start_position' : [],
|
|
" \ 'cursor_position' : [],
|
|
|
|
" is_operator:
|
|
" Store is_operator value first because mode(1) value will be
|
|
" changed by some operation.
|
|
" dot_* :
|
|
" These values are used when '.' repeat for automatically
|
|
" select marker/label characters.(Using count avoid recursive
|
|
" prompt)
|
|
" changedtick:
|
|
" :h b:changedtick
|
|
" This value is used to avoid side effect of overwriting buffer text
|
|
" which will change b:changedtick value. To overwrite g:repeat_tick
|
|
" value(defined tpope/vim-repeat), I can avoid this side effect of
|
|
" conflicting with tpope/vim-repeat
|
|
" start_position:
|
|
" Original, start cursor position.
|
|
" cursor_position:
|
|
" Usually, this values is same with start_position, but in
|
|
" visualmode and 'n' key motion, this value could be different.
|
|
return ""
|
|
endfunction "}}}
|
|
|
|
" Motion Functions: {{{
|
|
" -- Find Motion -------------------------
|
|
" Note: {{{
|
|
" num_strokes:
|
|
" The number of input characters. Currently provide 1, 2, or -1.
|
|
" '-1' means no limit.
|
|
" visualmode:
|
|
" Vim script couldn't detect the function is called in visual mode by
|
|
" mode(1), so tell whether it is in visual mode by argument explicitly
|
|
" direction:
|
|
" 0 -> forward
|
|
" 1 -> backward
|
|
" 2 -> bi-direction (handle forward & backward at the same time) }}}
|
|
function! EasyMotion#S(num_strokes, visualmode, direction) " {{{
|
|
if a:direction == 1
|
|
let is_inclusive = 0
|
|
else
|
|
" Note: Handle bi-direction later because 'f' motion is inclusive but
|
|
" 'F' motion is exclusive
|
|
let is_inclusive = mode(1) ==# 'no' ? 1 : 0
|
|
endif
|
|
let s:flag.find_bd = a:direction == 2 ? 1 : 0
|
|
let re = s:findMotion(a:num_strokes, a:direction)
|
|
if s:handleEmpty(re, a:visualmode) | return | endif
|
|
call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', is_inclusive)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#OverwinF(num_strokes) " {{{
|
|
let re = s:findMotion(a:num_strokes, s:DIRECTION.bidirection)
|
|
call EasyMotion#reset()
|
|
if re isnot# ''
|
|
return EasyMotion#overwin#move(re)
|
|
endif
|
|
endfunction "}}}
|
|
function! EasyMotion#T(num_strokes, visualmode, direction) " {{{
|
|
if a:direction == 1
|
|
let is_inclusive = 0
|
|
else
|
|
" Note: Handle bi-direction later because 't' motion is inclusive but
|
|
" 'T' motion is exclusive
|
|
let is_inclusive = mode(1) ==# 'no' ? 1 : 0
|
|
endif
|
|
let s:flag.find_bd = a:direction == 2 ? 1 : 0
|
|
let re = s:findMotion(a:num_strokes, a:direction)
|
|
if s:handleEmpty(re, a:visualmode) | return | endif
|
|
if a:direction == 2
|
|
let s:flag.bd_t = 1
|
|
elseif a:direction == 1
|
|
let re = s:convert_t_regexp(re, 1) " backward
|
|
else
|
|
let re = s:convert_t_regexp(re, 0) " forward
|
|
endif
|
|
call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', is_inclusive)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" -- Word Motion -------------------------
|
|
function! EasyMotion#WB(visualmode, direction) " {{{
|
|
"FIXME: inconsistent with default vim motion
|
|
"FIXED: -> EasyMotion#WBK()
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
call s:EasyMotion('\(\<.\|^$\)', a:direction, a:visualmode ? visualmode() : '', 0)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#WBW(visualmode, direction) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let regex_without_file_ends = '\v(^|\s)\zs\S|^$'
|
|
let regex = l:regex_without_file_ends
|
|
\ . (a:direction == 1 ? '' : '|%$')
|
|
\ . (a:direction == 0 ? '' : '|%^')
|
|
call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#WBK(visualmode, direction) " {{{
|
|
" vim's iskeyword style word motion
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let regex_without_file_ends = '\v<|^\S|\s\zs\S|>\zs\S|^$'
|
|
let regex = l:regex_without_file_ends
|
|
\ . (a:direction == 1 ? '' : '|%$')
|
|
\ . (a:direction == 0 ? '' : '|%^')
|
|
call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#E(visualmode, direction) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let is_inclusive = mode(1) ==# 'no' ? 1 : 0
|
|
call s:EasyMotion('\(.\>\|^$\)', a:direction, a:visualmode ? visualmode() : '', is_inclusive)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#EW(visualmode, direction) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let is_inclusive = mode(1) ==# 'no' ? 1 : 0
|
|
" Note: The stopping positions for 'E' and 'gE' differs. Thus, the regex
|
|
" for direction==2 cannot be the same in both directions. This will be
|
|
" ignored.
|
|
let regex_stub = '\v\S(\s|$)'
|
|
let regex = l:regex_stub
|
|
\ . (a:direction == 0 ? '' : '|^$|%^')
|
|
\ . (a:direction == 1 ? '' : '|%$')
|
|
call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#EK(visualmode, direction) " {{{
|
|
" vim's iskeyword style word motion
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let is_inclusive = mode(1) ==# 'no' ? 1 : 0
|
|
" Note: The stopping positions for 'e' and 'ge' differs. Thus, the regex
|
|
" for direction==2 cannot be the same in both directions. This will be
|
|
" ignored.
|
|
let regex_stub = '\v.\ze>|\S\ze\s*$|\S\ze\s|\k\zs>\S\ze|\S<'
|
|
let regex = l:regex_stub
|
|
\ . (a:direction == 0 ? '' : '|^$|%^')
|
|
\ . (a:direction == 1 ? '' : '|%$')
|
|
call s:EasyMotion(l:regex, a:direction, a:visualmode ? visualmode() : '', 0)
|
|
|
|
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" -- JK Motion ---------------------------
|
|
function! EasyMotion#JK(visualmode, direction) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let s:flag.linewise = 1
|
|
|
|
if g:EasyMotion_startofline
|
|
call s:EasyMotion('^\(\w\|\s*\zs\|$\)', a:direction, a:visualmode ? visualmode() : '', 0)
|
|
else
|
|
let vcol = EasyMotion#helper#vcol('.')
|
|
let pattern = printf('^.\{-}\zs\(\%%<%dv.\%%>%dv\|$\)', vcol + 1, vcol)
|
|
call s:EasyMotion(pattern, a:direction, a:visualmode ? visualmode() : '', 0)
|
|
endif
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#Sol(visualmode, direction) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let s:flag.linewise = 1
|
|
call s:EasyMotion('^\(.\|$\)', a:direction, a:visualmode ? visualmode() : '', '')
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#Eol(visualmode, direction) " {{{
|
|
let s:flag.linewise = 1
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
call s:EasyMotion('\(\w\|\s*\zs\|.\|^\)$', a:direction, a:visualmode ? visualmode() : '', '')
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" -- Search Motion -----------------------
|
|
function! EasyMotion#Search(visualmode, direction, respect_direction) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let search_direction = a:respect_direction ?
|
|
\ (a:direction == 1 ? v:searchforward : 1-v:searchforward) :
|
|
\ (a:direction)
|
|
call s:EasyMotion(@/, search_direction, a:visualmode ? visualmode() : '', 0)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" -- JumpToAnywhere Motion ---------------
|
|
function! EasyMotion#JumpToAnywhere(visualmode, direction) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
call s:EasyMotion( g:EasyMotion_re_anywhere, a:direction, a:visualmode ? visualmode() : '', 0)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" -- Line Motion -------------------------
|
|
function! EasyMotion#SL(num_strokes, visualmode, direction) " {{{
|
|
let s:flag.within_line = 1
|
|
call EasyMotion#S(a:num_strokes, a:visualmode, a:direction)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#TL(num_strokes, visualmode, direction) " {{{
|
|
let s:flag.within_line = 1
|
|
call EasyMotion#T(a:num_strokes, a:visualmode, a:direction)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#WBL(visualmode, direction) " {{{
|
|
let s:flag.within_line = 1
|
|
call EasyMotion#WBK(a:visualmode, a:direction)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#EL(visualmode, direction) " {{{
|
|
let s:flag.within_line = 1
|
|
call EasyMotion#EK(a:visualmode, a:direction)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#LineAnywhere(visualmode, direction) " {{{
|
|
let s:flag.within_line = 1
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let re = g:EasyMotion_re_line_anywhere
|
|
call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', 0)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" -- User Motion -------------------------
|
|
let s:config = {
|
|
\ 'pattern': '',
|
|
\ 'visualmode': s:FALSE,
|
|
\ 'direction': s:DIRECTION.forward,
|
|
\ 'inclusive': s:FALSE,
|
|
\ 'accept_cursor_pos': s:FALSE,
|
|
\ 'overwin': s:FALSE
|
|
\ }
|
|
|
|
function! s:default_config() abort
|
|
let c = copy(s:config)
|
|
let m = mode(1)
|
|
let c.inclusive = m ==# 'no' ? s:TRUE : s:FALSE
|
|
return c
|
|
endfunction
|
|
|
|
function! EasyMotion#go(...) abort
|
|
let c = extend(s:default_config(), get(a:, 1, {}))
|
|
if c.overwin
|
|
return EasyMotion#overwin#move(c.pattern)
|
|
else
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
call s:EasyMotion(c.pattern, c.direction, c.visualmode ? visualmode() : '', c.inclusive, c)
|
|
return s:EasyMotion_is_cancelled
|
|
endif
|
|
endfunction
|
|
function! EasyMotion#User(pattern, visualmode, direction, inclusive, ...) " {{{
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
let is_inclusive = mode(1) ==# 'no' ? a:inclusive : 0
|
|
let re = a:pattern
|
|
call s:EasyMotion(re, a:direction, a:visualmode ? visualmode() : '', is_inclusive, get(a:, 1, {}))
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" -- Repeat Motion -----------------------
|
|
function! EasyMotion#Repeat(visualmode) " {{{
|
|
" Repeat previous motion with previous targets
|
|
if !has_key(s:previous, 'regexp')
|
|
call s:Message("Previous targets doesn't exist")
|
|
let s:EasyMotion_is_cancelled = 1
|
|
return s:EasyMotion_is_cancelled
|
|
endif
|
|
let re = s:previous.regexp
|
|
let direction = s:previous.direction
|
|
let s:flag.within_line = s:previous.line_flag
|
|
let s:flag.bd_t = s:previous.bd_t_flag
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
" FIXME: is_inclusive value is inappropriate but handling this value is
|
|
" difficult and priorities is low because this motion maybe used usually
|
|
" as a 'normal' motion.
|
|
let is_inclusive = mode(1) ==# 'no' ? 1 : 0
|
|
|
|
call s:EasyMotion(re, direction, a:visualmode ? visualmode() : '', is_inclusive)
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#DotRepeat() " {{{
|
|
let cnt = v:count1 " avoid overwriting
|
|
|
|
" Repeat previous '.' motion with previous targets and operator
|
|
if !has_key(s:dot_repeat, 'regexp')
|
|
call s:Message("Previous motion doesn't exist")
|
|
let s:EasyMotion_is_cancelled = 1
|
|
return s:EasyMotion_is_cancelled
|
|
endif
|
|
|
|
let re = s:dot_repeat.regexp
|
|
let direction = s:dot_repeat.direction
|
|
let is_inclusive = s:dot_repeat.is_inclusive
|
|
|
|
for i in range(cnt)
|
|
" s:EasyMotion() always call reset s:flag & s:current
|
|
let s:flag.dot_repeat = 1
|
|
let s:flag.within_line = s:dot_repeat.line_flag
|
|
let s:flag.bd_t = s:dot_repeat.bd_t_flag
|
|
let s:current.is_operator = 1
|
|
|
|
let s:flag.count_dot_repeat = (i > 0 ? 1 : 0)
|
|
silent call s:EasyMotion(re, direction, 0, is_inclusive)
|
|
endfor
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
function! EasyMotion#NextPrevious(visualmode, direction) " {{{
|
|
" Move next/previous destination using previous motion regexp
|
|
let cnt = v:count1 " avoid overwriting
|
|
if !has_key(s:previous, 'regexp')
|
|
call s:Message("Previous targets doesn't exist")
|
|
let s:EasyMotion_is_cancelled = 1
|
|
return s:EasyMotion_is_cancelled
|
|
endif
|
|
let re = s:previous.regexp
|
|
let search_direction = (a:direction == 1 ? 'b' : '')
|
|
|
|
if g:EasyMotion_move_highlight
|
|
call EasyMotion#highlight#attach_autocmd()
|
|
call EasyMotion#highlight#add_highlight(re, g:EasyMotion_hl_move)
|
|
endif
|
|
|
|
if ! empty(a:visualmode)
|
|
" FIXME: blink highlight
|
|
silent exec 'normal! gv'
|
|
endif
|
|
|
|
" Mark jump-list
|
|
if cnt > 1
|
|
" Consider Next/Previous motions as jump motion :h jump-motion
|
|
" Note: It should add jumplist even if the count isn't given
|
|
" considering vim's default behavior of `n` & `N`, but just
|
|
" I don't want to do it without the count. Should I add a
|
|
" option?
|
|
normal! m`
|
|
endif
|
|
|
|
" Jump
|
|
" @vimlint(EVL102, 1, l:_)
|
|
for _ in range(cnt)
|
|
keepjumps call searchpos(re, search_direction)
|
|
endfor
|
|
|
|
normal! zv
|
|
|
|
call EasyMotion#reset()
|
|
" -- Activate EasyMotion ----------------- {{{
|
|
let s:EasyMotion_is_active = 1
|
|
call EasyMotion#attach_active_autocmd() "}}}
|
|
return s:EasyMotion_is_cancelled
|
|
endfunction " }}}
|
|
" }}}
|
|
" Helper Functions: {{{
|
|
" -- Message -----------------------------
|
|
function! s:Message(message) " {{{
|
|
if g:EasyMotion_verbose
|
|
echo 'EasyMotion: ' . a:message
|
|
else
|
|
" Make the current message dissapear
|
|
echo ''
|
|
" redraw
|
|
endif
|
|
endfunction " }}}
|
|
function! s:Prompt(message) " {{{
|
|
echohl Question
|
|
echo a:message . ': '
|
|
echohl None
|
|
endfunction " }}}
|
|
function! s:Throw(message) "{{{
|
|
throw 'EasyMotion: ' . a:message
|
|
endfunction "}}}
|
|
|
|
" -- Save & Restore values ---------------
|
|
function! s:SaveValue() "{{{
|
|
if ! s:current.is_search
|
|
call EasyMotion#helper#VarReset('&scrolloff', 0)
|
|
endif
|
|
call EasyMotion#helper#VarReset('&modified', 0)
|
|
call EasyMotion#helper#VarReset('&modifiable', 1)
|
|
call EasyMotion#helper#VarReset('&readonly', 0)
|
|
call EasyMotion#helper#VarReset('&spell', 0)
|
|
call EasyMotion#helper#VarReset('&virtualedit', '')
|
|
" if &foldmethod !=# 'expr'
|
|
call EasyMotion#helper#VarReset('&foldmethod', 'manual')
|
|
" endif
|
|
endfunction "}}}
|
|
function! s:RestoreValue() "{{{
|
|
call EasyMotion#helper#VarReset('&scrolloff')
|
|
call EasyMotion#helper#VarReset('&modified')
|
|
call EasyMotion#helper#VarReset('&modifiable')
|
|
call EasyMotion#helper#VarReset('&readonly')
|
|
call EasyMotion#helper#VarReset('&spell')
|
|
call EasyMotion#helper#VarReset('&virtualedit')
|
|
" if &foldmethod !=# 'expr'
|
|
call EasyMotion#helper#VarReset('&foldmethod')
|
|
" endif
|
|
endfunction "}}}
|
|
function! s:turn_off_hl_error() "{{{
|
|
let s:error_hl = EasyMotion#highlight#capture('Error')
|
|
call EasyMotion#highlight#turn_off(s:error_hl)
|
|
let s:matchparen_hl = EasyMotion#highlight#capture('MatchParen')
|
|
call EasyMotion#highlight#turn_off(s:matchparen_hl)
|
|
endfunction "}}}
|
|
function! s:turn_on_hl_error() "{{{
|
|
if exists('s:error_hl')
|
|
call EasyMotion#highlight#turn_on(s:error_hl)
|
|
unlet s:error_hl
|
|
endif
|
|
|
|
if exists('s:matchparen_hl')
|
|
call EasyMotion#highlight#turn_on(s:matchparen_hl)
|
|
unlet s:matchparen_hl
|
|
endif
|
|
endfunction "}}}
|
|
|
|
" -- Draw --------------------------------
|
|
function! s:SetLines(lines, key) " {{{
|
|
for [line_num, line] in a:lines
|
|
keepjumps call setline(line_num, line[a:key])
|
|
endfor
|
|
endfunction " }}}
|
|
|
|
" -- Get characters from user input ------
|
|
function! s:GetChar(...) abort "{{{
|
|
let mode = get(a:, 1, 0)
|
|
while 1
|
|
" Workaround for https://github.com/osyo-manga/vital-over/issues/53
|
|
try
|
|
let char = call('getchar', a:000)
|
|
catch /^Vim:Interrupt$/
|
|
let char = 3 " <C-c>
|
|
endtry
|
|
if char == 27 || char == 3
|
|
" Escape or <C-c> key pressed
|
|
redraw
|
|
call s:Message('Cancelled')
|
|
return ''
|
|
endif
|
|
" Workaround for the <expr> mappings
|
|
if string(char) !=# "\x80\xfd`"
|
|
return mode == 1 ? !!char
|
|
\ : type(char) == type(0) ? nr2char(char) : char
|
|
endif
|
|
endwhile
|
|
endfunction "}}}
|
|
|
|
" -- Find Motion Helper ------------------
|
|
function! s:findMotion(num_strokes, direction) "{{{
|
|
" Find Motion: S,F,T
|
|
let s:current.is_operator = mode(1) ==# 'no' ? 1: 0
|
|
" store cursor pos because 'n' key find motion could be jump to offscreen
|
|
let s:current.original_position = [line('.'), col('.')]
|
|
let s:current.is_search = a:num_strokes == -1 ? 1: 0
|
|
let s:flag.regexp = a:num_strokes == -1 ? 1 : 0 " TODO: remove?
|
|
|
|
if g:EasyMotion_add_search_history && a:num_strokes == -1
|
|
let s:previous['input'] = @/
|
|
else
|
|
let s:previous['input'] = get(s:previous, 'input', '')
|
|
endif
|
|
let input = EasyMotion#command_line#GetInput(
|
|
\ a:num_strokes, s:previous.input, a:direction)
|
|
let s:previous['input'] = input
|
|
|
|
" Check that we have an input char
|
|
if empty(input)
|
|
return ''
|
|
endif
|
|
|
|
let re = s:convertRegep(input)
|
|
|
|
if g:EasyMotion_add_search_history && a:num_strokes == -1
|
|
let history_re = substitute(re, '\\c\|\\C', '', '')
|
|
let @/ = history_re "For textobject: 'gn'
|
|
call histadd('search', history_re)
|
|
endif
|
|
|
|
return re
|
|
endfunction "}}}
|
|
function! s:convertRegep(input) "{{{
|
|
" 1. regexp
|
|
" 2. migemo
|
|
" 3. smartsign
|
|
" 4. smartcase
|
|
let use_migemo = s:should_use_migemo(a:input)
|
|
let re = use_migemo || s:should_use_regexp() ? a:input : s:escape_regexp_char(a:input)
|
|
|
|
" Convert space to match only start of spaces
|
|
if re ==# ' '
|
|
let re = '\s\+'
|
|
endif
|
|
|
|
if use_migemo
|
|
let re = s:convertMigemo(re)
|
|
endif
|
|
|
|
if s:should_use_smartsign(a:input)
|
|
let r = s:convertSmartsign(a:input)
|
|
if use_migemo
|
|
let re = re . '\m\|' . r
|
|
else
|
|
let re = r
|
|
endif
|
|
endif
|
|
|
|
let case_flag = EasyMotion#helper#should_case_sensitive(
|
|
\ a:input, s:current.is_search) ? '\c' : '\C'
|
|
let re = case_flag . re
|
|
return re
|
|
endfunction "}}}
|
|
function! s:convertMigemo(re) "{{{
|
|
let re = a:re
|
|
|
|
if len(re) > 1
|
|
" System cmigemo
|
|
return EasyMotion#cmigemo#getMigemoPattern(re)
|
|
endif
|
|
|
|
" EasyMotion migemo one key dict
|
|
if ! has_key(s:migemo_dicts, &l:encoding)
|
|
let s:migemo_dicts[&l:encoding] = EasyMotion#helper#load_migemo_dict()
|
|
endif
|
|
return get(s:migemo_dicts[&l:encoding], re, a:re)
|
|
endfunction "}}}
|
|
function! s:convertSmartsign(chars) "{{{
|
|
" Convert given chars to smartsign string
|
|
" Example: 12 -> [1!][2@]
|
|
" a] -> a[]}]
|
|
|
|
" Load smartsign dictionary
|
|
let smart_dict = s:load_smart_dict()
|
|
" Prepare converted string
|
|
let converted_str = ''
|
|
" Get `upper_sign` for each given chars
|
|
" Split chars into list
|
|
for char in split(a:chars, '\zs')
|
|
let upper_sign = s:get_escaped_group_char(smart_dict, char)
|
|
if upper_sign ==# ''
|
|
let converted_str .= s:escape_regexp_char(char)
|
|
else
|
|
" [1!]
|
|
let converted_str .= '[' . char . upper_sign . ']'
|
|
endif
|
|
endfor
|
|
return converted_str
|
|
endfunction "}}}
|
|
function! s:get_escaped_group_char(dict, char) "{{{
|
|
" Get escaped char from given dictionary
|
|
" return '' if char is not find
|
|
" Used inside `[]`
|
|
return escape(get(a:dict, a:char, ''), '^')
|
|
endfunction "}}}
|
|
function! s:escape_regexp_char(char) "{{{
|
|
return escape(a:char, '.$^~\[]*')
|
|
endfunction "}}}
|
|
function! s:convertSmartcase(re, char) "{{{
|
|
let re = a:re
|
|
if a:char =~# '\U' "nonuppercase
|
|
return '\c' . re
|
|
else "uppercase
|
|
return '\C' . re
|
|
endif
|
|
endfunction "}}}
|
|
function! s:should_use_regexp() "{{{
|
|
return g:EasyMotion_use_regexp == 1 && s:flag.regexp == 1
|
|
endfunction "}}}
|
|
function! s:should_use_migemo(char) "{{{
|
|
if ! g:EasyMotion_use_migemo || match(a:char, '[^!-~]') != -1
|
|
return 0
|
|
endif
|
|
|
|
" TODO: use direction to improve
|
|
if s:flag.within_line == 1
|
|
let first_line = line('.')
|
|
let end_line = line('.')
|
|
else
|
|
let first_line = line('w0')
|
|
let end_line = line('w$')
|
|
endif
|
|
|
|
" Skip folded line and check if text include multibyte characters
|
|
for line in range(first_line, end_line)
|
|
if EasyMotion#helper#is_folded(line)
|
|
continue
|
|
endif
|
|
|
|
if EasyMotion#helper#include_multibyte_char(getline(line)) == 1
|
|
return 1
|
|
endif
|
|
endfor
|
|
|
|
return 0
|
|
endfunction "}}}
|
|
function! s:should_use_smartsign(char) "{{{
|
|
" Smartsign Dictionary exists?
|
|
" \A: non-alphabetic character
|
|
" Do not use smartsign for n-key find search motions
|
|
if (exists('g:EasyMotion_use_smartsign_us') ||
|
|
\ exists('g:EasyMotion_use_smartsign_jp')) &&
|
|
\ match(a:char, '\A') != -1 &&
|
|
\ exists('s:current.is_search') && s:current.is_search == 0
|
|
return 1
|
|
else
|
|
return 0
|
|
endif
|
|
endfunction "}}}
|
|
function! s:convert_t_regexp(re, direction) "{{{
|
|
if a:direction == 0 "forward
|
|
return '\_.\ze\('.a:re.'\)'
|
|
elseif a:direction == 1 "backward
|
|
return '\('.a:re.'\)\@<=\_.'
|
|
endif
|
|
endfunction "}}}
|
|
|
|
" -- Handle Visual Mode ------------------
|
|
function! s:GetVisualStartPosition(c_pos, v_start, v_end, search_direction) "{{{
|
|
let vmode = mode(1)
|
|
if vmode !~# "^[Vv\<C-v>]"
|
|
call s:Throw('Unkown visual mode:'.vmode)
|
|
endif
|
|
|
|
if vmode ==# 'V' "line-wise Visual
|
|
" Line-wise Visual {{{
|
|
if a:v_start[0] == a:v_end[0]
|
|
if a:search_direction == ''
|
|
return a:v_start
|
|
elseif a:search_direction == 'b'
|
|
return a:v_end
|
|
else
|
|
call s:throw('Unkown search_direction')
|
|
endif
|
|
else
|
|
if a:c_pos[0] == a:v_start[0]
|
|
return a:v_end
|
|
elseif a:c_pos[0] == a:v_end[0]
|
|
return a:v_start
|
|
endif
|
|
endif
|
|
"}}}
|
|
else
|
|
" Character-wise or Block-wise Visual"{{{
|
|
if a:c_pos == a:v_start
|
|
return a:v_end
|
|
elseif a:c_pos == a:v_end
|
|
return a:v_start
|
|
endif
|
|
|
|
" virtualedit
|
|
if a:c_pos[0] == a:v_start[0]
|
|
return a:v_end
|
|
elseif a:c_pos[0] == a:v_end[0]
|
|
return a:v_start
|
|
elseif EasyMotion#helper#is_greater_coords(a:c_pos, a:v_start) == 1
|
|
return a:v_end
|
|
else
|
|
return a:v_start
|
|
endif
|
|
"}}}
|
|
endif
|
|
endfunction "}}}
|
|
|
|
" -- Others ------------------------------
|
|
function! s:is_cmdwin() "{{{
|
|
return bufname('%') ==# '[Command Line]'
|
|
endfunction "}}}
|
|
function! s:should_use_wundo() "{{{
|
|
" wundu cannot use in command-line window and
|
|
" unless undolist is not empty
|
|
return ! s:is_cmdwin() && undotree().seq_last != 0
|
|
endfunction "}}}
|
|
function! s:handleEmpty(input, visualmode) "{{{
|
|
" if empty, reselect and return 1
|
|
if empty(a:input)
|
|
if ! empty(a:visualmode)
|
|
silent exec 'normal! gv'
|
|
endif
|
|
let s:EasyMotion_is_cancelled = 1 " Cancel
|
|
return 1
|
|
endif
|
|
return 0
|
|
endfunction "}}}
|
|
function! s:load_smart_dict() "{{{
|
|
if exists('g:EasyMotion_use_smartsign_us')
|
|
return g:EasyMotion#sticky_table#us
|
|
elseif exists('g:EasyMotion_use_smartsign_jp')
|
|
return g:EasyMotion#sticky_table#jp
|
|
else
|
|
return {}
|
|
endif
|
|
endfunction "}}}
|
|
function! EasyMotion#attach_active_autocmd() "{{{
|
|
" Reference: https://github.com/justinmk/vim-sneak
|
|
augroup plugin-easymotion-active
|
|
autocmd!
|
|
autocmd InsertEnter,WinLeave,BufLeave <buffer>
|
|
\ let s:EasyMotion_is_active = 0
|
|
\ | autocmd! plugin-easymotion-active * <buffer>
|
|
autocmd CursorMoved <buffer>
|
|
\ autocmd plugin-easymotion-active CursorMoved <buffer>
|
|
\ let s:EasyMotion_is_active = 0
|
|
\ | autocmd! plugin-easymotion-active * <buffer>
|
|
augroup END
|
|
endfunction "}}}
|
|
function! EasyMotion#is_active() "{{{
|
|
return s:EasyMotion_is_active
|
|
endfunction "}}}
|
|
function! EasyMotion#activate(is_visual) "{{{
|
|
let s:EasyMotion_is_active = 1
|
|
call EasyMotion#attach_active_autocmd()
|
|
call EasyMotion#highlight#add_highlight(s:previous.regexp,
|
|
\ g:EasyMotion_hl_move)
|
|
call EasyMotion#highlight#attach_autocmd()
|
|
if a:is_visual == 1
|
|
normal! gv
|
|
endif
|
|
endfunction "}}}
|
|
function! s:restore_cursor_state(visualmode) "{{{
|
|
" -- Restore original cursor position/selection
|
|
if ! empty(a:visualmode)
|
|
silent exec 'normal! gv'
|
|
keepjumps call cursor(s:current.cursor_position)
|
|
else
|
|
keepjumps call cursor(s:current.original_position)
|
|
endif
|
|
endfunction " }}}
|
|
" Grouping Algorithms: {{{
|
|
let s:grouping_algorithms = {
|
|
\ 1: 'SCTree'
|
|
\ , 2: 'Original'
|
|
\ }
|
|
" -- Single-key/closest target priority tree {{{
|
|
" This algorithm tries to assign one-key jumps to all the targets closest to the cursor.
|
|
" It works recursively and will work correctly with as few keys as two.
|
|
function! s:GroupingAlgorithmSCTree(targets, keys) "{{{
|
|
" Prepare variables for working
|
|
let targets_len = len(a:targets)
|
|
let keys_len = len(a:keys)
|
|
|
|
let groups = {}
|
|
|
|
let keys = reverse(copy(a:keys))
|
|
|
|
" Semi-recursively count targets {{{
|
|
" We need to know exactly how many child nodes (targets) this branch will have
|
|
" in order to pass the correct amount of targets to the recursive function.
|
|
|
|
" Prepare sorted target count list {{{
|
|
" This is horrible, I know. But dicts aren't sorted in vim, so we need to
|
|
" work around that. That is done by having one sorted list with key counts,
|
|
" and a dict which connects the key with the keys_count list.
|
|
|
|
let keys_count = []
|
|
let keys_count_keys = {}
|
|
|
|
let i = 0
|
|
for key in keys
|
|
call add(keys_count, 0)
|
|
|
|
let keys_count_keys[key] = i
|
|
|
|
let i += 1
|
|
endfor
|
|
" }}}
|
|
|
|
let targets_left = targets_len
|
|
let level = 0
|
|
let i = 0
|
|
|
|
while targets_left > 0
|
|
" Calculate the amount of child nodes based on the current level
|
|
let childs_len = (level == 0 ? 1 : (keys_len - 1) )
|
|
|
|
for key in keys
|
|
" Add child node count to the keys_count array
|
|
let keys_count[keys_count_keys[key]] += childs_len
|
|
|
|
" Subtract the child node count
|
|
let targets_left -= childs_len
|
|
|
|
if targets_left <= 0
|
|
" Subtract the targets left if we added too many too
|
|
" many child nodes to the key count
|
|
let keys_count[keys_count_keys[key]] += targets_left
|
|
|
|
break
|
|
endif
|
|
|
|
let i += 1
|
|
endfor
|
|
|
|
let level += 1
|
|
endwhile
|
|
" }}}
|
|
" Create group tree {{{
|
|
let i = 0
|
|
let key = 0
|
|
|
|
call reverse(keys_count)
|
|
|
|
for key_count in keys_count
|
|
if key_count > 1
|
|
" We need to create a subgroup
|
|
" Recurse one level deeper
|
|
let groups[a:keys[key]] = s:GroupingAlgorithmSCTree(a:targets[i : i + key_count - 1], a:keys)
|
|
elseif key_count == 1
|
|
" Assign single target key
|
|
let groups[a:keys[key]] = a:targets[i]
|
|
else
|
|
" No target
|
|
continue
|
|
endif
|
|
|
|
let key += 1
|
|
let i += key_count
|
|
endfor
|
|
" }}}
|
|
|
|
" Finally!
|
|
return groups
|
|
endfunction "}}}
|
|
" }}}
|
|
" -- Original ---------------------------- {{{
|
|
function! s:GroupingAlgorithmOriginal(targets, keys)
|
|
" Split targets into groups (1 level)
|
|
let targets_len = len(a:targets)
|
|
" let keys_len = len(a:keys)
|
|
|
|
let groups = {}
|
|
|
|
let i = 0
|
|
let root_group = 0
|
|
try
|
|
while root_group < targets_len
|
|
let groups[a:keys[root_group]] = {}
|
|
|
|
for key in a:keys
|
|
let groups[a:keys[root_group]][key] = a:targets[i]
|
|
|
|
let i += 1
|
|
endfor
|
|
|
|
let root_group += 1
|
|
endwhile
|
|
catch | endtry
|
|
|
|
" Flatten the group array
|
|
if len(groups) == 1
|
|
let groups = groups[a:keys[0]]
|
|
endif
|
|
|
|
return groups
|
|
endfunction
|
|
" }}}
|
|
|
|
" -- Coord/key dictionary creation ------- {{{
|
|
function! s:CreateCoordKeyDict(groups, ...)
|
|
" Dict structure:
|
|
" 1,2 : a
|
|
" 2,3 : b
|
|
let sort_list = []
|
|
let coord_keys = {}
|
|
let group_key = a:0 == 1 ? a:1 : ''
|
|
|
|
for [key, item] in items(a:groups)
|
|
let key = group_key . key
|
|
"let key = ( ! empty(group_key) ? group_key : key)
|
|
|
|
if type(item) == type([]) " List
|
|
" Destination coords
|
|
|
|
" The key needs to be zero-padded in order to
|
|
" sort correctly
|
|
let dict_key = printf('%05d,%05d', item[0], item[1])
|
|
let coord_keys[dict_key] = key
|
|
|
|
" We need a sorting list to loop correctly in
|
|
" PromptUser, dicts are unsorted
|
|
call add(sort_list, dict_key)
|
|
else
|
|
" Item is a dict (has children)
|
|
let coord_key_dict = s:CreateCoordKeyDict(item, key)
|
|
|
|
" Make sure to extend both the sort list and the
|
|
" coord key dict
|
|
call extend(sort_list, coord_key_dict[0])
|
|
call extend(coord_keys, coord_key_dict[1])
|
|
endif
|
|
|
|
unlet item
|
|
endfor
|
|
|
|
return [sort_list, coord_keys]
|
|
endfunction
|
|
" }}}
|
|
" }}}
|
|
"}}}
|
|
" Core Functions: {{{
|
|
function! s:PromptUser(groups) "{{{
|
|
" Recursive
|
|
let group_values = values(a:groups)
|
|
|
|
" -- If only one possible match, jump directly to it {{{
|
|
if len(group_values) == 1
|
|
if mode(1) ==# 'no'
|
|
" Consider jump to first match
|
|
" NOTE: matchstr() handles multibyte characters.
|
|
let s:dot_repeat['target'] = matchstr(g:EasyMotion_keys, '^.')
|
|
endif
|
|
redraw
|
|
return group_values[0]
|
|
endif
|
|
" }}}
|
|
|
|
" -- Prepare marker lines ---------------- {{{
|
|
let lines = {}
|
|
|
|
let coord_key_dict = s:CreateCoordKeyDict(a:groups)
|
|
|
|
let prev_col_num = 0
|
|
for dict_key in sort(coord_key_dict[0])
|
|
" NOTE: {{{
|
|
" let g:EasyMotion_keys = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
" Perform <Plug>(easymotion-w)
|
|
"
|
|
" lines[line_num]['orig']:
|
|
" Lorem ipsum dolor sit amet consectetur adipisicing
|
|
"
|
|
" {target_char}:
|
|
" {L}orem {i}psum {d}olor {s}it {a}met {c}onsectetur {a}dipisicing
|
|
"
|
|
" lines[line_num]['marker'], {marker_chars}:
|
|
" {A}orem {B}psum {C}olor {D}it {E}met {F}onsectetur {G}dipisicing
|
|
" 2-key-combo: {marker_chars} could be 1 or 2 chars like {AB}
|
|
"
|
|
" }}}
|
|
|
|
" Prepare original line and marker line {{{
|
|
let [line_num, col_num] = split(dict_key, ',')
|
|
|
|
let line_num = str2nr(line_num)
|
|
let col_num = str2nr(col_num)
|
|
if ! has_key(lines, line_num)
|
|
let current_line = getline(line_num)
|
|
let lines[line_num] = {
|
|
\ 'orig': current_line,
|
|
\ 'marker': current_line,
|
|
\ 'mb_compensation': 0,
|
|
\ }
|
|
" mb_compensation -> multibyte compensation
|
|
let prev_col_num = 0
|
|
endif "}}}
|
|
|
|
" Multibyte Compensation: {{{
|
|
" Solve multibyte issues by matching the byte column
|
|
" number instead of the visual column
|
|
" Compensate for byte difference between marker
|
|
" character and target character
|
|
"
|
|
" This has to be done in order to match the correct
|
|
" column; \%c matches the byte column and not display
|
|
" column.
|
|
let col_num = max([prev_col_num + 1,
|
|
\ col_num - lines[line_num]['mb_compensation']])
|
|
let prev_col_num = col_num
|
|
"}}}
|
|
|
|
" Prepare marker characters {{{
|
|
let marker_chars = coord_key_dict[1][dict_key]
|
|
let marker_chars_len = EasyMotion#helper#strchars(marker_chars)
|
|
"}}}
|
|
|
|
" Replace {target} with {marker} & Highlight {{{
|
|
let col_add = 0 " Column add byte length
|
|
" Disable two-key-combo feature?
|
|
let marker_max_length = g:EasyMotion_disable_two_key_combo == 1
|
|
\ ? 1 : 2
|
|
for i in range(min([marker_chars_len, marker_max_length]))
|
|
let marker_char = split(marker_chars, '\zs')[i]
|
|
" EOL {{{
|
|
if strlen(lines[line_num]['marker']) < col_num + col_add
|
|
" Append marker chars if target is EOL
|
|
let lines[line_num]['marker'] .= ' '
|
|
endif "}}}
|
|
|
|
let target_col_regexp = '\%' . (col_num + col_add) . 'c.'
|
|
let target_char = matchstr(lines[line_num]['marker'],
|
|
\ target_col_regexp)
|
|
let space_len = strdisplaywidth(target_char)
|
|
\ - strdisplaywidth(marker_char)
|
|
" Substitute marker character
|
|
let substitute_expr = marker_char . repeat(' ', space_len)
|
|
|
|
let lines[line_num]['marker'] = substitute(
|
|
\ lines[line_num]['marker'],
|
|
\ target_col_regexp,
|
|
\ escape(substitute_expr,'&'),
|
|
\ '')
|
|
|
|
" Highlight targets {{{
|
|
let _hl_group =
|
|
\ (marker_chars_len == 1) ? g:EasyMotion_hl_group_target
|
|
\ : (i == 0) ? g:EasyMotion_hl2_first_group_target
|
|
\ : g:EasyMotion_hl2_second_group_target
|
|
|
|
if exists('*matchaddpos')
|
|
call EasyMotion#highlight#add_pos_highlight(
|
|
\ line_num, col_num + col_add, _hl_group)
|
|
else
|
|
call EasyMotion#highlight#add_highlight(
|
|
\ '\%' . line_num . 'l' . target_col_regexp,
|
|
\ _hl_group)
|
|
endif
|
|
"}}}
|
|
|
|
" Add marker/target length difference for multibyte compensation
|
|
let lines[line_num]['mb_compensation'] +=
|
|
\ strlen(target_char) - strlen(substitute_expr)
|
|
" Shift column
|
|
let col_add += strlen(marker_char)
|
|
endfor
|
|
"}}}
|
|
endfor
|
|
|
|
let lines_items = items(lines)
|
|
" }}}
|
|
|
|
" -- Put labels on targets & Get User Input & Restore all {{{
|
|
" Save undo tree {{{
|
|
let s:undo_file = tempname()
|
|
if s:should_use_wundo()
|
|
execute "wundo" s:undo_file
|
|
endif
|
|
"}}}
|
|
try
|
|
" Set lines with markers {{{
|
|
call s:SetLines(lines_items, 'marker')
|
|
redraw "}}}
|
|
|
|
" Get target character {{{
|
|
call s:Prompt('Target key')
|
|
let char = s:GetChar()
|
|
"}}}
|
|
|
|
" Convert uppercase {{{
|
|
if g:EasyMotion_use_upper == 1 && match(g:EasyMotion_keys, '\l') == -1
|
|
let char = toupper(char)
|
|
endif "}}}
|
|
|
|
" Jump first target when Enter or Space key is pressed "{{{
|
|
if (char ==# "\<CR>" && g:EasyMotion_enter_jump_first == 1) ||
|
|
\ (char ==# "\<Space>" && g:EasyMotion_space_jump_first == 1)
|
|
" NOTE: matchstr() is multibyte aware.
|
|
let char = matchstr(g:EasyMotion_keys, '^.')
|
|
endif "}}}
|
|
|
|
" For dot repeat {{{
|
|
if mode(1) ==# 'no'
|
|
" Store previous target when operator pending mode
|
|
if s:current.dot_prompt_user_cnt == 0
|
|
" Store
|
|
let s:dot_repeat['target'] = char
|
|
else
|
|
" Append target chars
|
|
let s:dot_repeat['target'] .= char
|
|
endif
|
|
endif "}}}
|
|
|
|
finally
|
|
" Restore original lines
|
|
call s:SetLines(lines_items, 'orig')
|
|
|
|
" Un-highlight targets {{{
|
|
call EasyMotion#highlight#delete_highlight(
|
|
\ g:EasyMotion_hl_group_target,
|
|
\ g:EasyMotion_hl2_first_group_target,
|
|
\ g:EasyMotion_hl2_second_group_target,
|
|
\ )
|
|
" }}}
|
|
|
|
" Restore undo tree {{{
|
|
if s:should_use_wundo() && filereadable(s:undo_file)
|
|
silent execute "rundo" s:undo_file
|
|
call delete(s:undo_file)
|
|
unlet s:undo_file
|
|
else
|
|
" Break undo history (undobreak)
|
|
let old_undolevels = &undolevels
|
|
set undolevels=-1
|
|
keepjumps call setline('.', getline('.'))
|
|
let &undolevels = old_undolevels
|
|
unlet old_undolevels
|
|
" FIXME: Error occur by GundoToggle for undo number 2 is empty
|
|
keepjumps call setline('.', getline('.'))
|
|
endif "}}}
|
|
|
|
redraw
|
|
endtry "}}}
|
|
|
|
" -- Check if we have an input char ------ {{{
|
|
if empty(char)
|
|
call s:Throw('Cancelled')
|
|
endif
|
|
" }}}
|
|
" -- Check if the input char is valid ---- {{{
|
|
if ! has_key(a:groups, char)
|
|
call s:Throw('Invalid target')
|
|
endif
|
|
" }}}
|
|
|
|
let target = a:groups[char]
|
|
|
|
if type(target) == type([])
|
|
" Return target coordinates
|
|
return target
|
|
else
|
|
" Prompt for new target character
|
|
let s:current.dot_prompt_user_cnt += 1
|
|
return s:PromptUser(target)
|
|
endif
|
|
endfunction "}}}
|
|
function! s:DotPromptUser(groups) "{{{
|
|
" Get char from previous target
|
|
let char = s:dot_repeat.target[s:current.dot_repeat_target_cnt]
|
|
" For dot repeat target chars
|
|
let s:current.dot_repeat_target_cnt += 1
|
|
|
|
let target = a:groups[char]
|
|
|
|
if type(target) == type([])
|
|
" Return target coordinates
|
|
return target
|
|
else
|
|
" Prompt for new target character
|
|
return s:PromptUser(target)
|
|
endif
|
|
endfunction "}}}
|
|
|
|
function! s:EasyMotion(regexp, direction, visualmode, is_inclusive, ...) " {{{
|
|
let config = extend(s:default_config(), get(a:, 1, {}))
|
|
" Store s:current original_position & cursor_position {{{
|
|
" current cursor pos.
|
|
let s:current.cursor_position = [line('.'), col('.')]
|
|
" original start position. This value could be changed later in visual
|
|
" mode
|
|
let s:current.original_position =
|
|
\ get(s:current, 'original_position', s:current.cursor_position)
|
|
"}}}
|
|
|
|
let win_first_line = line('w0') " visible first line num
|
|
let win_last_line = line('w$') " visible last line num
|
|
|
|
" Store the target positions list
|
|
" e.g. targets = [ [line, col], [line2, col2], ...]
|
|
let targets = []
|
|
|
|
" Store info for Repeat motion {{{
|
|
if s:flag.dot_repeat != 1
|
|
" Store Regular Expression
|
|
let s:previous['regexp'] = a:regexp
|
|
let s:previous['direction'] = a:direction
|
|
let s:previous['operator'] = v:operator
|
|
|
|
" Note: 'is_inclusive' value could be changed later when
|
|
" bi-directional find motion depend on 'true' direction the cursor
|
|
" will move.
|
|
let s:previous['is_inclusive'] = a:is_inclusive
|
|
|
|
" For special motion flag
|
|
let s:previous['line_flag'] = s:flag.within_line
|
|
let s:previous['bd_t_flag'] = s:flag.bd_t " bi-directional t motion
|
|
endif "}}}
|
|
|
|
" To avoid side effect of overwriting buffer for tpope/repeat
|
|
" store current b:changedtick. Use this value later
|
|
let s:current.changedtick = b:changedtick
|
|
|
|
try
|
|
" -- Reset properties -------------------- {{{
|
|
" Save original value and set new value
|
|
call s:SaveValue()
|
|
call s:turn_off_hl_error()
|
|
" }}}
|
|
" Setup searchpos args {{{
|
|
let search_direction = (a:direction == 1 ? 'b' : '')
|
|
let search_stopline = a:direction == 1 ? win_first_line : win_last_line
|
|
|
|
if s:flag.within_line == 1
|
|
let search_stopline = s:current.original_position[0]
|
|
endif
|
|
"}}}
|
|
|
|
" Handle visual mode {{{
|
|
if ! empty(a:visualmode)
|
|
" Decide at where visual mode start {{{
|
|
normal! gv
|
|
let v_start = [line("'<"),col("'<")] " visual_start_position
|
|
let v_end = [line("'>"),col("'>")] " visual_end_position
|
|
|
|
let v_original_pos = s:GetVisualStartPosition(
|
|
\ s:current.cursor_position, v_start, v_end, search_direction)
|
|
"}}}
|
|
|
|
" Reselect visual text {{{
|
|
keepjumps call cursor(v_original_pos)
|
|
exec "normal! " . a:visualmode
|
|
keepjumps call cursor(s:current.cursor_position)
|
|
"}}}
|
|
" Update s:current.original_position
|
|
" overwrite original start position
|
|
let s:current.original_position = v_original_pos
|
|
endif "}}}
|
|
|
|
" Handle bi-directional t motion {{{
|
|
if s:flag.bd_t == 1
|
|
let regexp = s:convert_t_regexp(a:regexp, 0) "forward
|
|
else
|
|
let regexp = a:regexp
|
|
endif
|
|
"}}}
|
|
|
|
" Handle dot repeat with count
|
|
if s:flag.count_dot_repeat
|
|
let cursor_char = EasyMotion#helper#get_char_by_coord(s:current.cursor_position)
|
|
if cursor_char =~# regexp
|
|
call add(targets, s:current.cursor_position)
|
|
endif
|
|
endif
|
|
|
|
" Construct match dict {{{
|
|
" Note: searchpos() has side effect which jump cursor position.
|
|
" You can disable this side effect by add 'n' flags,
|
|
" but in this case, it's better to allows jump side effect
|
|
" to gathering matched targets coordinates.
|
|
let pos = searchpos(regexp, search_direction . (config.accept_cursor_pos ? 'c' : ''), search_stopline)
|
|
while 1
|
|
" Reached end of search range
|
|
if pos == [0, 0]
|
|
break
|
|
endif
|
|
|
|
" Skip folded lines {{{
|
|
if EasyMotion#helper#is_folded(pos[0])
|
|
if search_direction ==# 'b'
|
|
" FIXME: Hmm... I should use filter()
|
|
" keepjumps call cursor(foldclosed(pos[0]), 0)
|
|
else
|
|
keepjumps call cursor(foldclosedend(pos[0]+1), 0)
|
|
endif
|
|
else
|
|
call add(targets, pos)
|
|
endif
|
|
"}}}
|
|
let pos = searchpos(regexp, search_direction, search_stopline)
|
|
endwhile
|
|
"}}}
|
|
|
|
" Handle bidirection "{{{
|
|
" For bi-directional t motion {{{
|
|
if s:flag.bd_t == 1
|
|
let regexp = s:convert_t_regexp(a:regexp, 1) "backward
|
|
endif
|
|
"}}}
|
|
" Reconstruct match dict
|
|
if a:direction == 2
|
|
" Backward
|
|
|
|
" Jump back cursor_position
|
|
keepjumps call cursor(s:current.cursor_position[0],
|
|
\ s:current.cursor_position[1])
|
|
|
|
let targets2 = []
|
|
if s:flag.within_line == 0
|
|
let search_stopline = win_first_line
|
|
else
|
|
let search_stopline = s:current.cursor_position[0]
|
|
endif
|
|
while 1
|
|
" TODO: refactoring
|
|
let pos = searchpos(regexp, 'b', search_stopline)
|
|
" Reached end of search range
|
|
if pos == [0, 0]
|
|
break
|
|
endif
|
|
|
|
" Skip folded lines {{{
|
|
if EasyMotion#helper#is_folded(pos[0])
|
|
" keepjumps call cursor(foldclosedend(pos[0]+1), 0)
|
|
continue
|
|
endif
|
|
"}}}
|
|
|
|
call add(targets2, pos)
|
|
endwhile
|
|
" Merge match target dict"{{{
|
|
let t1 = 0 " forward
|
|
let t2 = 0 " backward
|
|
let targets3 = []
|
|
while t1 < len(targets) || t2 < len(targets2)
|
|
" Forward -> Backward -> F -> B -> ...
|
|
if t1 < len(targets)
|
|
call add(targets3, targets[t1])
|
|
let t1 += 1
|
|
endif
|
|
if t2 < len(targets2)
|
|
call add(targets3, targets2[t2])
|
|
let t2 += 1
|
|
endif
|
|
endwhile
|
|
let targets = targets3
|
|
"}}}
|
|
endif
|
|
"}}}
|
|
" Handle no match"{{{
|
|
let targets_len = len(targets)
|
|
if targets_len == 0
|
|
call s:Throw('No matches')
|
|
endif
|
|
"}}}
|
|
|
|
" Attach specific key as marker to gathered matched coordinates
|
|
let GroupingFn = function('s:GroupingAlgorithm' . s:grouping_algorithms[g:EasyMotion_grouping])
|
|
let groups = GroupingFn(targets, split(g:EasyMotion_keys, '\zs'))
|
|
|
|
" -- Shade inactive source --------------- {{{
|
|
if g:EasyMotion_do_shade && targets_len != 1 && s:flag.dot_repeat != 1
|
|
if a:direction == 1 " Backward
|
|
let shade_hl_re = s:flag.within_line
|
|
\ ? '^.*\%#'
|
|
\ : '\%'. win_first_line .'l\_.*\%#'
|
|
elseif a:direction == 0 " Forward
|
|
let shade_hl_re = s:flag.within_line
|
|
\ ? '\%#.*$'
|
|
\ : '\%#\_.*\%'. win_last_line .'l'
|
|
else " Both directions
|
|
let shade_hl_re = s:flag.within_line
|
|
\ ? '^.*\%#.*$'
|
|
\ : '\_.*'
|
|
endif
|
|
|
|
call EasyMotion#highlight#add_highlight(
|
|
\ shade_hl_re, g:EasyMotion_hl_group_shade)
|
|
if g:EasyMotion_cursor_highlight
|
|
let cursor_hl_re = '\%#'
|
|
call EasyMotion#highlight#add_highlight(cursor_hl_re,
|
|
\ g:EasyMotion_hl_inc_cursor)
|
|
endif
|
|
endif
|
|
" }}}
|
|
|
|
" -- Jump back before prompt for visual scroll {{{
|
|
" Because searchpos() change current cursor position and
|
|
" if you just use cursor(s:current.cursor_position) to jump back,
|
|
" current line will become middle of line window
|
|
if ! empty(a:visualmode)
|
|
keepjumps call winrestview({'lnum' : s:current.cursor_position[0], 'topline' : win_first_line})
|
|
else
|
|
" for adjusting cursorline
|
|
keepjumps call cursor(s:current.cursor_position)
|
|
endif
|
|
"}}}
|
|
|
|
" -- Prompt user for target group/character {{{
|
|
if s:flag.dot_repeat != 1
|
|
let coords = s:PromptUser(groups)
|
|
else
|
|
let coords = s:DotPromptUser(groups)
|
|
endif
|
|
"}}}
|
|
|
|
" -- Update cursor position -------------- {{{
|
|
" First, jump back cursor to original position
|
|
keepjumps call cursor(s:current.original_position)
|
|
|
|
" Consider EasyMotion as jump motion :h jump-motion
|
|
normal! m`
|
|
|
|
" Update selection for visual mode {{{
|
|
if ! empty(a:visualmode)
|
|
exec 'normal! ' . a:visualmode
|
|
endif
|
|
" }}}
|
|
|
|
" For bi-directional motion, checking again whether the motion is
|
|
" inclusive is necessary. This value will might be updated later
|
|
let is_inclusive_check = a:is_inclusive
|
|
" For bi-directional motion, store 'true' direction for dot repeat
|
|
" to handling inclusive/exclusive motion
|
|
if a:direction == 2
|
|
let true_direction =
|
|
\ EasyMotion#helper#is_greater_coords(
|
|
\ s:current.original_position, coords) > 0 ?
|
|
\ 0 : 1
|
|
" forward : backward
|
|
else
|
|
let true_direction = a:direction
|
|
endif
|
|
|
|
if s:flag.dot_repeat == 1
|
|
" support dot repeat {{{
|
|
" Use visual mode to emulate dot repeat
|
|
normal! v
|
|
|
|
" Deal with exclusive {{{
|
|
if s:dot_repeat.is_inclusive == 0
|
|
" exclusive
|
|
if s:dot_repeat.true_direction == 0 "Forward
|
|
let coords[1] -= 1
|
|
elseif s:dot_repeat.true_direction == 1 "Backward
|
|
" Shift visual selection to left by making cursor one key
|
|
" left.
|
|
normal! hoh
|
|
endif
|
|
endif "}}}
|
|
|
|
" Jump to destination
|
|
keepjumps call cursor(coords[0], coords[1])
|
|
|
|
" Execute previous operator
|
|
let cmd = s:dot_repeat.operator
|
|
if s:dot_repeat.operator ==# 'c'
|
|
let cmd .= getreg('.')
|
|
endif
|
|
exec 'normal! ' . cmd
|
|
"}}}
|
|
else
|
|
" Handle inclusive & exclusive {{{
|
|
" Overwrite inclusive flag for special case {{{
|
|
if s:flag.find_bd == 1 && true_direction == 1
|
|
" Note: For bi-directional find motion s(f) & t
|
|
" If true_direction is backward, the motion is 'exclusive'
|
|
let is_inclusive_check = 0 " overwrite
|
|
let s:previous.is_inclusive = 0 " overwrite
|
|
endif "}}}
|
|
if is_inclusive_check
|
|
" Note: {{{
|
|
" Inclusive motion requires that we eat one more
|
|
" character to the right by forcing the motion to inclusive
|
|
" if we're using a forward motion because
|
|
" > :h exclusive
|
|
" > Note that when using ':' any motion becomes characterwise
|
|
" > exclusive.
|
|
" and EasyMotion use ':'
|
|
" See: h: o_v }}}
|
|
normal! v
|
|
endif " }}}
|
|
|
|
if s:current.is_operator && s:flag.linewise
|
|
" TODO: Is there better solution?
|
|
" Maike it linewise
|
|
normal! V
|
|
endif
|
|
|
|
" Adjust screen especially for visual scroll & offscreen search {{{
|
|
" Otherwise, cursor line will move middle line of window
|
|
keepjumps call winrestview({'lnum' : win_first_line, 'topline' : win_first_line})
|
|
|
|
" Jump to destination
|
|
keepjumps call cursor(coords[0], coords[1])
|
|
|
|
" To avoid side effect of overwriting buffer {{{
|
|
" for tpope/vim-repeat
|
|
" See: :h b:changedtick
|
|
if exists('g:repeat_tick')
|
|
if g:repeat_tick == s:current.changedtick
|
|
let g:repeat_tick = b:changedtick
|
|
endif
|
|
endif "}}}
|
|
endif
|
|
|
|
" Set tpope/vim-repeat {{{
|
|
if s:current.is_operator == 1 &&
|
|
\ !(v:operator ==# 'y' && match(&cpo, 'y') == -1)
|
|
" Store previous info for dot repeat {{{
|
|
let s:dot_repeat.regexp = a:regexp
|
|
let s:dot_repeat.direction = a:direction
|
|
let s:dot_repeat.line_flag = s:flag.within_line
|
|
let s:dot_repeat.is_inclusive = is_inclusive_check
|
|
let s:dot_repeat.operator = v:operator
|
|
let s:dot_repeat.bd_t_flag = s:flag.bd_t " Bidirectional t motion
|
|
let s:dot_repeat.true_direction = true_direction " Check inclusive
|
|
"}}}
|
|
silent! call repeat#set("\<Plug>(easymotion-dotrepeat)")
|
|
endif "}}}
|
|
|
|
" Highlight all the matches by n-key find motions {{{
|
|
if s:current.is_search == 1 && s:current.is_operator == 0 && g:EasyMotion_add_search_history
|
|
" It seems let &hlsearch=&hlsearch doesn't work when called
|
|
" in script, so use :h feedkeys() instead.
|
|
" Ref: :h v:hlsearch
|
|
" FIXME: doesn't work with `c` operator
|
|
call EasyMotion#helper#silent_feedkeys(
|
|
\ ":let &hlsearch=&hlsearch\<CR>",
|
|
\ 'hlsearch', 'n')
|
|
endif "}}}
|
|
|
|
call s:Message('Jumping to [' . coords[0] . ', ' . coords[1] . ']')
|
|
let s:EasyMotion_is_cancelled = 0 " Success
|
|
"}}}
|
|
catch /^EasyMotion:.*/
|
|
redraw
|
|
|
|
" Show exception message
|
|
" The verbose option will take precedence
|
|
if g:EasyMotion_verbose == 1 && g:EasyMotion_ignore_exception != 1
|
|
echo v:exception
|
|
endif
|
|
|
|
let s:previous['regexp'] = a:regexp
|
|
" -- Activate EasyMotion ----------------- {{{
|
|
let s:EasyMotion_is_active = 1
|
|
call EasyMotion#attach_active_autocmd() "}}}
|
|
|
|
call s:restore_cursor_state(a:visualmode)
|
|
let s:EasyMotion_is_cancelled = 1 " Cancel
|
|
catch
|
|
call s:Message(v:exception . ' : ' . v:throwpoint)
|
|
call s:restore_cursor_state(a:visualmode)
|
|
let s:EasyMotion_is_cancelled = 1 " Cancel
|
|
finally
|
|
" -- Restore properties ------------------ {{{
|
|
call s:RestoreValue()
|
|
call s:turn_on_hl_error()
|
|
call EasyMotion#reset()
|
|
" }}}
|
|
" -- Remove shading ---------------------- {{{
|
|
call EasyMotion#highlight#delete_highlight()
|
|
" }}}
|
|
|
|
if s:EasyMotion_is_cancelled == 0 " Success
|
|
" -- Landing Highlight ------------------- {{{
|
|
if g:EasyMotion_landing_highlight
|
|
call EasyMotion#highlight#add_highlight(a:regexp,
|
|
\ g:EasyMotion_hl_move)
|
|
call EasyMotion#highlight#attach_autocmd()
|
|
endif "}}}
|
|
" -- Activate EasyMotion ----------------- {{{
|
|
let s:EasyMotion_is_active = 1
|
|
call EasyMotion#attach_active_autocmd() "}}}
|
|
endif
|
|
endtry
|
|
endfunction " }}}
|
|
"}}}
|
|
" }}}
|
|
|
|
call EasyMotion#init()
|
|
" Restore 'cpoptions' {{{
|
|
let &cpo = s:save_cpo
|
|
unlet s:save_cpo
|
|
" }}}
|
|
" vim: fdm=marker:et:ts=4:sw=4:sts=4
|