patch 9.2.0494: User commands cannot handle single args with spaces

Commit: 
https://github.com/vim/vim/commit/f0e874a129a702457a383017b576eef1553f95d8
Author: Maxim Kim <[email protected]>
Date:   Sun May 17 17:58:15 2026 +0000

    patch 9.2.0494: User commands cannot handle single args with spaces
    
    Problem:  User commands cannot handle single args with spaces
    Solution: Add the -nargs=_ attribute (Maxim Kim)
    
    -nargs=_ allow user commands to have a single argument with spaces.
    
    For example given the following Test command and TestComplete function:
    
    ```
    vim9script
    def TestComplete(A: string, _: string, _: number): list<string>
        var all = ["qqqq", "aaaa", "qq aa"]
        return all->matchfuzzy(A)
    enddef
    command! -nargs=_ -complete=customlist,TestComplete Test echo <q-args>
    ```
    
    `:Test q a<tab>` should successfully complete `qq aa`
    
    fixes:  #20102
    closes: #20189
    
    Signed-off-by: Maxim Kim <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt
index a79c0388c..5d9bed83b 100644
--- a/runtime/doc/map.txt
+++ b/runtime/doc/map.txt
@@ -1,4 +1,4 @@
-*map.txt*      For Vim version 9.2.  Last change: 2026 May 02
+*map.txt*      For Vim version 9.2.  Last change: 2026 May 17
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -1593,7 +1593,10 @@ reported if any are supplied).  However, it is possible 
to specify that the
 command can take arguments, using the -nargs attribute.  Valid cases are:
 
        -nargs=0    No arguments are allowed (the default)
-       -nargs=1    Exactly one argument is required, it includes spaces
+       -nargs=1    Exactly one argument is required, it includes spaces;
+                   completion treats white spaces as argument separation
+       -nargs=_    Exactly one argument is required, it includes spaces;
+                   completion treats white spaces as part of the argument
        -nargs=*    Any number of arguments are allowed (0, 1, or many),
                    separated by white space
        -nargs=?    0 or 1 arguments are allowed
@@ -1601,7 +1604,23 @@ command can take arguments, using the -nargs attribute.  
Valid cases are:
 
 Arguments are considered to be separated by (unescaped) spaces or tabs in this
 context, except when there is one argument, then the white space is part of
-the argument.
+the argument. The difference between the "-nargs=1" and "-nargs=_": >
+
+       func MyComplete(ArgLead, CmdLine, CursorPos)
+         return ["one value", "two values", "three values"]
+               \->matchfuzzy(a:ArgLead)
+       endfunc
+       :command -nargs=1 -complete=customlist,MyComplete MyCmd1 echo <q-args>
+       :command -nargs=_ -complete=customlist,MyComplete MyCmd2 echo <q-args>
+
+Completing ":MyCmd1 two va<tab>" will complete with: >
+
+       :MyCmd1 two one value
+
+Completing ":MyCmd2 two va<tab>" will complete with: >
+
+       :MyCmd2 two values
+
 
 Note that arguments are used as text, not as expressions.  Specifically,
 "s:var" will use the script-local variable in the script where the command was
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 5b4bb1990..a8ea821f4 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -52632,6 +52632,8 @@ Other ~
 - |C-indenting| detects comments better.
 - The |package-hlyank| can now optionally highlight the last put region as
   well.
+- New argument handling for user commands |:command-nargs| using the "-nars=_"
+  attribute to handle completion of single arguments with spaces as expected.
 
 Platform specific ~
 -----------------
diff --git a/src/ex_cmds.h b/src/ex_cmds.h
index 162156973..1a3ff0985 100644
--- a/src/ex_cmds.h
+++ b/src/ex_cmds.h
@@ -31,7 +31,8 @@
 #define EX_BANG                0x002   // allow a ! after the command name
 #define EX_EXTRA       0x004   // allow extra args after command name
 #define EX_XFILE       0x008   // expand wildcards in extra part
-#define EX_NOSPC       0x010   // no spaces allowed in the extra part
+#define EX_NOSPC       0x010   // extra part is a single argument (no split on
+                               // whitespace)
 #define        EX_DFLALL       0x020   // default file range is 1,$
 #define EX_WHOLEFOLD   0x040   // extend range to include whole fold also
                                // when less than two numbers given
@@ -60,6 +61,7 @@
 #define EX_EXPR_ARG    0x8000000  // argument is an expression
 #define EX_WHOLE      0x10000000  // command name cannot be shortened in Vim9
 #define EX_EXPORT     0x20000000  // command can be used after :export
+#define EX_ARGSPACE   0x40000000  // completion: keep spaces in arg lead
 
 #define EX_FILES (EX_XFILE | EX_EXTRA) // multiple extra files allowed
 #define EX_FILE1 (EX_FILES | EX_NOSPC) // 1 file, defaults to current file
diff --git a/src/testdir/test_usercommands.vim 
b/src/testdir/test_usercommands.vim
index 5afd8f127..6ad39787e 100644
--- a/src/testdir/test_usercommands.vim
+++ b/src/testdir/test_usercommands.vim
@@ -339,6 +339,9 @@ func Test_CmdErrors()
   com! -nargs=1 DoCmd :
   call assert_fails('DoCmd', 'E471:')
 
