From: Maxim Kim Date: Sun, 17 May 2026 17:58:15 +0000 (+0000) Subject: patch 9.2.0494: User commands cannot handle single args with spaces X-Git-Tag: v9.2.0494^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f0e874a129a702457a383017b576eef1553f95d8;p=thirdparty%2Fvim.git 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 var all = ["qqqq", "aaaa", "qq aa"] return all->matchfuzzy(A) enddef command! -nargs=_ -complete=customlist,TestComplete Test echo ``` `:Test q a` should successfully complete `qq aa` fixes: #20102 closes: #20189 Signed-off-by: Maxim Kim Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index a79c0388cb..5d9bed83b0 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 + :command -nargs=_ -complete=customlist,MyComplete MyCmd2 echo + +Completing ":MyCmd1 two va" will complete with: > + + :MyCmd1 two one value + +Completing ":MyCmd2 two va" 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 5b4bb19906..a8ea821f4c 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 1621569732..1a3ff0985a 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 5afd8f1278..6ad39787e3 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 -\\\"\", '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=\\\"\", 'tx') - call assert_equal('"com -nargs=* + 0 1 ?', @:) + call assert_equal('"com -nargs=* + 0 1 ? _', @:) call feedkeys(":com -addr=\\\"\", '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 \\\"\", 'tx') call assert_equal('"DoCmd mswin xterm', @:) + com! -nargs=_ -complete=behave DoCmd : + call feedkeys(":DoCmd \\\"\", 'tx') + call assert_equal('"DoCmd mswin xterm', @:) + com! -nargs=1 -complete=retab DoCmd : call feedkeys(":DoCmd \\\"\", 'tx') call assert_equal('"DoCmd -indentonly', @:) + com! -nargs=_ -complete=retab DoCmd : + call feedkeys(":DoCmd \\\"\", 'tx') + call assert_equal('"DoCmd -indentonly', @:) + " Test for file name completion com! -nargs=1 -complete=file DoCmd : call feedkeys(":DoCmd READM\\\"\", 'tx') call assert_equal('"DoCmd README.txt', @:) + com! -nargs=_ -complete=file DoCmd : + call feedkeys(":DoCmd READM\\\"\", '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\\\"\", 'tx') call assert_equal('"DoCmd BufFor', @:) + com! -nargs=_ -complete=buffer DoCmd : + let bnum = bufadd('BufForUserCmd') + call setbufvar(bnum, '&buflisted', 1) + call feedkeys(":DoCmd BufFor\\\"\", 'tx') + call assert_equal('"DoCmd BufForUserCmd', @:) + bwipe BufForUserCmd + call feedkeys(":DoCmd BufFor\\\"\", 'tx') + call assert_equal('"DoCmd BufFor', @:) + com! -nargs=* -complete=custom,CustomComplete DoCmd : call feedkeys(":DoCmd \\\"\", 'tx') call assert_equal('"DoCmd January February Mars', @:) @@ -453,6 +485,14 @@ func Test_CmdCompletion() call feedkeys(":DoCmd \\\"\", 'tx') call assert_equal('"DoCmd Monday Tuesday Wednesday', @:) + com! -nargs=_ -complete=customlist,CustomCompleteListWithSpaces DoCmd : + call feedkeys(":DoCmd \\\"\", 'tx') + call assert_equal('"DoCmd Monday Here Tuesday There Wednesday OK', @:) + + com! -nargs=_ -complete=customlist,CustomCompleteListFuzzy DoCmd : + call feedkeys(":DoCmd mo he\\\"\", 'tx') + call assert_equal('"DoCmd Monday Here', @:) + com! -nargs=+ -complete=custom,CustomCompleteList DoCmd : call assert_fails("call feedkeys(':DoCmd \', '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 /: + " the whole argument is one token, whitespace is part of it. + let g:res = [] + com! -nargs=1 DoCmd1 call add(g:res, []) + com! -nargs=_ DoCmdU call add(g:res, []) + 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, ) + 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("\n Name Args Address Complete Definition" @@ -614,6 +675,10 @@ func Test_command_list() call assert_equal("\n Name Args Address Complete Definition" \ .. "\n DoCmd 1 arglist :", \ execute('command DoCmd')) + command! -nargs=_ -complete=arglist DoCmd : + call assert_equal("\n Name Args Address Complete Definition" + \ .. "\n DoCmd _ arglist :", + \ execute('command DoCmd')) command! -nargs=* -complete=augroup DoCmd : call assert_equal("\n Name Args Address Complete Definition" \ .. "\n DoCmd * augroup :", @@ -636,6 +701,10 @@ func Test_command_list() call assert_equal("\n Name Args Address Complete Definition" \ .. "\n DoCmd 1 :", \ execute('command DoCmd')) + command! -nargs=_ DoCmd : + call assert_equal("\n Name Args Address Complete Definition" + \ .. "\n DoCmd _ :", + \ execute('command DoCmd')) command! -nargs=* DoCmd : call assert_equal("\n Name Args Address Complete Definition" \ .. "\n DoCmd * :", diff --git a/src/usercmd.c b/src/usercmd.c index cef1d18b79..2d47569653 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 82e24f7d12..f6e20cfa07 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, /**/