vim-patch:9.1.1166: command-line auto-completion hard with wildmenu

Problem:  command-line auto-completion hard with wildmenu
Solution: implement "noselect" wildoption value (Girish Palya)

When `noselect` is present in `wildmode` and 'wildmenu' is enabled, the
completion menu appears without pre-selecting the first item.

This change makes it easier to implement command-line auto-completion,
where the menu dynamically appears as characters are typed, and `<Tab>`
can be used to manually select an item. This can be achieved by
leveraging the `CmdlineChanged` event to insert `wildchar(m)`,
triggering completion menu.

Without this change, auto-completion using the 'wildmenu' mechanism is
not feasible, as it automatically inserts the first match, preventing
dynamic selection.

The following Vimscript snippet demonstrates how to configure
auto-completion using `noselect`:

```vim
vim9script
set wim=noselect:lastused,full wop=pum wcm=<C-@> wmnu
autocmd CmdlineChanged : timer_start(0, function(CmdComplete, [getcmdline()]))

def CmdComplete(cur_cmdline: string, timer: number)
  var [cmdline, curpos] = [getcmdline(), getcmdpos()]
  if cur_cmdline ==# cmdline  # Avoid completing each character in keymaps and pasted text
    && !pumvisible() && curpos == cmdline->len() + 1

    if cmdline[curpos - 2] =~ '[\w*/:]'  # Reduce noise by completing only selected characters
      feedkeys("\<C-@>", "ti")
      set eventignore+=CmdlineChanged  # Suppress redundant completion attempts
      timer_start(0, (_) => {
        getcmdline()->substitute('\%x00$', '', '')->setcmdline()  # Remove <C-@> if no completion items exist
        set eventignore-=CmdlineChanged
      })
    endif
  endif
enddef
```

fixes: vim/vim#16551
closes: vim/vim#16759

2bacc3e5fb

Cherry-pick Wildmode_Tests() change from patch 9.0.0418.

Co-authored-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
This commit is contained in:
Tomas Slusny
2025-03-03 17:51:42 +01:00
committed by zeertzjq
parent 948179cb19
commit 99d688e645
9 changed files with 105 additions and 19 deletions

View File

@@ -329,6 +329,7 @@ OPTIONS
• 'completeopt' flag "fuzzy" enables |fuzzy-matching| during |ins-completion|. • 'completeopt' flag "fuzzy" enables |fuzzy-matching| during |ins-completion|.
• 'completeopt' flag "preinsert" highlights text to be inserted. • 'completeopt' flag "preinsert" highlights text to be inserted.
• 'wildmode' flag "noselect" shows 'wildmenu' without selecting an entry.
• 'messagesopt' configures |:messages| and |hit-enter| prompt. • 'messagesopt' configures |:messages| and |hit-enter| prompt.
• 'tabclose' controls which tab page to focus when closing a tab page. • 'tabclose' controls which tab page to focus when closing a tab page.
• 'eventignorewin' to persistently ignore events in a window. • 'eventignorewin' to persistently ignore events in a window.

View File

@@ -7050,7 +7050,10 @@ A jump table for the options with a short description can be found at |Q_op|.
"lastused" When completing buffer names and more than one buffer "lastused" When completing buffer names and more than one buffer
matches, sort buffers by time last used (other than matches, sort buffers by time last used (other than
the current buffer). the current buffer).
When there is only a single match, it is fully completed in all cases. "noselect" Do not pre-select first menu item and start 'wildmenu'
if it is enabled.
When there is only a single match, it is fully completed in all cases
except when "noselect" is present.
Examples of useful colon-separated values: Examples of useful colon-separated values:
"longest:full" Like "longest", but also start 'wildmenu' if it is "longest:full" Like "longest", but also start 'wildmenu' if it is
@@ -7073,7 +7076,11 @@ A jump table for the options with a short description can be found at |Q_op|.
set wildmode=list,full set wildmode=list,full
< List all matches without completing, then each full match >vim < List all matches without completing, then each full match >vim
set wildmode=longest,list set wildmode=longest,list
< Complete longest common string, then list alternatives. < Complete longest common string, then list alternatives >vim
set wildmode=noselect:full
< Display 'wildmenu' without completing, then each full match >vim
set wildmode=noselect:lastused,full
< Same as above, but sort buffers by time last used.
More info here: |cmdline-completion|. More info here: |cmdline-completion|.
*'wildoptions'* *'wop'* *'wildoptions'* *'wop'*

View File

