Repository: neovim/neovim
Pull request #32709: vim-patch:9.1.{1166,1168}: 'wildmode' noselect
Author: login: deathbeam
Labels: name: vim-patch
description: See https://neovim.io/doc/user/dev_vimpatch.html
Created: 2025-03-03T17:56:49Z | Merged: 2025-03-03T22:41:41Z

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
```

closes: vim/vim#16759

https://github.com/vim/vim/commit/2bacc3e5fb3569e0fd98e129cb1e422ca18b80a6


Diff:
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index a131934a8efe9e..415a2342543ff9 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -329,6 +329,7 @@ OPTIONS
 
 • 'completeopt' flag "fuzzy" enables |fuzzy-matching| during |ins-completion|.
 • 'completeopt' flag "preinsert" highlights text to be inserted.
+• 'wildmode' flag "noselect" shows 'wildmenu' without selecting an entry.
 • 'messagesopt' configures |:messages| and |hit-enter| prompt.
 • 'tabclose' controls which tab page to focus when closing a tab page.
 • 'eventignorewin' to persistently ignore events in a window.
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index a6f5f85d377afb..a3cef8bacbf805 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -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
 			matches, sort buffers by time last used (other than
 			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:
 	"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
 <	List all matches without completing, then each full match >vim
 		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|.
 
 						*'wildoptions'* *'wop'*
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
index 8df174a83830b9..8e6acecc2813b9 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -7691,7 +7691,10 @@ vim.go.wmnu = vim.go.wildmenu
 --- "lastused"	When completing buffer names and more than one buffer
 --- 		matches, sort buffers by time last used (other than
 --- 		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:
 --- "longest:full"	Like "longest", but also start 'wildmenu' if it is
@@ -7729,7 +7732,17 @@ vim.go.wmnu = vim.go.wildmenu
 --- ```vim
 --- 	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`.
 ---
 --- @type string
diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c
index 33ccc834d2a4cf..fcfeda5482601a 100644
--- a/src/nvim/cmdexpand.c
+++ b/src/nvim/cmdexpand.c
@@ -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);
     }
     // 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_ADD_SLASH
                              | WILD_SILENT
@@ -339,7 +339,7 @@ int nextwild(expand_T *xp, int type, int options, bool escape)
 
   if (xp->xp_numfiles <= 0 && p2 == NULL) {
     beep_flush();
-  } else if (xp->xp_numfiles == 1) {
+  } else if (xp->xp_numfiles == 1 && !(options & WILD_KEEP_SOLE_ITEM)) {
     // free expanded pattern
     ExpandOne(xp, NULL, NULL, 0, WILD_FREE);
   }
diff --git a/src/nvim/cmdexpand.h b/src/nvim/cmdexpand.h
index 33ff78758912fd..59c734e9a30481 100644
--- a/src/nvim/cmdexpand.h
+++ b/src/nvim/cmdexpand.h
@@ -40,6 +40,7 @@ enum {
   WILD_NOERROR              = 0x800,  ///< sets EW_NOERROR
   WILD_BUFLASTUSED          = 0x1000,
   BUF_DIFF_FILTER           = 0x2000,
+  WILD_KEEP_SOLE_ITEM       = 0x4000,
 };
 
 #ifdef INCLUDE_GENERATED_DECLARATIONS
diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c
index fc2074830969c0..240bdae6cb7841 100644
--- a/src/nvim/ex_getln.c
+++ b/src/nvim/ex_getln.c
@@ -1082,6 +1082,9 @@ static int command_line_wildchar_complete(CommandLineState *s)
   if (wim_flags[s->wim_index] & kOptWimFlagLastused) {
     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 'wildmode' contains "list" may still need to list
     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
     // "list", or no change and 'wildmode' contains "longest,list",
     // 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
       // "list:longest")
       if (wim_flags[0] == kOptWimFlagLongest && ccline.cmdpos == j) {
         s->wim_index = 1;
       }
       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)) {
           int p_wmnu_save = p_wmnu;
           p_wmnu = 0;
           // remove match
-          nextwild(&s->xpc, WILD_PREV, 0, s->firstc != '@');
+          nextwild(&s->xpc, WILD_PREV, options, s->firstc != '@');
           p_wmnu = p_wmnu_save;
         }
 
@@ -1146,7 +1150,8 @@ static int command_line_wildchar_complete(CommandLineState *s)
 
         if (wim_flags[s->wim_index] & kOptWimFlagLongest) {
           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 != '@');
         }
       } else {
@@ -2875,6 +2880,8 @@ int check_opt_wim(void)
       new_wim_flags[idx] |= kOptWimFlagList;
     } else if (i == 8 && strncmp(p, "lastused", 8) == 0) {
       new_wim_flags[idx] |= kOptWimFlagLastused;
+    } else if (i == 8 && strncmp(p, "noselect", 8) == 0) {
+      new_wim_flags[idx] |= kOptWimFlagNoselect;
     } else {
       return FAIL;
     }
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
index d1d5a560827701..2516d1a970105c 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -10020,7 +10020,7 @@ local options = {
       cb = 'did_set_wildmode',
       defaults = 'full',
       -- Keep this in sync with check_opt_wim().
-      values = { 'full', 'longest', 'list', 'lastused' },
+      values = { 'full', 'longest', 'list', 'lastused', 'noselect' },
       flags = true,
       deny_duplicates = false,
       desc = [=[
@@ -10042,7 +10042,10 @@ local options = {
         "lastused"	When completing buffer names and more than one buffer
         		matches, sort buffers by time last used (other than
         		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:
         "longest:full"	Like "longest", but also start 'wildmenu' if it is
@@ -10065,7 +10068,11 @@ local options = {
         	set wildmode=list,full
         <	List all matches without completing, then each full match >vim
         	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|.
       ]=],
       full_name = 'wildmode',
diff --git a/test/old/testdir/gen_opt_test.vim b/test/old/testdir/gen_opt_test.vim
index 64876ce3186561..d1d27cf578411c 100644
--- a/test/old/testdir/gen_opt_test.vim
+++ b/test/old/testdir/gen_opt_test.vim
@@ -352,6 +352,7 @@ let test_values = {
       \		'bs'],
       \		['xxx']],
       \ 'wildmode': [['', 'full', 'longest', 'list', 'lastused', 'list:full',
+      \		'noselect', 'noselect,full', 'noselect:lastused,full',
       \		'full,longest', 'full,full,full,full'],
       \		['xxx', 'a4', 'full,full,full,full,full']],
       \ 'wildoptions': [['', 'tagfile', 'pum', 'fuzzy'], ['xxx']],
diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim
index d4ad63d43e1bc5..d9496ba2a67a03 100644
--- a/test/old/testdir/test_cmdline.vim
+++ b/test/old/testdir/test_cmdline.vim
@@ -2168,22 +2168,58 @@ func Wildmode_tests()
   call assert_equal('AAA    AAAA   AAAAA', g:Sline)
   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
   " should be ignored (happens with :help)
   set wildmode=longest,full
-  set wildmenu
   call feedkeys(":help a*\t\<C-B>\"\<CR>", 'xt')
   call assert_equal('"help a', @:)
   " non existing file
   call feedkeys(":e a1b2y3z4\t\<C-B>\"\<CR>", 'xt')
   call assert_equal('"e a1b2y3z4', @:)
-  set wildmenu&
 
   " Test for longest file name completion with 'fileignorecase'
   " On MS-Windows, file names are case insensitive.
   if has('unix')
-    call writefile([], 'XTESTfoo')
-    call writefile([], 'Xtestbar')
+    call writefile([], 'XTESTfoo', 'D')
+    call writefile([], 'Xtestbar', 'D')
     set nofileignorecase
     call feedkeys(":e XT\<Tab>\<C-B>\"\<CR>", 'xt')
     call assert_equal('"e XTESTfoo', @:)
@@ -2195,10 +2231,23 @@ func Wildmode_tests()
     call feedkeys(":e Xt\<Tab>\<C-B>\"\<CR>", 'xt')
     call assert_equal('"e Xtest', @:)
     set fileignorecase&
-    call delete('XTESTfoo')
-    call delete('Xtestbar')
   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
   delcommand MyCmd
   delfunc T


Reviews:
user: login: zeertzjq
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T21:56:47Z
user: login: deathbeam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T22:10:40Z
user: login: deathbeam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T22:12:55Z
user: login: zeertzjq
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T22:15:16Z
user: login: deathbeam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T22:17:41Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T16:28:21Z
user: login: zeertzjq
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T21:32:04Z
user: login: justinmk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T22:38:54Z

Review comments:
diff_hunk: @@ -1095,7 +1098,8 @@ static int command_line_wildchar_complete(CommandLineState *s)
 
     if (wim_flags[s->wim_index] & kOptWimFlagLongest) {
       res = 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)) {
path: src/nvim/ex_getln.c
user: login: zeertzjq
body: This is not a part of the patch.
created_at: 2025-03-03T21:56:47Z
author_association: MEMBER
diff_hunk: @@ -1095,7 +1098,8 @@ static int command_line_wildchar_complete(CommandLineState *s)
 
     if (wim_flags[s->wim_index] & kOptWimFlagLongest) {
       res = 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)) {
path: src/nvim/ex_getln.c
user: login: deathbeam
body: That is exactly what the original patch does, im just porting it (unless i missed something): https://github.com/vim/vim/pull/16759/files#diff-ce008235f71546c776cd0a06eb681020b37ed07657064b739864fb3d460be94bR989
created_at: 2025-03-03T22:10:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1095,7 +1098,8 @@ static int command_line_wildchar_complete(CommandLineState *s)
 
     if (wim_flags[s->wim_index] & kOptWimFlagLongest) {
       res = 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)) {
path: src/nvim/ex_getln.c
user: login: deathbeam
body: oh wait i think i see it now
created_at: 2025-03-03T22:12:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1095,7 +1098,8 @@ static int command_line_wildchar_complete(CommandLineState *s)
 
     if (wim_flags[s->wim_index] & kOptWimFlagLongest) {
       res = 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)) {
path: src/nvim/ex_getln.c
user: login: zeertzjq
body: No it doesn't. The original patch only changed the one in the `else` branch of the `xp->xp_numfiles > 0`.
created_at: 2025-03-03T22:15:16Z
author_association: MEMBER
diff_hunk: @@ -1095,7 +1098,8 @@ static int command_line_wildchar_complete(CommandLineState *s)
 
     if (wim_flags[s->wim_index] & kOptWimFlagLongest) {
       res = 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)) {
path: src/nvim/ex_getln.c
user: login: deathbeam
body: Yea already fixed, completely missed that there are 2 very similar conditions and did both.
created_at: 2025-03-03T22:17:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2168,22 +2168,58 @@ func Wildmode_tests()
   call assert_equal('AAA    AAAA   AAAAA', g:Sline)
   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
path: test/old/testdir/test_cmdline.vim
user: login: justinmk
body: Seems to be order-sensitive, that doesn't seem right? Test fails with this change:

    set wildmode=full,noselect

created_at: 2025-03-07T16:28:21Z
author_association: MEMBER
diff_hunk: @@ -2168,22 +2168,58 @@ func Wildmode_tests()
   call assert_equal('AAA    AAAA   AAAAA', g:Sline)
   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
path: test/old/testdir/test_cmdline.vim
user: login: zeertzjq
body: Isn't this just how `'wildmode'` works?
created_at: 2025-03-07T21:32:04Z
author_association: MEMBER
diff_hunk: @@ -2168,22 +2168,58 @@ func Wildmode_tests()
   call assert_equal('AAA    AAAA   AAAAA', g:Sline)
   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
path: test/old/testdir/test_cmdline.vim
user: login: justinmk
body: That means `set wildmode+=noselect` doesn't work with the default value. Seems strange, and probably should be documented if this is expected.
created_at: 2025-03-07T22:38:54Z
author_association: MEMBER

PR comments:
user: login: deathbeam
created_at: 2025-03-03T17:58:12Z
body: Test use case in neovim:

```lua
local M = {}

