1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 06:30:03 +08:00
SpaceVim/bundle/clever-f.vim/autoload/clever_f.vim
2022-04-09 00:20:50 +08:00

531 lines
15 KiB
VimL
Vendored

let s:save_cpo = &cpo
set cpo&vim
" SpaceVim API
let s:HI = SpaceVim#api#import('vim#highlight')
" constants
let s:ESC_CODE = char2nr("\<Esc>")
let s:HAS_TIMER = has('timers')
" configurations
let g:clever_f_across_no_line = get(g:, 'clever_f_across_no_line', 0)
let g:clever_f_ignore_case = get(g:, 'clever_f_ignore_case', 0)
let g:clever_f_use_migemo = get(g:, 'clever_f_use_migemo', 0)
let g:clever_f_fix_key_direction = get(g:, 'clever_f_fix_key_direction', 0)
let g:clever_f_show_prompt = get(g:, 'clever_f_show_prompt', 0)
let g:clever_f_smart_case = get(g:, 'clever_f_smart_case', 0)
let g:clever_f_chars_match_any_signs = get(g:, 'clever_f_chars_match_any_signs', '')
let g:clever_f_mark_cursor = get(g:, 'clever_f_mark_cursor', 1)
let g:clever_f_hide_cursor_on_cmdline = get(g:, 'clever_f_hide_cursor_on_cmdline', 1)
let g:clever_f_timeout_ms = get(g:, 'clever_f_timeout_ms', 0)
let g:clever_f_mark_char = get(g:, 'clever_f_mark_char', 1)
let g:clever_f_repeat_last_char_inputs = get(g:, 'clever_f_repeat_last_char_inputs', ["\<CR>"])
let g:clever_f_mark_direct = get(g:, 'clever_f_mark_direct', 0)
let g:clever_f_highlight_timeout_ms = get(g:, 'clever_f_highlight_timeout_ms', 0)
" below variable must be set before loading this script
let g:clever_f_clean_labels_eagerly = get(g:, 'clever_f_clean_labels_eagerly', 1)
" highlight labels
augroup plugin-clever-f-highlight
autocmd!
autocmd ColorScheme * highlight default CleverFDefaultLabel ctermfg=red ctermbg=NONE cterm=bold,underline guifg=red guibg=NONE gui=bold,underline
augroup END
highlight default CleverFDefaultLabel ctermfg=red ctermbg=NONE cterm=bold,underline guifg=red guibg=NONE gui=bold,underline
" Priority of highlight customization is:
" High: When g:clever_f_*_color
" Middle: :highlight in a colorscheme
" Low: Default highlights
" When the variable is defined, it should be linked with :hi! since :hi does
" not overwrite existing highlight group. (#50)
if g:clever_f_mark_cursor
if exists('g:clever_f_mark_cursor_color')
execute 'highlight! link CleverFCursor' g:clever_f_mark_cursor_color
else
highlight link CleverFCursor Cursor
endif
endif
if g:clever_f_mark_char
if exists('g:clever_f_mark_char_color')
execute 'highlight! link CleverFChar' g:clever_f_mark_char_color
else
highlight link CleverFChar CleverFDefaultLabel
endif
endif
if g:clever_f_mark_direct
if exists('g:clever_f_mark_direct_color')
execute 'highlight! link CleverFDirect' g:clever_f_mark_direct_color
else
highlight link CleverFDirect CleverFDefaultLabel
endif
endif
if g:clever_f_clean_labels_eagerly
augroup plugin-clever-f-permanent-finalizer
autocmd!
autocmd WinEnter,WinLeave,CmdWinLeave * if g:clever_f_mark_char | call s:remove_highlight() | endif
augroup END
endif
augroup plugin-clever-f-finalizer
autocmd!
augroup END
" initialize the internal state
let s:last_mode = ''
let s:previous_map = {}
let s:previous_pos = {}
let s:first_move = {}
let s:migemo_dicts = {}
let s:previous_char_num = {}
let s:timestamp = [0, 0]
let s:highlight_timer = -1
" keys are mode string returned from mode()
function! clever_f#reset() abort
let s:previous_map = {}
let s:previous_pos = {}
let s:first_move = {}
let s:migemo_dicts = {}
" Note:
" [0, 0] may be invalid because the representation of return value of reltime() depends on implementation.
let s:timestamp = [0, 0]
call s:remove_highlight()
return ''
endfunction
" hidden API for debug
function! clever_f#_reset_all() abort
call clever_f#reset()
let s:last_mode = ''
let s:previous_char_num = {}
autocmd! plugin-clever-f-finalizer
unlet! s:moved_forward
endfunction
function! s:remove_highlight() abort
if s:highlight_timer >= 0
call timer_stop(s:highlight_timer)
let s:highlight_timer = -1
endif
for h in filter(getmatches(), 'v:val.group ==# "CleverFChar"')
call matchdelete(h.id)
endfor
endfunction
function! s:is_timedout() abort
let cur = reltime()
let rel = reltimestr(reltime(s:timestamp, cur))
let elapsed_ms = float2nr(str2float(rel) * 1000.0)
let s:timestamp = cur
return elapsed_ms > g:clever_f_timeout_ms
endfunction
function! s:on_highlight_timer_expired(timer) abort
if s:highlight_timer != a:timer
return
endif
let s:highlight_timer = -1
call s:remove_highlight()
endfunction
" highlight characters to which the cursor can be moved directly
" Note: public function for test
function! clever_f#_mark_direct(forward, count) abort
let line = getline('.')
let [_, l, c, _] = getpos('.')
if (a:forward && c >= len(line)) || (!a:forward && c == 1)
" there is no matching characters
return []
endif
if g:clever_f_ignore_case
let line = tolower(line)
endif
let char_count = {}
let matches = []
let indices = a:forward ? range(c, len(line) - 1, 1) : range(c - 2, 0, -1)
for i in indices
let ch = line[i]
" only matches to ASCII
if ch !~# '^[\x00-\x7F]$' | continue | endif
let ch_lower = tolower(ch)
let char_count[ch] = get(char_count, ch, 0) + 1
if g:clever_f_smart_case && ch =~# '\u'
" uppercase characters are doubly counted
let char_count[ch_lower] = get(char_count, ch_lower, 0) + 1
endif
if char_count[ch] == a:count ||
\ (g:clever_f_smart_case && char_count[ch_lower] == a:count)
" NOTE: should not use `matchaddpos(group, [...position])`,
" because the maximum number of position is 8
let m = matchaddpos('CleverFDirect', [[l, i + 1]])
call add(matches, m)
endif
endfor
return matches
endfunction
function! s:mark_char_in_current_line(map, char) abort
let regex = '\%' . line('.') . 'l' . s:generate_pattern(a:map, a:char)
call matchadd('CleverFChar', regex , 999)
endfunction
" Note:
" \x80\xfd` seems to be sent by a terminal.
" Below is a workaround for the sequence.
function! s:getchar() abort
while 1
let cn = getchar()
if type(cn) != type('') || cn !=# "\x80\xfd`"
return cn
endif
endwhile
endfunction
function! s:include_multibyte_char(str) abort
return strlen(a:str) != clever_f#compat#strchars(a:str)
endfunction
function! clever_f#find_with(map) abort
if a:map !~# '^[fFtT]$'
throw "clever-f: Invalid mapping '" . a:map . "'"
endif
if &foldopen =~# '\<\%(all\|hor\)\>'
while foldclosed(line('.')) >= 0
foldopen
endwhile
endif
let current_pos = getpos('.')[1 : 2]
let mode = s:mode()
let highlight_timer_enabled = g:clever_f_mark_char && g:clever_f_highlight_timeout_ms > 0 && s:HAS_TIMER
let in_macro = clever_f#compat#reg_executing() !=# ''
" When 'f' is run while executing a macro, do not repeat previous
" character. See #59 for more details
if current_pos != get(s:previous_pos, mode, [0, 0]) || in_macro
let should_redraw = !in_macro
let back = 0
if g:clever_f_mark_cursor
let cursor_marker = matchadd('CleverFCursor', '\%#', 999)
if should_redraw
redraw
endif
endif
" block-NONE does not work on Neovim
if g:clever_f_hide_cursor_on_cmdline
let save_tve = &t_ve
setlocal t_ve=
let cursor_hi = {}
let cursor_hi = s:HI.group2dict('Cursor')
let lcursor_hi = s:HI.group2dict('lCursor')
let guicursor = &guicursor
call s:HI.hide_in_normal('Cursor')
call s:HI.hide_in_normal('lCursor')
if has('nvim')
set guicursor+=a:Cursor/lCursor
endif
endif
try
if g:clever_f_mark_direct && should_redraw
let direct_markers = clever_f#_mark_direct(a:map =~# '\l', v:count1)
redraw
endif
if g:clever_f_show_prompt
echon 'clever-f: '
endif
let s:previous_map[mode] = a:map
let s:first_move[mode] = 1
let cn = s:getchar()
if cn == s:ESC_CODE
return "\<Esc>"
endif
if index(map(deepcopy(g:clever_f_repeat_last_char_inputs), 'char2nr(v:val)'), cn) == -1
let s:previous_char_num[mode] = cn
else
if has_key(s:previous_char_num, s:last_mode)
let s:previous_char_num[mode] = s:previous_char_num[s:last_mode]
else
echohl ErrorMsg | echo 'Previous input not found.' | echohl None
return ''
endif
endif
let s:last_mode = mode
if g:clever_f_timeout_ms > 0
let s:timestamp = reltime()
endif
if g:clever_f_mark_char
call s:remove_highlight()
if mode ==# 'n' || mode ==? 'v' || mode ==# "\<C-v>" ||
\ mode ==# 'ce' || mode ==? 's' || mode ==# "\<C-s>"
augroup plugin-clever-f-finalizer
autocmd CursorMoved <buffer> call s:maybe_finalize()
autocmd InsertEnter <buffer> call s:finalize()
augroup END
call s:mark_char_in_current_line(s:previous_map[mode], s:previous_char_num[mode])
endif
endif
if g:clever_f_show_prompt && should_redraw
redraw!
endif
finally
if g:clever_f_mark_cursor | call matchdelete(cursor_marker) | endif
if g:clever_f_mark_direct && exists('l:direct_markers')
for m in direct_markers
call matchdelete(m)
endfor
endif
if g:clever_f_hide_cursor_on_cmdline
let &t_ve = save_tve
call s:HI.hi(cursor_hi)
call s:HI.hi(lcursor_hi)
let &guicursor = guicursor
endif
endtry
else
" When repeated
let back = a:map =~# '\u'
if g:clever_f_fix_key_direction && s:previous_map[mode] =~# '\u'
let back = !back
endif
" reset and retry if timed out
if g:clever_f_timeout_ms > 0 && s:is_timedout()
call clever_f#reset()
return clever_f#find_with(a:map)
endif
" Restore highlights which were removed by timeout
if highlight_timer_enabled && s:highlight_timer < 0
call s:remove_highlight()
if mode ==# 'n' || mode ==? 'v' || mode ==# "\<C-v>" ||
\ mode ==# 'ce' || mode ==? 's' || mode ==# "\<C-s>"
call s:mark_char_in_current_line(s:previous_map[mode], s:previous_char_num[mode])
endif
endif
endif
if highlight_timer_enabled
if s:highlight_timer >= 0
call timer_stop(s:highlight_timer)
endif
let s:highlight_timer = timer_start(g:clever_f_highlight_timeout_ms, funcref('s:on_highlight_timer_expired'))
endif
return clever_f#repeat(back)
endfunction
function! clever_f#repeat(back) abort
let mode = s:mode()
let pmap = get(s:previous_map, mode, '')
let prev_char_num = get(s:previous_char_num, mode, 0)
if pmap ==# ''
return ''
endif
" ignore special characters like \<Left>
if type(prev_char_num) == type('') && char2nr(prev_char_num) == 128
return ''
endif
if a:back
let pmap = s:swapcase(pmap)
endif
if mode[0] ==? 'v' || mode[0] ==# "\<C-v>"
let cmd = s:move_cmd_for_visualmode(pmap, prev_char_num)
else
let inclusive = mode ==# 'no' && pmap =~# '\l'
let cmd = printf("%s:\<C-u>call clever_f#find(%s, %s)\<CR>",
\ inclusive ? 'v' : '',
\ string(pmap), prev_char_num)
endif
return cmd
endfunction
" absolutely moved forward?
function! s:moves_forward(p, n) abort
if a:p[0] != a:n[0]
return a:p[0] < a:n[0]
endif
if a:p[1] != a:n[1]
return a:p[1] < a:n[1]
endif
return 0
endfunction
function! clever_f#find(map, char_num) abort
let before_pos = getpos('.')[1 : 2]
let next_pos = s:next_pos(a:map, a:char_num, v:count1)
if next_pos == [0, 0]
return
endif
let moves_forward = s:moves_forward(before_pos, next_pos)
" update highlight when cursor moves across lines
let mode = s:mode()
if g:clever_f_mark_char
if next_pos[0] != before_pos[0]
\ || (a:map ==? 't' && !s:first_move[mode] && clever_f#compat#xor(s:moved_forward, moves_forward))
call s:remove_highlight()
call s:mark_char_in_current_line(a:map, a:char_num)
endif
endif
let s:moved_forward = moves_forward
let s:previous_pos[mode] = next_pos
let s:first_move[mode] = 0
endfunction
function! s:finalize() abort
autocmd! plugin-clever-f-finalizer
call s:remove_highlight()
let s:previous_pos = {}
let s:moved_forward = 0
endfunction
function! s:maybe_finalize() abort
let pp = get(s:previous_pos, s:last_mode, [0, 0])
if getpos('.')[1 : 2] != pp
call s:finalize()
endif
endfunction
function! s:move_cmd_for_visualmode(map, char_num) abort
let next_pos = s:next_pos(a:map, a:char_num, v:count1)
if next_pos == [0, 0]
return ''
endif
let m = s:mode()
call setpos("''", [0] + next_pos + [0])
let s:previous_pos[m] = next_pos
let s:first_move[m] = 0
return '``'
endfunction
function! s:search(pat, flag) abort
if g:clever_f_across_no_line
return search(a:pat, a:flag, line('.'))
else
return search(a:pat, a:flag)
endif
endfunction
function! s:should_use_migemo(char) abort
if !g:clever_f_use_migemo || a:char !~# '^\a$'
return 0
endif
if !g:clever_f_across_no_line
return 1
endif
return s:include_multibyte_char(getline('.'))
endfunction
function! s:load_migemo_dict() abort
let enc = &l:encoding
if enc ==# 'utf-8'
return clever_f#migemo#utf8#load_dict()
elseif enc ==# 'cp932'
return clever_f#migemo#cp932#load_dict()
elseif enc ==# 'euc-jp'
return clever_f#migemo#eucjp#load_dict()
else
let g:clever_f_use_migemo = 0
throw "clever-f: Encoding '" . enc . "' is not supported. Migemo is disabled"
endif
endfunction
function! s:generate_pattern(map, char_num) abort
let char = type(a:char_num) == type(0) ? nr2char(a:char_num) : a:char_num
let regex = char
let should_use_migemo = s:should_use_migemo(char)
if should_use_migemo
if !has_key(s:migemo_dicts, &l:encoding)
let s:migemo_dicts[&l:encoding] = s:load_migemo_dict()
endif
let regex = s:migemo_dicts[&l:encoding][regex] . '\&\%(' . char . '\|\A\)'
elseif stridx(g:clever_f_chars_match_any_signs, char) != -1
let regex = '\[!"#$%&''()=~|\-^\\@`[\]{};:+*<>,.?_/]'
elseif char ==# '\'
let regex = '\\'
endif
let is_exclusive_visual = &selection ==# 'exclusive' && s:mode()[0] ==? 'v'
if a:map ==# 't' && !is_exclusive_visual
let regex = '\_.\ze\%(' . regex . '\)'
elseif is_exclusive_visual && a:map ==# 'f'
let regex = '\%(' . regex . '\)\zs\_.'
elseif a:map ==# 'T'
let regex = '\%(' . regex . '\)\@<=\_.'
endif
if !should_use_migemo
let regex = '\V'.regex
endif
return ((g:clever_f_smart_case && char =~# '\l') || g:clever_f_ignore_case ? '\c' : '\C') . regex
endfunction
function! s:next_pos(map, char_num, count) abort
let mode = s:mode()
let search_flag = a:map =~# '\l' ? 'W' : 'bW'
let cnt = a:count
let pattern = s:generate_pattern(a:map, a:char_num)
if a:map ==? 't' && get(s:first_move, mode, 1)
if !s:search(pattern, search_flag . 'c')
return [0, 0]
endif
let cnt -= 1
endif
while 0 < cnt
if !s:search(pattern, search_flag)
return [0, 0]
endif
let cnt -= 1
endwhile
return getpos('.')[1 : 2]
endfunction
function! s:swapcase(char) abort
return a:char =~# '\u' ? tolower(a:char) : toupper(a:char)
endfunction
" Drop forced visual mode character ('nov' -> 'no')
function! s:mode() abort
let mode = mode(1)
if mode =~# '^no'
let mode = mode[0 : 1]
endif
return mode
endfunction
let &cpo = s:save_cpo
unlet s:save_cpo