Skip to content

Commit f3c4fec

Browse files
zeertzjqgirishji
andcommitted
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](vim/vim#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 vim/vim@92f68e2 Co-authored-by: Girish Palya <girishji@gmail.com>
1 parent 652c3e7 commit f3c4fec

File tree

12 files changed

+238
-2
lines changed

12 files changed

+238
-2
lines changed

runtime/doc/autocmd.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,16 @@ CmdlineLeave Before leaving the command-line (including
400400
Note: `abort` can only be changed from false
401401
to true: cannot execute an already aborted
402402
cmdline by changing it to false.
403+
*CmdlineLeavePre*
404+
CmdlineLeavePre Just before leaving the command line, and
405+
before |CmdlineLeave|. Useful for capturing
406+
completion info with |cmdcomplete_info()|, as
407+
this information is cleared before
408+
|CmdlineLeave| is triggered. Triggered for
409+
non-interactive use of ":" in a mapping, but
410+
not when using |<Cmd>|. Also triggered when
411+
abandoning the command line by typing CTRL-C
412+
or <Esc>. <afile> is set to |cmdline-char|.
403413
*CmdwinEnter*
404414
CmdwinEnter After entering the command-line window.
405415
Useful for setting options specifically for

runtime/doc/builtin.txt

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtime/doc/news.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ EDITOR
122122

123123
EVENTS
124124

125-
todo
125+
|CmdlineLeavePre| triggered before preparing to leave the command line.
126126

127127
HIGHLIGHTS
128128

@@ -180,7 +180,7 @@ UI
180180

181181
VIMSCRIPT
182182

183-
todo
183+
|cmdcomplete_info()| gets current cmdline completion info.
184184

185185
==============================================================================
186186
CHANGED FEATURES *news-changed*

runtime/doc/usr_41.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,7 @@ Command line: *command-line-functions*
921921
getcmdwintype() return the current command-line window type
922922
getcompletion() list of command-line completion matches
923923
fullcommand() get full command name
924+
cmdcomplete_info() get current completion information
924925

925926
Quickfix and location lists: *quickfix-functions*
926927
getqflist() list of quickfix errors

runtime/lua/vim/_meta/vimfn.lua

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/nvim/auevents.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ return {
2929
CmdlineChanged = false, -- command line was modified
3030
CmdlineEnter = false, -- after entering cmdline mode
3131
CmdlineLeave = false, -- before leaving cmdline mode
32+
CmdlineLeavePre = false, -- just before leaving the command line
3233
CmdwinEnter = false, -- after entering the cmdline window
3334
CmdwinLeave = false, -- before leaving the cmdline window
3435
ColorScheme = false, -- after loading a colorscheme

src/nvim/autocmd.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
17261726
// Don't try expanding the following events.
17271727
if (event == EVENT_CMDLINECHANGED
17281728
|| event == EVENT_CMDLINEENTER
1729+
|| event == EVENT_CMDLINELEAVEPRE
17291730
|| event == EVENT_CMDLINELEAVE
17301731
|| event == EVENT_CMDUNDEFINED
17311732
|| event == EVENT_CURSORMOVEDC

src/nvim/cmdexpand.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ static int compl_match_arraysize;
9191
/// First column in cmdline of the matched item for completion.
9292
static int compl_startcol;
9393
static int compl_selected;
94+
/// cmdline before expansion
95+
static char *cmdline_orig = NULL;
9496

9597
#define SHOW_MATCH(m) (showtail ? showmatches_gettail(matches[m], false) : matches[m])
9698

@@ -401,6 +403,7 @@ void cmdline_pum_remove(void)
401403
{
402404
pum_undisplay(true);
403405
XFREE_CLEAR(compl_match_array);
406+
compl_match_arraysize = 0;
404407
}
405408

406409
void cmdline_pum_cleanup(CmdlineInfo *cclp)
@@ -967,6 +970,7 @@ void ExpandInit(expand_T *xp)
967970
xp->xp_backslash = XP_BS_NONE;
968971
xp->xp_prefix = XP_PREFIX_NONE;
969972
xp->xp_numfiles = -1;
973+
XFREE_CLEAR(cmdline_orig);
970974
}
971975

972976
/// Cleanup an expand structure after use.
@@ -1059,6 +1063,11 @@ int showmatches(expand_T *xp, bool wildmenu)
10591063
int columns;
10601064
bool showtail;
10611065

1066+
// Save cmdline before expansion
1067+
if (ccline->cmdbuff != NULL) {
1068+
cmdline_orig = xstrnsave(ccline->cmdbuff, (size_t)ccline->cmdlen);
1069+
}
1070+
10621071
if (xp->xp_numfiles == -1) {
10631072
set_expand_context(xp);
10641073
if (xp->xp_context == EXPAND_LUA) {
@@ -3653,3 +3662,30 @@ void f_getcompletion(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
36533662
xfree(pat);
36543663
ExpandCleanup(&xpc);
36553664
}
3665+
3666+
/// "cmdcomplete_info()" function
3667+
void f_cmdcomplete_info(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
3668+
{
3669+
CmdlineInfo *ccline = get_cmdline_info();
3670+
3671+
tv_dict_alloc_ret(rettv);
3672+
if (ccline == NULL || ccline->xpc == NULL || ccline->xpc->xp_files == NULL) {
3673+
return;
3674+
}
3675+
3676+
dict_T *retdict = rettv->vval.v_dict;
3677+
int ret = tv_dict_add_str(retdict, S_LEN("cmdline_orig"), cmdline_orig);
3678+
if (ret == OK) {
3679+
ret = tv_dict_add_nr(retdict, S_LEN("pum_visible"), pum_visible());
3680+
}
3681+
if (ret == OK) {
3682+
ret = tv_dict_add_nr(retdict, S_LEN("selected"), ccline->xpc->xp_selected);
3683+
}
3684+
if (ret == OK) {
3685+
list_T *li = tv_list_alloc(ccline->xpc->xp_numfiles);
3686+
ret = tv_dict_add_list(retdict, S_LEN("matches"), li);
3687+
for (int idx = 0; ret == OK && idx < ccline->xpc->xp_numfiles; idx++) {
3688+
tv_list_append_string(li, ccline->xpc->xp_files[idx], -1);
3689+
}
3690+
}
3691+
}

src/nvim/eval.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,33 @@ M.funcs = {
12541254
returns = false,
12551255
signature = 'clearmatches([{win}])',
12561256
},
1257+
cmdcomplete_info = {
1258+
args = 0,
1259+
desc = [=[
1260+
Returns a |Dictionary| with information about cmdline
1261+
completion. See |cmdline-completion|.
1262+
The items are:
1263+
cmdline_orig The original command-line string before
1264+
completion began.
1265+
pum_visible |TRUE| if popup menu is visible.
1266+
See |pumvisible()|.
1267+
matches List of all completion candidates. Each item
1268+
is a string.
1269+
selected Selected item index. First index is zero.
1270+
Index is -1 if no item is selected (showing
1271+
typed text only, or the last completion after
1272+
no item is selected when using the <Up> or
1273+
<Down> keys)
1274+
1275+
Returns an empty |Dictionary| if no completion was attempted,
1276+
if there was only one candidate and it was fully completed, or
1277+
if an error occurred.
1278+
]=],
1279+
name = 'cmdcomplete_info',
1280+
params = {},
1281+
returns = 'table<string,any>',
1282+
signature = 'cmdcomplete_info([{what}])',
1283+
},
12571284
col = {
12581285
args = { 1, 2 },
12591286
base = 1,

src/nvim/ex_getln.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,12 @@ static int command_line_execute(VimState *state, int key)
13011301
}
13021302
}
13031303

1304+
// Trigger CmdlineLeavePre autocommand
1305+
if (ccline.cmdfirstc != NUL && (s->c == '\n' || s->c == '\r' || s->c == K_KENTER
1306+
|| s->c == ESC || s->c == Ctrl_C)) {
1307+
trigger_cmd_autocmd(get_cmdline_type(), EVENT_CMDLINELEAVEPRE);
1308+
}
1309+
13041310
// The wildmenu is cleared if the pressed key is not used for
13051311
// navigating the wild menu (i.e. the key is not 'wildchar' or
13061312
// 'wildcharm' or Ctrl-N or Ctrl-P or Ctrl-A or Ctrl-L).

0 commit comments

Comments
 (0)