function M.setup()
    local term = vim.api.nvim_replace_termcodes('<C-z>', true, true, true)

    vim.opt.wildmenu = true
    vim.opt.wildoptions = 'pum,fuzzy'
    vim.opt.wildmode = 'noselect:lastused,full'
    vim.opt.wildcharm = vim.fn.char2nr(term)

    vim.keymap.set('c', '<Up>', '<End><C-U><Up>', { silent = true })
    vim.keymap.set('c', '<Down>', '<End><C-U><Down>', { silent = true })

    vim.api.nvim_create_autocmd('CmdlineChanged', {
        pattern = ':',
        callback = function()
            local cmdline = vim.fn.getcmdline()
            local curpos = vim.fn.getcmdpos()
            local last_char = cmdline:sub(-1)

            if
                curpos == #cmdline + 1
                and vim.fn.pumvisible() == 0
                and last_char:match('[%w%/%: ]')
                and not cmdline:match('^%d+$')
            then
                vim.opt.eventignore:append('CmdlineChanged')
                vim.api.nvim_feedkeys(term, 'ti', false)
                vim.schedule(function()
                    vim.opt.eventignore:remove('CmdlineChanged')
                end)
            end
        end,
    })
end

return M
```
author_association: CONTRIBUTOR
user: login: deathbeam
created_at: 2025-03-03T17:59:04Z
body: Also the script for auto creating the PR did not worked for me probably set it up incorrectly but rest did (there was a bit of manual adjustments here also, wasnt clean patch)

EDIT:

Not sure why is `make doc` reverting the documentation changes either, I ported them from vim mostly (unless i shouldnt?)
author_association: CONTRIBUTOR
user: login: zeertzjq
created_at: 2025-03-03T22:21:47Z
body: I forced-pushed here to fix the doc generation and add patch 9.1.1168.
author_association: MEMBER