@@ -7691,7 +7691,10 @@ vim.go.wmnu = vim.go.wildmenu
--- "lastused" When completing buffer names and more than one buffer --- "lastused" When completing buffer names and more than one buffer
--- matches, sort buffers by time last used (other than --- matches, sort buffers by time last used (other than
--- the current buffer). --- the current buffer).
--- When there is only a single match, it is fully completed in all cases. --- "noselect" Do not pre-select first menu item and start 'wildmenu'
--- if it is enabled.
--- When there is only a single match, it is fully completed in all cases
--- except when "noselect" is present.
--- ---
--- Examples of useful colon-separated values: --- Examples of useful colon-separated values:
--- "longest:full" Like "longest", but also start 'wildmenu' if it is --- "longest:full" Like "longest", but also start 'wildmenu' if it is
@@ -7729,7 +7732,17 @@ vim.go.wmnu = vim.go.wildmenu
--- ```vim --- ```vim
--- set wildmode=longest,list --- set wildmode=longest,list
--- ``` --- ```
--- Complete longest common string, then list alternatives. --- Complete longest common string, then list alternatives
---
--- ```vim
--- set wildmode=noselect:full
--- ```
--- Display 'wildmenu' without completing, then each full match
---
--- ```vim
--- set wildmode=noselect:lastused,full
--- ```
--- Same as above, but sort buffers by time last used.
--- More info here: `cmdline-completion`. --- More info here: `cmdline-completion`.
--- ---
--- @type string --- @type string

View File

