let s:t_funcref = type(function('tr')) let s:t_list = type([]) let s:PREFIX = '_vital_action_binder_' let s:UNIQUE = sha256(expand(':p')) let s:MODIFIERS = [ \ 'aboveleft', \ 'belowright', \ 'botright', \ 'browse', \ 'confirm', \ 'hide', \ 'keepalt', \ 'keepjumps', \ 'keepmarks', \ 'keeppatterns', \ 'lockmarks', \ 'noswapfile', \ 'silent', \ 'tab', \ 'topleft', \ 'verbose', \ 'vertical', \] function! s:_vital_loaded(V) abort let s:Revelator = a:V.import('App.Revelator') endfunction function! s:_vital_depends() abort return ['App.Revelator'] endfunction function! s:_vital_created(module) abort let a:module.name = 'action' let a:module.mark_sign_text = '*' endfunction function! s:get() abort return get(b:, s:PREFIX . s:UNIQUE, v:null) endfunction function! s:attach(candidates, ...) abort dict let binder = copy(s:binder) call extend(binder, { \ 'name': substitute(self.name, '\W', '-', 'g'), \ '_candidates': a:candidates, \ 'actions': {}, \ 'aliases': {}, \ 'markable': 0, \}) call extend(binder, get(a:000, 0, {})) " Lock methods lockvar binder._get_candidates lockvar binder._get_marked_candidates lockvar binder.define lockvar binder.alias lockvar binder.action lockvar binder.call " Define builtin actions call binder.define('builtin:echo', function('s:_action_echo'), { \ 'hidden': 1, \ 'description': 'Echo candidates', \ 'clear_marks': 0, \}) call binder.define('builtin:help', function('s:_action_help'), { \ 'description': 'Show a help of actions', \ 'mapping_mode': 'n', \ 'repeatable': 0, \ 'use_marks': 0, \ 'clear_marks': 0, \}) call binder.define('builtin:help:all', function('s:_action_help'), { \ 'description': 'Show a help of actions including hidden actions', \ 'mapping_mode': 'n', \ 'options': { 'all': 1 }, \ 'repeatable': 0, \ 'use_marks': 0, \ 'clear_marks': 0, \}) call binder.define('builtin:choice', function('s:_action_choice'), { \ 'description': 'Select an action to perform', \ 'mapping_mode': 'inv', \ 'repeatable': 0, \ 'clear_marks': 0, \}) call binder.define('builtin:repeat', function('s:_action_repeat'), { \ 'description': 'Repeat a previous repeatable action', \ 'mapping_mode': 'inv', \ 'repeatable': 0, \}) call binder.alias('echo', 'builtin:echo') call binder.alias('help', 'builtin:help') call binder.alias('help:all', 'builtin:help:all') execute printf('nmap ? (%s-builtin-help)', binder.name) execute printf('nmap a (%s-builtin-choice)', binder.name) execute printf('vmap a (%s-builtin-choice)', binder.name) execute printf('imap a (%s-builtin-choice)', binder.name) execute printf('nmap . (%s-builtin-repeat)', binder.name) execute printf('vmap . (%s-builtin-repeat)', binder.name) execute printf('imap . (%s-builtin-repeat)', binder.name) if binder.markable execute printf( \ 'sign define %s linehl=%s texthl=%s text=%s', \ 'VitalActionMarkSelectedSign', \ 'VitalActionMarkSelectedLine', \ 'VitalActionMarkSelected', \ self.mark_sign_text, \) call binder.define('builtin:mark', function('s:_action_mark'), { \ 'description': 'Mark/Unmark selected candidates', \ 'mapping_mode': 'nv', \ 'requirements': ['__lnum'], \ 'use_marks': 0, \ 'clear_marks': 0, \}) call binder.define('builtin:mark:set', function('s:_action_mark_set'), { \ 'hidden': 1, \ 'description': 'Mark selected candidates', \ 'mapping_mode': 'nv', \ 'requirements': ['__lnum'], \ 'use_marks': 0, \ 'clear_marks': 0, \}) call binder.define('builtin:mark:unset', function('s:_action_mark_unset'), { \ 'hidden': 1, \ 'description': 'Unmark selected candidates', \ 'mapping_mode': 'nv', \ 'requirements': ['__lnum'], \ 'use_marks': 0, \ 'clear_marks': 0, \}) call binder.define('builtin:mark:unall', function('s:_action_mark_unall'), { \ 'hidden': 1, \ 'description': 'Unmark all candidates', \ 'mapping_mode': 'n', \ 'use_marks': 0, \ 'clear_marks': 0, \}) call binder.alias('mark', 'builtin:mark') call binder.alias('mark:unall', 'builtin:mark:unall') execute printf('nmap mm (%s-builtin-mark)', binder.name) execute printf('vmap mm (%s-builtin-mark)', binder.name) execute printf('nmap m+ (%s-builtin-mark-set)', binder.name) execute printf('vmap m+ (%s-builtin-mark-set)', binder.name) execute printf('nmap m- (%s-builtin-mark-unset)', binder.name) execute printf('vmap m- (%s-builtin-mark-unset)', binder.name) execute printf('nmap m* (%s-builtin-mark-unall)', binder.name) execute printf('nmap (%s-builtin-mark)j', binder.name) execute printf('nmap k(%s-builtin-mark)', binder.name) endif let b:{s:PREFIX . s:UNIQUE} = binder return binder endfunction " Instance ------------------------------------------------------------------- let s:binder = {} function! s:binder._get_candidates(...) abort let fline = get(a:000, 0, line('.')) let lline = get(a:000, 1, fline) if type(self._candidates) == s:t_funcref let candidates = self._candidates(fline, lline) else let candidates = self._candidates[fline-1 : lline-1] endif return map( \ deepcopy(candidates), \ 'extend(v:val, {''__lnum'': fline + v:key})' \) endfunction function! s:binder._get_marked_candidates() abort if !self.markable throw 'vital: Action: The action binder is not markable' endif let signs = s:_get_signs() let lnums = map(signs, 'v:val.line') let candidates = [] call map(lnums, 'extend(candidates, self._get_candidates(v:val, v:val))') return candidates endfunction function! s:binder.define(name, callback, ...) abort let action = extend({ \ 'callback': a:callback, \ 'name': a:name, \ 'description': '', \ 'mapping': '', \ 'mapping_mode': '', \ 'requirements': [], \ 'options': {}, \ 'hidden': 0, \ 'repeatable': 1, \ 'use_marks': 1, \ 'clear_marks': 1, \}, get(a:000, 0, {}), \) if empty(action.mapping) let action.mapping = printf( \ '(%s-%s)', \ substitute(self.name, '\W', '-', 'g'), \ substitute(action.name, '\W', '-', 'g'), \) endif for mode in split(action.mapping_mode, '\zs') execute printf( \ '%snoremap %s %s:%scall _call_for_mapping(''%s'')', \ mode, \ action.mapping, \ mode =~# '[i]' ? '' : '', \ mode =~# '[ni]' ? '' : '', \ a:name, \) endfor let self.actions[action.name] = action endfunction function! s:binder.alias(alias, expr, ...) abort let alias = extend({ \ 'name': matchstr(a:expr, '\S\+$'), \ 'expr': a:expr, \ 'alias': a:alias, \}, get(a:000, 0, {}), \) let self.aliases[a:alias] = alias endfunction function! s:binder.action(expr) abort let mods = matchstr(a:expr, '^\%(.* \)\?\ze\S\+$') let name = matchstr(a:expr, '\S\+$') if has_key(self.aliases, name) let alias = self.aliases[name] return self.action(mods . alias.expr) elseif has_key(self.actions, name) return [mods, self.actions[name]] endif " If only one action/alias could be determine, use it. let candidates = filter( \ keys(self.aliases) + keys(self.actions), \ 'v:val =~# ''^'' . name' \) " Shorter to Longer call sort(candidates, { a, b -> len(a) - len(b) }) if empty(candidates) throw s:Revelator.warning(printf( \ 'No corresponding action has found for "%s"', \ a:expr \)) else " Use the first match return self.action(mods . candidates[0]) endif endfunction function! s:binder.call(expr, ...) abort range let [mods, action] = self.action(a:expr) if a:0 == 1 && type(a:1) == s:t_list let candidates = a:1 elseif self.markable && action.use_marks let candidates = self._get_marked_candidates() let candidates = empty(candidates) \ ? call(self._get_candidates, a:000, self) \ : candidates else let candidates = call(self._get_candidates, a:000, self) endif if !empty(action.requirements) call filter( \ candidates, \ 's:_is_satisfied(v:val, action.requirements)', \) endif let options = extend({ \ 'mods': mods, \}, action.options \) if self.markable && action.clear_marks call self.call('builtin:mark:unall') endif call call(action.callback, [candidates, options], self) return [mods, action] endfunction " Actions -------------------------------------------------------------------- function! s:_action_echo(candidates, options) abort for candidate in a:candidates echo string(candidate) endfor endfunction function! s:_action_help(candidates, options) abort dict let mappings = s:_find_mappings(self) let actions = values(self.actions) if !get(a:options, 'all') call filter(actions, '!v:val.hidden') endif let rows = [] let longest1 = 0 let longest2 = 0 let longest3 = 0 for action in actions let mapping = get(mappings, action.mapping, {}) let lhs = !empty(action.mapping) && !empty(mapping) ? mapping.lhs : '' let alias = s:_find_alias(self, action) let identifier = empty(alias) \ ? action.name \ : printf('%s [%s]', action.name, alias) let hidden = action.hidden ? '*' : ' ' let description = action.description let mapping = printf('%s [%s]', action.mapping, action.mapping_mode) call add(rows, [ \ lhs, \ identifier, \ hidden, \ description, \ mapping, \]) let longest1 = len(lhs) > longest1 ? len(lhs) : longest1 let longest2 = len(identifier) > longest2 ? len(identifier) : longest2 let longest3 = len(description) > longest3 ? len(description) : longest3 endfor let content = [] let pattern = printf( \ '%%-%ds %%-%ds %%s %%-%ds %%s', \ longest1, \ longest2, \ longest3, \) for params in sort(rows, 's:_compare') call add(content, call('printf', [pattern] + params)) endfor redraw | echo join(content, "\n") endfunction function! s:_action_choice(candidates, options) abort dict let s:_binder = self call inputsave() try echohl Question redraw | echo let fname = s:_get_function_name(function('s:_complete_action_aliases')) let aname = input( \ 'action: ', '', \ printf('customlist,%s', fname), \) redraw | echo finally echohl None call inputrestore() endtry if empty(aname) return endif let [mods, action] = self.call(aname, a:candidates) if action.repeatable let self._previous_action = [mods, action] endif endfunction function! s:_action_repeat(candidates, options) abort dict let [mods, action] = get(self, '_previous_action', ['', {}]) if empty(action) return endif return self.call(join([mods, action.name]), a:candidates) endfunction function! s:_action_mark(candidates, options) abort dict let set_candidates = [] let unset_candidates = [] let signmap = s:_get_signmap() for candidate in a:candidates if has_key(signmap, string(candidate.__lnum)) call add(unset_candidates, candidate) else call add(set_candidates, candidate) endif endfor call self.call('builtin:mark:set', set_candidates) call self.call('builtin:mark:unset', unset_candidates) endfunction function! s:_action_mark_set(candidates, options) abort dict let options = extend({}, a:options) let lnums = map(a:candidates, 'v:val.__lnum') let bufnr = bufnr('%') for lnum in lnums execute printf( \ 'sign place %d line=%d name=%s buffer=%d', \ lnum, lnum, \ 'VitalActionMarkSelectedSign', \ bufnr, \) endfor endfunction function! s:_action_mark_unset(candidates, options) abort dict let options = extend({}, a:options) let lnums = map(a:candidates, 'v:val.__lnum') let bufnr = bufnr('%') for lnum in lnums execute printf( \ 'sign unplace %d buffer=%d', \ lnum, bufnr, \) endfor endfunction function! s:_action_mark_unall(candidates, options) abort dict let options = extend({}, a:options) let bufnr = bufnr('%') execute printf('sign unplace * buffer=%d', bufnr) endfunction " Privates ------------------------------------------------------------------- function! s:_is_satisfied(candidate, requirements) abort for requirement in a:requirements if !has_key(a:candidate, requirement) return 0 endif endfor return 1 endfunction function! s:_compare(i1, i2) abort return a:i1[1] == a:i2[1] ? 0 : a:i1[1] > a:i2[1] ? 1 : -1 endfunction function! s:_compare_length(w1, w2) abort let l1 = len(a:w1) let l2 = len(a:w2) return l1 == l2 ? 0 : l1 > l2 ? 1 : -1 endfunction function! s:_find_mappings(binder) abort let content = s:_execute('map') let rhss = filter( \ map(values(a:binder.actions), 'v:val.mapping'), \ '!empty(v:val)' \) let rhsp = printf('\%%(%s\)', join(map(rhss, 'escape(v:val, ''\'')'), '\|')) let rows = filter(split(content, '\r\?\n'), 'v:val =~# ''@.*'' . rhsp') let pattern = '\(...\)\(\S\+\)' let mappings = {} for row in rows let [mode, lhs] = matchlist(row, pattern)[1 : 2] let rhs = matchstr(row, rhsp) let mappings[rhs] = { \ 'mode': mode, \ 'lhs': lhs, \ 'rhs': rhs, \} endfor return mappings endfunction function! s:_find_alias(binder, action) abort let aliases = filter( \ values(a:binder.aliases), \ 'v:val.name ==# a:action.name', \) " Remove aliases with mods call filter(aliases, 'v:val.name ==# v:val.expr') " Prefer shorter name let candidates = sort( \ map(aliases, 'v:val.alias'), \ function('s:_compare_length') \) return get(candidates, 0, '') endfunction function! s:_complete_action_aliases(arglead, cmdline, cursorpos) abort let terms = split(a:arglead, ' ', 1) " Build modifier candidates let modifiers = terms[:-2] let candidates = map( \ filter(copy(s:MODIFIERS), 'index(modifiers, v:val) == -1'), \ 'v:val . '' ''' \) " Build action/alias candidates let arglead = terms[-1] let binder = s:_binder let actions = values(binder.actions) if empty(arglead) call filter(actions, '!v:val.hidden') endif call extend(candidates, sort(extend( \ map(actions, 'v:val.name'), \ keys(binder.aliases), \)), 0) call filter(uniq(candidates), 'v:val =~# ''^'' . arglead') call map(candidates, 'join(modifiers + [v:val])') return candidates endfunction function! s:_call_for_mapping(name) abort range let binder = s:get() return s:Revelator.call(binder.call, [a:name, a:firstline, a:lastline], binder) endfunction function! s:_get_signs() abort let content = split(s:_execute('sign place buffer=' . bufnr('%')), '\r\?\n') let signs = map( \ filter(content, 'v:val =~# ''^\s\{4}'''), \ 's:_parse_sign(v:val)', \) return filter(signs, 'v:val.name ==# ''VitalActionMarkSelectedSign''') endfunction function! s:_get_signmap() abort let signmap = {} for sign in s:_get_signs() let signmap[sign.line] = sign endfor return signmap endfunction function! s:_parse_sign(sign) abort let terms = split(a:sign) let sign = {} for term in terms let m = split(term, '=') let sign[m[0]] = m[1] endfor return sign endfunction " Compatibility -------------------------------------------------------------- if has('patch-7.4.1842') function! s:_get_function_name(fn) abort return get(a:fn, 'name') endfunction else function! s:_get_function_name(fn) abort return matchstr(string(a:fn), 'function(''\zs.*\ze''') endfunction endif if exists('*execute') let s:_execute = function('execute') else function! s:_execute(command) abort try redir => content silent execute a:command finally redir END endtry return content endfunction endif " Highlight ------------------------------------------------------------------ function! s:_define_mark_highlights() abort highlight default link VitalActionMarkSelectedLine Search highlight default link VitalActionMarkSelected Search endfunction augroup vital_action_internal autocmd! * autocmd ColorScheme * call s:_define_mark_highlights() augroup END call s:_define_mark_highlights()