mirror of
https://github.com/vim/vim.git
synced 2025-09-27 04:14:06 -04:00
patch 9.1.1329: cannot get information about command line completion
Problem: cannot get information about command line completion Solution: add CmdlineLeavePre autocommand and cmdcomplete_info() Vim script function (Girish Palya) This commit introduces two features to improve introspection and control over command-line completion in Vim: - Add CmdlineLeavePre autocmd event: A new event triggered just before leaving the command line and before CmdlineLeave. It allows capturing completion-related state that is otherwise cleared by the time CmdlineLeave fires. - Add cmdcomplete_info() Vim script function: Returns a Dictionary with details about the current command-line completion state. These are similar in spirit to InsertLeavePre and complete_info(), but focused on command-line mode. **Use case:** In [[PR #16759](https://github.com/vim/vim/pull/16759)], two examples demonstrate command-line completion: one for live grep, and another for fuzzy file finding. However, both examples share two key limitations: 1. **Broken history recall (`<Up>`)** When selecting a completion item via `<Tab>` or `<C-n>`, the original pattern used for searching (e.g., a regex or fuzzy string) is overwritten in the command-line history. This makes it impossible to recall the original query later. This is especially problematic for interactive grep workflows, where it’s useful to recall a previous search and simply select a different match from the menu. 2. **Lack of default selection on `<CR>`** Often, it’s helpful to allow `<CR>` (Enter) to accept the first match in the completion list, even when no item is explicitly selected. This behavior is particularly useful in fuzzy file finding. ---- Below are the updated examples incorporating these improvements: **Live grep, fuzzy find file, fuzzy find buffer:** ```vim command! -nargs=+ -complete=customlist,GrepComplete Grep VisitFile() def GrepComplete(arglead: string, cmdline: string, cursorpos: number): list<any> return arglead->len() > 1 ? systemlist($'grep -REIHns "{arglead}"' .. ' --exclude-dir=.git --exclude=".*" --exclude="tags" --exclude="*.swp"') : [] enddef def VisitFile() if (selected_match != null_string) var qfitem = getqflist({lines: [selected_match]}).items[0] if qfitem->has_key('bufnr') && qfitem.lnum > 0 var pos = qfitem.vcol > 0 ? 'setcharpos' : 'setpos' exec $':b +call\ {pos}(".",\ [0,\ {qfitem.lnum},\ {qfitem.col},\ 0]) {qfitem.bufnr}' setbufvar(qfitem.bufnr, '&buflisted', 1) endif endif enddef nnoremap <leader>g :Grep<space> nnoremap <leader>G :Grep <c-r>=expand("<cword>")<cr> command! -nargs=* -complete=customlist,FuzzyFind Find execute(selected_match != '' ? $'edit {selected_match}' : '') var allfiles: list<string> autocmd CmdlineEnter : allfiles = null_list def FuzzyFind(arglead: string, _: string, _: number): list<string> if allfiles == null_list allfiles = systemlist($'find {get(g:, "fzfind_root", ".")} \! \( -path "*/.git" -prune -o -name "*.swp" \) -type f -follow') endif return arglead == '' ? allfiles : allfiles->matchfuzzy(arglead) enddef nnoremap <leader><space> :<c-r>=execute('let fzfind_root="."')\|''<cr>Find<space><c-@> nnoremap <leader>fv :<c-r>=execute('let fzfind_root="$HOME/.vim"')\|''<cr>Find<space><c-@> nnoremap <leader>fV :<c-r>=execute('let fzfind_root="$VIMRUNTIME"')\|''<cr>Find<space><c-@> command! -nargs=* -complete=customlist,FuzzyBuffer Buffer execute('b ' .. selected_match->matchstr('\d\+')) def FuzzyBuffer(arglead: string, _: string, _: number): list<string> var bufs = execute('buffers', 'silent!')->split("\n") var altbuf = bufs->indexof((_, v) => v =~ '^\s*\d\+\s\+#') if altbuf != -1 [bufs[0], bufs[altbuf]] = [bufs[altbuf], bufs[0]] endif return arglead == '' ? bufs : bufs->matchfuzzy(arglead) enddef nnoremap <leader><bs> :Buffer <c-@> var selected_match = null_string autocmd CmdlineLeavePre : SelectItem() def SelectItem() selected_match = '' if getcmdline() =~ '^\s*\%(Grep\|Find\|Buffer\)\s' var info = cmdcomplete_info() if info != {} && info.pum_visible && !info.matches->empty() selected_match = info.selected != -1 ? info.matches[info.selected] : info.matches[0] setcmdline(info.cmdline_orig). # Preserve search pattern in history endif endif enddef ``` **Auto-completion snippet:** ```vim set wim=noselect:lastused,full wop=pum wcm=<C-@> wmnu autocmd CmdlineChanged : CmdComplete() def CmdComplete() var [cmdline, curpos] = [getcmdline(), getcmdpos()] if getchar(1, {number: true}) == 0 # Typehead is empty (no more pasted input) && !pumvisible() && curpos == cmdline->len() + 1 && cmdline =~ '\%(\w\|[*/:.-]\)$' && cmdline !~ '^\d\+$' # Reduce noise feedkeys("\<C-@>", "ti") SkipCmdlineChanged() # Suppress redundant completion attempts # Remove <C-@> that get inserted when no items are available timer_start(0, (_) => getcmdline()->substitute('\%x00', '', 'g')->setcmdline()) endif enddef cnoremap <expr> <up> SkipCmdlineChanged("\<up>") cnoremap <expr> <down> SkipCmdlineChanged("\<down>") autocmd CmdlineEnter : set bo+=error autocmd CmdlineLeave : set bo-=error def SkipCmdlineChanged(key = ''): string set ei+=CmdlineChanged timer_start(0, (_) => execute('set ei-=CmdlineChanged')) return key != '' ? ((pumvisible() ? "\<c-e>" : '') .. key) : '' enddef ``` These customizable snippets can serve as *lightweight* and *native* alternatives to picker plugins like **FZF** or **Telescope** for common, everyday workflows. Also, live grep snippet can replace **cscope** without the overhead of building its database. closes: #17115 Signed-off-by: Girish Palya <girishji@gmail.com> Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
committed by
Christian Brabandt
parent
eac45c558e
commit
92f68e26ec
@@ -110,6 +110,7 @@ static keyvalue_T event_tab[NUM_EVENTS] = {
|
||||
KEYVALUE_ENTRY(EVENT_CMDLINECHANGED, "CmdlineChanged"),
|
||||
KEYVALUE_ENTRY(EVENT_CMDLINEENTER, "CmdlineEnter"),
|
||||
KEYVALUE_ENTRY(EVENT_CMDLINELEAVE, "CmdlineLeave"),
|
||||
KEYVALUE_ENTRY(EVENT_CMDLINELEAVEPRE, "CmdlineLeavePre"),
|
||||
KEYVALUE_ENTRY(EVENT_CMDUNDEFINED, "CmdUndefined"),
|
||||
KEYVALUE_ENTRY(EVENT_CMDWINENTER, "CmdwinEnter"),
|
||||
KEYVALUE_ENTRY(EVENT_CMDWINLEAVE, "CmdwinLeave"),
|
||||
@@ -2253,6 +2254,7 @@ apply_autocmds_group(
|
||||
|| event == EVENT_SYNTAX
|
||||
|| event == EVENT_CMDLINECHANGED
|
||||
|| event == EVENT_CMDLINEENTER
|
||||
|| event == EVENT_CMDLINELEAVEPRE
|
||||
|| event == EVENT_CMDLINELEAVE
|
||||
|| event == EVENT_CURSORMOVEDC
|
||||
|| event == EVENT_CMDWINENTER
|
||||
|
@@ -32,6 +32,8 @@ static int compl_match_arraysize;
|
||||
// First column in cmdline of the matched item for completion.
|
||||
static int compl_startcol;
|
||||
static int compl_selected;
|
||||
// cmdline before expansion
|
||||
static char_u *cmdline_orig = NULL;
|
||||
|
||||
#define SHOW_MATCH(m) (showtail ? showmatches_gettail(matches[m]) : matches[m])
|
||||
|
||||
@@ -432,6 +434,7 @@ cmdline_pum_remove(cmdline_info_T *cclp UNUSED)
|
||||
|
||||
pum_undisplay();
|
||||
VIM_CLEAR(compl_match_array);
|
||||
compl_match_arraysize = 0;
|
||||
p_lz = FALSE; // avoid the popup menu hanging around
|
||||
update_screen(0);
|
||||
p_lz = save_p_lz;
|
||||
@@ -1112,6 +1115,7 @@ ExpandInit(expand_T *xp)
|
||||
xp->xp_backslash = XP_BS_NONE;
|
||||
xp->xp_prefix = XP_PREFIX_NONE;
|
||||
xp->xp_numfiles = -1;
|
||||
VIM_CLEAR(cmdline_orig);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1238,6 +1242,10 @@ showmatches(expand_T *xp, int wildmenu UNUSED)
|
||||
int attr;
|
||||
int showtail;
|
||||
|
||||
// Save cmdline before expansion
|
||||
if (ccline->cmdbuff != NULL)
|
||||
cmdline_orig = vim_strnsave(ccline->cmdbuff, ccline->cmdlen);
|
||||
|
||||
if (xp->xp_numfiles == -1)
|
||||
{
|
||||
set_expand_context(xp);
|
||||
@@ -4299,4 +4307,36 @@ f_getcompletion(typval_T *argvars, typval_T *rettv)
|
||||
vim_free(pat);
|
||||
ExpandCleanup(&xpc);
|
||||
}
|
||||
|
||||
/*
|
||||
* "cmdcomplete_info()" function
|
||||
*/
|
||||
void
|
||||
f_cmdcomplete_info(typval_T *argvars UNUSED, typval_T *rettv)
|
||||
{
|
||||
cmdline_info_T *ccline = get_cmdline_info();
|
||||
dict_T *retdict;
|
||||
list_T *li;
|
||||
int idx;
|
||||
int ret = OK;
|
||||
|
||||
if (rettv_dict_alloc(rettv) == FAIL || ccline == NULL
|
||||
|| ccline->xpc == NULL || ccline->xpc->xp_files == NULL)
|
||||
return;
|
||||
retdict = rettv->vval.v_dict;
|
||||
ret = dict_add_string(retdict, "cmdline_orig", cmdline_orig);
|
||||
if (ret == OK)
|
||||
ret = dict_add_number(retdict, "pum_visible", pum_visible());
|
||||
if (ret == OK)
|
||||
ret = dict_add_number(retdict, "selected", ccline->xpc->xp_selected);
|
||||
if (ret == OK)
|
||||
{
|
||||
li = list_alloc();
|
||||
if (li == NULL)
|
||||
return;
|
||||
ret = dict_add_list(retdict, "matches", li);
|
||||
for (idx = 0; ret == OK && idx < ccline->xpc->xp_numfiles; idx++)
|
||||
list_append_string(li, ccline->xpc->xp_files[idx], -1);
|
||||
}
|
||||
}
|
||||
#endif // FEAT_EVAL
|
||||
|
@@ -2092,6 +2092,8 @@ static funcentry_T global_functions[] =
|
||||
ret_number, f_cindent},
|
||||
{"clearmatches", 0, 1, FEARG_1, arg1_number,
|
||||
ret_void, f_clearmatches},
|
||||
{"cmdcomplete_info",0, 0, 0, NULL,
|
||||
ret_dict_any, f_cmdcomplete_info},
|
||||
{"col", 1, 2, FEARG_1, arg2_string_or_list_number,
|
||||
ret_number, f_col},
|
||||
{"complete", 2, 2, FEARG_2, arg2_number_list,
|
||||
|
@@ -1915,6 +1915,11 @@ getcmdline_int(
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger CmdlineLeavePre autocommand
|
||||
if (ccline.cmdfirstc != NUL && (c == '\n' || c == '\r' || c == K_KENTER
|
||||
|| c == ESC || c == Ctrl_C))
|
||||
trigger_cmd_autocmd(cmdline_type, EVENT_CMDLINELEAVEPRE);
|
||||
|
||||
// The wildmenu is cleared if the pressed key is not used for
|
||||
// navigating the wild menu (i.e. the key is not 'wildchar' or
|
||||
// 'wildcharm' or Ctrl-N or Ctrl-P or Ctrl-A or Ctrl-L).
|
||||
|
@@ -23,4 +23,5 @@ int wildmenu_translate_key(cmdline_info_T *cclp, int key, expand_T *xp, int did_
|
||||
int wildmenu_process_key(cmdline_info_T *cclp, int key, expand_T *xp);
|
||||
void wildmenu_cleanup(cmdline_info_T *cclp);
|
||||
void f_getcompletion(typval_T *argvars, typval_T *rettv);
|
||||
void f_cmdcomplete_info(typval_T *argvars UNUSED, typval_T *rettv);
|
||||
/* vim: set ft=c : */
|
||||
|
@@ -2000,6 +2000,47 @@ func Test_QuitPre()
|
||||
bwipe Xbar
|
||||
endfunc
|
||||
|
||||
func Test_Cmdline_Trigger()
|
||||
autocmd CmdlineLeavePre : let g:log = "CmdlineLeavePre"
|
||||
new
|
||||
let g:log = ''
|
||||
nnoremap <F1> <Cmd>echo "hello"<CR>
|
||||
call feedkeys("\<F1>", 'x')
|
||||
call assert_equal('', g:log)
|
||||
nunmap <F1>
|
||||
let g:log = ''
|
||||
nnoremap <F1> :echo "hello"<CR>
|
||||
call feedkeys("\<F1>", 'x')
|
||||
call assert_equal('CmdlineLeavePre', g:log)
|
||||
nunmap <F1>
|
||||
let g:log = ''
|
||||
split
|
||||
call assert_equal('', g:log)
|
||||
call feedkeys(":echo hello", "tx")
|
||||
call assert_equal('CmdlineLeavePre', g:log)
|
||||
let g:log = ''
|
||||
close
|
||||
call assert_equal('', g:log)
|
||||
call feedkeys(":echo hello", "tx")
|
||||
call assert_equal('CmdlineLeavePre', g:log)
|
||||
let g:log = ''
|
||||
tabnew
|
||||
call assert_equal('', g:log)
|
||||
call feedkeys(":echo hello", "tx")
|
||||
call assert_equal('CmdlineLeavePre', g:log)
|
||||
let g:log = ''
|
||||
split
|
||||
call assert_equal('', g:log)
|
||||
call feedkeys(":echo hello", "tx")
|
||||
call assert_equal('CmdlineLeavePre', g:log)
|
||||
let g:log = ''
|
||||
tabclose
|
||||
call assert_equal('', g:log)
|
||||
call feedkeys(":echo hello", "tx")
|
||||
call assert_equal('CmdlineLeavePre', g:log)
|
||||
bw!
|
||||
endfunc
|
||||
|
||||
func Test_Cmdline()
|
||||
au! CmdlineChanged : let g:text = getcmdline()
|
||||
let g:text = 0
|
||||
@@ -2073,30 +2114,54 @@ func Test_Cmdline()
|
||||
|
||||
au! CmdlineEnter : let g:entered = expand('<afile>')
|
||||
au! CmdlineLeave : let g:left = expand('<afile>')
|
||||
au! CmdlineLeavePre : let g:leftpre = expand('<afile>')
|
||||
let g:entered = 0
|
||||
let g:left = 0
|
||||
let g:leftpre = 0
|
||||
call feedkeys(":echo 'hello'\<CR>", 'xt')
|
||||
call assert_equal(':', g:entered)
|
||||
call assert_equal(':', g:left)
|
||||
call assert_equal(':', g:leftpre)
|
||||
au! CmdlineEnter
|
||||
au! CmdlineLeave
|
||||
au! CmdlineLeavePre
|
||||
|
||||
let save_shellslash = &shellslash
|
||||
set noshellslash
|
||||
au! CmdlineEnter / let g:entered = expand('<afile>')
|
||||
au! CmdlineLeave / let g:left = expand('<afile>')
|
||||
au! CmdlineLeavePre / let g:leftpre = expand('<afile>')
|
||||
let g:entered = 0
|
||||
let g:left = 0
|
||||
let g:leftpre = 0
|
||||
new
|
||||
call setline(1, 'hello')
|
||||
call feedkeys("/hello\<CR>", 'xt')
|
||||
call assert_equal('/', g:entered)
|
||||
call assert_equal('/', g:left)
|
||||
call assert_equal('/', g:leftpre)
|
||||
bwipe!
|
||||
au! CmdlineEnter
|
||||
au! CmdlineLeave
|
||||
au! CmdlineLeavePre
|
||||
let &shellslash = save_shellslash
|
||||
|
||||
let g:left = "cancelled"
|
||||
let g:leftpre = "cancelled"
|
||||
au! CmdlineLeave : let g:left = "triggered"
|
||||
au! CmdlineLeavePre : let g:leftpre = "triggered"
|
||||
call feedkeys(":echo 'hello'\<esc>", 'xt')
|
||||
call assert_equal('triggered', g:left)
|
||||
call assert_equal('triggered', g:leftpre)
|
||||
let g:left = "cancelled"
|
||||
let g:leftpre = "cancelled"
|
||||
au! CmdlineLeave : let g:left = "triggered"
|
||||
call feedkeys(":echo 'hello'\<c-c>", 'xt')
|
||||
call assert_equal('triggered', g:left)
|
||||
call assert_equal('triggered', g:leftpre)
|
||||
au! CmdlineLeave
|
||||
au! CmdlineLeavePre
|
||||
|
||||
au! CursorMovedC : let g:pos += [getcmdpos()]
|
||||
let g:pos = []
|
||||
call feedkeys(":foo bar baz\<C-W>\<C-W>\<C-W>\<Esc>", 'xt')
|
||||
|
@@ -4268,4 +4268,48 @@ func Test_cd_bslash_completion_windows()
|
||||
let &shellslash = save_shellslash
|
||||
endfunc
|
||||
|
||||
" Testg cmdcomplete_info() with CmdlineLeavePre autocmd
|
||||
func Test_cmdcomplete_info()
|
||||
augroup test_CmdlineLeavePre
|
||||
autocmd!
|
||||
autocmd CmdlineLeavePre * let g:cmdcomplete_info = string(cmdcomplete_info())
|
||||
augroup END
|
||||
new
|
||||
call assert_equal({}, cmdcomplete_info())
|
||||
call feedkeys(":h echom\<cr>", "tx") " No expansion
|
||||
call assert_equal('{}', g:cmdcomplete_info)
|
||||
call feedkeys(":h echoms\<tab>\<cr>", "tx")
|
||||
call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info)
|
||||
call feedkeys(":h echom\<tab>\<cr>", "tx")
|
||||
call assert_equal(
|
||||
\ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 0, ''matches'': ['':echom'', '':echomsg''], ''selected'': 0}',
|
||||
\ g:cmdcomplete_info)
|
||||
call feedkeys(":h echom\<tab>\<tab>\<cr>", "tx")
|
||||
call assert_equal(
|
||||
\ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 0, ''matches'': ['':echom'', '':echomsg''], ''selected'': 1}',
|
||||
\ g:cmdcomplete_info)
|
||||
call feedkeys(":h echom\<tab>\<tab>\<tab>\<cr>", "tx")
|
||||
call assert_equal(
|
||||
\ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 0, ''matches'': ['':echom'', '':echomsg''], ''selected'': -1}',
|
||||
\ g:cmdcomplete_info)
|
||||
|
||||
set wildoptions=pum
|
||||
call feedkeys(":h echoms\<tab>\<cr>", "tx")
|
||||
call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info)
|
||||
call feedkeys(":h echom\<tab>\<cr>", "tx")
|
||||
call assert_equal(
|
||||
\ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 1, ''matches'': ['':echom'', '':echomsg''], ''selected'': 0}',
|
||||
\ g:cmdcomplete_info)
|
||||
call feedkeys(":h echom\<tab>\<tab>\<cr>", "tx")
|
||||
call assert_equal(
|
||||
\ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 1, ''matches'': ['':echom'', '':echomsg''], ''selected'': 1}',
|
||||
\ g:cmdcomplete_info)
|
||||
call feedkeys(":h echom\<tab>\<tab>\<tab>\<cr>", "tx")
|
||||
call assert_equal(
|
||||
\ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 1, ''matches'': ['':echom'', '':echomsg''], ''selected'': -1}',
|
||||
\ g:cmdcomplete_info)
|
||||
bw!
|
||||
set wildoptions&
|
||||
endfunc
|
||||
|
||||
" vim: shiftwidth=2 sts=2 expandtab
|
||||
|
@@ -704,6 +704,8 @@ static char *(features[]) =
|
||||
|
||||
static int included_patches[] =
|
||||
{ /* Add new patch number below this line */
|
||||
/**/
|
||||
1329,
|
||||
/**/
|
||||
1328,
|
||||
/**/
|
||||
|
@@ -1371,6 +1371,7 @@ enum auto_event
|
||||
EVENT_BUFWRITEPRE, // before writing a buffer
|
||||
EVENT_CMDLINECHANGED, // command line was modified
|
||||
EVENT_CMDLINEENTER, // after entering the command line
|
||||
EVENT_CMDLINELEAVEPRE, // just before leaving the command line
|
||||
EVENT_CMDLINELEAVE, // before leaving the command line
|
||||
EVENT_CMDUNDEFINED, // command undefined
|
||||
EVENT_CMDWINENTER, // after entering the cmdline window
|
||||
|
Reference in New Issue
Block a user