Skip to content

Commit 92f68e2

Browse files
girishjichrisbra
authored andcommitted
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](#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>
1 parent eac45c5 commit 92f68e2

File tree

14 files changed

+209
-4
lines changed

14 files changed

+209
-4
lines changed

runtime/doc/autocmd.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*autocmd.txt* For Vim version 9.1. Last change: 2025 Apr 04
1+
*autocmd.txt* For Vim version 9.1. Last change: 2025 Apr 21
22

33

44
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -398,6 +398,7 @@ Name triggered by ~
398398
|CmdlineChanged| after a change was made to the command-line text
399399
|CmdlineEnter| after the cursor moves to the command line
400400
|CmdlineLeave| before the cursor leaves the command line
401+
|CmdlineLeavePre| before preparing to leave the command line
401402

402403
|InsertEnter| starting Insert mode
403404
|InsertChange| when typing <Insert> while in Insert or Replace mode
@@ -639,6 +640,18 @@ CmdlineLeave Before leaving the command line; including
639640
<afile> is set to a single character,
640641
indicating the type of command-line.
641642
|cmdwin-char|
643+
*CmdlineLeavePre*
644+
CmdlineLeavePre Just before leaving the command line, and
645+
before |CmdlineLeave|. Useful for capturing
646+
completion info with |cmdcomplete_info()|, as
647+
this information is cleared before
648+
|CmdlineLeave| is triggered. Triggered for
649+
non-interactive use of ":" in a mapping, but
650+
not when using |<Cmd>|. Also triggered when
651+
abandoning the command line by typing CTRL-C
652+
or <Esc>. <afile> is set to a single
653+
character indicating the command-line type.
654+
See |cmdwin-char| for details.
642655
*CmdwinEnter*
643656
CmdwinEnter After entering the command-line window.
644657
Useful for setting options specifically for

runtime/doc/builtin.txt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*builtin.txt* For Vim version 9.1. Last change: 2025 Apr 18
1+
*builtin.txt* For Vim version 9.1. Last change: 2025 Apr 21
22

33

44
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -129,6 +129,8 @@ charidx({string}, {idx} [, {countcc} [, {utf16}]])
129129
chdir({dir}) String change current working directory
130130
cindent({lnum}) Number C indent for line {lnum}
131131
clearmatches([{win}]) none clear all matches
132+
cmdcomplete_info() Dict get current cmdline completion
133+
information
132134
col({expr} [, {winid}]) Number column byte index of cursor or mark
133135
complete({startcol}, {matches}) none set Insert mode completion
134136
complete_add({expr}) Number add completion match
@@ -1832,6 +1834,29 @@ clearmatches([{win}]) *clearmatches()*
18321834
Return type: |Number|
18331835

18341836

1837+
cmdcomplete_info([{what}]) *cmdcomplete_info()*
1838+
Returns a |Dictionary| with information about cmdline
1839+
completion. See |cmdline-completion|.
1840+
The items are:
1841+
cmdline_orig The original command-line string before
1842+
completion began.
1843+
pum_visible |TRUE| if popup menu is visible.
1844+
See |pumvisible()|.
1845+
matches List of all completion candidates. Each item
1846+
is a string.
1847+
selected Selected item index. First index is zero.
1848+
Index is -1 if no item is selected (showing
1849+
typed text only, or the last completion after
1850+
no item is selected when using the <Up> or
1851+
<Down> keys)
1852+
1853+
Returns an empty |Dictionary| if no completion was attempted,
1854+
if there was only one candidate and it was fully completed, or
1855+
if an error occurred.
1856+
1857+
Return type: dict<any>
1858+
1859+
18351860
col({expr} [, {winid}]) *col()*
18361861
The result is a Number, which is the byte index of the column
18371862
position given with {expr}.

runtime/doc/tags

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4079,6 +4079,7 @@ Cmdline-mode cmdline.txt /*Cmdline-mode*
40794079
CmdlineChanged autocmd.txt /*CmdlineChanged*
40804080
CmdlineEnter autocmd.txt /*CmdlineEnter*
40814081
CmdlineLeave autocmd.txt /*CmdlineLeave*
4082+
CmdlineLeavePre autocmd.txt /*CmdlineLeavePre*
40824083
CmdwinEnter autocmd.txt /*CmdwinEnter*
40834084
CmdwinLeave autocmd.txt /*CmdwinLeave*
40844085
ColorScheme autocmd.txt /*ColorScheme*
@@ -6568,6 +6569,7 @@ close_cb channel.txt /*close_cb*
65686569
closure eval.txt /*closure*
65696570
cmdarg-variable eval.txt /*cmdarg-variable*
65706571
cmdbang-variable eval.txt /*cmdbang-variable*
6572+
cmdcomplete_info() builtin.txt /*cmdcomplete_info()*
65716573
cmdline-arguments vi_diff.txt /*cmdline-arguments*
65726574
cmdline-changed version5.txt /*cmdline-changed*
65736575
cmdline-completion cmdline.txt /*cmdline-completion*

runtime/doc/usr_41.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*usr_41.txt* For Vim version 9.1. Last change: 2025 Mar 30
1+
*usr_41.txt* For Vim version 9.1. Last change: 2025 Apr 21
22

33
VIM USER MANUAL - by Bram Moolenaar
44

@@ -1111,6 +1111,7 @@ Command line: *command-line-functions*
11111111
getcmdwintype() return the current command-line window type
11121112
getcompletion() list of command-line completion matches
11131113
fullcommand() get full command name
1114+
cmdcomplete_info() get current completion information
11141115

11151116
Quickfix and location lists: *quickfix-functions*
11161117
getqflist() list of quickfix errors

runtime/doc/version9.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*version9.txt* For Vim version 9.1. Last change: 2025 Apr 18
1+
*version9.txt* For Vim version 9.1. Last change: 2025 Apr 21
22

33

44
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -41685,6 +41685,7 @@ Functions: ~
4168541685
|base64_encode()| encode a blob into a base64 string
4168641686
|blob2str()| convert a blob into a List of strings
4168741687
|bindtextdomain()| set message lookup translation base path
41688+
|cmdcomplete_info()| get current cmdline completion info
4168841689
|diff()| diff two Lists of strings
4168941690
|filecopy()| copy a file {from} to {to}
4169041691
|foreach()| apply function to List items
@@ -41708,6 +41709,7 @@ Functions: ~
4170841709

4170941710
Autocommands: ~
4171041711

41712+
|CmdlineLeavePre| before preparing to leave the command line
4171141713
|CursorMovedC| after the cursor was moved in the command-line
4171241714
|KeyInputPre| before processing any key event in any mode
4171341715
|SessionWritePost| after writing the session file |:mksession|

src/autocmd.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ static keyvalue_T event_tab[NUM_EVENTS] = {
110110
KEYVALUE_ENTRY(EVENT_CMDLINECHANGED, "CmdlineChanged"),
111111
KEYVALUE_ENTRY(EVENT_CMDLINEENTER, "CmdlineEnter"),
112112
KEYVALUE_ENTRY(EVENT_CMDLINELEAVE, "CmdlineLeave"),
113+
KEYVALUE_ENTRY(EVENT_CMDLINELEAVEPRE, "CmdlineLeavePre"),
113114
KEYVALUE_ENTRY(EVENT_CMDUNDEFINED, "CmdUndefined"),
114115
KEYVALUE_ENTRY(EVENT_CMDWINENTER, "CmdwinEnter"),
115116
KEYVALUE_ENTRY(EVENT_CMDWINLEAVE, "CmdwinLeave"),
@@ -2253,6 +2254,7 @@ apply_autocmds_group(
22532254
|| event == EVENT_SYNTAX
22542255
|| event == EVENT_CMDLINECHANGED
22552256
|| event == EVENT_CMDLINEENTER
2257+
|| event == EVENT_CMDLINELEAVEPRE
22562258
|| event == EVENT_CMDLINELEAVE
22572259
|| event == EVENT_CURSORMOVEDC
22582260
|| event == EVENT_CMDWINENTER

src/cmdexpand.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ static int compl_match_arraysize;
3232
// First column in cmdline of the matched item for completion.
3333
static int compl_startcol;
3434
static int compl_selected;
35+
// cmdline before expansion
36+
static char_u *cmdline_orig = NULL;
3537

3638
#define SHOW_MATCH(m) (showtail ? showmatches_gettail(matches[m]) : matches[m])
3739

@@ -432,6 +434,7 @@ cmdline_pum_remove(cmdline_info_T *cclp UNUSED)
432434

433435
pum_undisplay();
434436
VIM_CLEAR(compl_match_array);
437+
compl_match_arraysize = 0;
435438
p_lz = FALSE; // avoid the popup menu hanging around
436439
update_screen(0);
437440
p_lz = save_p_lz;
@@ -1112,6 +1115,7 @@ ExpandInit(expand_T *xp)
11121115
xp->xp_backslash = XP_BS_NONE;
11131116
xp->xp_prefix = XP_PREFIX_NONE;
11141117
xp->xp_numfiles = -1;
1118+
VIM_CLEAR(cmdline_orig);
11151119
}
11161120

11171121
/*
@@ -1238,6 +1242,10 @@ showmatches(expand_T *xp, int wildmenu UNUSED)
12381242
int attr;
12391243
int showtail;
12401244

1245+
// Save cmdline before expansion
1246+
if (ccline->cmdbuff != NULL)
1247+
cmdline_orig = vim_strnsave(ccline->cmdbuff, ccline->cmdlen);
1248+
12411249
if (xp->xp_numfiles == -1)
12421250
{
12431251
set_expand_context(xp);
@@ -4299,4 +4307,36 @@ f_getcompletion(typval_T *argvars, typval_T *rettv)
42994307
vim_free(pat);
43004308
ExpandCleanup(&xpc);
43014309
}
4310+
4311+
/*
4312+
* "cmdcomplete_info()" function
4313+
*/
4314+
void
4315+
f_cmdcomplete_info(typval_T *argvars UNUSED, typval_T *rettv)
4316+
{
4317+
cmdline_info_T *ccline = get_cmdline_info();
4318+
dict_T *retdict;
4319+
list_T *li;
4320+
int idx;
4321+
int ret = OK;
4322+
4323+
if (rettv_dict_alloc(rettv) == FAIL || ccline == NULL
4324+
|| ccline->xpc == NULL || ccline->xpc->xp_files == NULL)
4325+
return;
4326+
retdict = rettv->vval.v_dict;
4327+
ret = dict_add_string(retdict, "cmdline_orig", cmdline_orig);
4328+
if (ret == OK)
4329+
ret = dict_add_number(retdict, "pum_visible", pum_visible());
4330+
if (ret == OK)
4331+
ret = dict_add_number(retdict, "selected", ccline->xpc->xp_selected);
4332+
if (ret == OK)
4333+
{
4334+
li = list_alloc();
4335+
if (li == NULL)
4336+
return;
4337+
ret = dict_add_list(retdict, "matches", li);
4338+
for (idx = 0; ret == OK && idx < ccline->xpc->xp_numfiles; idx++)
4339+
list_append_string(li, ccline->xpc->xp_files[idx], -1);
4340+
}
4341+
}
43024342
#endif // FEAT_EVAL

src/evalfunc.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2092,6 +2092,8 @@ static funcentry_T global_functions[] =
20922092
ret_number, f_cindent},
20932093
{"clearmatches", 0, 1, FEARG_1, arg1_number,
20942094
ret_void, f_clearmatches},
2095+
{"cmdcomplete_info",0, 0, 0, NULL,
2096+
ret_dict_any, f_cmdcomplete_info},
20952097
{"col", 1, 2, FEARG_1, arg2_string_or_list_number,
20962098
ret_number, f_col},
20972099
{"complete", 2, 2, FEARG_2, arg2_number_list,

src/ex_getln.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,6 +1915,11 @@ getcmdline_int(
19151915
}
19161916
}
19171917

1918+
// Trigger CmdlineLeavePre autocommand
1919+
if (ccline.cmdfirstc != NUL && (c == '\n' || c == '\r' || c == K_KENTER
1920+
|| c == ESC || c == Ctrl_C))
1921+
trigger_cmd_autocmd(cmdline_type, EVENT_CMDLINELEAVEPRE);
1922+
19181923
// The wildmenu is cleared if the pressed key is not used for
19191924
// navigating the wild menu (i.e. the key is not 'wildchar' or
19201925
// 'wildcharm' or Ctrl-N or Ctrl-P or Ctrl-A or Ctrl-L).

src/proto/cmdexpand.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ int wildmenu_translate_key(cmdline_info_T *cclp, int key, expand_T *xp, int did_
2323
int wildmenu_process_key(cmdline_info_T *cclp, int key, expand_T *xp);
2424
void wildmenu_cleanup(cmdline_info_T *cclp);
2525
void f_getcompletion(typval_T *argvars, typval_T *rettv);
26+
void f_cmdcomplete_info(typval_T *argvars UNUSED, typval_T *rettv);
2627
/* vim: set ft=c : */

0 commit comments

Comments
 (0)