@@ -288,7 +288,7 @@ int nextwild(expand_T *xp, int type, int options, bool escape)
p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context); p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context);
} }
// Translate string into pattern and expand it. // Translate string into pattern and expand it.
const int use_options = (options const int use_options = ((options & ~WILD_KEEP_SOLE_ITEM)
| WILD_HOME_REPLACE | WILD_HOME_REPLACE
| WILD_ADD_SLASH | WILD_ADD_SLASH
| WILD_SILENT | WILD_SILENT
@@ -339,7 +339,7 @@ int nextwild(expand_T *xp, int type, int options, bool escape)
if (xp->xp_numfiles <= 0 && p2 == NULL) { if (xp->xp_numfiles <= 0 && p2 == NULL) {
beep_flush(); beep_flush();
} else if (xp->xp_numfiles == 1) { } else if (xp->xp_numfiles == 1 && !(options & WILD_KEEP_SOLE_ITEM)) {
// free expanded pattern // free expanded pattern
ExpandOne(xp, NULL, NULL, 0, WILD_FREE); ExpandOne(xp, NULL, NULL, 0, WILD_FREE);
} }

View File

@@ -40,6 +40,7 @@ enum {
WILD_NOERROR = 0x800, ///< sets EW_NOERROR WILD_NOERROR = 0x800, ///< sets EW_NOERROR
WILD_BUFLASTUSED = 0x1000, WILD_BUFLASTUSED = 0x1000,
BUF_DIFF_FILTER = 0x2000, BUF_DIFF_FILTER = 0x2000,
WILD_KEEP_SOLE_ITEM = 0x4000,
}; };
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS

View File

@@ -1082,6 +1082,9 @@ static int command_line_wildchar_complete(CommandLineState *s)
if (wim_flags[s->wim_index] & kOptWimFlagLastused) { if (wim_flags[s->wim_index] & kOptWimFlagLastused) {
options |= WILD_BUFLASTUSED; options |= WILD_BUFLASTUSED;
} }
if (wim_flags[0] & kOptWimFlagNoselect) {
options |= WILD_KEEP_SOLE_ITEM;
}
if (s->xpc.xp_numfiles > 0) { // typed p_wc at least twice if (s->xpc.xp_numfiles > 0) { // typed p_wc at least twice
// if 'wildmode' contains "list" may still need to list // if 'wildmode' contains "list" may still need to list
if (s->xpc.xp_numfiles > 1 if (s->xpc.xp_numfiles > 1
@@ -1124,19 +1127,20 @@ static int command_line_wildchar_complete(CommandLineState *s)
// when more than one match, and 'wildmode' first contains // when more than one match, and 'wildmode' first contains
// "list", or no change and 'wildmode' contains "longest,list", // "list", or no change and 'wildmode' contains "longest,list",
// list all matches // list all matches
if (res == OK && s->xpc.xp_numfiles > 1) { if (res == OK
&& s->xpc.xp_numfiles > ((wim_flags[s->wim_index] & kOptWimFlagNoselect) ? 0 : 1)) {
// a "longest" that didn't do anything is skipped (but not // a "longest" that didn't do anything is skipped (but not
// "list:longest") // "list:longest")
if (wim_flags[0] == kOptWimFlagLongest && ccline.cmdpos == j) { if (wim_flags[0] == kOptWimFlagLongest && ccline.cmdpos == j) {
s->wim_index = 1; s->wim_index = 1;
} }
if ((wim_flags[s->wim_index] & kOptWimFlagList) if ((wim_flags[s->wim_index] & kOptWimFlagList)
|| (p_wmnu && (wim_flags[s->wim_index] & kOptWimFlagFull) != 0)) { || (p_wmnu && (wim_flags[s->wim_index] & (kOptWimFlagFull|kOptWimFlagNoselect)))) {
if (!(wim_flags[0] & kOptWimFlagLongest)) { if (!(wim_flags[0] & kOptWimFlagLongest)) {
int p_wmnu_save = p_wmnu; int p_wmnu_save = p_wmnu;
p_wmnu = 0; p_wmnu = 0;
// remove match // remove match
nextwild(&s->xpc, WILD_PREV, 0, s->firstc != '@'); nextwild(&s->xpc, WILD_PREV, 0 | (options & ~kOptWimFlagNoselect), s->firstc != '@');
p_wmnu = p_wmnu_save; p_wmnu = p_wmnu_save;
} }
@@ -1146,7 +1150,8 @@ static int command_line_wildchar_complete(CommandLineState *s)
if (wim_flags[s->wim_index] & kOptWimFlagLongest) { if (wim_flags[s->wim_index] & kOptWimFlagLongest) {
nextwild(&s->xpc, WILD_LONGEST, options, s->firstc != '@'); nextwild(&s->xpc, WILD_LONGEST, options, s->firstc != '@');
} else if (wim_flags[s->wim_index] & kOptWimFlagFull) { } else if ((wim_flags[s->wim_index] & kOptWimFlagFull)
&& !(wim_flags[s->wim_index] & kOptWimFlagNoselect)) {
nextwild(&s->xpc, WILD_NEXT, options, s->firstc != '@'); nextwild(&s->xpc, WILD_NEXT, options, s->firstc != '@');
} }
} else { } else {
@@ -2875,6 +2880,8 @@ int check_opt_wim(void)
new_wim_flags[idx] |= kOptWimFlagList; new_wim_flags[idx] |= kOptWimFlagList;
} else if (i == 8 && strncmp(p, "lastused", 8) == 0) { } else if (i == 8 && strncmp(p, "lastused", 8) == 0) {
new_wim_flags[idx] |= kOptWimFlagLastused; new_wim_flags[idx] |= kOptWimFlagLastused;
} else if (i == 8 && strncmp(p, "noselect", 8) == 0) {
new_wim_flags[idx] |= kOptWimFlagNoselect;
} else { } else {
return FAIL; return FAIL;
} }

View File

@@ -10020,7 +10020,7 @@ local options = {
cb = 'did_set_wildmode', cb = 'did_set_wildmode',
defaults = 'full', defaults = 'full',
-- Keep this in sync with check_opt_wim(). -- Keep this in sync with check_opt_wim().
values = { 'full', 'longest', 'list', 'lastused' }, values = { 'full', 'longest', 'list', 'lastused', 'noselect' },
flags = true, flags = true,
deny_duplicates = false, deny_duplicates = false,
desc = [=[ desc = [=[
@@ -10042,7 +10042,10 @@ local options = {
"lastused" When completing buffer names and more than one buffer "lastused" When completing buffer names and more than one buffer
matches, sort buffers by time last used (other than matches, sort buffers by time last used (other than
the current buffer). the current buffer).
When there is only a single match, it is fully completed in all cases. "noselect" Do not pre-select first menu item and start 'wildmenu'
if it is enabled.
When there is only a single match, it is fully completed in all cases
except when "noselect" is present.
Examples of useful colon-separated values: Examples of useful colon-separated values:
"longest:full" Like "longest", but also start 'wildmenu' if it is "longest:full" Like "longest", but also start 'wildmenu' if it is
@@ -10065,7 +10068,11 @@ local options = {
set wildmode=list,full set wildmode=list,full
< List all matches without completing, then each full match >vim < List all matches without completing, then each full match >vim
set wildmode=longest,list set wildmode=longest,list
< Complete longest common string, then list alternatives. < Complete longest common string, then list alternatives >vim
set wildmode=noselect:full
< Display 'wildmenu' without completing, then each full match >vim
set wildmode=noselect:lastused,full
< Same as above, but sort buffers by time last used.
More info here: |cmdline-completion|. More info here: |cmdline-completion|.
]=], ]=],
full_name = 'wildmode', full_name = 'wildmode',

View File

@@ -352,6 +352,7 @@ let test_values = {
\ 'bs'], \ 'bs'],
\ ['xxx']], \ ['xxx']],
\ 'wildmode': [['', 'full', 'longest', 'list', 'lastused', 'list:full', \ 'wildmode': [['', 'full', 'longest', 'list', 'lastused', 'list:full',
\ 'noselect', 'noselect,full', 'noselect:lastused,full',
\ 'full,longest', 'full,full,full,full'], \ 'full,longest', 'full,full,full,full'],
\ ['xxx', 'a4', 'full,full,full,full,full']], \ ['xxx', 'a4', 'full,full,full,full,full']],
\ 'wildoptions': [['', 'tagfile', 'pum', 'fuzzy'], ['xxx']], \ 'wildoptions': [['', 'tagfile', 'pum', 'fuzzy'], ['xxx']],

View File

@@ -2168,22 +2168,58 @@ func Wildmode_tests()
call assert_equal('AAA AAAA AAAAA', g:Sline) call assert_equal('AAA AAAA AAAAA', g:Sline)
call assert_equal('"b A', @:) call assert_equal('"b A', @:)
" When 'wildmenu' is not set, 'noselect' completes first item
set wildmode=noselect
call feedkeys(":MyCmd o\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd oneA', @:)
" When 'noselect' is present, do not complete first <tab>.
set wildmenu
set wildmode=noselect
call feedkeys(":MyCmd o\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd o', @:)
call feedkeys(":MyCmd o\t\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd o', @:)
call feedkeys(":MyCmd o\t\t\<C-Y>\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd o', @:)
" When 'full' is present, complete after first <tab>.
set wildmode=noselect,full
call feedkeys(":MyCmd o\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd o', @:)
call feedkeys(":MyCmd o\t\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd oneA', @:)
call feedkeys(":MyCmd o\t\t\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd oneB', @:)
call feedkeys(":MyCmd o\t\t\t\<C-Y>\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd oneB', @:)
" 'noselect' has no effect when 'longest' is present.
set wildmode=noselect:longest
call feedkeys(":MyCmd o\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd one', @:)
" Complete 'noselect' value in 'wildmode' option
set wildmode&
call feedkeys(":set wildmode=n\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"set wildmode=noselect', @:)
call feedkeys(":set wildmode=\t\t\t\t\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"set wildmode=noselect', @:)
" when using longest completion match, matches shorter than the argument " when using longest completion match, matches shorter than the argument
" should be ignored (happens with :help) " should be ignored (happens with :help)
set wildmode=longest,full set wildmode=longest,full
set wildmenu
call feedkeys(":help a*\t\<C-B>\"\<CR>", 'xt') call feedkeys(":help a*\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"help a', @:) call assert_equal('"help a', @:)
" non existing file " non existing file
call feedkeys(":e a1b2y3z4\t\<C-B>\"\<CR>", 'xt') call feedkeys(":e a1b2y3z4\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"e a1b2y3z4', @:) call assert_equal('"e a1b2y3z4', @:)
set wildmenu&
" Test for longest file name completion with 'fileignorecase' " Test for longest file name completion with 'fileignorecase'
" On MS-Windows, file names are case insensitive. " On MS-Windows, file names are case insensitive.
if has('unix') if has('unix')
call writefile([], 'XTESTfoo') call writefile([], 'XTESTfoo', 'D')
call writefile([], 'Xtestbar') call writefile([], 'Xtestbar', 'D')
set nofileignorecase set nofileignorecase
call feedkeys(":e XT\<Tab>\<C-B>\"\<CR>", 'xt') call feedkeys(":e XT\<Tab>\<C-B>\"\<CR>", 'xt')
call assert_equal('"e XTESTfoo', @:) call assert_equal('"e XTESTfoo', @:)
@@ -2195,10 +2231,23 @@ func Wildmode_tests()
call feedkeys(":e Xt\<Tab>\<C-B>\"\<CR>", 'xt') call feedkeys(":e Xt\<Tab>\<C-B>\"\<CR>", 'xt')
call assert_equal('"e Xtest', @:) call assert_equal('"e Xtest', @:)
set fileignorecase& set fileignorecase&
call delete('XTESTfoo')
call delete('Xtestbar')
endif endif
" If 'noselect' is present, single item menu should not insert item
func! T(a, c, p)
return "oneA"
endfunc
command! -nargs=1 -complete=custom,T MyCmd
set wildmode=noselect,full
call feedkeys(":MyCmd o\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd o', @:)
call feedkeys(":MyCmd o\t\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd oneA', @:)
" 'nowildmenu' should make 'noselect' ineffective
set nowildmenu
call feedkeys(":MyCmd o\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"MyCmd oneA', @:)
%argdelete %argdelete
delcommand MyCmd delcommand MyCmd
delfunc T delfunc T