patch 9.1.1576: cannot easily trigger wildcard expansion

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 #16759.

closes: #17806

Signed-off-by: Girish Palya <girishji@gmail.com>
Co-authored-by: zeertzjq <zeertzjq@outlook.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Girish Palya
2025-07-21 21:26:32 +02:00
committed by Christian Brabandt
parent 689f3bf313
commit b486ed8266
15 changed files with 191 additions and 70 deletions

View File

@ -238,6 +238,7 @@ nextwild(
cmdline_info_T *ccline = get_cmdline_info();
int i;
char_u *p;
int from_wildtrigger_func = options & WILD_FUNC_TRIGGER;
if (xp->xp_numfiles == -1)
{
@ -269,17 +270,22 @@ nextwild(
return FAIL;
}
i = (int)(xp->xp_pattern - ccline->cmdbuff);
xp->xp_pattern_len = ccline->cmdpos - 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)
if (!cmd_silent && !from_wildtrigger_func)
{
msg_puts("..."); // show that we are busy
out_flush();
}
i = (int)(xp->xp_pattern - ccline->cmdbuff);
xp->xp_pattern_len = ccline->cmdpos - i;
if (type == WILD_NEXT || type == WILD_PREV
|| type == WILD_PAGEUP || type == WILD_PAGEDOWN)
{

View File

@ -3126,6 +3126,8 @@ static funcentry_T global_functions[] =
ret_string, f_visualmode},
{"wildmenumode", 0, 0, 0, NULL,
ret_number, f_wildmenumode},
{"wildtrigger", 0, 0, 0, NULL,
ret_void, f_wildtrigger},
{"win_execute", 2, 3, FEARG_2, arg23_win_execute,
ret_string, f_win_execute},
{"win_findbuf", 1, 1, FEARG_1, arg1_number,

View File

@ -957,9 +957,11 @@ cmdline_wildchar_complete(
}
else // typed p_wc first time
{
if (c == p_wc || c == p_wcm)
if (c == p_wc || c == p_wcm || c == K_WILD)
{
options |= WILD_MAY_EXPAND_PATTERN;
if (c == K_WILD)
options |= WILD_FUNC_TRIGGER;
if (pre_incsearch_pos)
xp->xp_pre_incsearch_pos = *pre_incsearch_pos;
else
@ -1395,7 +1397,7 @@ cmdline_browse_history(
for (;;)
{
// one step backwards
if (c == K_UP|| c == K_S_UP || c == Ctrl_P
if (c == K_UP || c == K_S_UP || c == Ctrl_P
|| c == K_PAGEUP || c == K_KPAGEUP)
{
if (hiscnt == get_hislen()) // first time
@ -1818,9 +1820,9 @@ getcmdline_int(
*/
for (;;)
{
int trigger_cmdlinechanged = TRUE;
int end_wildmenu;
int prev_cmdpos = ccline.cmdpos;
int trigger_cmdlinechanged = TRUE;
int end_wildmenu;
int prev_cmdpos = ccline.cmdpos;
VIM_CLEAR(prev_cmdbuff);
@ -2058,9 +2060,11 @@ getcmdline_int(
}
}
// Completion for 'wildchar' or 'wildcharm' key.
if ((c == p_wc && !gotesc && KeyTyped) || c == p_wcm)
// Completion for 'wildchar', 'wildcharm', and wildtrigger()
if ((c == p_wc && !gotesc && KeyTyped) || c == p_wcm || c == K_WILD)
{
if (c == K_WILD)
++emsg_silent; // Silence the bell
res = cmdline_wildchar_complete(c, firstc != '@', &did_wild_list,
&wim_index, &xpc, &gotesc,
#ifdef FEAT_SEARCH_EXTRA
@ -2069,8 +2073,12 @@ getcmdline_int(
NULL
#endif
);
if (c == K_WILD)
--emsg_silent;
if (res == CMDLINE_CHANGED)
goto cmdline_changed;
if (c == K_WILD)
goto cmdline_not_changed;
}
gotesc = FALSE;
@ -5109,3 +5117,30 @@ get_user_input(
cmd_silent = cmd_silent_save;
}
#endif
/*
* "wildtrigger()" function
*/
void
f_wildtrigger(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
{
if (!(State & MODE_CMDLINE) || char_avail() || wild_menu_showing
|| 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
char_u 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(key_string, REMAP_NONE, 0, TRUE, FALSE);
}
}

View File

@ -279,6 +279,7 @@ enum key_extra
, KE_S_BS = 105 // shift + <BS>
, KE_SID = 106 // <SID> special key, followed by {nr};
, KE_ESC = 107 // used for K_ESC
, KE_WILD = 108 // triggers wildmode completion
};
/*
@ -491,6 +492,8 @@ enum key_extra
#define K_SCRIPT_COMMAND TERMCAP2KEY(KS_EXTRA, KE_SCRIPT_COMMAND)
#define K_SID TERMCAP2KEY(KS_EXTRA, KE_SID)
#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

@ -39,6 +39,7 @@ void f_getcmdscreenpos(typval_T *argvars, typval_T *rettv);
void f_getcmdtype(typval_T *argvars, typval_T *rettv);
void f_setcmdline(typval_T *argvars, typval_T *rettv);
void f_setcmdpos(typval_T *argvars, typval_T *rettv);
void f_wildtrigger(typval_T *argvars, typval_T *rettv);
int get_cmdline_firstc(void);
int get_list_range(char_u **str, int *num1, int *num2);
char *did_set_cedit(optset_T *args);

View File

@ -4329,42 +4329,63 @@ func Test_cmdcomplete_info()
autocmd CmdlineLeavePre * call expand('test_cmdline.*')
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&
" Disable char_avail so that wildtrigger() does not bail out
call test_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{trig}\<cr>", "tx")
call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info)
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{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{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{trig}\<cr>", "tx")
call assert_equal('{''cmdline_orig'': '''', ''pum_visible'': 0, ''matches'': [], ''selected'': 0}', g:cmdcomplete_info)
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{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{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&
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 test_override("char_avail", 0)
unlet g:cmdcomplete_info
cunmap <F8>
endfunc
func Test_redrawtabpanel_error()
@ -4387,6 +4408,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
@ -4397,7 +4419,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)
@ -4610,10 +4632,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')
@ -4699,25 +4722,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 test_override("char_avail", 1)
new
cnoremap <buffer> <F8> <C-R>=wildtrigger()[-1]<CR>
set wildmenu
set wildmode=full
let g:cmdprefix = 'echomsg'
let g:cmdchg_count = 0
call feedkeys(":echomsg\<Tab>", "tx")
call assert_equal(1, g:cmdchg_count) " once only for 'g', not again for <Tab>
for trig in ["\<Tab>", "\<F8>"]
let g:cmdchg_count = 0
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")
call assert_equal(1, g:cmdchg_count) " (once for 'h' and) once for 'o'
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")
call assert_equal(1, g:cmdchg_count) " once for 'h', not again for <tab>
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
@ -4726,10 +4759,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")
call assert_equal(2, g:cmdchg_count) " once for 'c', again for 'AbC'
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&
@ -4738,6 +4773,7 @@ func Test_cmdline_changed()
unlet g:cmdprefix
delfunc TestComplete
delcommand Test
call test_override("char_avail", 0)
endfunc
" vim: shiftwidth=2 sts=2 expandtab

View File

@ -719,6 +719,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
1576,
/**/
1575,
/**/

View File

@ -897,6 +897,7 @@ extern int (*dyn_libintl_wputenv)(const wchar_t *envstring);
#define BUF_DIFF_FILTER 0x2000
#define WILD_KEEP_SOLE_ITEM 0x4000
#define WILD_MAY_EXPAND_PATTERN 0x8000
#define WILD_FUNC_TRIGGER 0x10000 // called from wildtrigger()
// Flags for expand_wildcards()
#define EW_DIR 0x01 // include directory names