-*map.txt* For Vim version 9.2. Last change: 2026 May 23
+*map.txt* For Vim version 9.2. Last change: 2026 Jun 13
VIM REFERENCE MANUAL by Bram Moolenaar
:MyCmd2 two values
+Use -nargs=_ when the whole argument area should be taken as a single value
+as-is. When you need several arguments and an individual argument may itself
+contain spaces (so the splitter must keep running), use -nargs=+/* together
+with |:command-completeopt| (-completeopt=escape) instead.
+
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
<
This example does not work for file names with spaces!
+ *:command-completeopt*
+For the "custom" and "customlist" types you can further opt into specific
+behaviors with the -completeopt= attribute. It takes a comma-separated list
+of option names; currently only "escape" is recognized:
+
+ -completeopt=escape
+ The completion function may return matches containing spaces,
+ tabs or backslashes. When such a match is inserted into the
+ command line each space, tab and backslash is preceded by a
+ backslash so the value is preserved as a single argument. The
+ unescaped form is used for display (popup menu / wildmenu /
+ |getcompletion()|). The ArgLead passed to the completion
+ function is also unescaped: when the user types
+ `:Cmd foo\ b<Tab>` the function is called with `foo b`, not
+ `foo\ b` . In the command's replacement text |<q-args>| and
+ |<f-args>| likewise yield the unescaped value.
+
+Example: >
+ :func MyComplete(ArgLead, CmdLine, CursorPos)
+ : return filter(['hello world', 'good morning'],
+ : \ {_, v -> stridx(v, a:ArgLead) ==# 0})
+ :endfunc
+ :com! -nargs=1 -complete=customlist,MyComplete -completeopt=escape MyCmd
+ \ echo <q-args>
+<
+After `:MyCmd <Tab>` the popup shows `hello world` and `good morning`. Typing
+`:MyCmd hello\ w<Tab>` calls MyComplete with ArgLead set to `hello w` (the
+backslash before the space is removed), the filter finds `hello world`, and
+`<Tab>` inserts it as `:MyCmd hello\ world`. `<q-args>` then evaluates to the
+logical `hello world`.
+
+Without -completeopt=escape the literal string `hello world` would be inserted
+into the command line, so Vim's argument splitter would treat it as two
+arguments.
+
+-completeopt=escape and -nargs=_ (see |:command-nargs|) target different
+shapes; they are not competing solutions for the same one:
+ "whole line as one argument, no escaping"
+ use -nargs=_
+ "multiple arguments, each may contain spaces"
+ use -nargs=+ or -nargs=* with -completeopt=escape
+Because -nargs=_ disables the argument splitter, escaping has no effect there,
+so combining -nargs=_ with -completeopt=escape is an error (E1579).
+
Range handling ~
*E177* *E178* *:command-range* *:command-count*
:command-bar map.txt /*:command-bar*
:command-buffer map.txt /*:command-buffer*
:command-complete map.txt /*:command-complete*
+:command-completeopt map.txt /*:command-completeopt*
:command-completion map.txt /*:command-completion*
:command-completion-custom map.txt /*:command-completion-custom*
:command-completion-customlist map.txt /*:command-completion-customlist*
Using |:cscope| is no longer allowed.
+Commands ~
+--------
+- |:command-completion-customlist| can return a list of dictionaries with
+ kind/menu/info/abbr for the popup menu.
+- New argument handling for user commands |:command-nargs| using the "-nars=_"
+ attribute to handle completion of single arguments with spaces as expected.
+- New :command attribute |:command-completeopt| escapes spaces in
+ custom completion matches so they survive as a single argument.
+
Other ~
-----
- The new |xdg.vim| script for full XDG compatibility is included.
'completeopt' option
- Channel can handle |Blob| messages |channel-open-options|.
- Added the "u" flag to 'shortmess' to silence undo/redo messages: |shm-u|
-- |:command-completion-customlist| can return a list of dictionaries with
- kind/menu/info/abbr for the popup menu.
- |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.
- Support %0{} in 'statusline' to insert the expression result verbatim and
not drop leading spaces |stl-%0{|.
- Generated Session and View files are written in Vim9 script, see |:mksession|,
#if defined(FEAT_EVAL)
static int ExpandUserDefined(char_u *pat, expand_T *xp, regmatch_T *regmatch, char_u ***matches, int *numMatches);
static int ExpandUserList(expand_T *xp, char_u ***matches, int *numMatches);
+static char_u *apply_user_completeopt_escape(expand_T *xp, char_u *str);
+static char_u *unescape_user_completeopt_pat(expand_T *xp, char_u *src, int srclen, int *new_lenp);
#endif
static int expand_pattern_in_buf(char_u *pat, int dir, char_u ***matches, int *numMatches);
else
{
char_u *tmp;
+ char_u *pat_src = xp->xp_pattern;
+ int pat_len = xp->xp_pattern_len;
+#if defined(FEAT_EVAL)
+ char_u *unesc = unescape_user_completeopt_pat(xp, pat_src, pat_len,
+ &pat_len);
+ if (unesc != NULL)
+ pat_src = unesc;
+#endif
if (cmdline_fuzzy_completion_supported(xp)
|| xp->xp_context == EXPAND_PATTERN_IN_BUF)
// Don't modify the search string
- tmp = vim_strnsave(xp->xp_pattern, xp->xp_pattern_len);
+ tmp = vim_strnsave(pat_src, pat_len);
else
- tmp = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context);
+ tmp = addstar(pat_src, pat_len, xp->xp_context);
+#if defined(FEAT_EVAL)
+ vim_free(unesc);
+#endif
// Translate string into pattern and expand it.
if (tmp == NULL)
vim_free(buf);
}
+#if defined(FEAT_EVAL)
+/*
+ * Apply -completeopt=escape to a string about to be inserted into the command
+ * line as a completion result. If "str" is non-NULL and the active expansion
+ * context is a customlist/custom user command with UCC_ESCAPE set, free "str"
+ * and return a newly-allocated copy with spaces, tabs and backslashes prefixed
+ * by a backslash. Otherwise return "str" unchanged.
+ */
+ static char_u *
+apply_user_completeopt_escape(expand_T *xp, char_u *str)
+{
+ char_u *p;
+
+ if (str == NULL)
+ return NULL;
+ if ((xp->xp_context != EXPAND_USER_DEFINED
+ && xp->xp_context != EXPAND_USER_LIST)
+ || !(xp->xp_complete_opt & UCC_ESCAPE))
+ return str;
+ p = vim_strsave_escaped(str, (char_u *)" \t\\");
+ if (p == NULL)
+ return str;
+ vim_free(str);
+ return p;
+}
+
+/*
+ * For -completeopt=escape on a user command, build the "logical" ArgLead by
+ * collapsing a backslash before a space, tab or backslash in the typed text.
+ * The completion function then sees "foo bar" instead of "foo\ bar".
+ * Returns a newly-allocated string and stores its length in "*new_lenp", or
+ * NULL when no unescape is applicable (caller should keep the original).
+ */
+ static char_u *
+unescape_user_completeopt_pat(
+ expand_T *xp,
+ char_u *src,
+ int srclen,
+ int *new_lenp)
+{
+ char_u *buf, *p, *d, *end;
+
+ if ((xp->xp_context != EXPAND_USER_DEFINED
+ && xp->xp_context != EXPAND_USER_LIST)
+ || !(xp->xp_complete_opt & UCC_ESCAPE))
+ return NULL;
+
+ buf = alloc(srclen + 1);
+ if (buf == NULL)
+ return NULL;
+
+ d = buf;
+ end = src + srclen;
+ for (p = src; p < end; ++p)
+ {
+ if (*p == '\\' && p + 1 < end
+ && (p[1] == ' ' || p[1] == TAB || p[1] == '\\'))
+ ++p;
+ *d++ = *p;
+ }
+ *d = NUL;
+ *new_lenp = (int)(d - buf);
+ return buf;
+}
+#endif
+
/*
* Get the next or prev cmdline completion match. The index of the match is set
* in "xp->xp_selected"
xp->xp_selected = findex;
// Return the original text or the selected match
- return vim_strsave(findex == -1 ? xp->xp_orig : xp->xp_files[findex]);
+ if (findex == -1)
+ return vim_strsave(xp->xp_orig);
+#if defined(FEAT_EVAL)
+ return apply_user_completeopt_escape(xp, vim_strsave(xp->xp_files[findex]));
+#else
+ return vim_strsave(xp->xp_files[findex]);
+#endif
}
/*
{
char_u *ss = NULL;
int orig_saved = FALSE;
+#if defined(FEAT_EVAL)
+ // ss_is_match is TRUE when ss is derived from xp_files and should be
+ // escaped per -completeopt=escape before being inserted. WILD_CANCEL
+ // and WILD_APPLY-without-selection return xp_orig unchanged.
+ int ss_is_match = FALSE;
+#endif
// first handle the case of using an old match
if (mode == WILD_NEXT || mode == WILD_PREV
if (mode == WILD_CANCEL)
ss = vim_strsave(xp->xp_orig ? xp->xp_orig : (char_u *)"");
else if (mode == WILD_APPLY)
- ss = vim_strsave(xp->xp_selected == -1
- ? (xp->xp_orig ? xp->xp_orig : (char_u *)"")
- : xp->xp_files[xp->xp_selected]);
+ {
+ if (xp->xp_selected == -1)
+ ss = vim_strsave(xp->xp_orig ? xp->xp_orig : (char_u *)"");
+ else
+ {
+ ss = vim_strsave(xp->xp_files[xp->xp_selected]);
+#if defined(FEAT_EVAL)
+ ss_is_match = TRUE;
+#endif
+ }
+ }
// free old names
if (xp->xp_numfiles != -1 && mode != WILD_ALL && mode != WILD_LONGEST)
orig_saved = TRUE;
ss = ExpandOne_start(mode, xp, str, options);
+#if defined(FEAT_EVAL)
+ if (ss != NULL)
+ ss_is_match = TRUE;
+#endif
}
// Find longest common part
{
ss = find_longest_match(xp, options);
xp->xp_selected = -1; // next p_wc gets first one
+#if defined(FEAT_EVAL)
+ if (ss != NULL)
+ ss_is_match = TRUE;
+#endif
}
// Concatenate all matching names. Unless interrupted, this can be slow
char *suffix = (options & WILD_USE_NL) ? "\n" : " ";
int n = xp->xp_numfiles - 1;
int i;
+#if defined(FEAT_EVAL)
+ char_u **files = xp->xp_files;
+ char_u **escaped = NULL;
+
+ // When -completeopt=escape is set for a user command, escape each
+ // match before joining so the separator spaces stay unescaped.
+ if ((xp->xp_context == EXPAND_USER_DEFINED
+ || xp->xp_context == EXPAND_USER_LIST)
+ && (xp->xp_complete_opt & UCC_ESCAPE))
+ {
+ escaped = ALLOC_MULT(char_u *, xp->xp_numfiles);
+ if (escaped != NULL)
+ {
+ for (i = 0; i < xp->xp_numfiles; ++i)
+ {
+ escaped[i] = vim_strsave_escaped(xp->xp_files[i],
+ (char_u *)" \t\\");
+ if (escaped[i] == NULL)
+ {
+ while (--i >= 0)
+ vim_free(escaped[i]);
+ VIM_CLEAR(escaped);
+ break;
+ }
+ }
+ if (escaped != NULL)
+ files = escaped;
+ }
+ }
+#else
+ char_u **files = xp->xp_files;
+#endif
if (xp->xp_prefix == XP_PREFIX_NO)
{
}
for (i = 0; i < xp->xp_numfiles; ++i)
- ss_size += STRLEN(xp->xp_files[i]) + 1; // +1 for the suffix
+ ss_size += STRLEN(files[i]) + 1; // +1 for the suffix
++ss_size; // +1 for the NUL
ss = alloc(ss_size);
ss_size - ss_len,
"%s%s%s",
(i > 0) ? prefix : "",
- (char *)xp->xp_files[i],
+ (char *)files[i],
(i < n) ? suffix : "");
}
}
+#if defined(FEAT_EVAL)
+ if (escaped != NULL)
+ {
+ for (i = 0; i < xp->xp_numfiles; ++i)
+ vim_free(escaped[i]);
+ vim_free(escaped);
+ }
+#endif
}
if (mode == WILD_EXPAND_FREE || mode == WILD_ALL)
if (!orig_saved)
vim_free(orig);
+#if defined(FEAT_EVAL)
+ // WILD_ALL already escaped its component matches in place, so don't
+ // re-escape the joined string (its separator spaces would break).
+ if (ss_is_match && mode != WILD_ALL)
+ ss = apply_user_completeopt_escape(xp, ss);
+#endif
return ss;
}
// add star to file name, or convert to regexp if not exp. files.
xp->xp_pattern_len = (int)(str + col - xp->xp_pattern);
- if (cmdline_fuzzy_completion_supported(xp))
- // If fuzzy matching, don't modify the search string
- file_str = vim_strsave(xp->xp_pattern);
- else
- file_str = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context);
+ {
+ char_u *pat_src = xp->xp_pattern;
+ int pat_len = xp->xp_pattern_len;
+#if defined(FEAT_EVAL)
+ char_u *unesc = unescape_user_completeopt_pat(xp, pat_src, pat_len,
+ &pat_len);
+ if (unesc != NULL)
+ pat_src = unesc;
+#endif
+
+ if (cmdline_fuzzy_completion_supported(xp))
+ // If fuzzy matching, don't modify the search string
+ file_str = vim_strnsave(pat_src, pat_len);
+ else
+ file_str = addstar(pat_src, pat_len, xp->xp_context);
+#if defined(FEAT_EVAL)
+ vim_free(unesc);
+#endif
+ }
if (file_str == NULL)
return EXPAND_UNSUCCESSFUL;
{EXPAND_USER_CMD_FLAGS, get_user_cmd_flags, FALSE, TRUE},
{EXPAND_USER_NARGS, get_user_cmd_nargs, FALSE, TRUE},
{EXPAND_USER_COMPLETE, get_user_cmd_complete, FALSE, TRUE},
+ {EXPAND_USER_COMPLETEOPT, get_user_cmd_completeopt, FALSE, TRUE},
#ifdef FEAT_EVAL
{EXPAND_USER_VARS, get_user_var_name, FALSE, TRUE},
{EXPAND_FUNCTIONS, get_function_name, FALSE, TRUE},
ccline->cmdbuff[ccline->cmdlen] = 0;
}
- pat = vim_strnsave(xp->xp_pattern, xp->xp_pattern_len);
+ {
+ int unesc_len;
+ pat = unescape_user_completeopt_pat(xp, xp->xp_pattern,
+ xp->xp_pattern_len, &unesc_len);
+ if (pat == NULL)
+ pat = vim_strnsave(xp->xp_pattern, xp->xp_pattern_len);
+ }
args[0].v_type = VAR_STRING;
args[0].vval.v_string = pat;
}
}
- if (cmdline_fuzzy_completion_supported(&xpc))
- // when fuzzy matching, don't modify the search string
- pat = vim_strnsave(xpc.xp_pattern, xpc.xp_pattern_len);
- else
- pat = addstar(xpc.xp_pattern, xpc.xp_pattern_len, xpc.xp_context);
+ {
+ char_u *pat_src = xpc.xp_pattern;
+ int pat_len = xpc.xp_pattern_len;
+ char_u *unesc = unescape_user_completeopt_pat(&xpc, pat_src, pat_len,
+ &pat_len);
+ if (unesc != NULL)
+ pat_src = unesc;
+
+ if (cmdline_fuzzy_completion_supported(&xpc))
+ // when fuzzy matching, don't modify the search string
+ pat = vim_strnsave(pat_src, pat_len);
+ else
+ pat = addstar(pat_src, pat_len, xpc.xp_context);
+ vim_free(unesc);
+ }
if (rettv_list_alloc(rettv) == OK && pat != NULL)
{
EXTERN char e_too_many_postponed_prefixes_spell[]
INIT(= N_("E1578: Too many postponed prefixes and/or compound flags"));
#endif
+EXTERN char e_completeopt_escape_cannot_be_used_with_nargs_underscore[]
+ INIT(= N_("E1579: -completeopt=escape cannot be used with -nargs=_"));
msgstr ""
"Project-Id-Version: Vim\n"
"Report-Msgid-Bugs-To: vim-dev@vim.org\n"
-"POT-Creation-Date: 2026-06-13 17:59+0000\n"
+"POT-Creation-Date: 2026-06-13 19:24+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
msgid "E1578: Too many postponed prefixes and/or compound flags"
msgstr ""
+msgid "E1579: -completeopt=escape cannot be used with -nargs=_"
+msgstr ""
+
#. type of cmdline window or 0
#. result of cmdline window or 0
#. buffer of cmdline window or NULL
char_u *get_user_cmd_flags(expand_T *xp, int idx);
char_u *get_user_cmd_nargs(expand_T *xp, int idx);
char_u *get_user_cmd_complete(expand_T *xp, int idx);
+char_u *get_user_cmd_completeopt(expand_T *xp, int idx);
char_u *cmdcomplete_type_to_str(int expand, char_u *compl_arg);
int cmdcomplete_str_to_type(char_u *complete_str);
char *uc_fun_cmd(void);
xp_prefix_T xp_prefix;
#if defined(FEAT_EVAL)
char_u *xp_arg; // completion function
+ int xp_complete_opt; // UCC_ flags for user command
sctx_T xp_script_ctx; // SCTX for completion function
#endif
int xp_backslash; // one of the XP_BS_ values
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', @:)
+ call assert_equal('"com -addr bang bar buffer complete completeopt count keepscript nargs range register', @:)
call feedkeys(":com -nargs=0 -\<C-A>\<C-B>\"\<CR>", 'tx')
- call assert_equal('"com -nargs=0 -addr bang bar buffer complete count keepscript nargs range register', @:)
+ call assert_equal('"com -nargs=0 -addr bang bar buffer complete completeopt count keepscript nargs range register', @:)
call feedkeys(":com -nargs=\<C-A>\<C-B>\"\<CR>", 'tx')
call assert_equal('"com -nargs=* + 0 1 ? _', @:)
delcom DoCmd
endfunc
+" Test for -completeopt=escape: spaces, tabs and backslashes returned by a
+" customlist/custom completion function are backslash-escaped when inserted
+" into the command line, so the value survives as a single argument. The
+" ArgLead passed to the function and the matches shown in the popup menu
+" remain unescaped.
+func Test_command_completeopt_escape()
+ let g:EscArgLead = ''
+ func! EscOne(A, L, P)
+ let g:EscArgLead = a:A
+ return ['hello world']
+ endfunc
+ func! EscBs(A, L, P)
+ let g:EscArgLead = a:A
+ return ['foo\bar']
+ endfunc
+ func! EscBoth(A, L, P)
+ let g:EscArgLead = a:A
+ return ['foo bar\baz']
+ endfunc
+ func! EscTab(A, L, P)
+ let g:EscArgLead = a:A
+ return ["foo\tbar"]
+ endfunc
+ func! EscCustom(A, L, P)
+ let g:EscArgLead = a:A
+ return "hello world"
+ endfunc
+ func! EscMulti(A, L, P)
+ let g:EscArgLead = a:A
+ return filter(['one value', 'two values', 'three values'],
+ \ {_, v -> stridx(v, a:A) == 0})
+ endfunc
+
+ " customlist + -completeopt=escape: spaces and backslashes are escaped.
+ com! -nargs=1 -complete=customlist,EscOne -completeopt=escape DoCmd
+ \ let g:EscQargs = <q-args>
+ call feedkeys(":DoCmd \<Tab>\<C-B>\"\<CR>", 'tx')
+ call assert_equal('"DoCmd hello\ world', @:)
+ " <q-args> yields the unescaped value.
+ let g:EscQargs = ''
+ call feedkeys(":DoCmd \<Tab>\<CR>", 'tx')
+ call assert_equal('hello world', g:EscQargs)
+ delcom DoCmd
+
+ com! -nargs=1 -complete=customlist,EscBs -completeopt=escape DoCmd echo <q-args>
+ call feedkeys(":DoCmd \<Tab>\<C-B>\"\<CR>", 'tx')
+ call assert_equal('"DoCmd foo\\bar', @:)
+ delcom DoCmd
+
+ com! -nargs=1 -complete=customlist,EscBoth -completeopt=escape DoCmd echo <q-args>
+ call feedkeys(":DoCmd \<Tab>\<C-B>\"\<CR>", 'tx')
+ call assert_equal('"DoCmd foo\ bar\\baz', @:)
+ delcom DoCmd
+
+ " A tab in a match is escaped like a space (the argument splitter also
+ " splits on tabs) and <q-args> yields the literal tab again.
+ com! -nargs=1 -complete=customlist,EscTab -completeopt=escape DoCmd
+ \ let g:EscQargs = <q-args>
+ call feedkeys(":DoCmd \<Tab>\<C-B>\"\<CR>", 'tx')
+ call assert_equal("\"DoCmd foo\\\tbar", @:)
+ " CTRL-A (insert all matches) escapes each match too.
+ call feedkeys(":DoCmd \<C-A>\<C-B>\"\<CR>", 'tx')
+ call assert_equal("\"DoCmd foo\\\tbar", @:)
+ let g:EscQargs = ''
+ call feedkeys(":DoCmd \<Tab>\<CR>", 'tx')
+ call assert_equal("foo\tbar", g:EscQargs)
+ let g:EscArgLead = ''
+ call assert_equal(["foo\tbar"], getcompletion("DoCmd foo\\\tb", 'cmdline'))
+ call assert_equal("foo\tb", g:EscArgLead)
+ delcom DoCmd
+ unlet g:EscQargs
+
+ " custom (newline-separated) + -completeopt=escape.
+ com! -nargs=1 -complete=custom,EscCustom -completeopt=escape DoCmd echo <q-args>
+ call feedkeys(":DoCmd \<Tab>\<C-B>\"\<CR>", 'tx')
+ call assert_equal('"DoCmd hello\ world', @:)
+ delcom DoCmd
+
+ " Without -completeopt=escape the literal string is inserted as-is.
+ com! -nargs=1 -complete=customlist,EscOne DoCmd echo <q-args>
+ call feedkeys(":DoCmd \<Tab>\<C-B>\"\<CR>", 'tx')
+ call assert_equal('"DoCmd hello world', @:)
+ delcom DoCmd
+
+ " getcompletion() returns the unescaped form even with -completeopt=escape,
+ " because the escape only applies at cmdline insertion, not the pum/list.
+ com! -nargs=1 -complete=customlist,EscBoth -completeopt=escape DoCmd echo <q-args>
+ call assert_equal(['foo bar\baz'], getcompletion('DoCmd ', 'cmdline'))
+ delcom DoCmd
+
+ " ArgLead passed to the completion function is unescaped: the user typed
+ " `foo\ b` (logical "foo b"), so the function should see "foo b", not
+ " "foo\ b", and `getcompletion()` should find the "foo bar\baz" match.
+ com! -nargs=1 -complete=customlist,EscBoth -completeopt=escape DoCmd echo <q-args>
+ let g:EscArgLead = ''
+ call assert_equal(['foo bar\baz'], getcompletion('DoCmd foo\ b', 'cmdline'))
+ call assert_equal('foo b', g:EscArgLead)
+ delcom DoCmd
+
+ " Same logic applies to custom (newline-separated): the regex used for
+ " filtering is built from the unescaped pattern, so "hello\ wo" matches
+ " "hello world".
+ com! -nargs=1 -complete=custom,EscCustom -completeopt=escape DoCmd echo <q-args>
+ let g:EscArgLead = ''
+ call assert_equal(['hello world'], getcompletion('DoCmd hello\ wo', 'cmdline'))
+ call assert_equal('hello wo', g:EscArgLead)
+ delcom DoCmd
+
+ " Multi-argument case: with -nargs=+ the argument splitter still runs, so an
+ " escaped match stays a single argument and <f-args> splits the line on the
+ " unescaped spaces. This is the scenario -nargs=_ cannot express.
+ com! -nargs=+ -complete=customlist,EscMulti -completeopt=escape DoCmd
+ \ let g:EscArgs = [<f-args>]
+ let g:EscArgs = []
+ call feedkeys(":DoCmd one\<Tab> two\<Tab>\<CR>", 'tx')
+ call assert_equal(['one value', 'two values'], g:EscArgs)
+ delcom DoCmd
+ unlet g:EscArgs
+
+ " Tab-completion on the attribute value.
+ call feedkeys(":com -completeopt=esc\<Tab>\<C-B>\"\<CR>", 'tx')
+ call assert_equal('"com -completeopt=escape', @:)
+
+ " Invalid value gives E475; empty value gives E179.
+ call assert_fails('com! -nargs=1 -complete=customlist,EscOne -completeopt=bogus DoCmd :',
+ \ 'E475:')
+ call assert_fails('com! -nargs=1 -complete=customlist,EscOne -completeopt= DoCmd :',
+ \ 'E179:')
+
+ " -completeopt=escape is meaningless with -nargs=_ (the splitter is disabled),
+ " so the combination is rejected at command-definition time.
+ call assert_fails('com! -nargs=_ -complete=customlist,EscOne -completeopt=escape DoCmd :',
+ \ 'E1579:')
+
+ delfunc EscOne
+ delfunc EscBs
+ delfunc EscBoth
+ delfunc EscTab
+ delfunc EscCustom
+ delfunc EscMulti
+ unlet g:EscArgLead
+endfunc
+
func CallExecute(A, L, P)
" Drop first '\n'
return execute('echo "hi"')[1:]
cmd_addr_T uc_addr_type; // The command's address type
sctx_T uc_script_ctx; // SCTX where the command was defined
int uc_flags; // some UC_ flags
+ int uc_compl_opt; // completion options (UCC_ flags)
#ifdef FEAT_EVAL
char_u *uc_compl_arg; // completion argument if any
#endif
if (xp != NULL)
{
xp->xp_arg = uc->uc_compl_arg;
+ xp->xp_complete_opt = uc->uc_compl_opt;
xp->xp_script_ctx = uc->uc_script_ctx;
xp->xp_script_ctx.sc_lnum += SOURCING_LNUM;
}
xp->xp_context = EXPAND_USER_COMPLETE;
xp->xp_pattern = p + 1;
}
+ else if (STRNICMP(arg, "completeopt", p - arg) == 0)
+ {
+ xp->xp_context = EXPAND_USER_COMPLETEOPT;
+ // xp_pattern points to the last comma-separated item being
+ // typed so that completion replaces only that item.
+ xp->xp_pattern = p + 1;
+ for (char_u *c = p + 1; *c != NUL; ++c)
+ if (*c == ',')
+ xp->xp_pattern = c + 1;
+ }
else if (STRNICMP(arg, "nargs", p - arg) == 0)
{
xp->xp_context = EXPAND_USER_NARGS;
{
static char *user_cmd_flags[] = {
"addr", "bang", "bar", "buffer", "complete",
- "count", "nargs", "range", "register", "keepscript"
+ "completeopt", "count", "nargs", "range", "register", "keepscript"
};
if (idx < 0 || idx >= (int)ARRAY_LENGTH(user_cmd_flags))
return command_complete_tab[idx].value.string;
}
+/*
+ * Names of options accepted by -completeopt= for :command. Keep in sync with
+ * parse_completeopt_arg() below.
+ */
+static char *user_cmd_completeopt_tab[] = {
+ "escape"
+};
+
+/*
+ * Function given to ExpandGeneric() to obtain the list of values for
+ * -completeopt.
+ */
+ char_u *
+get_user_cmd_completeopt(expand_T *xp UNUSED, int idx)
+{
+ if (idx < 0 || idx >= (int)ARRAY_LENGTH(user_cmd_completeopt_tab))
+ return NULL;
+ return (char_u *)user_cmd_completeopt_tab[idx];
+}
+
/*
* Return the row in the command_complete_tab table that contains the given key.
*/
len += (int)uc_compl_arglen;
}
}
+ if (p_verbose > 0 && (cmd->uc_compl_opt & UCC_ESCAPE))
+ len += vim_snprintf((char *)IObuff + len, IOSIZE - len,
+ " -completeopt=escape");
#endif
}
return OK;
}
+/*
+ * Parse a -completeopt= value. "value" points to a comma-separated list of
+ * option names; on success the corresponding UCC_ flags are OR'ed into
+ * "*compl_opt".
+ * Returns FAIL on an unknown name.
+ */
+ static int
+parse_completeopt_arg(char_u *value, int vallen, int *compl_opt)
+{
+ char_u *p = value;
+ char_u *end = value + vallen;
+ int flags = 0;
+
+ if (vallen == 0)
+ {
+ semsg(_(e_argument_required_for_str), "-completeopt");
+ return FAIL;
+ }
+
+ while (p < end)
+ {
+ char_u *comma;
+ size_t itemlen;
+ int matched = FALSE;
+ int i;
+
+ comma = vim_strchr(p, ',');
+ if (comma == NULL || comma > end)
+ itemlen = (size_t)(end - p);
+ else
+ itemlen = (size_t)(comma - p);
+
+ if (itemlen == 0)
+ {
+ semsg(_(e_invalid_value_for_argument_str), "completeopt");
+ return FAIL;
+ }
+
+ for (i = 0; i < (int)ARRAY_LENGTH(user_cmd_completeopt_tab); ++i)
+ {
+ char *name = user_cmd_completeopt_tab[i];
+
+ if (STRLEN(name) == itemlen
+ && STRNCMP(p, name, itemlen) == 0)
+ {
+ if (STRCMP(name, "escape") == 0)
+ flags |= UCC_ESCAPE;
+ matched = TRUE;
+ break;
+ }
+ }
+ if (!matched)
+ {
+ semsg(_(e_invalid_value_for_argument_str), "completeopt");
+ return FAIL;
+ }
+
+ p += itemlen;
+ if (p < end && *p == ',')
+ ++p;
+ }
+
+ *compl_opt |= flags;
+ return OK;
+}
+
/*
* Scan attributes in the ":command" command.
* Return FAIL when something is wrong.
int *flags,
int *complp,
char_u **compl_arg,
+ int *compl_opt,
cmd_addr_T *addr_type_arg)
{
char_u *p;
== FAIL)
return FAIL;
}
+ else if (STRNICMP(attr, "completeopt", attrlen) == 0)
+ {
+ if (val == NULL)
+ {
+ semsg(_(e_argument_required_for_str), "-completeopt");
+ return FAIL;
+ }
+
+ if (parse_completeopt_arg(val, (int)vallen, compl_opt) == FAIL)
+ return FAIL;
+ }
else if (STRNICMP(attr, "addr", attrlen) == 0)
{
*argt |= EX_RANGE;
int flags,
int compl,
char_u *compl_arg UNUSED,
+ int compl_opt,
cmd_addr_T addr_type,
int force)
{
cmd->uc_argt = argt;
cmd->uc_def = def;
cmd->uc_compl = compl;
+ cmd->uc_compl_opt = compl_opt;
cmd->uc_script_ctx = current_sctx;
if (flags & UC_VIM9)
cmd->uc_script_ctx.sc_version = SCRIPT_VERSION_VIM9;
int flags = 0;
int compl = EXPAND_NOTHING;
char_u *compl_arg = NULL;
+ int compl_opt = 0;
cmd_addr_T addr_type_arg = ADDR_NONE;
int has_attr = (eap->arg[0] == '-');
int name_len;
++p;
end = skiptowhite(p);
if (uc_scan_attr(p, end - p, &argt, &def, &flags, &compl,
- &compl_arg, &addr_type_arg) == FAIL)
+ &compl_arg, &compl_opt, &addr_type_arg) == FAIL)
goto theend;
p = skipwhite(end);
}
(char_u *)_(e_complete_used_without_allowing_arguments),
TRUE, TRUE);
}
+ else if ((compl_opt & UCC_ESCAPE) && (argt & EX_ARGSPACE))
+ {
+ // -nargs=_ disables the argument splitter, so escaping spaces in
+ // inserted matches has no effect. Reject the combination instead of
+ // silently ignoring it.
+ emsg(_(e_completeopt_escape_cannot_be_used_with_nargs_underscore));
+ }
else
{
char_u *tofree = NULL;
p = may_get_cmd_block(eap, p, &tofree, &flags);
uc_add_command(name, end - name, p, argt, def, flags, compl, compl_arg,
- addr_type_arg, eap->forceit);
+ compl_opt, addr_type_arg, eap->forceit);
vim_free(tofree);
return; // success
STRCPY(buf, eap->arg);
break;
case 1: // Quote, but don't split
+ {
+ // For -completeopt=escape give <q-args> the logical value: a
+ // backslash before a space, tab or backslash is collapsed.
+ int unesc = (cmd->uc_compl_opt & UCC_ESCAPE) != 0;
+
result = STRLEN(eap->arg) + 2;
for (p = eap->arg; *p; ++p)
{
// DBCS can contain \ in a trail byte, skip the
// double-byte character.
++p;
+ else if (unesc && *p == '\\'
+ && (VIM_ISWHITE(p[1]) || p[1] == '\\'))
+ {
+ if (VIM_ISWHITE(p[1]))
+ --result;
+ ++p;
+ }
else
if (*p == '\\' || *p == '"')
++result;
// DBCS can contain \ in a trail byte, copy the
// double-byte character to avoid escaping.
*buf++ = *p++;
+ else if (unesc && *p == '\\'
+ && (VIM_ISWHITE(p[1]) || p[1] == '\\'))
+ {
+ ++p; // drop the escaping backslash
+ if (*p == '\\')
+ *buf++ = '\\'; // re-escape for the quotes
+ }
else
if (*p == '\\' || *p == '"')
*buf++ = '\\';
}
break;
+ }
case 2: // Quote and split (<f-args>)
// This is hard, so only do it once, and cache the result
if (*split_buf == NULL)
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 638,
/**/
637,
/**/
#define EXPAND_FILETYPECMD 63
#define EXPAND_PATTERN_IN_BUF 64
#define EXPAND_RETAB 65
+#define EXPAND_USER_COMPLETEOPT 66
// Values for exmode_active (0 is no exmode)
#define UC_BUFFER 1 // -buffer: local to current buffer
#define UC_VIM9 2 // {} argument: Vim9 syntax.
+// flags for the -completeopt= attribute of :command
+#define UCC_ESCAPE 0x1 // escape spaces, tabs and backslashes in
+ // matches
+
// flags used by vim_strsave_fnameescape()
#define VSE_NONE 0
#define VSE_SHELL 1 // escape for a shell command