haya14busa aaa9a4ab95 Preserve syntax
- Problem: Flick occurs when vertical splitting
2016-01-21 04:27:10 +09:00

704 lines
20 KiB
VimL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function! s:_vital_loaded(V) abort
let s:Hint = a:V.import('HitAHint.Hint')
let s:PHighlight = a:V.import('Palette.Highlight')
let s:Buffer = a:V.import('Vim.Buffer')
let s:Prelude = a:V.import('Prelude')
let s:Set = a:V.import('Data.Set')
endfunction
function! s:_vital_depends() abort
return [
\ 'HitAHint.Hint',
\ 'Palette.Highlight',
\ 'Vim.Buffer',
\ 'Prelude',
\ 'Data.Set',
\ ]
endfunction
let s:TRUE = !0
let s:FALSE = 0
let s:DIRECTION = {'forward': 0, 'backward': 1}
" s:move() moves cursor over/accross window with Hit-A-Hint feature like
" vim-easymotion
" @param {dict} config
function! s:move(pattern, ...) abort
let o = s:new_overwin(get(a:, 1, {}))
return o.pattern(a:pattern)
endfunction
function! s:move_f(...) abort
echo 'Target: '
let c = s:getchar()
return s:move(c, get(a:, 1, {}))
endfunction
function! s:move_f2() abort
echo 'Target: '
let c = s:getchar()
redraw
echo 'Target: ' . c
let c2 = s:getchar()
return s:move(s:Prelude.escape_pattern(c . c2), get(a:, 1, {}))
endfunction
let s:overwin = {
\ 'config': {
\ 'keys': 'asdghklqwertyuiopzxcvbnmfj;',
\ 'use_upper': s:FALSE,
\ 'auto_land': s:TRUE,
\ 'highlight': {
\ 'shade': 'HitAHintShade',
\ 'target': 'HitAHintTarget',
\ },
\ 'jump_first_target_keys': [],
\ }
\ }
function! s:_init_hl() abort
highlight default HitAHintShade ctermfg=242 guifg=#777777
highlight default HitAHintTarget ctermfg=81 guifg=#66D9EF
endfunction
call s:_init_hl()
augroup vital-hit-a-hint-motion-default-highlight
autocmd!
autocmd ColorScheme * call s:_init_hl()
augroup END
function! s:new_overwin(...) abort
let o = deepcopy(s:overwin)
call s:deepextend(o.config, get(a:, 1, {}))
return o
endfunction
function! s:overwin.pattern(pattern) abort
let winpos = self.select_winpos(self.gather_poses_overwin(a:pattern), self.config.keys)
if winpos is# -1
else
call s:move_to_winpos(winpos)
endif
endfunction
" @param {{winnr: [lnum, cnum]}}
function! s:move_to_winpos(winpos) abort
let [winnr_str, pos] = a:winpos
let winnr = str2nr(winnr_str)
let is_win_moved = !(winnr is# winnr())
if is_win_moved
doautocmd WinLeave *
call s:move_to_win(winnr)
else
normal! m`
endif
call cursor(pos)
if is_win_moved
doautocmd WinEnter *
endif
endfunction
function! s:overwin.select_winpos(winnr2poses, keys) abort
let wposes = s:winnr2poses_to_list(a:winnr2poses)
if self.config.auto_land && len(wposes) is# 1
return wposes[0]
endif
return self.choose_prompt(s:Hint.create(wposes, a:keys))
endfunction
" s:wpos_to_hint() returns dict whose key is position with window and whose
" value is the hints.
" @param Tree{string: ((winnr, (number,number))|Tree)} hint_dict
" @return {{winnr: {string: list<char>}}} poskey to hint for each window
" e.g.
" {
" '1': {
" '00168:00004': ['b', 'c', 'b'],
" '00174:00001': ['b', 'c', 'a'],
" '00188:00004': ['b', 'b'],
" '00190:00001': ['b', 'a'],
" '00191:00016': ['a', 'c'],
" '00192:00004': ['a', 'b'],
" '00195:00035': ['a', 'a']
" },
" '3': {
" '00168:00004': ['c', 'c', 'c'],
" '00174:00001': ['c', 'c', 'b'],
" '00188:00004': ['c', 'c', 'a'],
" '00190:00001': ['c', 'b'],
" '00191:00016': ['c', 'a'],
" '00192:00004': ['b', 'c', 'c']
" }
" }
function! s:create_win2pos2hint(hint_dict) abort
return s:_create_win2pos2hint({}, a:hint_dict)
endfunction
function! s:_create_win2pos2hint(dict, hint_dict, ...) abort
let prefix = get(a:, 1, [])
for [hint, v] in items(a:hint_dict)
if type(v) is# type({})
call s:_create_win2pos2hint(a:dict, v, prefix + [hint])
else
let [winnr, pos] = v
let a:dict[winnr] = get(a:dict, winnr, {})
let a:dict[winnr][s:pos2poskey(pos)] = prefix + [hint]
endif
unlet v
endfor
return a:dict
endfunction
" s:pos2poskey() convertes pos to poskey to use pos as dictionary keys and
" sort pos correctly.
" @param {(number,number)} pos
" @return string
" e.g. [1, 1] -> '00001:00001'
function! s:pos2poskey(pos) abort
return join(map(copy(a:pos), "printf('%05d', v:val)"), ':')
endfunction
" s:poskey2pos() convertes poskey to pos.
" @param {string} poskey e.g. '00001:00001'
" @return {(number,number)}
" e.g. '00001:00001' -> [1, 1]
function! s:poskey2pos(poskey) abort
return map(split(a:poskey, ':'), 'str2nr(v:val)')
endfunction
function! s:overwin.choose_prompt(hint_dict) abort
if empty(a:hint_dict)
redraw
echo 'No target'
return -1
endif
let hinter = s:Hinter.new(a:hint_dict, self.config)
try
call hinter.before()
call hinter.show_hint()
redraw
echo 'Target key: '
let c = s:getchar()
if self.config.use_upper
let c = toupper(c)
endif
catch
echo v:exception
return -1
finally
call hinter.after()
endtry
" Jump to first target if target key is in config.jump_first_target_keys.
if index(self.config.jump_first_target_keys, c) isnot# -1
let c = split(self.config.keys, '\zs')[0]
endif
if has_key(a:hint_dict, c)
let target = a:hint_dict[c]
return type(target) is# type({}) ? self.choose_prompt(target) : target
else
redraw
echo 'Invalid target: ' . c
return -1
endif
endfunction
" Hinter show hints accross window.
" save_lines: {{winnr: {lnum: string}}}
" w2l2c2h: winnr to lnum to col num to hints. col2hints is tuple because we
" need sorted col to hints pair.
" save_syntax: {{winnr: &syntax}}
" {{winnr: {lnum: list<(cnum, list<char>)>}}}
let s:Hinter = {
\ 'save_lines': {},
\ 'w2l2c2h': {},
\ 'winnrs': [],
\ 'save_syntax': {},
\ 'save_conceallevel': {},
\ 'save_concealcursor': {},
\ 'save_modified': {},
\ 'save_modifiable': {},
\ 'save_readonly': {},
\ 'save_undo': {},
\ 'highlight_ids': {},
\ }
function! s:Hinter.new(hint_dict, config) abort
let s = deepcopy(self)
let s.config = a:config
let win2pos2hint = s:create_win2pos2hint(a:hint_dict)
let s.winnrs = map(keys(win2pos2hint), 'str2nr(v:val)')
let s.win2pos2hint = win2pos2hint
let s.w2l2c2h = s:win2pos2hint_to_w2l2c2h(win2pos2hint)
call s._save_lines()
return s
endfunction
function! s:Hinter.before() abort
call self.modify_env()
call self.disable_conceal_in_other_win()
endfunction
function! s:Hinter.after() abort
call self.restore_lines()
call self.restore_env()
call self.restore_conceal_in_other_win()
endfunction
function! s:Hinter._save_lines() abort
let nr = winnr()
try
for [winnr, pos2hint] in items(self.win2pos2hint)
call s:move_to_win(winnr)
let lnums = map(copy(keys(pos2hint)), 's:poskey2pos(v:val)[0]')
let self.save_lines[winnr] = get(self.save_lines, winnr, {})
for lnum in lnums
let self.save_lines[winnr][lnum] = getline(lnum)
endfor
endfor
finally
call s:move_to_win(nr)
endtry
endfunction
function! s:Hinter.restore_lines() abort
let nr = winnr()
try
for [winnr, lnum2line] in items(self.save_lines)
call s:move_to_win(winnr)
for [lnum, line] in items(lnum2line)
call s:setline(lnum, line)
endfor
endfor
finally
call s:move_to_win(nr)
endtry
endfunction
function! s:Hinter.modify_env() abort
let nr = winnr()
try
let self.highlight_id_cursor = matchadd('Cursor', '\%#', 1000001)
for winnr in self.winnrs
call s:move_to_win(winnr)
let self.save_conceal = s:PHighlight.get('Conceal')
let self.save_syntax[winnr] = &syntax
let self.save_conceallevel[winnr] = &l:conceallevel
let self.save_concealcursor[winnr] = &l:concealcursor
let self.save_modified[winnr] = &l:modified
let self.save_modifiable[winnr] = &l:modifiable
let self.save_readonly[winnr] = &l:readonly
let self.save_undo[winnr] = s:undo_lock.save()
setlocal modifiable
setlocal noreadonly
ownsyntax overwin
syntax enable
" syntax clear
setlocal conceallevel=2
setlocal concealcursor=ncv
execute 'highlight! link Conceal' self.config.highlight.target
let self.highlight_ids[winnr] = get(self.highlight_ids, winnr, [])
" let self.highlight_ids[winnr] += [matchadd(self.config.highlight.shade, '\_.*', 100)]
endfor
catch
call s:throw(v:throwpoint . ' ' . v:exception)
finally
call s:move_to_win(nr)
endtry
endfunction
function! s:Hinter.restore_env() abort
let nr = winnr()
try
call matchdelete(self.highlight_id_cursor)
for winnr in self.winnrs
call s:move_to_win(winnr)
" Clear syntax defined by Hit-A-Hint motion before restoring syntax.
syntax clear HitAHintTarget
let &syntax = self.save_syntax[winnr]
call s:PHighlight.set('Conceal', self.save_conceal)
let &l:conceallevel = self.save_conceallevel[winnr]
let &l:concealcursor = self.save_concealcursor[winnr]
call self.save_undo[winnr].restore()
let &l:modified = self.save_modified[winnr]
let &l:modifiable = self.save_modifiable[winnr]
let &l:readonly = self.save_readonly[winnr]
for id in self.highlight_ids[winnr]
call matchdelete(id)
endfor
endfor
catch
call s:throw(v:throwpoint . ' ' . v:exception)
finally
call s:move_to_win(nr)
endtry
endfunction
let s:undo_lock = {}
function! s:undo_lock.save() abort
let undo = deepcopy(self)
call undo._save()
return undo
endfunction
function! s:undo_lock._save() abort
if undotree().seq_last == 0
" if there are no undo history, disable undo feature by setting
" 'undolevels' to -1 and restore it.
let self.save_undolevels = &l:undolevels
let &l:undolevels = -1
elseif !s:Buffer.is_cmdwin()
" command line window doesn't support :wundo.
let self.undofile = tempname()
execute 'wundo!' self.undofile
else
let self.is_cmdwin = s:TRUE
endif
endfunction
function! s:undo_lock.restore() abort
if has_key(self, 'save_undolevels')
let &l:undolevels = self.save_undolevels
endif
if has_key(self, 'undofile') && filereadable(self.undofile)
silent execute 'rundo' self.undofile
call delete(self.undofile)
endif
if has_key(self, 'is_cmdwin')
" XXX: it breaks undo history. AFAIK, there are no way to save and restore
" undo history in commandline window.
call self.undobreak()
endif
endfunction
function! s:undo_lock.undobreak() abort
let old_undolevels = &l:undolevels
setlocal undolevels=-1
keepjumps call setline('.', getline('.'))
let &l:undolevels = old_undolevels
endfunction
function! s:Hinter.disable_conceal_in_other_win() abort
let allwinnrs = s:Set.set(range(1, winnr('$')))
let other_winnrs = allwinnrs.sub(self.winnrs).to_list()
for w in other_winnrs
if 'help' !=# getwinvar(w, '&buftype')
call setwinvar(w, 'overwin_save_conceallevel', getwinvar(w, '&conceallevel'))
call setwinvar(w, '&conceallevel', 0)
endif
endfor
endfunction
function! s:Hinter.restore_conceal_in_other_win() abort
let allwinnrs = s:Set.set(range(1, winnr('$')))
let other_winnrs = allwinnrs.sub(self.winnrs).to_list()
for w in other_winnrs
if 'help' !=# getwinvar(w, '&buftype')
call setwinvar(w, '&conceallevel', getwinvar(w, 'overwin_save_conceallevel'))
endif
endfor
endfunction
" ._pos2hint_to_line2col2hint() converts pos2hint to line2col2hint dict whose
" key is line number and whose value is list of tuple of col number to hint.
" line2col2hint is for show hint with replacing line by line.
" col should be sorted.
" @param {{string: list<char>}} pos2hint
" @return {number: [(number, list<char>)]}
function! s:Hinter._pos2hint_to_line2col2hint(pos2hint) abort
let line2col2hint = {}
let poskeys = sort(keys(a:pos2hint))
for poskey in poskeys
let [lnum, cnum] = s:poskey2pos(poskey)
let line2col2hint[lnum] = get(line2col2hint, lnum, [])
let line2col2hint[lnum] += [[cnum, a:pos2hint[poskey]]]
endfor
return line2col2hint
endfunction
function! s:Hinter.show_hint() abort
let nr = winnr()
try
for winnr in self.winnrs
call s:move_to_win(winnr)
call self._show_hint_for_win(winnr)
endfor
finally
call s:move_to_win(nr)
endtry
endfunction
function! s:Hinter._show_hint_for_win(winnr) abort
for [lnum, col2hint] in items(self.w2l2c2h[a:winnr])
call self._show_hint_for_line(a:winnr, lnum, col2hint)
endfor
endfunction
function! s:Hinter._show_hint_for_line(winnr, lnum, col2hint) abort
let line = self.save_lines[a:winnr][a:lnum]
let col_offset = 0
let prev_cnum = -1
let next_offset = 0
for [cnum, hint] in a:col2hint
let col_num = cnum + col_offset
let is_consecutive = cnum is# prev_cnum + 1
if !is_consecutive
let col_num += next_offset
else
let save_next_offset = next_offset
endif
let [line, offset, next_offset] = self._replace_line_for_hint(a:lnum, col_num, line, hint)
if is_consecutive
let col_offset += save_next_offset
endif
let col_offset += offset
call s:show_hint_pos(a:lnum, col_num, hint[0])
if len(hint) > 1
" call s:show_hint_pos(a:lnum, col_num + 1, hint[1])
call s:show_hint_pos(a:lnum, col_num + len(nr2char(8233)), hint[1])
endif
let prev_cnum = cnum
endfor
call s:setline(a:lnum, line)
endfunction
" ._replace_line_for_hint() replaces line to show hints.
" - It appends space if the line is empty
" - It replaces <Tab> to space if the target character is <Tab>
" - It replaces next target character if it's <Tab> and len(hint) > 1
" Replacing line changes col number, so it returns offset of col number.
" As for replaceing next target character, the timing to calculate offset
" depends on the col number of next hint in the same line, so it returns
" `next_offset` instead of returning offset all at once.
" @return {(string, number, number)} (line, offset, next_offset)
function! s:Hinter._replace_line_for_hint(lnum, col_num, line, hint) abort
let line = a:line
let col_num = a:col_num
let target = matchstr(line, '\%' . col_num .'c.')
" Append one space for empty line or match at end of line
if target is# ''
let hintwidth = strdisplaywidth(join(a:hint[:1], ''))
let line .= repeat(' ', hintwidth)
return [line, hintwidth, 0]
endif
let offset = 0
if target is# "\t"
let [line, offset] = self._replace_tab_target(a:lnum, col_num, line)
elseif strdisplaywidth(target) > 1
let line = self._replace_text_to_space(line, a:lnum, col_num, strdisplaywidth(target))
let offset = strdisplaywidth(target) - len(target)
else
" let line = substitute(line, '\%' . col_num . 'c.', ' ', '')
" 8233
" let space = ''
let space = nr2char(8233)
let line = substitute(line, '\%' . col_num . 'c.', space, '')
let offset = len(space) - len(target)
endif
let next_offset = 0
if len(a:hint) > 1
" pass [] as hint to stop recursion.
let [line, next_offset, _] = self._replace_line_for_hint(a:lnum, col_num + offset + 1, line, [])
endif
return [line, offset, next_offset]
endfunction
" @return {(line, offset)}
function! s:Hinter._replace_tab_target(lnum, col_num, line) abort
let space_len = s:tab2spacelen(a:line, a:col_num)
let line = self._replace_text_to_space(a:line, a:lnum, a:col_num, space_len)
return [line, space_len - 1]
endfunction
function! s:Hinter._replace_text_to_space(line, lnum, col_num, len) abort
let target = printf('\%%%dc.', a:col_num)
let line = substitute(a:line, target, repeat(' ', a:len), '')
return line
endfunction
" @param {number} col_num col_num is 1 origin like col()
function! s:tab2spacelen(line, col_num) abort
let before_line = a:col_num > 2 ? a:line[: a:col_num - 2]
\ : a:col_num is# 2 ? a:line[0]
\ : ''
let vcol_num = 1
for c in split(before_line, '\zs')
let vcol_num += c is# "\t" ? s:_virtual_tab2spacelen(vcol_num) : len(c)
endfor
return s:_virtual_tab2spacelen(vcol_num)
endfunction
function! s:_virtual_tab2spacelen(col_num) abort
return &tabstop - ((a:col_num - 1) % &tabstop)
endfunction
function! s:win2pos2hint_to_w2l2c2h(win2pos2hint) abort
let w2l2c2h = {}
for [winnr, pos2hint] in items(a:win2pos2hint)
let w2l2c2h[winnr] = s:pos2hint_to_line2col2hint(pos2hint)
endfor
return w2l2c2h
endfunction
" s:pos2hint_to_line2col2hint() converts pos2hint to line2col2hint dict whose
" key is line number and whose value is list of tuple of col number to hint.
" line2col2hint is for show hint with replacing line by line.
" col should be sorted.
" @param {{string: list<char>}} pos2hint
" @return {number: [(number, list<char>)]}
function! s:pos2hint_to_line2col2hint(pos2hint) abort
let line2col2hint = {}
let poskeys = sort(keys(a:pos2hint))
for poskey in poskeys
let [lnum, cnum] = s:poskey2pos(poskey)
let line2col2hint[lnum] = get(line2col2hint, lnum, [])
let line2col2hint[lnum] += [[cnum, a:pos2hint[poskey]]]
endfor
return line2col2hint
endfunction
" @param {number} winnr
function! s:move_to_win(winnr) abort
if a:winnr !=# winnr()
execute 'noautocmd' a:winnr . 'wincmd w'
endif
endfunction
" @param {regex} pattern
" @return {{winnr: list<list<(number,number))>}}
function! s:overwin.gather_poses_overwin(pattern) abort
return s:wincall(function('s:gather_poses'), [a:pattern])
endfunction
" s:gather_poses() aggregates patterm matched positions in visible current
" window for both direction excluding poses in fold.
" @return {{list<list<(number,number))>}}
function! s:gather_poses(pattern) abort
let f = s:gather_visible_matched_poses(a:pattern, s:DIRECTION.forward, s:TRUE)
let b = s:gather_visible_matched_poses(a:pattern, s:DIRECTION.backward, s:FALSE)
return filter(f + b, '!s:is_in_fold(v:val[0])')
endfunction
" s:gather_visible_matched_poses() aggregates pattern matched positions in visible current
" window.
" @param {regex} pattern
" @param {enum<DIRECTION>} direction see s:DIRECTION
" @param {bool} allow_cursor_pos_match
" @return {list<list<(number,number)>>} positions
function! s:gather_visible_matched_poses(pattern, direction, allow_cursor_pos_match) abort
let stop_line = line(a:direction is# s:DIRECTION.forward ? 'w$' : 'w0')
let search_flag = (a:direction is# s:DIRECTION.forward ? '' : 'b')
let c_flag = a:allow_cursor_pos_match ? 'c' : ''
let view = winsaveview()
let poses = []
keepjumps let pos = searchpos(a:pattern, c_flag . search_flag, stop_line)
while pos != [0, 0]
let poses += [pos]
keepjumps let pos = searchpos(a:pattern, search_flag, stop_line)
endwhile
call winrestview(view)
return poses
endfunction
" @param {{winnr: list<list<(number,number))>}} winnr2poses
" @param {number?} first_winnr the top winnr poses in returned list
" @return {list<{list<(winnr, (number,number))}>}
function! s:winnr2poses_to_list(winnr2poses, ...) abort
let first_winnr = get(a:, 1, winnr())
let first_winnr_poses = []
let other_poses = []
for [winnr_str, poses] in items(a:winnr2poses)
let winnr = str2nr(winnr_str)
if winnr is# first_winnr
let first_winnr_poses = map(copy(poses), '[winnr, v:val]')
else
let other_poses += map(copy(poses), '[winnr, v:val]')
endif
endfor
return first_winnr_poses + other_poses
endfunction
" @param {number} lnum line number
function! s:is_in_fold(lnum) abort
return foldclosed(a:lnum) != -1
endfunction
function! s:getchar(...) abort
let mode = get(a:, 1, 0)
while 1
let char = call('getchar', a:000)
" Workaround for the <expr> mappings
if string(char) !~# "\x80\xfd`"
return mode == 1 ? !!char
\ : type(char) == type(0) ? nr2char(char) : char
endif
endwhile
endfunction
" @param {funcref} func
" @param {arglist} list<S>
" @param {dict?} dict for :h call()
" @return {{winnr: <T>}}
function! s:wincall(func, arglist, ...) abort
let dict = get(a:, 1, {})
let r = {}
let start_winnr = winnr()
let r[start_winnr] = call(a:func, a:arglist, dict)
if s:Buffer.is_cmdwin()
return r
endif
noautocmd wincmd w
while winnr() isnot# start_winnr
let r[winnr()] = call(a:func, a:arglist, dict)
noautocmd wincmd w
endwhile
return r
endfunction
function! s:show_hint_pos(lnum, cnum, char) abort
let p = '\%'. a:lnum . 'l\%'. a:cnum . 'c.'
exec "syntax match HitAHintTarget '". p . "' contains=NONE containedin=.* conceal cchar=". a:char
endfunction
" deepextend (nest: 1)
function! s:deepextend(expr1, expr2) abort
let expr2 = copy(a:expr2)
for [k, V] in items(a:expr1)
if (type(V) is type({}) || type(V) is type([])) && has_key(expr2, k)
let a:expr1[k] = extend(a:expr1[k], expr2[k])
unlet expr2[k]
endif
unlet V
endfor
return extend(a:expr1, expr2)
endfunction
function! s:setline(lnum, text) abort
if getline(a:lnum) isnot# a:text
call setline(a:lnum, a:text)
endif
endfunction
function! s:throw(message) abort
throw 'vital: HitAHint.Motion: ' . a:message
endfunction