From f3c4fec43ffeff454c391ab9f5a860c78c660f85 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Mon, 21 Apr 2025 18:59:01 +0800 Subject: [PATCH] vim-patch:9.1.1329: cannot get information about command line completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 vim/vim#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 (``)** When selecting a completion item via `` or ``, 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 ``** Often, it’s helpful to allow `` (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 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 g :Grep nnoremap G :Grep =expand("") command! -nargs=* -complete=customlist,FuzzyFind Find execute(selected_match != '' ? $'edit {selected_match}' : '') var allfiles: list autocmd CmdlineEnter : allfiles = null_list def FuzzyFind(arglead: string, _: string, _: number): list 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 :=execute('let fzfind_root="."')\|''Find nnoremap fv :=execute('let fzfind_root="$HOME/.vim"')\|''Find nnoremap fV :=execute('let fzfind_root="$VIMRUNTIME"')\|''Find command! -nargs=* -complete=customlist,FuzzyBuffer Buffer execute('b ' .. selected_match->matchstr('\d\+')) def FuzzyBuffer(arglead: string, _: string, _: number): list 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 :Buffer 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= 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("\", "ti") SkipCmdlineChanged() # Suppress redundant completion attempts # Remove that get inserted when no items are available timer_start(0, (_) => getcmdline()->substitute('\%x00', '', 'g')->setcmdline()) endif enddef cnoremap SkipCmdlineChanged("\") cnoremap SkipCmdlineChanged("\") 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() ? "\" : '') .. 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: vim/vim#17115 https://github.com/vim/vim/commit/92f68e26ec36f2c263db5bea4f39d8503e0b741c Co-authored-by: Girish Palya --- runtime/doc/autocmd.txt | 10 +++++ runtime/doc/builtin.txt | 23 +++++++++++ runtime/doc/news.txt | 4 +- runtime/doc/usr_41.txt | 1 + runtime/lua/vim/_meta/vimfn.lua | 22 +++++++++++ src/nvim/auevents.lua | 1 + src/nvim/autocmd.c | 1 + src/nvim/cmdexpand.c | 36 +++++++++++++++++ src/nvim/eval.lua | 27 +++++++++++++ src/nvim/ex_getln.c | 6 +++ test/old/testdir/test_autocmd.vim | 65 +++++++++++++++++++++++++++++++ test/old/testdir/test_cmdline.vim | 44 +++++++++++++++++++++ 12 files changed, 238 insertions(+), 2 deletions(-) diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index d0f95f43d9..e8baad6d86 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -400,6 +400,16 @@ CmdlineLeave Before leaving the command-line (including Note: `abort` can only be changed from false to true: cannot execute an already aborted cmdline by changing it to false. + *CmdlineLeavePre* +CmdlineLeavePre Just before leaving the command line, and + before |CmdlineLeave|. Useful for capturing + completion info with |cmdcomplete_info()|, as + this information is cleared before + |CmdlineLeave| is triggered. Triggered for + non-interactive use of ":" in a mapping, but + not when using ||. Also triggered when + abandoning the command line by typing CTRL-C + or . is set to |cmdline-char|. *CmdwinEnter* CmdwinEnter After entering the command-line window. Useful for setting options specifically for diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 351b17fc59..9a7b9b1f30 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -1061,6 +1061,29 @@ clearmatches([{win}]) *clearmatches()* Parameters: ~ • {win} (`integer?`) +cmdcomplete_info([{what}]) *cmdcomplete_info()* + Returns a |Dictionary| with information about cmdline + completion. See |cmdline-completion|. + The items are: + cmdline_orig The original command-line string before + completion began. + pum_visible |TRUE| if popup menu is visible. + See |pumvisible()|. + matches List of all completion candidates. Each item + is a string. + selected Selected item index. First index is zero. + Index is -1 if no item is selected (showing + typed text only, or the last completion after + no item is selected when using the or + keys) + + Returns an empty |Dictionary| if no completion was attempted, + if there was only one candidate and it was fully completed, or + if an error occurred. + + Return: ~ + (`table`) + col({expr} [, {winid}]) *col()* The result is a Number, which is the byte index of the column position given with {expr}. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 82e9d0223e..4527ae0d15 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -122,7 +122,7 @@ EDITOR EVENTS -• todo +• |CmdlineLeavePre| triggered before preparing to leave the command line. HIGHLIGHTS @@ -180,7 +180,7 @@ UI VIMSCRIPT -• todo +• |cmdcomplete_info()| gets current cmdline completion info. ============================================================================== CHANGED FEATURES *news-changed* diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index f958491ccf..d951928dcc 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -921,6 +921,7 @@ Command line: *command-line-functions* getcmdwintype() return the current command-line window type getcompletion() list of command-line completion matches fullcommand() get full command name + cmdcomplete_info() get current completion information Quickfix and location lists: *quickfix-functions* getqflist() list of quickfix errors diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index c1387954df..a253a87a49 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -919,6 +919,28 @@ function vim.fn.cindent(lnum) end --- @param win? integer function vim.fn.clearmatches(win) end +--- Returns a |Dictionary| with information about cmdline +--- completion. See |cmdline-completion|. +--- The items are: +--- cmdline_orig The original command-line string before +--- completion began. +--- pum_visible |TRUE| if popup menu is visible. +--- See |pumvisible()|. +--- matches List of all completion candidates. Each item +--- is a string. +--- selected Selected item index. First index is zero. +--- Index is -1 if no item is selected (showing +--- typed text only, or the last completion after +--- no item is selected when using the or +--- keys) +--- +--- Returns an empty |Dictionary| if no completion was attempted, +--- if there was only one candidate and it was fully completed, or +--- if an error occurred. +--- +--- @return table +function vim.fn.cmdcomplete_info() end + --- The result is a Number, which is the byte index of the column --- position given with {expr}. --- For accepted positions see |getpos()|. diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua index a9b63b0b17..ce5058d7de 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -29,6 +29,7 @@ return { CmdlineChanged = false, -- command line was modified CmdlineEnter = false, -- after entering cmdline mode CmdlineLeave = false, -- before leaving cmdline mode + CmdlineLeavePre = false, -- just before leaving the command line CmdwinEnter = false, -- after entering the cmdline window CmdwinLeave = false, -- before leaving the cmdline window ColorScheme = false, -- after loading a colorscheme diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index d795dd2aa7..1ce1816dbe 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -1726,6 +1726,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force // Don't try expanding the following events. if (event == EVENT_CMDLINECHANGED || event == EVENT_CMDLINEENTER + || event == EVENT_CMDLINELEAVEPRE || event == EVENT_CMDLINELEAVE || event == EVENT_CMDUNDEFINED || event == EVENT_CURSORMOVEDC diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index 5c3b17b903..f856084637 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -91,6 +91,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 *cmdline_orig = NULL; #define SHOW_MATCH(m) (showtail ? showmatches_gettail(matches[m], false) : matches[m]) @@ -401,6 +403,7 @@ void cmdline_pum_remove(void) { pum_undisplay(true); XFREE_CLEAR(compl_match_array); + compl_match_arraysize = 0; } void cmdline_pum_cleanup(CmdlineInfo *cclp) @@ -967,6 +970,7 @@ void ExpandInit(expand_T *xp) xp->xp_backslash = XP_BS_NONE; xp->xp_prefix = XP_PREFIX_NONE; xp->xp_numfiles = -1; + XFREE_CLEAR(cmdline_orig); } /// Cleanup an expand structure after use. @@ -1059,6 +1063,11 @@ int showmatches(expand_T *xp, bool wildmenu) int columns; bool showtail; + // Save cmdline before expansion + if (ccline->cmdbuff != NULL) { + cmdline_orig = xstrnsave(ccline->cmdbuff, (size_t)ccline->cmdlen); + } + if (xp->xp_numfiles == -1) { set_expand_context(xp); if (xp->xp_context == EXPAND_LUA) { @@ -3653,3 +3662,30 @@ theend: xfree(pat); ExpandCleanup(&xpc); } + +/// "cmdcomplete_info()" function +void f_cmdcomplete_info(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + CmdlineInfo *ccline = get_cmdline_info(); + + tv_dict_alloc_ret(rettv); + if (ccline == NULL || ccline->xpc == NULL || ccline->xpc->xp_files == NULL) { + return; + } + + dict_T *retdict = rettv->vval.v_dict; + int ret = tv_dict_add_str(retdict, S_LEN("cmdline_orig"), cmdline_orig); + if (ret == OK) { + ret = tv_dict_add_nr(retdict, S_LEN("pum_visible"), pum_visible()); + } + if (ret == OK) { + ret = tv_dict_add_nr(retdict, S_LEN("selected"), ccline->xpc->xp_selected); + } + if (ret == OK) { + list_T *li = tv_list_alloc(ccline->xpc->xp_numfiles); + ret = tv_dict_add_list(retdict, S_LEN("matches"), li); + for (int idx = 0; ret == OK && idx < ccline->xpc->xp_numfiles; idx++) { + tv_list_append_string(li, ccline->xpc->xp_files[idx], -1); + } + } +} diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 1433394e19..d50ca45dbf 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -1254,6 +1254,33 @@ M.funcs = { returns = false, signature = 'clearmatches([{win}])', }, + cmdcomplete_info = { + args = 0, + desc = [=[ + Returns a |Dictionary| with information about cmdline + completion. See |cmdline-completion|. + The items are: + cmdline_orig The original command-line string before + completion began. + pum_visible |TRUE| if popup menu is visible. + See |pumvisible()|. + matches List of all completion candidates. Each item + is a string. + selected Selected item index. First index is zero. + Index is -1 if no item is selected (showing + typed text only, or the last completion after + no item is selected when using the or + keys) + + Returns an empty |Dictionary| if no completion was attempted, + if there was only one candidate and it was fully completed, or + if an error occurred. + ]=], + name = 'cmdcomplete_info', + params = {}, + returns = 'table', + signature = 'cmdcomplete_info([{what}])', + }, col = { args = { 1, 2 }, base = 1, diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 22b5d7accd..a4b8258c7a 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1301,6 +1301,12 @@ static int command_line_execute(VimState *state, int key) } } + // Trigger CmdlineLeavePre autocommand + if (ccline.cmdfirstc != NUL && (s->c == '\n' || s->c == '\r' || s->c == K_KENTER + || s->c == ESC || s->c == Ctrl_C)) { + trigger_cmd_autocmd(get_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). diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim index 701b0a17a4..87fc167e2e 100644 --- a/test/old/testdir/test_autocmd.vim +++ b/test/old/testdir/test_autocmd.vim @@ -1921,6 +1921,47 @@ func Test_QuitPre() bwipe Xbar endfunc +func Test_Cmdline_Trigger() + autocmd CmdlineLeavePre : let g:log = "CmdlineLeavePre" + new + let g:log = '' + nnoremap echo "hello" + call feedkeys("\", 'x') + call assert_equal('', g:log) + nunmap + let g:log = '' + nnoremap :echo "hello" + call feedkeys("\", 'x') + call assert_equal('CmdlineLeavePre', g:log) + nunmap + 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 @@ -1994,13 +2035,17 @@ func Test_Cmdline() au! CmdlineEnter : let g:entered = expand('') au! CmdlineLeave : let g:left = expand('') + au! CmdlineLeavePre : let g:leftpre = expand('') let g:entered = 0 let g:left = 0 + let g:leftpre = 0 call feedkeys(":echo 'hello'\", '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 " Nvim doesn't allow setting value of a hidden option to non-default value @@ -2009,18 +2054,38 @@ func Test_Cmdline() endif au! CmdlineEnter / let g:entered = expand('') au! CmdlineLeave / let g:left = expand('') + au! CmdlineLeavePre / let g:leftpre = expand('') let g:entered = 0 let g:left = 0 + let g:leftpre = 0 new call setline(1, 'hello') call feedkeys("/hello\", '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'\", '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'\", '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\\\\", 'xt') diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index 075bdae8d1..127f3593bf 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -4287,4 +4287,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\", "tx") " No expansion + call assert_equal('{}', g:cmdcomplete_info) + call feedkeys(":h echoms\\", "tx") + call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info) + call feedkeys(":h echom\\", "tx") + call assert_equal( + \ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 0, ''matches'': ['':echom'', '':echomsg''], ''selected'': 0}', + \ g:cmdcomplete_info) + call feedkeys(":h echom\\\", "tx") + call assert_equal( + \ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 0, ''matches'': ['':echom'', '':echomsg''], ''selected'': 1}', + \ g:cmdcomplete_info) + call feedkeys(":h echom\\\\", "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\\", "tx") + call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info) + call feedkeys(":h echom\\", "tx") + call assert_equal( + \ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 1, ''matches'': ['':echom'', '':echomsg''], ''selected'': 0}', + \ g:cmdcomplete_info) + call feedkeys(":h echom\\\", "tx") + call assert_equal( + \ '{''cmdline_orig'': ''h echom'', ''pum_visible'': 1, ''matches'': ['':echom'', '':echomsg''], ''selected'': 1}', + \ g:cmdcomplete_info) + call feedkeys(":h echom\\\\", "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