+  com! -nargs=_ DoCmd :
+  call assert_fails('DoCmd', 'E471:')
+
   com! -nargs=+ DoCmd :
   call assert_fails('DoCmd', 'E471:')
 
@@ -360,6 +363,14 @@ func CustomCompleteList(A, L, P)
   return [ "Monday", "Tuesday", "Wednesday", {}, test_null_string()]
 endfunc
 
+func CustomCompleteListWithSpaces(A, L, P)
+  return [ "Monday Here", "Tuesday There", "Wednesday OK", {}, 
test_null_string()]
+endfunc
+
+func CustomCompleteListFuzzy(A, L, P)
+  return [ "Monday Here", "Tuesday There", "Wednesday OK", {}, 
test_null_string()]->matchfuzzy(a:A)
+endfunc
+
 func Test_CmdCompletion()
   call feedkeys(":com -\<C-A>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"com -addr bang bar buffer complete count keepscript 
nargs range register', @:)
@@ -368,7 +379,7 @@ func Test_CmdCompletion()
   call assert_equal('"com -nargs=0 -addr bang bar buffer complete count 
keepscript nargs range register', @:)
 
   call feedkeys(":com -nargs=\<C-A>\<C-B>\"\<CR>", 'tx')
-  call assert_equal('"com -nargs=* + 0 1 ?', @:)
+  call assert_equal('"com -nargs=* + 0 1 ? _', @:)
 
   call feedkeys(":com -addr=\<C-A>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"com -addr=arguments buffers lines loaded_buffers other 
quickfix tabs windows', @:)
@@ -426,15 +437,27 @@ func Test_CmdCompletion()
   call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"DoCmd mswin xterm', @:)
 
+  com! -nargs=_ -complete=behave DoCmd :
+  call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
+  call assert_equal('"DoCmd mswin xterm', @:)
+
   com! -nargs=1 -complete=retab DoCmd :
   call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"DoCmd -indentonly', @:)
 
+  com! -nargs=_ -complete=retab DoCmd :
+  call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
+  call assert_equal('"DoCmd -indentonly', @:)
+
   " Test for file name completion
   com! -nargs=1 -complete=file DoCmd :
   call feedkeys(":DoCmd READM\<Tab>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"DoCmd README.txt', @:)
 
+  com! -nargs=_ -complete=file DoCmd :
+  call feedkeys(":DoCmd READM\<Tab>\<C-B>\"\<CR>", 'tx')
+  call assert_equal('"DoCmd README.txt', @:)
+
   " Test for buffer name completion
   com! -nargs=1 -complete=buffer DoCmd :
   let bnum = bufadd('BufForUserCmd')
@@ -445,6 +468,15 @@ func Test_CmdCompletion()
   call feedkeys(":DoCmd BufFor\<Tab>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"DoCmd BufFor', @:)
 
+  com! -nargs=_ -complete=buffer DoCmd :
+  let bnum = bufadd('BufForUserCmd')
+  call setbufvar(bnum, '&buflisted', 1)
+  call feedkeys(":DoCmd BufFor\<Tab>\<C-B>\"\<CR>", 'tx')
+  call assert_equal('"DoCmd BufForUserCmd', @:)
+  bwipe BufForUserCmd
+  call feedkeys(":DoCmd BufFor\<Tab>\<C-B>\"\<CR>", 'tx')
+  call assert_equal('"DoCmd BufFor', @:)
+
   com! -nargs=* -complete=custom,CustomComplete DoCmd :
   call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"DoCmd January February Mars', @:)
@@ -453,6 +485,14 @@ func Test_CmdCompletion()
   call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"DoCmd Monday Tuesday Wednesday', @:)
 
+  com! -nargs=_ -complete=customlist,CustomCompleteListWithSpaces DoCmd :
+  call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
+  call assert_equal('"DoCmd Monday Here Tuesday There Wednesday OK', @:)
+
+  com! -nargs=_ -complete=customlist,CustomCompleteListFuzzy DoCmd :
+  call feedkeys(":DoCmd mo he\<C-A>\<C-B>\"\<CR>", 'tx')
+  call assert_equal('"DoCmd Monday Here', @:)
+
   com! -nargs=+ -complete=custom,CustomCompleteList DoCmd :
   call assert_fails("call feedkeys(':DoCmd \<C-D>', 'tx')", 'E730:')
 
@@ -555,6 +595,27 @@ func Test_addr_all()
   delcommand DoSomething
 endfunc
 
+func Test_nargs_underscore_fargs()
+  " -nargs=_ must behave like -nargs=1 for <f-args>/<q-args>:
+  " the whole argument is one token, whitespace is part of it.
+  let g:res = []
+  com! -nargs=1 DoCmd1 call add(g:res, [<f-args>])
+  com! -nargs=_ DoCmdU call add(g:res, [<f-args>])
+  DoCmd1 a b c
+  DoCmdU a b c
+  call assert_equal([['a b c'], ['a b c']], g:res)
+
+  let g:res = []
+  com! -nargs=_ DoCmdQ call add(g:res, <q-args>)
+  DoCmdQ a b c
+  call assert_equal(['a b c'], g:res)
+
+  delcom DoCmd1
+  delcom DoCmdU
+  delcom DoCmdQ
+  unlet g:res
+endfunc
+
 func Test_command_list()
   command! DoCmd :
   call assert_equal("
    Name              Args Address Complete    Definition"
@@ -614,6 +675,10 @@ func Test_command_list()
   call assert_equal("
    Name              Args Address Complete    Definition"
         \        .. "
    DoCmd             1            arglist     :",
         \           execute('command DoCmd'))
+  command! -nargs=_ -complete=arglist DoCmd :
+  call assert_equal("
    Name              Args Address Complete    Definition"
+        \        .. "
    DoCmd             _            arglist     :",
+        \           execute('command DoCmd'))
   command! -nargs=* -complete=augroup DoCmd :
   call assert_equal("
    Name              Args Address Complete    Definition"
         \        .. "
    DoCmd             *            augroup     :",
@@ -636,6 +701,10 @@ func Test_command_list()
   call assert_equal("
    Name              Args Address Complete    Definition"
         \        .. "
    DoCmd             1                        :",
         \           execute('command DoCmd'))
+  command! -nargs=_ DoCmd :
+  call assert_equal("
    Name              Args Address Complete    Definition"
+        \        .. "
    DoCmd             _                        :",
+        \           execute('command DoCmd'))
   command! -nargs=* DoCmd :
   call assert_equal("
    Name              Args Address Complete    Definition"
         \        .. "
    DoCmd             *                        :",
diff --git a/src/usercmd.c b/src/usercmd.c
index cef1d18b7..2d4756965 100644
--- a/src/usercmd.c
+++ b/src/usercmd.c
@@ -344,15 +344,18 @@ set_context_in_user_cmdarg(
        return set_context_in_map_cmd(xp, (char_u *)"map", arg, forceit, FALSE,
                                                        FALSE, CMD_map);
     // Find start of last argument.
-    p = arg;
-    while (*p)
+    if (!(argt & EX_ARGSPACE))
     {
-       if (*p == ' ')
-           // argument starts after a space
-           arg = p + 1;
-       else if (*p == '\' && *(p + 1) != NUL)
-           ++p; // skip over escaped character
-       MB_PTR_ADV(p);
+       p = arg;
+       while (*p)
+       {
+           if (*p == ' ')
+               // argument starts after a space
+               arg = p + 1;
+           else if (*p == '\' && *(p + 1) != NUL)
+               ++p; // skip over escaped character
+           MB_PTR_ADV(p);
+       }
     }
     xp->xp_pattern = arg;
     xp->xp_context = context;
@@ -451,7 +454,7 @@ get_user_cmd_flags(expand_T *xp UNUSED, int idx)
     char_u *
 get_user_cmd_nargs(expand_T *xp UNUSED, int idx)
 {
-    static char *user_cmd_nargs[] = {"0", "1", "*", "?", "+"};
+    static char *user_cmd_nargs[] = {"0", "1", "_", "*", "?", "+"};
 
     if (idx < 0 || idx >= (int)ARRAY_LENGTH(user_cmd_nargs))
        return NULL;
@@ -640,13 +643,14 @@ uc_list(char_u *name, size_t name_len)
            len = 0;
 
            // Arguments
-           switch ((int)(a & (EX_EXTRA|EX_NOSPC|EX_NEEDARG)))
+           switch ((int)(a & (EX_EXTRA|EX_NOSPC|EX_NEEDARG|EX_ARGSPACE)))
            {
                case 0:                         IObuff[len++] = '0'; break;
                case (EX_EXTRA):                IObuff[len++] = '*'; break;
                case (EX_EXTRA|EX_NOSPC):       IObuff[len++] = '?'; break;
                case (EX_EXTRA|EX_NEEDARG):     IObuff[len++] = '+'; break;
                case (EX_EXTRA|EX_NOSPC|EX_NEEDARG): IObuff[len++] = '1'; break;
+               case (EX_EXTRA|EX_NOSPC|EX_NEEDARG|EX_ARGSPACE): IObuff[len++] 
= '_'; break;
            }
 
            do
@@ -975,6 +979,8 @@ uc_scan_attr(
                    *argt |= (EX_EXTRA | EX_NOSPC);
                else if (*val == '+')
                    *argt |= (EX_EXTRA | EX_NEEDARG);
+               else if (*val == '_')
+                   *argt |= (EX_EXTRA | EX_NOSPC | EX_NEEDARG | EX_ARGSPACE);
                else
                    goto wrong_nargs;
            }
diff --git a/src/version.c b/src/version.c
index 82e24f7d1..f6e20cfa0 100644
--- a/src/version.c
+++ b/src/version.c
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    494,
 /**/
     493,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wOg13-009vNg-CC%40256bit.org.

Raspunde prin e-mail lui