commit f3c4fec43ffeff454c391ab9f5a860c78c660f85
parent 652c3e76c7120adfd074994cb4524e4870df0389
Author: zeertzjq <zeertzjq@outlook.com>
Date: Mon, 21 Apr 2025 18:59:01 +0800
vim-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 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 (`<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: vim/vim#17115
https://github.com/vim/vim/commit/92f68e26ec36f2c263db5bea4f39d8503e0b741c
Co-authored-by: Girish Palya <girishji@gmail.com>
Diffstat:
12 files changed, 238 insertions(+), 2 deletions(-)
diff --git 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 |<Cmd>|. Also triggered when
+ abandoning the command line by typing CTRL-C
+ or <Esc>. <afile> 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
@@ -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 <Up> or
+ <Down> 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<string,any>`)
+
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
@@ -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
@@ -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
@@ -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 <Up> or
+--- <Down> 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<string,any>
+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
@@ -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
@@ -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
@@ -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
@@ -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 <Up> or
+ <Down> 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<string,any>',
+ signature = 'cmdcomplete_info([{what}])',
+ },
col = {
args = { 1, 2 },
base = 1,
diff --git 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
@@ -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 <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
@@ -1994,13 +2035,17 @@ 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
" 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('<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')
diff --git 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\<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