let s:save_cpo = &cpo set cpo&vim " SpaceVim API let s:HI = SpaceVim#api#import('vim#highlight') " constants let s:ESC_CODE = char2nr("\") 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', ["\"]) 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 "\" 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 ==# "\" || \ mode ==# 'ce' || mode ==? 's' || mode ==# "\" augroup plugin-clever-f-finalizer autocmd CursorMoved call s:maybe_finalize() autocmd InsertEnter 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 ==# "\" || \ mode ==# 'ce' || mode ==? 's' || mode ==# "\" 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 \ 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] ==# "\" let cmd = s:move_cmd_for_visualmode(pmap, prev_char_num) else let inclusive = mode ==# 'no' && pmap =~# '\l' let cmd = printf("%s:\call clever_f#find(%s, %s)\", \ 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