Commit: patch 9.2.0494: User commands cannot handle single args with spaces

3 views
Skip to first unread message

Christian Brabandt

unread,
May 17, 2026, 2:15:15 PM (2 days ago) May 17
to vim...@googlegroups.com
patch 9.2.0494: User commands cannot handle single args with spaces

Commit: https://github.com/vim/vim/commit/f0e874a129a702457a383017b576eef1553f95d8
Author: Maxim Kim <hab...@gmail.com>
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 <hab...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

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,
/**/
Reply all
Reply to author
Forward
0 new messages