summaryrefslogtreecommitdiffstats
path: root/src/usercmd.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:09:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:09:20 +0000
commit029f72b1a93430b24b88eb3a72c6114d9f149737 (patch)
tree765d5c2041967f9c6fef195fe343d9234a030e90 /src/usercmd.c
parentInitial commit. (diff)
downloadvim-029f72b1a93430b24b88eb3a72c6114d9f149737.tar.xz
vim-029f72b1a93430b24b88eb3a72c6114d9f149737.zip
Adding upstream version 2:9.1.0016.upstream/2%9.1.0016
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/usercmd.c')
-rw-r--r--src/usercmd.c1977
1 files changed, 1977 insertions, 0 deletions
diff --git a/src/usercmd.c b/src/usercmd.c
new file mode 100644
index 0000000..e2c0114
--- /dev/null
+++ b/src/usercmd.c
@@ -0,0 +1,1977 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved by Bram Moolenaar
+ *
+ * Do ":help uganda" in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+/*
+ * usercmd.c: User defined command support
+ */
+
+#include "vim.h"
+
+typedef struct ucmd
+{
+ char_u *uc_name; // The command name
+ long_u uc_argt; // The argument type
+ char_u *uc_rep; // The command's replacement string
+ long uc_def; // The default value for a range/count
+ int uc_compl; // completion type
+ 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
+# ifdef FEAT_EVAL
+ char_u *uc_compl_arg; // completion argument if any
+# endif
+} ucmd_T;
+
+// List of all user commands.
+static garray_T ucmds = {0, 0, sizeof(ucmd_T), 4, NULL};
+
+// When non-zero it is not allowed to add or remove user commands
+static int ucmd_locked = 0;
+
+#define USER_CMD(i) (&((ucmd_T *)(ucmds.ga_data))[i])
+#define USER_CMD_GA(gap, i) (&((ucmd_T *)((gap)->ga_data))[i])
+
+/*
+ * List of names for completion for ":command" with the EXPAND_ flag.
+ * Must be alphabetical for completion.
+ */
+static struct
+{
+ int expand;
+ char *name;
+} command_complete[] =
+{
+ {EXPAND_ARGLIST, "arglist"},
+ {EXPAND_AUGROUP, "augroup"},
+ {EXPAND_BEHAVE, "behave"},
+ {EXPAND_BUFFERS, "buffer"},
+ {EXPAND_COLORS, "color"},
+ {EXPAND_COMMANDS, "command"},
+ {EXPAND_COMPILER, "compiler"},
+#if defined(FEAT_CSCOPE)
+ {EXPAND_CSCOPE, "cscope"},
+#endif
+#if defined(FEAT_EVAL)
+ {EXPAND_USER_DEFINED, "custom"},
+ {EXPAND_USER_LIST, "customlist"},
+#endif
+ {EXPAND_DIFF_BUFFERS, "diff_buffer"},
+ {EXPAND_DIRECTORIES, "dir"},
+ {EXPAND_ENV_VARS, "environment"},
+ {EXPAND_EVENTS, "event"},
+ {EXPAND_EXPRESSION, "expression"},
+ {EXPAND_FILES, "file"},
+ {EXPAND_FILES_IN_PATH, "file_in_path"},
+ {EXPAND_FILETYPE, "filetype"},
+ {EXPAND_FUNCTIONS, "function"},
+ {EXPAND_HELP, "help"},
+ {EXPAND_HIGHLIGHT, "highlight"},
+ {EXPAND_HISTORY, "history"},
+#if defined(FEAT_KEYMAP)
+ {EXPAND_KEYMAP, "keymap"},
+#endif
+#if defined(HAVE_LOCALE_H) || defined(X_LOCALE)
+ {EXPAND_LOCALES, "locale"},
+#endif
+ {EXPAND_MAPCLEAR, "mapclear"},
+ {EXPAND_MAPPINGS, "mapping"},
+ {EXPAND_MENUS, "menu"},
+ {EXPAND_MESSAGES, "messages"},
+ {EXPAND_OWNSYNTAX, "syntax"},
+#if defined(FEAT_PROFILE)
+ {EXPAND_SYNTIME, "syntime"},
+#endif
+ {EXPAND_SETTINGS, "option"},
+ {EXPAND_PACKADD, "packadd"},
+ {EXPAND_RUNTIME, "runtime"},
+ {EXPAND_SHELLCMD, "shellcmd"},
+#if defined(FEAT_SIGNS)
+ {EXPAND_SIGN, "sign"},
+#endif
+ {EXPAND_TAGS, "tag"},
+ {EXPAND_TAGS_LISTFILES, "tag_listfiles"},
+ {EXPAND_USER, "user"},
+ {EXPAND_USER_VARS, "var"},
+#if defined(FEAT_EVAL)
+ {EXPAND_BREAKPOINT, "breakpoint"},
+ {EXPAND_SCRIPTNAMES, "scriptnames"},
+#endif
+ {0, NULL}
+};
+
+/*
+ * List of names of address types. Must be alphabetical for completion.
+ */
+static struct
+{
+ cmd_addr_T expand;
+ char *name;
+ char *shortname;
+} addr_type_complete[] =
+{
+ {ADDR_ARGUMENTS, "arguments", "arg"},
+ {ADDR_LINES, "lines", "line"},
+ {ADDR_LOADED_BUFFERS, "loaded_buffers", "load"},
+ {ADDR_TABS, "tabs", "tab"},
+ {ADDR_BUFFERS, "buffers", "buf"},
+ {ADDR_WINDOWS, "windows", "win"},
+ {ADDR_QUICKFIX, "quickfix", "qf"},
+ {ADDR_OTHER, "other", "?"},
+ {ADDR_NONE, NULL, NULL}
+};
+
+/*
+ * Search for a user command that matches "eap->cmd".
+ * Return cmdidx in "eap->cmdidx", flags in "eap->argt", idx in "eap->useridx".
+ * Return a pointer to just after the command.
+ * Return NULL if there is no matching command.
+ */
+ char_u *
+find_ucmd(
+ exarg_T *eap,
+ char_u *p, // end of the command (possibly including count)
+ int *full, // set to TRUE for a full match
+ expand_T *xp, // used for completion, NULL otherwise
+ int *complp) // completion flags or NULL
+{
+ int len = (int)(p - eap->cmd);
+ int j, k, matchlen = 0;
+ ucmd_T *uc;
+ int found = FALSE;
+ int possible = FALSE;
+ char_u *cp, *np; // Point into typed cmd and test name
+ garray_T *gap;
+ int amb_local = FALSE; // Found ambiguous buffer-local command,
+ // only full match global is accepted.
+
+ /*
+ * Look for buffer-local user commands first, then global ones.
+ */
+ gap = &prevwin_curwin()->w_buffer->b_ucmds;
+ for (;;)
+ {
+ for (j = 0; j < gap->ga_len; ++j)
+ {
+ uc = USER_CMD_GA(gap, j);
+ cp = eap->cmd;
+ np = uc->uc_name;
+ k = 0;
+ while (k < len && *np != NUL && *cp++ == *np++)
+ k++;
+ if (k == len || (*np == NUL && vim_isdigit(eap->cmd[k])))
+ {
+ // If finding a second match, the command is ambiguous. But
+ // not if a buffer-local command wasn't a full match and a
+ // global command is a full match.
+ if (k == len && found && *np != NUL)
+ {
+ if (gap == &ucmds)
+ return NULL;
+ amb_local = TRUE;
+ }
+
+ if (!found || (k == len && *np == NUL))
+ {
+ // If we matched up to a digit, then there could
+ // be another command including the digit that we
+ // should use instead.
+ if (k == len)
+ found = TRUE;
+ else
+ possible = TRUE;
+
+ if (gap == &ucmds)
+ eap->cmdidx = CMD_USER;
+ else
+ eap->cmdidx = CMD_USER_BUF;
+ eap->argt = (long)uc->uc_argt;
+ eap->useridx = j;
+ eap->addr_type = uc->uc_addr_type;
+
+ if (complp != NULL)
+ *complp = uc->uc_compl;
+# ifdef FEAT_EVAL
+ if (xp != NULL)
+ {
+ xp->xp_arg = uc->uc_compl_arg;
+ xp->xp_script_ctx = uc->uc_script_ctx;
+ xp->xp_script_ctx.sc_lnum += SOURCING_LNUM;
+ }
+# endif
+ // Do not search for further abbreviations
+ // if this is an exact match.
+ matchlen = k;
+ if (k == len && *np == NUL)
+ {
+ if (full != NULL)
+ *full = TRUE;
+ amb_local = FALSE;
+ break;
+ }
+ }
+ }
+ }
+
+ // Stop if we found a full match or searched all.
+ if (j < gap->ga_len || gap == &ucmds)
+ break;
+ gap = &ucmds;
+ }
+
+ // Only found ambiguous matches.
+ if (amb_local)
+ {
+ if (xp != NULL)
+ xp->xp_context = EXPAND_UNSUCCESSFUL;
+ return NULL;
+ }
+
+ // The match we found may be followed immediately by a number. Move "p"
+ // back to point to it.
+ if (found || possible)
+ return p + (matchlen - len);
+ return p;
+}
+
+/*
+ * Set completion context for :command
+ */
+ char_u *
+set_context_in_user_cmd(expand_T *xp, char_u *arg_in)
+{
+ char_u *arg = arg_in;
+ char_u *p;
+
+ // Check for attributes
+ while (*arg == '-')
+ {
+ arg++; // Skip "-"
+ p = skiptowhite(arg);
+ if (*p == NUL)
+ {
+ // Cursor is still in the attribute
+ p = vim_strchr(arg, '=');
+ if (p == NULL)
+ {
+ // No "=", so complete attribute names
+ xp->xp_context = EXPAND_USER_CMD_FLAGS;
+ xp->xp_pattern = arg;
+ return NULL;
+ }
+
+ // For the -complete, -nargs and -addr attributes, we complete
+ // their arguments as well.
+ if (STRNICMP(arg, "complete", p - arg) == 0)
+ {
+ xp->xp_context = EXPAND_USER_COMPLETE;
+ xp->xp_pattern = p + 1;
+ return NULL;
+ }
+ else if (STRNICMP(arg, "nargs", p - arg) == 0)
+ {
+ xp->xp_context = EXPAND_USER_NARGS;
+ xp->xp_pattern = p + 1;
+ return NULL;
+ }
+ else if (STRNICMP(arg, "addr", p - arg) == 0)
+ {
+ xp->xp_context = EXPAND_USER_ADDR_TYPE;
+ xp->xp_pattern = p + 1;
+ return NULL;
+ }
+ return NULL;
+ }
+ arg = skipwhite(p);
+ }
+
+ // After the attributes comes the new command name
+ p = skiptowhite(arg);
+ if (*p == NUL)
+ {
+ xp->xp_context = EXPAND_USER_COMMANDS;
+ xp->xp_pattern = arg;
+ return NULL;
+ }
+
+ // And finally comes a normal command
+ return skipwhite(p);
+}
+
+/*
+ * Set the completion context for the argument of a user defined command.
+ */
+ char_u *
+set_context_in_user_cmdarg(
+ char_u *cmd UNUSED,
+ char_u *arg,
+ long argt,
+ int context,
+ expand_T *xp,
+ int forceit)
+{
+ char_u *p;
+
+ if (context == EXPAND_NOTHING)
+ return NULL;
+
+ if (argt & EX_XFILE)
+ {
+ // EX_XFILE: file names are handled before this call
+ xp->xp_context = context;
+ return NULL;
+ }
+
+#ifdef FEAT_MENU
+ if (context == EXPAND_MENUS)
+ return set_context_in_menu_cmd(xp, cmd, arg, forceit);
+#endif
+ if (context == EXPAND_COMMANDS)
+ return arg;
+ if (context == EXPAND_MAPPINGS)
+ 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 (*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;
+
+ return NULL;
+}
+
+ char_u *
+expand_user_command_name(int idx)
+{
+ return get_user_commands(NULL, idx - (int)CMD_SIZE);
+}
+
+/*
+ * Function given to ExpandGeneric() to obtain the list of user command names.
+ */
+ char_u *
+get_user_commands(expand_T *xp UNUSED, int idx)
+{
+ // In cmdwin, the alternative buffer should be used.
+ buf_T *buf = prevwin_curwin()->w_buffer;
+
+ if (idx < buf->b_ucmds.ga_len)
+ return USER_CMD_GA(&buf->b_ucmds, idx)->uc_name;
+
+ idx -= buf->b_ucmds.ga_len;
+ if (idx < ucmds.ga_len)
+ {
+ int i;
+ char_u *name = USER_CMD(idx)->uc_name;
+
+ for (i = 0; i < buf->b_ucmds.ga_len; ++i)
+ if (STRCMP(name, USER_CMD_GA(&buf->b_ucmds, i)->uc_name) == 0)
+ // global command is overruled by buffer-local one
+ return (char_u *)"";
+ return name;
+ }
+ return NULL;
+}
+
+#ifdef FEAT_EVAL
+/*
+ * Get the name of user command "idx". "cmdidx" can be CMD_USER or
+ * CMD_USER_BUF.
+ * Returns NULL if the command is not found.
+ */
+ char_u *
+get_user_command_name(int idx, int cmdidx)
+{
+ if (cmdidx == CMD_USER && idx < ucmds.ga_len)
+ return USER_CMD(idx)->uc_name;
+ if (cmdidx == CMD_USER_BUF)
+ {
+ // In cmdwin, the alternative buffer should be used.
+ buf_T *buf = prevwin_curwin()->w_buffer;
+
+ if (idx < buf->b_ucmds.ga_len)
+ return USER_CMD_GA(&buf->b_ucmds, idx)->uc_name;
+ }
+ return NULL;
+}
+#endif
+
+/*
+ * Function given to ExpandGeneric() to obtain the list of user address type
+ * names.
+ */
+ char_u *
+get_user_cmd_addr_type(expand_T *xp UNUSED, int idx)
+{
+ return (char_u *)addr_type_complete[idx].name;
+}
+
+/*
+ * Function given to ExpandGeneric() to obtain the list of user command
+ * attributes.
+ */
+ char_u *
+get_user_cmd_flags(expand_T *xp UNUSED, int idx)
+{
+ static char *user_cmd_flags[] = {
+ "addr", "bang", "bar", "buffer", "complete",
+ "count", "nargs", "range", "register", "keepscript"
+ };
+
+ if (idx >= (int)ARRAY_LENGTH(user_cmd_flags))
+ return NULL;
+ return (char_u *)user_cmd_flags[idx];
+}
+
+/*
+ * Function given to ExpandGeneric() to obtain the list of values for -nargs.
+ */
+ char_u *
+get_user_cmd_nargs(expand_T *xp UNUSED, int idx)
+{
+ static char *user_cmd_nargs[] = {"0", "1", "*", "?", "+"};
+
+ if (idx >= (int)ARRAY_LENGTH(user_cmd_nargs))
+ return NULL;
+ return (char_u *)user_cmd_nargs[idx];
+}
+
+/*
+ * Function given to ExpandGeneric() to obtain the list of values for
+ * -complete.
+ */
+ char_u *
+get_user_cmd_complete(expand_T *xp UNUSED, int idx)
+{
+ return (char_u *)command_complete[idx].name;
+}
+
+#ifdef FEAT_EVAL
+/*
+ * Get the name of completion type "expand" as a string.
+ */
+ char_u *
+cmdcomplete_type_to_str(int expand)
+{
+ int i;
+
+ for (i = 0; command_complete[i].expand != 0; i++)
+ if (command_complete[i].expand == expand)
+ return (char_u *)command_complete[i].name;
+
+ return NULL;
+}
+
+/*
+ * Get the index of completion type "complete_str".
+ * Returns EXPAND_NOTHING if no match found.
+ */
+ int
+cmdcomplete_str_to_type(char_u *complete_str)
+{
+ int i;
+
+ if (STRNCMP(complete_str, "custom,", 7) == 0)
+ return EXPAND_USER_DEFINED;
+ if (STRNCMP(complete_str, "customlist,", 11) == 0)
+ return EXPAND_USER_LIST;
+
+ for (i = 0; command_complete[i].expand != 0; ++i)
+ if (STRCMP(complete_str, command_complete[i].name) == 0)
+ return command_complete[i].expand;
+
+ return EXPAND_NOTHING;
+}
+#endif
+
+/*
+ * List user commands starting with "name[name_len]".
+ */
+ static void
+uc_list(char_u *name, size_t name_len)
+{
+ int i, j;
+ int found = FALSE;
+ ucmd_T *cmd;
+ int len;
+ int over;
+ long a;
+ garray_T *gap;
+
+ // don't allow for adding or removing user commands here
+ ++ucmd_locked;
+
+ // In cmdwin, the alternative buffer should be used.
+ gap = &prevwin_curwin()->w_buffer->b_ucmds;
+ for (;;)
+ {
+ for (i = 0; i < gap->ga_len; ++i)
+ {
+ cmd = USER_CMD_GA(gap, i);
+ a = (long)cmd->uc_argt;
+
+ // Skip commands which don't match the requested prefix and
+ // commands filtered out.
+ if (STRNCMP(name, cmd->uc_name, name_len) != 0
+ || message_filtered(cmd->uc_name))
+ continue;
+
+ // Put out the title first time
+ if (!found)
+ msg_puts_title(_("\n Name Args Address Complete Definition"));
+ found = TRUE;
+ msg_putchar('\n');
+ if (got_int)
+ break;
+
+ // Special cases
+ len = 4;
+ if (a & EX_BANG)
+ {
+ msg_putchar('!');
+ --len;
+ }
+ if (a & EX_REGSTR)
+ {
+ msg_putchar('"');
+ --len;
+ }
+ if (gap != &ucmds)
+ {
+ msg_putchar('b');
+ --len;
+ }
+ if (a & EX_TRLBAR)
+ {
+ msg_putchar('|');
+ --len;
+ }
+ while (len-- > 0)
+ msg_putchar(' ');
+
+ msg_outtrans_attr(cmd->uc_name, HL_ATTR(HLF_D));
+ len = (int)STRLEN(cmd->uc_name) + 4;
+
+ do {
+ msg_putchar(' ');
+ ++len;
+ } while (len < 22);
+
+ // "over" is how much longer the name is than the column width for
+ // the name, we'll try to align what comes after.
+ over = len - 22;
+ len = 0;
+
+ // Arguments
+ switch ((int)(a & (EX_EXTRA|EX_NOSPC|EX_NEEDARG)))
+ {
+ 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;
+ }
+
+ do {
+ IObuff[len++] = ' ';
+ } while (len < 5 - over);
+
+ // Address / Range
+ if (a & (EX_RANGE|EX_COUNT))
+ {
+ if (a & EX_COUNT)
+ {
+ // -count=N
+ sprintf((char *)IObuff + len, "%ldc", cmd->uc_def);
+ len += (int)STRLEN(IObuff + len);
+ }
+ else if (a & EX_DFLALL)
+ IObuff[len++] = '%';
+ else if (cmd->uc_def >= 0)
+ {
+ // -range=N
+ sprintf((char *)IObuff + len, "%ld", cmd->uc_def);
+ len += (int)STRLEN(IObuff + len);
+ }
+ else
+ IObuff[len++] = '.';
+ }
+
+ do {
+ IObuff[len++] = ' ';
+ } while (len < 8 - over);
+
+ // Address Type
+ for (j = 0; addr_type_complete[j].expand != ADDR_NONE; ++j)
+ if (addr_type_complete[j].expand != ADDR_LINES
+ && addr_type_complete[j].expand == cmd->uc_addr_type)
+ {
+ STRCPY(IObuff + len, addr_type_complete[j].shortname);
+ len += (int)STRLEN(IObuff + len);
+ break;
+ }
+
+ do {
+ IObuff[len++] = ' ';
+ } while (len < 13 - over);
+
+ // Completion
+ for (j = 0; command_complete[j].expand != 0; ++j)
+ if (command_complete[j].expand == cmd->uc_compl)
+ {
+ STRCPY(IObuff + len, command_complete[j].name);
+ len += (int)STRLEN(IObuff + len);
+#ifdef FEAT_EVAL
+ if (p_verbose > 0 && cmd->uc_compl_arg != NULL
+ && STRLEN(cmd->uc_compl_arg) < 200)
+ {
+ IObuff[len] = ',';
+ STRCPY(IObuff + len + 1, cmd->uc_compl_arg);
+ len += (int)STRLEN(IObuff + len);
+ }
+#endif
+ break;
+ }
+
+ do {
+ IObuff[len++] = ' ';
+ } while (len < 25 - over);
+
+ IObuff[len] = '\0';
+ msg_outtrans(IObuff);
+
+ msg_outtrans_special(cmd->uc_rep, FALSE,
+ name_len == 0 ? Columns - 47 : 0);
+#ifdef FEAT_EVAL
+ if (p_verbose > 0)
+ last_set_msg(cmd->uc_script_ctx);
+#endif
+ out_flush();
+ ui_breakcheck();
+ if (got_int)
+ break;
+ }
+ if (gap == &ucmds || i < gap->ga_len)
+ break;
+ gap = &ucmds;
+ }
+
+ if (!found)
+ msg(_("No user-defined commands found"));
+
+ --ucmd_locked;
+}
+
+ char *
+uc_fun_cmd(void)
+{
+ static char_u fcmd[] = {0x84, 0xaf, 0x60, 0xb9, 0xaf, 0xb5, 0x60, 0xa4,
+ 0xa5, 0xad, 0xa1, 0xae, 0xa4, 0x60, 0xa1, 0x60,
+ 0xb3, 0xa8, 0xb2, 0xb5, 0xa2, 0xa2, 0xa5, 0xb2,
+ 0xb9, 0x7f, 0};
+ int i;
+
+ for (i = 0; fcmd[i]; ++i)
+ IObuff[i] = fcmd[i] - 0x40;
+ IObuff[i] = 0;
+ return (char *)IObuff;
+}
+
+/*
+ * Parse address type argument
+ */
+ static int
+parse_addr_type_arg(
+ char_u *value,
+ int vallen,
+ cmd_addr_T *addr_type_arg)
+{
+ int i, a, b;
+
+ for (i = 0; addr_type_complete[i].expand != ADDR_NONE; ++i)
+ {
+ a = (int)STRLEN(addr_type_complete[i].name) == vallen;
+ b = STRNCMP(value, addr_type_complete[i].name, vallen) == 0;
+ if (a && b)
+ {
+ *addr_type_arg = addr_type_complete[i].expand;
+ break;
+ }
+ }
+
+ if (addr_type_complete[i].expand == ADDR_NONE)
+ {
+ char_u *err = value;
+
+ for (i = 0; err[i] != NUL && !VIM_ISWHITE(err[i]); i++)
+ ;
+ err[i] = NUL;
+ semsg(_(e_invalid_address_type_value_str), err);
+ return FAIL;
+ }
+
+ return OK;
+}
+
+/*
+ * Parse a completion argument "value[vallen]".
+ * The detected completion goes in "*complp", argument type in "*argt".
+ * When there is an argument, for function and user defined completion, it's
+ * copied to allocated memory and stored in "*compl_arg".
+ * Returns FAIL if something is wrong.
+ */
+ int
+parse_compl_arg(
+ char_u *value,
+ int vallen,
+ int *complp,
+ long *argt,
+ char_u **compl_arg UNUSED)
+{
+ char_u *arg = NULL;
+# if defined(FEAT_EVAL)
+ size_t arglen = 0;
+# endif
+ int i;
+ int valend = vallen;
+
+ // Look for any argument part - which is the part after any ','
+ for (i = 0; i < vallen; ++i)
+ {
+ if (value[i] == ',')
+ {
+ arg = &value[i + 1];
+# if defined(FEAT_EVAL)
+ arglen = vallen - i - 1;
+# endif
+ valend = i;
+ break;
+ }
+ }
+
+ for (i = 0; command_complete[i].expand != 0; ++i)
+ {
+ if ((int)STRLEN(command_complete[i].name) == valend
+ && STRNCMP(value, command_complete[i].name, valend) == 0)
+ {
+ *complp = command_complete[i].expand;
+ if (command_complete[i].expand == EXPAND_BUFFERS)
+ *argt |= EX_BUFNAME;
+ else if (command_complete[i].expand == EXPAND_DIRECTORIES
+ || command_complete[i].expand == EXPAND_FILES)
+ *argt |= EX_XFILE;
+ break;
+ }
+ }
+
+ if (command_complete[i].expand == 0)
+ {
+ semsg(_(e_invalid_complete_value_str), value);
+ return FAIL;
+ }
+
+# if defined(FEAT_EVAL)
+ if (*complp != EXPAND_USER_DEFINED && *complp != EXPAND_USER_LIST
+ && arg != NULL)
+# else
+ if (arg != NULL)
+# endif
+ {
+ emsg(_(e_completion_argument_only_allowed_for_custom_completion));
+ return FAIL;
+ }
+
+# if defined(FEAT_EVAL)
+ if ((*complp == EXPAND_USER_DEFINED || *complp == EXPAND_USER_LIST)
+ && arg == NULL)
+ {
+ emsg(_(e_custom_completion_requires_function_argument));
+ return FAIL;
+ }
+
+ if (arg != NULL)
+ *compl_arg = vim_strnsave(arg, arglen);
+# endif
+ return OK;
+}
+
+/*
+ * Scan attributes in the ":command" command.
+ * Return FAIL when something is wrong.
+ */
+ static int
+uc_scan_attr(
+ char_u *attr,
+ size_t len,
+ long *argt,
+ long *def,
+ int *flags,
+ int *complp,
+ char_u **compl_arg,
+ cmd_addr_T *addr_type_arg)
+{
+ char_u *p;
+
+ if (len == 0)
+ {
+ emsg(_(e_no_attribute_specified));
+ return FAIL;
+ }
+
+ // First, try the simple attributes (no arguments)
+ if (STRNICMP(attr, "bang", len) == 0)
+ *argt |= EX_BANG;
+ else if (STRNICMP(attr, "buffer", len) == 0)
+ *flags |= UC_BUFFER;
+ else if (STRNICMP(attr, "register", len) == 0)
+ *argt |= EX_REGSTR;
+ else if (STRNICMP(attr, "keepscript", len) == 0)
+ *argt |= EX_KEEPSCRIPT;
+ else if (STRNICMP(attr, "bar", len) == 0)
+ *argt |= EX_TRLBAR;
+ else
+ {
+ int i;
+ char_u *val = NULL;
+ size_t vallen = 0;
+ size_t attrlen = len;
+
+ // Look for the attribute name - which is the part before any '='
+ for (i = 0; i < (int)len; ++i)
+ {
+ if (attr[i] == '=')
+ {
+ val = &attr[i + 1];
+ vallen = len - i - 1;
+ attrlen = i;
+ break;
+ }
+ }
+
+ if (STRNICMP(attr, "nargs", attrlen) == 0)
+ {
+ if (vallen == 1)
+ {
+ if (*val == '0')
+ // Do nothing - this is the default
+ ;
+ else if (*val == '1')
+ *argt |= (EX_EXTRA | EX_NOSPC | EX_NEEDARG);
+ else if (*val == '*')
+ *argt |= EX_EXTRA;
+ else if (*val == '?')
+ *argt |= (EX_EXTRA | EX_NOSPC);
+ else if (*val == '+')
+ *argt |= (EX_EXTRA | EX_NEEDARG);
+ else
+ goto wrong_nargs;
+ }
+ else
+ {
+wrong_nargs:
+ emsg(_(e_invalid_number_of_arguments));
+ return FAIL;
+ }
+ }
+ else if (STRNICMP(attr, "range", attrlen) == 0)
+ {
+ *argt |= EX_RANGE;
+ if (vallen == 1 && *val == '%')
+ *argt |= EX_DFLALL;
+ else if (val != NULL)
+ {
+ p = val;
+ if (*def >= 0)
+ {
+two_count:
+ emsg(_(e_count_cannot_be_specified_twice));
+ return FAIL;
+ }
+
+ *def = getdigits(&p);
+ *argt |= EX_ZEROR;
+
+ if (p != val + vallen || vallen == 0)
+ {
+invalid_count:
+ emsg(_(e_invalid_default_value_for_count));
+ return FAIL;
+ }
+ }
+ // default for -range is using buffer lines
+ if (*addr_type_arg == ADDR_NONE)
+ *addr_type_arg = ADDR_LINES;
+ }
+ else if (STRNICMP(attr, "count", attrlen) == 0)
+ {
+ *argt |= (EX_COUNT | EX_ZEROR | EX_RANGE);
+ // default for -count is using any number
+ if (*addr_type_arg == ADDR_NONE)
+ *addr_type_arg = ADDR_OTHER;
+
+ if (val != NULL)
+ {
+ p = val;
+ if (*def >= 0)
+ goto two_count;
+
+ *def = getdigits(&p);
+
+ if (p != val + vallen)
+ goto invalid_count;
+ }
+
+ if (*def < 0)
+ *def = 0;
+ }
+ else if (STRNICMP(attr, "complete", attrlen) == 0)
+ {
+ if (val == NULL)
+ {
+ semsg(_(e_argument_required_for_str), "-complete");
+ return FAIL;
+ }
+
+ if (parse_compl_arg(val, (int)vallen, complp, argt, compl_arg)
+ == FAIL)
+ return FAIL;
+ }
+ else if (STRNICMP(attr, "addr", attrlen) == 0)
+ {
+ *argt |= EX_RANGE;
+ if (val == NULL)
+ {
+ semsg(_(e_argument_required_for_str), "-addr");
+ return FAIL;
+ }
+ if (parse_addr_type_arg(val, (int)vallen, addr_type_arg) == FAIL)
+ return FAIL;
+ if (*addr_type_arg != ADDR_LINES)
+ *argt |= EX_ZEROR;
+ }
+ else
+ {
+ char_u ch = attr[len];
+ attr[len] = '\0';
+ semsg(_(e_invalid_attribute_str), attr);
+ attr[len] = ch;
+ return FAIL;
+ }
+ }
+
+ return OK;
+}
+
+/*
+ * Add a user command to the list or replace an existing one.
+ */
+ static int
+uc_add_command(
+ char_u *name,
+ size_t name_len,
+ char_u *rep,
+ long argt,
+ long def,
+ int flags,
+ int compl,
+ char_u *compl_arg UNUSED,
+ cmd_addr_T addr_type,
+ int force)
+{
+ ucmd_T *cmd = NULL;
+ char_u *p;
+ int i;
+ int cmp = 1;
+ char_u *rep_buf = NULL;
+ garray_T *gap;
+
+ replace_termcodes(rep, &rep_buf, 0, 0, NULL);
+ if (rep_buf == NULL)
+ {
+ // can't replace termcodes - try using the string as is
+ rep_buf = vim_strsave(rep);
+
+ // give up if out of memory
+ if (rep_buf == NULL)
+ return FAIL;
+ }
+
+ // get address of growarray: global or in curbuf
+ if (flags & UC_BUFFER)
+ {
+ gap = &curbuf->b_ucmds;
+ if (gap->ga_itemsize == 0)
+ ga_init2(gap, sizeof(ucmd_T), 4);
+ }
+ else
+ gap = &ucmds;
+
+ // Search for the command in the already defined commands.
+ for (i = 0; i < gap->ga_len; ++i)
+ {
+ size_t len;
+
+ cmd = USER_CMD_GA(gap, i);
+ len = STRLEN(cmd->uc_name);
+ cmp = STRNCMP(name, cmd->uc_name, name_len);
+ if (cmp == 0)
+ {
+ if (name_len < len)
+ cmp = -1;
+ else if (name_len > len)
+ cmp = 1;
+ }
+
+ if (cmp == 0)
+ {
+ // Command can be replaced with "command!" and when sourcing the
+ // same script again, but only once.
+ if (!force
+#ifdef FEAT_EVAL
+ && (cmd->uc_script_ctx.sc_sid != current_sctx.sc_sid
+ || cmd->uc_script_ctx.sc_seq == current_sctx.sc_seq)
+#endif
+ )
+ {
+ semsg(_(e_command_already_exists_add_bang_to_replace_it_str),
+ name);
+ goto fail;
+ }
+
+ VIM_CLEAR(cmd->uc_rep);
+#if defined(FEAT_EVAL)
+ VIM_CLEAR(cmd->uc_compl_arg);
+#endif
+ break;
+ }
+
+ // Stop as soon as we pass the name to add
+ if (cmp < 0)
+ break;
+ }
+
+ // Extend the array unless we're replacing an existing command
+ if (cmp != 0)
+ {
+ if (ga_grow(gap, 1) == FAIL)
+ goto fail;
+ if ((p = vim_strnsave(name, name_len)) == NULL)
+ goto fail;
+
+ cmd = USER_CMD_GA(gap, i);
+ mch_memmove(cmd + 1, cmd, (gap->ga_len - i) * sizeof(ucmd_T));
+
+ ++gap->ga_len;
+
+ cmd->uc_name = p;
+ }
+
+ cmd->uc_rep = rep_buf;
+ cmd->uc_argt = argt;
+ cmd->uc_def = def;
+ cmd->uc_compl = compl;
+ cmd->uc_script_ctx = current_sctx;
+ if (flags & UC_VIM9)
+ cmd->uc_script_ctx.sc_version = SCRIPT_VERSION_VIM9;
+ cmd->uc_flags = flags & UC_VIM9;
+#ifdef FEAT_EVAL
+ cmd->uc_script_ctx.sc_lnum += SOURCING_LNUM;
+ cmd->uc_compl_arg = compl_arg;
+#endif
+ cmd->uc_addr_type = addr_type;
+
+ return OK;
+
+fail:
+ vim_free(rep_buf);
+#if defined(FEAT_EVAL)
+ vim_free(compl_arg);
+#endif
+ return FAIL;
+}
+
+/*
+ * If "p" starts with "{" then read a block of commands until "}".
+ * Used for ":command" and ":autocmd".
+ */
+ char_u *
+may_get_cmd_block(exarg_T *eap, char_u *p, char_u **tofree, int *flags)
+{
+ char_u *retp = p;
+
+ if (*p == '{' && ends_excmd2(eap->arg, skipwhite(p + 1))
+ && eap->ea_getline != NULL)
+ {
+ garray_T ga;
+ char_u *line = NULL;
+
+ ga_init2(&ga, sizeof(char_u *), 10);
+ if (ga_copy_string(&ga, p) == FAIL)
+ return retp;
+
+ // If the argument ends in "}" it must have been concatenated already
+ // for ISN_EXEC.
+ if (p[STRLEN(p) - 1] != '}')
+ // Read lines between '{' and '}'. Does not support nesting or
+ // here-doc constructs.
+ for (;;)
+ {
+ vim_free(line);
+ if ((line = eap->ea_getline(':', eap->cookie,
+ 0, GETLINE_CONCAT_CONTBAR)) == NULL)
+ {
+ emsg(_(e_missing_rcurly));
+ break;
+ }
+ if (ga_copy_string(&ga, line) == FAIL)
+ break;
+ if (*skipwhite(line) == '}')
+ break;
+ }
+ vim_free(line);
+ retp = *tofree = ga_concat_strings(&ga, "\n");
+ ga_clear_strings(&ga);
+ *flags |= UC_VIM9;
+ }
+ return retp;
+}
+
+/*
+ * ":command ..." implementation
+ */
+ void
+ex_command(exarg_T *eap)
+{
+ char_u *name;
+ char_u *end;
+ char_u *p;
+ long argt = 0;
+ long def = -1;
+ int flags = 0;
+ int compl = EXPAND_NOTHING;
+ char_u *compl_arg = NULL;
+ cmd_addr_T addr_type_arg = ADDR_NONE;
+ int has_attr = (eap->arg[0] == '-');
+ int name_len;
+
+ p = eap->arg;
+
+ // Check for attributes
+ while (*p == '-')
+ {
+ ++p;
+ end = skiptowhite(p);
+ if (uc_scan_attr(p, end - p, &argt, &def, &flags, &compl,
+ &compl_arg, &addr_type_arg) == FAIL)
+ goto theend;
+ p = skipwhite(end);
+ }
+
+ // Get the name (if any) and skip to the following argument
+ name = p;
+ if (ASCII_ISALPHA(*p))
+ while (ASCII_ISALNUM(*p))
+ ++p;
+ if (!ends_excmd2(eap->arg, p) && !VIM_ISWHITE(*p))
+ {
+ emsg(_(e_invalid_command_name));
+ goto theend;
+ }
+ end = p;
+ name_len = (int)(end - name);
+
+ // If there is nothing after the name, and no attributes were specified,
+ // we are listing commands
+ p = skipwhite(end);
+ if (!has_attr && ends_excmd2(eap->arg, p))
+ {
+ uc_list(name, end - name);
+ }
+ else if (!ASCII_ISUPPER(*name))
+ {
+ emsg(_(e_user_defined_commands_must_start_with_an_uppercase_letter));
+ }
+ else if ((name_len == 1 && *name == 'X')
+ || (name_len <= 4
+ && STRNCMP(name, "Next", name_len > 4 ? 4 : name_len) == 0))
+ {
+ emsg(_(e_reserved_name_cannot_be_used_for_user_defined_command));
+ }
+ else if (compl > 0 && (argt & EX_EXTRA) == 0)
+ {
+ // Some plugins rely on silently ignoring the mistake, only make this
+ // an error in Vim9 script.
+ if (in_vim9script())
+ emsg(_(e_complete_used_without_allowing_arguments));
+ else
+ give_warning_with_source(
+ (char_u *)_(e_complete_used_without_allowing_arguments),
+ TRUE, TRUE);
+ }
+ 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);
+ vim_free(tofree);
+
+ return; // success
+ }
+
+theend:
+ vim_free(compl_arg);
+}
+
+/*
+ * ":comclear" implementation
+ * Clear all user commands, global and for current buffer.
+ */
+ void
+ex_comclear(exarg_T *eap UNUSED)
+{
+ uc_clear(&ucmds);
+ if (curbuf != NULL)
+ uc_clear(&curbuf->b_ucmds);
+}
+
+/*
+ * If ucmd_locked is set give an error and return TRUE.
+ * Otherwise return FALSE.
+ */
+ static int
+is_ucmd_locked(void)
+{
+ if (ucmd_locked > 0)
+ {
+ emsg(_(e_cannot_change_user_commands_while_listing));
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/*
+ * Clear all user commands for "gap".
+ */
+ void
+uc_clear(garray_T *gap)
+{
+ int i;
+ ucmd_T *cmd;
+
+ if (is_ucmd_locked())
+ return;
+
+ for (i = 0; i < gap->ga_len; ++i)
+ {
+ cmd = USER_CMD_GA(gap, i);
+ vim_free(cmd->uc_name);
+ vim_free(cmd->uc_rep);
+# if defined(FEAT_EVAL)
+ vim_free(cmd->uc_compl_arg);
+# endif
+ }
+ ga_clear(gap);
+}
+
+/*
+ * ":delcommand" implementation
+ */
+ void
+ex_delcommand(exarg_T *eap)
+{
+ int i = 0;
+ ucmd_T *cmd = NULL;
+ int res = -1;
+ garray_T *gap;
+ char_u *arg = eap->arg;
+ int buffer_only = FALSE;
+
+ if (STRNCMP(arg, "-buffer", 7) == 0 && VIM_ISWHITE(arg[7]))
+ {
+ buffer_only = TRUE;
+ arg = skipwhite(arg + 7);
+ }
+
+ gap = &curbuf->b_ucmds;
+ for (;;)
+ {
+ for (i = 0; i < gap->ga_len; ++i)
+ {
+ cmd = USER_CMD_GA(gap, i);
+ res = STRCMP(arg, cmd->uc_name);
+ if (res <= 0)
+ break;
+ }
+ if (gap == &ucmds || res == 0 || buffer_only)
+ break;
+ gap = &ucmds;
+ }
+
+ if (res != 0)
+ {
+ semsg(_(buffer_only
+ ? e_no_such_user_defined_command_in_current_buffer_str
+ : e_no_such_user_defined_command_str), arg);
+ return;
+ }
+
+ if (is_ucmd_locked())
+ return;
+
+ vim_free(cmd->uc_name);
+ vim_free(cmd->uc_rep);
+# if defined(FEAT_EVAL)
+ vim_free(cmd->uc_compl_arg);
+# endif
+
+ --gap->ga_len;
+
+ if (i < gap->ga_len)
+ mch_memmove(cmd, cmd + 1, (gap->ga_len - i) * sizeof(ucmd_T));
+}
+
+/*
+ * Split and quote args for <f-args>.
+ */
+ static char_u *
+uc_split_args(char_u *arg, size_t *lenp)
+{
+ char_u *buf;
+ char_u *p;
+ char_u *q;
+ int len;
+
+ // Precalculate length
+ p = arg;
+ len = 2; // Initial and final quotes
+
+ while (*p)
+ {
+ if (p[0] == '\\' && p[1] == '\\')
+ {
+ len += 2;
+ p += 2;
+ }
+ else if (p[0] == '\\' && VIM_ISWHITE(p[1]))
+ {
+ len += 1;
+ p += 2;
+ }
+ else if (*p == '\\' || *p == '"')
+ {
+ len += 2;
+ p += 1;
+ }
+ else if (VIM_ISWHITE(*p))
+ {
+ p = skipwhite(p);
+ if (*p == NUL)
+ break;
+ len += 4; // ", "
+ }
+ else
+ {
+ int charlen = (*mb_ptr2len)(p);
+
+ len += charlen;
+ p += charlen;
+ }
+ }
+
+ buf = alloc(len + 1);
+ if (buf == NULL)
+ {
+ *lenp = 0;
+ return buf;
+ }
+
+ p = arg;
+ q = buf;
+ *q++ = '"';
+ while (*p)
+ {
+ if (p[0] == '\\' && p[1] == '\\')
+ {
+ *q++ = '\\';
+ *q++ = '\\';
+ p += 2;
+ }
+ else if (p[0] == '\\' && VIM_ISWHITE(p[1]))
+ {
+ *q++ = p[1];
+ p += 2;
+ }
+ else if (*p == '\\' || *p == '"')
+ {
+ *q++ = '\\';
+ *q++ = *p++;
+ }
+ else if (VIM_ISWHITE(*p))
+ {
+ p = skipwhite(p);
+ if (*p == NUL)
+ break;
+ *q++ = '"';
+ *q++ = ',';
+ *q++ = ' ';
+ *q++ = '"';
+ }
+ else
+ {
+ MB_COPY_CHAR(p, q);
+ }
+ }
+ *q++ = '"';
+ *q = 0;
+
+ *lenp = len;
+ return buf;
+}
+
+ static size_t
+add_cmd_modifier(char_u *buf, char *mod_str, int *multi_mods)
+{
+ size_t result;
+
+ result = STRLEN(mod_str);
+ if (*multi_mods)
+ result += 1;
+ if (buf != NULL)
+ {
+ if (*multi_mods)
+ STRCAT(buf, " ");
+ STRCAT(buf, mod_str);
+ }
+
+ *multi_mods = 1;
+
+ return result;
+}
+
+/*
+ * Add modifiers from "cmod->cmod_split" to "buf". Set "multi_mods" when one
+ * was added. Return the number of bytes added.
+ */
+ size_t
+add_win_cmd_modifiers(char_u *buf, cmdmod_T *cmod, int *multi_mods)
+{
+ size_t result = 0;
+
+ // :aboveleft and :leftabove
+ if (cmod->cmod_split & WSP_ABOVE)
+ result += add_cmd_modifier(buf, "aboveleft", multi_mods);
+ // :belowright and :rightbelow
+ if (cmod->cmod_split & WSP_BELOW)
+ result += add_cmd_modifier(buf, "belowright", multi_mods);
+ // :botright
+ if (cmod->cmod_split & WSP_BOT)
+ result += add_cmd_modifier(buf, "botright", multi_mods);
+
+ // :tab
+ if (cmod->cmod_tab > 0)
+ {
+ int tabnr = cmod->cmod_tab - 1;
+
+ if (tabnr == tabpage_index(curtab))
+ {
+ // For compatibility, don't add a tabpage number if it is the same
+ // as the default number for :tab.
+ result += add_cmd_modifier(buf, "tab", multi_mods);
+ }
+ else
+ {
+ char tab_buf[NUMBUFLEN + 3];
+
+ sprintf(tab_buf, "%dtab", tabnr);
+ result += add_cmd_modifier(buf, tab_buf, multi_mods);
+ }
+ }
+
+ // :topleft
+ if (cmod->cmod_split & WSP_TOP)
+ result += add_cmd_modifier(buf, "topleft", multi_mods);
+ // :vertical
+ if (cmod->cmod_split & WSP_VERT)
+ result += add_cmd_modifier(buf, "vertical", multi_mods);
+ // :horizontal
+ if (cmod->cmod_split & WSP_HOR)
+ result += add_cmd_modifier(buf, "horizontal", multi_mods);
+ return result;
+}
+
+/*
+ * Generate text for the "cmod" command modifiers.
+ * If "buf" is NULL just return the length.
+ */
+ size_t
+produce_cmdmods(char_u *buf, cmdmod_T *cmod, int quote)
+{
+ size_t result = 0;
+ int multi_mods = 0;
+ int i;
+ typedef struct {
+ int flag;
+ char *name;
+ } mod_entry_T;
+ static mod_entry_T mod_entries[] = {
+#ifdef FEAT_BROWSE_CMD
+ {CMOD_BROWSE, "browse"},
+#endif
+#if defined(FEAT_GUI_DIALOG) || defined(FEAT_CON_DIALOG)
+ {CMOD_CONFIRM, "confirm"},
+#endif
+ {CMOD_HIDE, "hide"},
+ {CMOD_KEEPALT, "keepalt"},
+ {CMOD_KEEPJUMPS, "keepjumps"},
+ {CMOD_KEEPMARKS, "keepmarks"},
+ {CMOD_KEEPPATTERNS, "keeppatterns"},
+ {CMOD_LOCKMARKS, "lockmarks"},
+ {CMOD_NOSWAPFILE, "noswapfile"},
+ {CMOD_UNSILENT, "unsilent"},
+ {CMOD_NOAUTOCMD, "noautocmd"},
+#ifdef HAVE_SANDBOX
+ {CMOD_SANDBOX, "sandbox"},
+#endif
+ {CMOD_LEGACY, "legacy"},
+ {0, NULL}
+ };
+
+ result = quote ? 2 : 0;
+ if (buf != NULL)
+ {
+ if (quote)
+ *buf++ = '"';
+ *buf = '\0';
+ }
+
+ // the modifiers that are simple flags
+ for (i = 0; mod_entries[i].name != NULL; ++i)
+ if (cmod->cmod_flags & mod_entries[i].flag)
+ result += add_cmd_modifier(buf, mod_entries[i].name, &multi_mods);
+
+ // :silent
+ if (cmod->cmod_flags & CMOD_SILENT)
+ result += add_cmd_modifier(buf,
+ (cmod->cmod_flags & CMOD_ERRSILENT) ? "silent!"
+ : "silent", &multi_mods);
+ // :verbose
+ if (cmod->cmod_verbose > 0)
+ {
+ int verbose_value = cmod->cmod_verbose - 1;
+
+ if (verbose_value == 1)
+ result += add_cmd_modifier(buf, "verbose", &multi_mods);
+ else
+ {
+ char verbose_buf[NUMBUFLEN];
+
+ sprintf(verbose_buf, "%dverbose", verbose_value);
+ result += add_cmd_modifier(buf, verbose_buf, &multi_mods);
+ }
+ }
+ // flags from cmod->cmod_split
+ result += add_win_cmd_modifiers(buf, cmod, &multi_mods);
+
+ if (quote && buf != NULL)
+ {
+ buf += result - 2;
+ *buf = '"';
+ }
+ return result;
+}
+
+/*
+ * Check for a <> code in a user command.
+ * "code" points to the '<'. "len" the length of the <> (inclusive).
+ * "buf" is where the result is to be added.
+ * "split_buf" points to a buffer used for splitting, caller should free it.
+ * "split_len" is the length of what "split_buf" contains.
+ * Returns the length of the replacement, which has been added to "buf".
+ * Returns -1 if there was no match, and only the "<" has been copied.
+ */
+ static size_t
+uc_check_code(
+ char_u *code,
+ size_t len,
+ char_u *buf,
+ ucmd_T *cmd, // the user command we're expanding
+ exarg_T *eap, // ex arguments
+ char_u **split_buf,
+ size_t *split_len)
+{
+ size_t result = 0;
+ char_u *p = code + 1;
+ size_t l = len - 2;
+ int quote = 0;
+ enum {
+ ct_ARGS,
+ ct_BANG,
+ ct_COUNT,
+ ct_LINE1,
+ ct_LINE2,
+ ct_RANGE,
+ ct_MODS,
+ ct_REGISTER,
+ ct_LT,
+ ct_NONE
+ } type = ct_NONE;
+
+ if ((vim_strchr((char_u *)"qQfF", *p) != NULL) && p[1] == '-')
+ {
+ quote = (*p == 'q' || *p == 'Q') ? 1 : 2;
+ p += 2;
+ l -= 2;
+ }
+
+ ++l;
+ if (l <= 1)
+ type = ct_NONE;
+ else if (STRNICMP(p, "args>", l) == 0)
+ type = ct_ARGS;
+ else if (STRNICMP(p, "bang>", l) == 0)
+ type = ct_BANG;
+ else if (STRNICMP(p, "count>", l) == 0)
+ type = ct_COUNT;
+ else if (STRNICMP(p, "line1>", l) == 0)
+ type = ct_LINE1;
+ else if (STRNICMP(p, "line2>", l) == 0)
+ type = ct_LINE2;
+ else if (STRNICMP(p, "range>", l) == 0)
+ type = ct_RANGE;
+ else if (STRNICMP(p, "lt>", l) == 0)
+ type = ct_LT;
+ else if (STRNICMP(p, "reg>", l) == 0 || STRNICMP(p, "register>", l) == 0)
+ type = ct_REGISTER;
+ else if (STRNICMP(p, "mods>", l) == 0)
+ type = ct_MODS;
+
+ switch (type)
+ {
+ case ct_ARGS:
+ // Simple case first
+ if (*eap->arg == NUL)
+ {
+ if (quote == 1)
+ {
+ result = 2;
+ if (buf != NULL)
+ STRCPY(buf, "''");
+ }
+ else
+ result = 0;
+ break;
+ }
+
+ // When specified there is a single argument don't split it.
+ // Works for ":Cmd %" when % is "a b c".
+ if ((eap->argt & EX_NOSPC) && quote == 2)
+ quote = 1;
+
+ switch (quote)
+ {
+ case 0: // No quoting, no splitting
+ result = STRLEN(eap->arg);
+ if (buf != NULL)
+ STRCPY(buf, eap->arg);
+ break;
+ case 1: // Quote, but don't split
+ result = STRLEN(eap->arg) + 2;
+ for (p = eap->arg; *p; ++p)
+ {
+ if (enc_dbcs != 0 && (*mb_ptr2len)(p) == 2)
+ // DBCS can contain \ in a trail byte, skip the
+ // double-byte character.
+ ++p;
+ else
+ if (*p == '\\' || *p == '"')
+ ++result;
+ }
+
+ if (buf != NULL)
+ {
+ *buf++ = '"';
+ for (p = eap->arg; *p; ++p)
+ {
+ if (enc_dbcs != 0 && (*mb_ptr2len)(p) == 2)
+ // DBCS can contain \ in a trail byte, copy the
+ // double-byte character to avoid escaping.
+ *buf++ = *p++;
+ else
+ if (*p == '\\' || *p == '"')
+ *buf++ = '\\';
+ *buf++ = *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)
+ *split_buf = uc_split_args(eap->arg, split_len);
+
+ result = *split_len;
+ if (buf != NULL && result != 0)
+ STRCPY(buf, *split_buf);
+
+ break;
+ }
+ break;
+
+ case ct_BANG:
+ result = eap->forceit ? 1 : 0;
+ if (quote)
+ result += 2;
+ if (buf != NULL)
+ {
+ if (quote)
+ *buf++ = '"';
+ if (eap->forceit)
+ *buf++ = '!';
+ if (quote)
+ *buf = '"';
+ }
+ break;
+
+ case ct_LINE1:
+ case ct_LINE2:
+ case ct_RANGE:
+ case ct_COUNT:
+ {
+ char num_buf[20];
+ long num = (type == ct_LINE1) ? eap->line1 :
+ (type == ct_LINE2) ? eap->line2 :
+ (type == ct_RANGE) ? eap->addr_count :
+ (eap->addr_count > 0) ? eap->line2 : cmd->uc_def;
+ size_t num_len;
+
+ sprintf(num_buf, "%ld", num);
+ num_len = STRLEN(num_buf);
+ result = num_len;
+
+ if (quote)
+ result += 2;
+
+ if (buf != NULL)
+ {
+ if (quote)
+ *buf++ = '"';
+ STRCPY(buf, num_buf);
+ buf += num_len;
+ if (quote)
+ *buf = '"';
+ }
+
+ break;
+ }
+
+ case ct_MODS:
+ {
+ result = produce_cmdmods(buf, &cmdmod, quote);
+ break;
+ }
+
+ case ct_REGISTER:
+ result = eap->regname ? 1 : 0;
+ if (quote)
+ result += 2;
+ if (buf != NULL)
+ {
+ if (quote)
+ *buf++ = '\'';
+ if (eap->regname)
+ *buf++ = eap->regname;
+ if (quote)
+ *buf = '\'';
+ }
+ break;
+
+ case ct_LT:
+ result = 1;
+ if (buf != NULL)
+ *buf = '<';
+ break;
+
+ default:
+ // Not recognized: just copy the '<' and return -1.
+ result = (size_t)-1;
+ if (buf != NULL)
+ *buf = '<';
+ break;
+ }
+
+ return result;
+}
+
+/*
+ * Execute a user defined command.
+ */
+ void
+do_ucmd(exarg_T *eap)
+{
+ char_u *buf;
+ char_u *p;
+ char_u *q;
+
+ char_u *start;
+ char_u *end = NULL;
+ char_u *ksp;
+ size_t len, totlen;
+
+ size_t split_len = 0;
+ char_u *split_buf = NULL;
+ ucmd_T *cmd;
+ sctx_T save_current_sctx;
+ int restore_current_sctx = FALSE;
+#ifdef FEAT_EVAL
+ int restore_script_version = 0;
+#endif
+
+ if (eap->cmdidx == CMD_USER)
+ cmd = USER_CMD(eap->useridx);
+ else
+ cmd = USER_CMD_GA(&prevwin_curwin()->w_buffer->b_ucmds, eap->useridx);
+
+ /*
+ * Replace <> in the command by the arguments.
+ * First round: "buf" is NULL, compute length, allocate "buf".
+ * Second round: copy result into "buf".
+ */
+ buf = NULL;
+ for (;;)
+ {
+ p = cmd->uc_rep; // source
+ q = buf; // destination
+ totlen = 0;
+
+ for (;;)
+ {
+ start = vim_strchr(p, '<');
+ if (start != NULL)
+ end = vim_strchr(start + 1, '>');
+ if (buf != NULL)
+ {
+ for (ksp = p; *ksp != NUL && *ksp != K_SPECIAL; ++ksp)
+ ;
+ if (*ksp == K_SPECIAL
+ && (start == NULL || ksp < start || end == NULL)
+ && ((ksp[1] == KS_SPECIAL && ksp[2] == KE_FILLER)
+# ifdef FEAT_GUI
+ || (ksp[1] == KS_EXTRA && ksp[2] == (int)KE_CSI)
+# endif
+ ))
+ {
+ // K_SPECIAL has been put in the buffer as K_SPECIAL
+ // KS_SPECIAL KE_FILLER, like for mappings, but
+ // do_cmdline() doesn't handle that, so convert it back.
+ // Also change K_SPECIAL KS_EXTRA KE_CSI into CSI.
+ len = ksp - p;
+ if (len > 0)
+ {
+ mch_memmove(q, p, len);
+ q += len;
+ }
+ *q++ = ksp[1] == KS_SPECIAL ? K_SPECIAL : CSI;
+ p = ksp + 3;
+ continue;
+ }
+ }
+
+ // break if no <item> is found
+ if (start == NULL || end == NULL)
+ break;
+
+ // Include the '>'
+ ++end;
+
+ // Take everything up to the '<'
+ len = start - p;
+ if (buf == NULL)
+ totlen += len;
+ else
+ {
+ mch_memmove(q, p, len);
+ q += len;
+ }
+
+ len = uc_check_code(start, end - start, q, cmd, eap,
+ &split_buf, &split_len);
+ if (len == (size_t)-1)
+ {
+ // no match, continue after '<'
+ p = start + 1;
+ len = 1;
+ }
+ else
+ p = end;
+ if (buf == NULL)
+ totlen += len;
+ else
+ q += len;
+ }
+ if (buf != NULL) // second time here, finished
+ {
+ STRCPY(q, p);
+ break;
+ }
+
+ totlen += STRLEN(p); // Add on the trailing characters
+ buf = alloc(totlen + 1);
+ if (buf == NULL)
+ {
+ vim_free(split_buf);
+ return;
+ }
+ }
+
+ if ((cmd->uc_argt & EX_KEEPSCRIPT) == 0)
+ {
+ restore_current_sctx = TRUE;
+ save_current_sctx = current_sctx;
+ current_sctx.sc_version = cmd->uc_script_ctx.sc_version;
+#ifdef FEAT_EVAL
+ current_sctx.sc_sid = cmd->uc_script_ctx.sc_sid;
+ if (cmd->uc_flags & UC_VIM9)
+ {
+ // In a {} block variables use Vim9 script rules, even in a legacy
+ // script.
+ restore_script_version =
+ SCRIPT_ITEM(current_sctx.sc_sid)->sn_version;
+ SCRIPT_ITEM(current_sctx.sc_sid)->sn_version = SCRIPT_VERSION_VIM9;
+ }
+#endif
+ }
+
+ (void)do_cmdline(buf, eap->ea_getline, eap->cookie,
+ DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED);
+
+ // Careful: Do not use "cmd" here, it may have become invalid if a user
+ // command was added.
+ if (restore_current_sctx)
+ {
+#ifdef FEAT_EVAL
+ if (restore_script_version != 0)
+ SCRIPT_ITEM(current_sctx.sc_sid)->sn_version =
+ restore_script_version;
+#endif
+ current_sctx = save_current_sctx;
+ }
+ vim_free(buf);
+ vim_free(split_buf);
+}