vim-patch:9.1.1576: cannot easily trigger wildcard expansion (#35022)

Problem:  cannot easily trigger wildcard expansion
Solution: Introduce wildtrigger() function
          (Girish Palya)

This PR introduces a new `wildtrigger()` function.

See `:h wildtrigger()`

`wildtrigger()` behaves like pressing the `wildchar,` but provides a
more refined and controlled completion experience:

- Suppresses beeps when no matches are found.
- Avoids displaying irrelevant completions (like full command lists)
  when the prefix is insufficient or doesn't match.
- Skips completion if the typeahead buffer has pending input or if a
  wildmenu is already active.
- Does not print "..." before completion.

This is an improvement on the `feedkeys()` based autocompletion script
given in vim/vim#16759.

closes: vim/vim#17806

b486ed8266

While at it, also make Ctrl-Z trigger search completion.

Co-authored-by: Girish Palya <girishji@gmail.com>
This commit is contained in:
zeertzjq
2025-07-23 06:12:50 +08:00
committed by GitHub
parent 8b5d8dfc73
commit 9377db2545
15 changed files with 243 additions and 65 deletions

View File

@@ -451,6 +451,8 @@ When repeating 'wildchar' or CTRL-N you cycle through the matches, eventually
ending up back to what was typed. If the first match is not what you wanted,
you can use <S-Tab> or CTRL-P to go straight back to what you typed.
See also |wildtrigger()|.
The 'wildmenu' option can be set to show the matches just above the command
line.

View File

@@ -296,6 +296,7 @@ VIMSCRIPT
• |cmdcomplete_info()| gets current cmdline completion info.
• |getcompletiontype()| gets command-line completion type for any string.
• |prompt_getinput()| gets current user-input in prompt-buffer.
• |wildtrigger()| triggers command-line expansion.
==============================================================================
CHANGED FEATURES *news-changed*

View File

@@ -7212,7 +7212,7 @@ A jump table for the options with a short description can be found at |Q_op|.
< 'wildchar' also enables completion in search pattern contexts such as
|/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal <Tab>
instead of triggering completion, type <C-V><Tab> or "\t".
See also |'wildoptions'|.
See also 'wildoptions' and |wildtrigger()|.
*'wildcharm'* *'wcm'*
'wildcharm' 'wcm' number (default 0)

View File

@@ -1036,6 +1036,7 @@ Mappings and Menus: *mapping-functions*
mapset() restore a mapping
menu_info() get information about a menu item
wildmenumode() check if the wildmode is active
wildtrigger() start wildcard expansion
Signs: *sign-functions*
sign_define() define or update a sign

View File

@@ -11878,6 +11878,33 @@ wildmenumode() *wildmenumode()*
Return: ~
(`any`)
wildtrigger() *wildtrigger()*
Start wildcard expansion in the command-line, using the
behavior defined by the 'wildmode' and 'wildoptions' settings.
See |cmdline-completion|.
This function also enables completion in search patterns such
as |/|, |?|, |:s|, |:g|, |:v| and |:vimgrep|.
Unlike pressing 'wildchar' manually, this function does not
produce a beep when no matches are found and generally
operates more quietly. This makes it suitable for triggering
completion automatically, such as from an |:autocmd|.
*cmdline-autocompletion*
Example: To make the completion menu pop up automatically as
you type on the command line, use: >vim
autocmd CmdlineChanged [:/?] call wildtrigger()
set wildmode=noselect:lastused,full wildoptions=pum
<
To retain normal history navigation (up/down keys): >vim
cnoremap <Up> <C-U><Up>
cnoremap <Down> <C-U><Down>
<
Return value is always 0.
Return: ~
(`number`)
win_execute({id}, {command} [, {silent}]) *win_execute()*
Like `execute()` but in the context of window {id}.
The window will temporarily be made the current window,

View File

@@ -7865,7 +7865,7 @@ vim.go.ww = vim.go.whichwrap
--- 'wildchar' also enables completion in search pattern contexts such as
--- `/`, `?`, `:s`, `:g`, `:v`, and `:vim`. To insert a literal <Tab>
--- instead of triggering completion, type <C-V><Tab> or "\t".
--- See also `'wildoptions'`.
--- See also 'wildoptions' and `wildtrigger()`.
---
--- @type integer
vim.o.wildchar = 9

View File

@@ -10813,6 +10813,32 @@ function vim.fn.wait(timeout, condition, interval) end
--- @return any
function vim.fn.wildmenumode() end
--- Start wildcard expansion in the command-line, using the
--- behavior defined by the 'wildmode' and 'wildoptions' settings.
--- See |cmdline-completion|.
---
--- This function also enables completion in search patterns such
--- as |/|, |?|, |:s|, |:g|, |:v| and |:vimgrep|.
---
--- Unlike pressing 'wildchar' manually, this function does not
--- produce a beep when no matches are found and generally
--- operates more quietly. This makes it suitable for triggering
--- completion automatically, such as from an |:autocmd|.
--- *cmdline-autocompletion*
--- Example: To make the completion menu pop up automatically as
--- you type on the command line, use: >vim
--- autocmd CmdlineChanged [:/?] call wildtrigger()
--- set wildmode=noselect:lastused,full wildoptions=pum
--- <
--- To retain normal history navigation (up/down keys): >vim
--- cnoremap <Up> <C-U><Up>
--- cnoremap <Down> <C-U><Down>
--- <
--- Return value is always 0.
---
--- @return number
function vim.fn.wildtrigger() end
--- Like `execute()` but in the context of window {id}.
--- The window will temporarily be made the current window,
--- without triggering autocommands or changing directory. When

View File

@@ -99,7 +99,7 @@ syn match vimUserAutoEvent contained "\<\h\w*\>" skipwhite nextgroup=vimUserAuto
" Highlight commonly used Groupnames {{{2
" GEN_SYN_VIM: vimGroup, START_STR='syn keyword vimGroup contained', END_STR=''
syn keyword vimGroup contained Added Boolean Changed Character Comment Conditional Constant Debug Define Delimiter Error Exception Float Function Identifier Ignore Include Keyword Label Macro Number Operator PreCondit PreProc Removed Repeat Special SpecialChar SpecialComment Statement StorageClass String Structure Tag Todo Type Typedef Underlined
syn keyword vimGroup contained Added Bold BoldItalic Boolean Changed Character Comment Conditional Constant Debug Define Delimiter Error Exception Float Function Identifier Ignore Include Italic Keyword Label Macro Number Operator PreCondit PreProc Removed Repeat Special SpecialChar SpecialComment Statement StorageClass String Structure Tag Todo Type Typedef Underlined
" Default highlighting groups {{{2
" GEN_SYN_VIM: vimHLGroup, START_STR='syn keyword vimHLGroup contained', END_STR=''

View File

@@ -254,6 +254,7 @@ int nextwild(expand_T *xp, int type, int options, bool escape)
{
CmdlineInfo *const ccline = get_cmdline_info();
char *p;
bool from_wildtrigger_func = options & WILD_FUNC_TRIGGER;
if (xp->xp_numfiles == -1) {
pre_incsearch_pos = xp->xp_pre_incsearch_pos;
@@ -280,17 +281,24 @@ int nextwild(expand_T *xp, int type, int options, bool escape)
return FAIL;
}
// If cmd_silent is set then don't show the dots, because redrawcmd() below
// won't remove them.
if (!cmd_silent && !(ui_has(kUICmdline) || ui_has(kUIWildmenu))) {
msg_puts("..."); // show that we are busy
ui_flush();
}
int i = (int)(xp->xp_pattern - ccline->cmdbuff);
assert(ccline->cmdpos >= i);
xp->xp_pattern_len = (size_t)ccline->cmdpos - (size_t)i;
// Skip showing matches if prefix is invalid during wildtrigger()
if (from_wildtrigger_func && xp->xp_context == EXPAND_COMMANDS
&& xp->xp_pattern_len == 0) {
return FAIL;
}
// If cmd_silent is set then don't show the dots, because redrawcmd() below
// won't remove them.
if (!cmd_silent && !from_wildtrigger_func
&& !(ui_has(kUICmdline) || ui_has(kUIWildmenu))) {
msg_puts("..."); // show that we are busy
ui_flush();
}
if (type == WILD_NEXT || type == WILD_PREV
|| type == WILD_PAGEUP || type == WILD_PAGEDOWN
|| type == WILD_PUM_WANT) {

View File

@@ -42,6 +42,7 @@ enum {
BUF_DIFF_FILTER = 0x2000,
WILD_KEEP_SOLE_ITEM = 0x4000,
WILD_MAY_EXPAND_PATTERN = 0x8000,
WILD_FUNC_TRIGGER = 0x10000, ///< called from wildtrigger()
};
#ifdef INCLUDE_GENERATED_DECLARATIONS

View File

@@ -13069,6 +13069,36 @@ M.funcs = {
params = {},
signature = 'wildmenumode()',
},
wildtrigger = {
desc = [==[
Start wildcard expansion in the command-line, using the
behavior defined by the 'wildmode' and 'wildoptions' settings.
See |cmdline-completion|.
This function also enables completion in search patterns such
as |/|, |?|, |:s|, |:g|, |:v| and |:vimgrep|.
Unlike pressing 'wildchar' manually, this function does not
produce a beep when no matches are found and generally
operates more quietly. This makes it suitable for triggering
completion automatically, such as from an |:autocmd|.
*cmdline-autocompletion*
Example: To make the completion menu pop up automatically as
you type on the command line, use: >vim
autocmd CmdlineChanged [:/?] call wildtrigger()
set wildmode=noselect:lastused,full wildoptions=pum
<
To retain normal history navigation (up/down keys): >vim
cnoremap <Up> <C-U><Up>
cnoremap <Down> <C-U><Down>
<
Return value is always 0.
]==],
name = 'wildtrigger',
params = {},
returns = 'number',
signature = 'wildtrigger()',
},
win_execute = {
args = { 2, 3 },
base = 2,

View File

@@ -1147,8 +1147,11 @@ static int command_line_wildchar_complete(CommandLineState *s)
res = OK; // don't insert 'wildchar' now
}
} else { // typed p_wc first time
if (s->c == p_wc || s->c == p_wcm) {
if (s->c == p_wc || s->c == p_wcm || s->c == K_WILD || s->c == Ctrl_Z) {
options |= WILD_MAY_EXPAND_PATTERN;
if (s->c == K_WILD) {
options |= WILD_FUNC_TRIGGER;
}
s->xpc.xp_pre_incsearch_pos = s->is_state.search_start;
}
s->wim_index = 0;
@@ -1442,11 +1445,22 @@ static int command_line_execute(VimState *state, int key)
}
}
// Completion for 'wildchar' or 'wildcharm' key.
if ((s->c == p_wc && !s->gotesc && KeyTyped) || s->c == p_wcm || s->c == Ctrl_Z) {
if (command_line_wildchar_complete(s) == CMDLINE_CHANGED) {
// Completion for 'wildchar', 'wildcharm', and wildtrigger()
if ((s->c == p_wc && !s->gotesc && KeyTyped) || s->c == p_wcm || s->c == K_WILD
|| s->c == Ctrl_Z) {
if (s->c == K_WILD) {
emsg_silent++; // Silence the bell
}
int res = command_line_wildchar_complete(s);
if (s->c == K_WILD) {
emsg_silent--;
}
if (res == CMDLINE_CHANGED) {
return command_line_changed(s);
}
if (s->c == K_WILD) {
return command_line_not_changed(s);
}
}
s->gotesc = false;
@@ -4907,3 +4921,27 @@ void get_user_input(const typval_T *const argvars, typval_T *const rettv, const
need_wait_return = false;
msg_didout = false;
}
/// "wildtrigger()" function
void f_wildtrigger(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
if (!(State & MODE_CMDLINE) || char_avail()
|| (wild_menu_showing != 0 && wild_menu_showing != WM_LIST)
|| cmdline_pum_active()) {
return;
}
int cmd_type = get_cmdline_type();
if (cmd_type == ':' || cmd_type == '/' || cmd_type == '?') {
// Add K_WILD as a single special key
uint8_t key_string[4];
key_string[0] = K_SPECIAL;
key_string[1] = KS_EXTRA;
key_string[2] = KE_WILD;
key_string[3] = NUL;
// Insert it into the typeahead buffer
ins_typebuf((char *)key_string, REMAP_NONE, 0, true, false);
}
}

View File

@@ -215,10 +215,15 @@ enum key_extra {
// KE_FOCUSGAINED = 98, // focus gained
// KE_FOCUSLOST = 99, // focus lost
KE_MOUSEMOVE = 100, // mouse moved with no button down
// KE_CANCEL = 101, // return from vgetc()
// KE_MOUSEMOVE_XY = 101,
// KE_CANCEL = 102, // return from vgetc()
KE_EVENT = 102, // event
KE_LUA = 103, // Lua special key
KE_COMMAND = 104, // <Cmd> special key
// KE_S_BS = 105,
// KE_SID = 106,
// KE_ESC = 107,
KE_WILD = 108, // triggers wildmode completion
};
// the three byte codes are replaced with the following int when using vgetc()
@@ -451,6 +456,8 @@ enum key_extra {
#define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND)
#define K_LUA TERMCAP2KEY(KS_EXTRA, KE_LUA)
#define K_WILD TERMCAP2KEY(KS_EXTRA, KE_WILD)
// Bits for modifier mask
// 0x01 cannot be used, because the modifier must be 0x02 or higher
#define MOD_MASK_SHIFT 0x02

View File

@@ -10173,7 +10173,7 @@ local options = {
< 'wildchar' also enables completion in search pattern contexts such as
|/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal <Tab>
instead of triggering completion, type <C-V><Tab> or "\t".
See also |'wildoptions'|.
See also 'wildoptions' and |wildtrigger()|.
]=],
full_name = 'wildchar',
scope = { 'global' },

View File

@@ -4361,42 +4361,64 @@ func Test_cmdcomplete_info()
autocmd CmdlineLeavePre * call expand('test_cmdline.*')
autocmd CmdlineLeavePre * let g:cmdcomplete_info = string(cmdcomplete_info())
augroup END
" Disable char_avail so that wildtrigger() does not bail out
call Ntest_override("char_avail", 1)
cnoremap <F8> <C-R>=wildtrigger()[-1]<CR>
call assert_equal({}, cmdcomplete_info())
for trig in ["\<Tab>", "\<F8>"]
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 feedkeys($":h echoms{trig}\<cr>", "tx")
call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info)
call feedkeys(":h echom\<tab>\<cr>", "tx")
call feedkeys($":h echom{trig}\<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 feedkeys($":h echom{trig}\<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 feedkeys($":h echom{trig}\<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 feedkeys($":h echoms{trig}\<cr>", "tx")
call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info)
call feedkeys(":h echom\<tab>\<cr>", "tx")
call feedkeys($":h echom{trig}\<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 feedkeys($":h echom{trig}\<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 feedkeys($":h echom{trig}\<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&
" set wildoptions&
set wildoptions= " Accommodate Nvim default
endfor
" wildtrigger() should not show matches when prefix is invalid
for pat in ["", " ", "22"]
call feedkeys($":{pat}\<F8>\<cr>", "tx") " No expansion
call assert_equal('{}', g:cmdcomplete_info)
endfor
augroup test_CmdlineLeavePre | autocmd! | augroup END
call Ntest_override("char_avail", 0)
unlet g:cmdcomplete_info
cunmap <F8>
endfunc
" Test wildcharm completion for '/' and '?' search
@@ -4414,6 +4436,7 @@ func Test_search_complete()
new
cnoremap <buffer><expr> <F9> GetComplInfo()
cnoremap <buffer> <F8> <C-R>=wildtrigger()[-1]<CR>
" Pressing <Tab> inserts tab character
set wildchar=0
@@ -4424,7 +4447,7 @@ func Test_search_complete()
call setline(1, ['the', 'these', 'thethe', 'thethere', 'foobar'])
for trig in ["\<tab>", "\<c-z>"]
for trig in ["\<tab>", "\<c-z>", "\<F8>"]
" Test menu first item and order
call feedkeys($"gg2j/t{trig}\<f9>", 'tx')
call assert_equal(['the', 'thethere', 'there', 'these', 'thethe'], g:compl_info.matches)
@@ -4637,10 +4660,11 @@ func Test_range_complete()
endfunc
new
cnoremap <buffer><expr> <F9> GetComplInfo()
cnoremap <buffer> <F8> <C-R>=wildtrigger()[-1]<CR>
call setline(1, ['ab', 'ba', 'ca', 'af'])
for trig in ["\<tab>", "\<c-z>"]
for trig in ["\<tab>", "\<c-z>", "\<F8>"]
call feedkeys($":%s/a{trig}\<f9>", 'xt')
call assert_equal(['ab', 'a', 'af'], g:compl_info.matches)
" call feedkeys($":vim9cmd :%s/a{trig}\<f9>", 'xt')
@@ -4727,25 +4751,35 @@ func Test_cmdline_changed()
autocmd CmdlineChanged * if getcmdline() =~ g:cmdprefix | let g:cmdchg_count += 1 | endif
augroup END
" Disable char_avail so that wildtrigger() does not bail out
call Ntest_override("char_avail", 1)
new
cnoremap <buffer> <F8> <C-R>=wildtrigger()[-1]<CR>
set wildmenu
set wildmode=full
let g:cmdprefix = 'echomsg'
for trig in ["\<Tab>", "\<F8>"]
let g:cmdchg_count = 0
call feedkeys(":echomsg\<Tab>", "tx")
call feedkeys($":echomsg{trig}", "tx")
call assert_equal(1, g:cmdchg_count) " once only for 'g', not again for <Tab>
endfor
let g:cmdchg_count = 0
let g:cmdprefix = 'echo'
call feedkeys(":ech\<Tab>", "tx")
for trig in ["\<Tab>", "\<F8>"]
let g:cmdchg_count = 0
call feedkeys($":ech{trig}", "tx")
call assert_equal(1, g:cmdchg_count) " (once for 'h' and) once for 'o'
endfor
set wildmode=noselect,full
let g:cmdchg_count = 0
let g:cmdprefix = 'ech'
call feedkeys(":ech\<Tab>", "tx")
for trig in ["\<Tab>", "\<F8>"]
let g:cmdchg_count = 0
call feedkeys($":ech{trig}", "tx")
call assert_equal(1, g:cmdchg_count) " once for 'h', not again for <tab>
endfor
command! -nargs=+ -complete=custom,TestComplete Test echo
@@ -4754,10 +4788,12 @@ func Test_cmdline_changed()
endfunc
set wildoptions=fuzzy wildmode=full
let g:cmdchg_count = 0
let g:cmdprefix = 'Test \(AbC\|abc\)'
call feedkeys(":Test abc\<Tab>", "tx")
for trig in ["\<Tab>", "\<F8>"]
let g:cmdchg_count = 0
call feedkeys($":Test abc{trig}", "tx")
call assert_equal(2, g:cmdchg_count) " once for 'c', again for 'AbC'
endfor
bw!
set wildmode& wildmenu& wildoptions&
@@ -4766,6 +4802,7 @@ func Test_cmdline_changed()
unlet g:cmdprefix
delfunc TestComplete
delcommand Test
call Ntest_override("char_avail", 0)
endfunc
" vim: shiftwidth=2 sts=2 expandtab