feat(api): add nvim_cmd

Adds the API function `nvim_cmd` which allows executing an Ex-command through a Dictionary which can have the same values as the return value of `nvim_parse_cmd()`. This makes it much easier to do things like passing arguments with a space to commands that otherwise may not allow it, or to make commands interpret certain characters literally when they otherwise would not.
This commit is contained in:
Famiu Haque 2022-05-08 17:39:45 +06:00
parent 77863b8e96
commit dfcc584665
10 changed files with 1131 additions and 132 deletions

View File

@ -1711,17 +1711,49 @@ nvim_call_function({fn}, {args}) *nvim_call_function()*
Return: ~
Result of the function call
nvim_cmd({*cmd}, {*opts}) *nvim_cmd()*
Executes an Ex command.
Unlike |nvim_command()| this command takes a structured
Dictionary instead of a String. This allows for easier
construction and manipulation of an Ex command. This also
allows for things such as having spaces inside a command
argument, expanding filenames in a command that otherwise
doesn't expand filenames, etc.
Parameters: ~
{cmd} Command to execute. Must be a Dictionary that can
contain the same values as the return value of
|nvim_parse_cmd()| except "addr", "nargs" and
"nextcmd" which are ignored if provided. All
values except for "cmd" are optional.
{opts} Optional parameters.
• output: (boolean, default false) Whether to
return command output.
Return: ~
Command output (non-error, non-shell |:!|) if `output` is
true, else empty string.
See also: ~
|nvim_exec()|
|nvim_command()|
nvim_command({command}) *nvim_command()*
Executes an ex-command.
Executes an Ex command.
On execution error: fails with VimL error, does not update
v:errmsg.
Parameters: ~
{command} Ex-command string
Prefer using |nvim_cmd()| or |nvim_exec()| over this. To
evaluate multiple lines of Vim script or an Ex command
directly, use |nvim_exec()|. To construct an Ex command using
a structured format and then execute it, use |nvim_cmd()|. To
modify an Ex command before evaluating it, use
|nvim_parse_cmd()| in conjunction with |nvim_cmd()|.
See also: ~
|nvim_exec()|
Parameters: ~
{command} Ex command string
nvim_eval({expr}) *nvim_eval()*
Evaluates a VimL |expression|. Dictionaries and Lists are
@ -1737,7 +1769,7 @@ nvim_eval({expr}) *nvim_eval()*
Evaluation result or expanded object
nvim_exec({src}, {output}) *nvim_exec()*
Executes Vimscript (multiline block of Ex-commands), like
Executes Vimscript (multiline block of Ex commands), like
anonymous |:source|.
Unlike |nvim_command()| this function supports heredocs,
@ -1758,6 +1790,7 @@ nvim_exec({src}, {output}) *nvim_exec()*
See also: ~
|execute()|
|nvim_command()|
|nvim_cmd()|
nvim_parse_cmd({str}, {opts}) *nvim_parse_cmd()*
Parse command line.

View File

@ -155,5 +155,44 @@ return {
create_augroup = {
"clear";
};
cmd = {
"cmd";
"range";
"count";
"reg";
"bang";
"args";
"magic";
"mods";
"nargs";
"addr";
"nextcmd";
};
cmd_magic = {
"file";
"bar";
};
cmd_mods = {
"silent";
"emsg_silent";
"sandbox";
"noautocmd";
"browse";
"confirm";
"hide";
"keepalt";
"keepjumps";
"keepmarks";
"keeppatterns";
"lockmarks";
"noswapfile";
"tab";
"verbose";
"vertical";
"split";
};
cmd_opts = {
"output";
};
}

View File

@ -19,6 +19,7 @@
#include "nvim/decoration.h"
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/extmark.h"
#include "nvim/fileio.h"
#include "nvim/getchar.h"
@ -1691,3 +1692,174 @@ int init_sign_text(char **sign_text, char *text)
return OK;
}
/// Check if a string contains only whitespace characters.
bool string_iswhite(String str)
{
for (size_t i = 0; i < str.size; i++) {
if (!ascii_iswhite(str.data[i])) {
// Found a non-whitespace character
return false;
} else if (str.data[i] == NUL) {
// Terminate at first occurence of a NUL character
break;
}
}
return true;
}
// Add modifier string for command into the command line. Includes trailing whitespace if non-empty.
// @return OK or FAIL.
static int add_cmd_modifier_str(char *cmdline, size_t *pos, const size_t bufsize,
CmdParseInfo *cmdinfo)
{
#define APPEND_MODIFIER(...) \
do { \
if (*pos < bufsize) { \
*pos += (size_t)snprintf(cmdline + *pos, bufsize - *pos, __VA_ARGS__); \
} \
if (*pos < bufsize) { \
cmdline[*pos] = ' '; \
*pos += 1; \
} else { \
goto err; \
} \
} while (0)
#define APPEND_MODIFIER_IF(cond, mod) \
do { \
if (cond) { \
APPEND_MODIFIER(mod); \
} \
} while (0)
if (cmdinfo->cmdmod.tab != 0) {
APPEND_MODIFIER("%dtab", cmdinfo->cmdmod.tab - 1);
}
if (cmdinfo->verbose != -1) {
APPEND_MODIFIER("%ldverbose", cmdinfo->verbose);
}
switch (cmdinfo->cmdmod.split & (WSP_ABOVE | WSP_BELOW | WSP_TOP | WSP_BOT)) {
case WSP_ABOVE:
APPEND_MODIFIER("aboveleft");
break;
case WSP_BELOW:
APPEND_MODIFIER("belowright");
break;
case WSP_TOP:
APPEND_MODIFIER("topleft");
break;
case WSP_BOT:
APPEND_MODIFIER("botright");
break;
default:
break;
}
APPEND_MODIFIER_IF(cmdinfo->cmdmod.split & WSP_VERT, "vertical");
if (cmdinfo->emsg_silent) {
APPEND_MODIFIER("silent!");
} else if (cmdinfo->silent) {
APPEND_MODIFIER("silent");
}
APPEND_MODIFIER_IF(cmdinfo->sandbox, "sandbox");
APPEND_MODIFIER_IF(cmdinfo->noautocmd, "noautocmd");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.browse, "browse");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.confirm, "confirm");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.hide, "hide");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.keepalt, "keepalt");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.keepjumps, "keepjumps");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.keepmarks, "keepmarks");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.keeppatterns, "keeppatterns");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.lockmarks, "lockmarks");
APPEND_MODIFIER_IF(cmdinfo->cmdmod.noswapfile, "noswapfile");
return OK;
err:
return FAIL;
#undef APPEND_MODIFIER
#undef APPEND_MODIFIER_IF
}
/// Build cmdline string for command, used by `nvim_cmd()`.
///
/// @return OK or FAIL.
int build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdinfo, char **args,
size_t argc)
{
const size_t bufsize = IOSIZE;
size_t pos = 0;
char *cmdline = xcalloc(bufsize, sizeof(char));
#define CMDLINE_APPEND(...) \
do { \
if (pos < bufsize) { \
pos += (size_t)snprintf(cmdline + pos, bufsize - pos, __VA_ARGS__); \
} else { \
goto err; \
} \
} while (0)
// Command modifiers.
if (add_cmd_modifier_str(cmdline, &pos, bufsize, cmdinfo) == FAIL) {
goto err;
}
// Command range / count.
if (eap->argt & EX_RANGE) {
if (eap->addr_count == 1) {
CMDLINE_APPEND("%ld", eap->line2);
} else if (eap->addr_count > 1) {
CMDLINE_APPEND("%ld,%ld", eap->line1, eap->line2);
eap->addr_count = 2; // Make sure address count is not greater than 2
}
}
// Keep the index of the position where command name starts, so eap->cmd can point to it.
size_t cmdname_idx = pos;
CMDLINE_APPEND("%s", eap->cmd);
eap->cmd = cmdline + cmdname_idx;
// Command bang.
if (eap->argt & EX_BANG && eap->forceit) {
CMDLINE_APPEND("!");
}
// Command register.
if (eap->argt & EX_REGSTR && eap->regname) {
CMDLINE_APPEND(" %c", eap->regname);
}
// Iterate through each argument and store the starting position and length of each argument in
// the cmdline string in `eap->args` and `eap->arglens`, respectively.
eap->args = xcalloc(argc, sizeof(char *));
eap->arglens = xcalloc(argc, sizeof(size_t));
for (size_t i = 0; i < argc; i++) {
eap->args[i] = cmdline + pos + 1; // add 1 to skip the leading space.
eap->arglens[i] = STRLEN(args[i]);
CMDLINE_APPEND(" %s", args[i]);
}
eap->argc = argc;
eap->arg = argc > 0 ? eap->args[0] : NULL;
// Replace, :make and :grep with 'makeprg' and 'grepprg'.
char *p = replace_makeprg(eap, eap->arg, cmdlinep);
if (p != eap->arg) {
// If replace_makeprg modified the cmdline string, correct the argument pointers.
assert(argc == 1);
eap->arg = p;
eap->args[0] = p;
}
*cmdlinep = cmdline;
return OK;
err:
xfree(cmdline);
return FAIL;
#undef CMDLINE_APPEND
}

View File

@ -10,10 +10,14 @@
#include "nvim/api/private/helpers.h"
#include "nvim/api/vimscript.h"
#include "nvim/ascii.h"
#include "nvim/autocmd.h"
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/userfunc.h"
#include "nvim/ex_cmds2.h"
#include "nvim/ops.h"
#include "nvim/strings.h"
#include "nvim/vim.h"
#include "nvim/viml/parser/expressions.h"
#include "nvim/viml/parser/parser.h"
#include "nvim/window.h"
@ -22,7 +26,9 @@
# include "api/vimscript.c.generated.h"
#endif
/// Executes Vimscript (multiline block of Ex-commands), like anonymous
#define IS_USER_CMDIDX(idx) ((int)(idx) < 0)
/// Executes Vimscript (multiline block of Ex commands), like anonymous
/// |:source|.
///
/// Unlike |nvim_command()| this function supports heredocs, script-scope (s:),
@ -32,6 +38,7 @@
///
/// @see |execute()|
/// @see |nvim_command()|
/// @see |nvim_cmd()|
///
/// @param src Vimscript code
/// @param output Capture and return all (non-error, non-shell |:!|) output
@ -89,13 +96,16 @@ theend:
return (String)STRING_INIT;
}
/// Executes an ex-command.
/// Executes an Ex command.
///
/// On execution error: fails with VimL error, does not update v:errmsg.
///
/// @see |nvim_exec()|
/// Prefer using |nvim_cmd()| or |nvim_exec()| over this. To evaluate multiple lines of Vim script
/// or an Ex command directly, use |nvim_exec()|. To construct an Ex command using a structured
/// format and then execute it, use |nvim_cmd()|. To modify an Ex command before evaluating it, use
/// |nvim_parse_cmd()| in conjunction with |nvim_cmd()|.
///
/// @param command Ex-command string
/// @param command Ex command string
/// @param[out] err Error details (Vim error), if any
void nvim_command(String command, Error *err)
FUNC_API_SINCE(1)
@ -978,3 +988,370 @@ end:
xfree(cmdline);
return result;
}
/// Executes an Ex command.
///
/// Unlike |nvim_command()| this command takes a structured Dictionary instead of a String. This
/// allows for easier construction and manipulation of an Ex command. This also allows for things
/// such as having spaces inside a command argument, expanding filenames in a command that otherwise
/// doesn't expand filenames, etc.
///
/// @see |nvim_exec()|
/// @see |nvim_command()|
///
/// @param cmd Command to execute. Must be a Dictionary that can contain the same values as
/// the return value of |nvim_parse_cmd()| except "addr", "nargs" and "nextcmd"
/// which are ignored if provided. All values except for "cmd" are optional.
/// @param opts Optional parameters.
/// - output: (boolean, default false) Whether to return command output.
/// @param[out] err Error details, if any.
/// @return Command output (non-error, non-shell |:!|) if `output` is true, else empty string.
String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error *err)
FUNC_API_SINCE(10)
{
exarg_T ea;
memset(&ea, 0, sizeof(ea));
ea.verbose_save = -1;
ea.save_msg_silent = -1;
CmdParseInfo cmdinfo;
memset(&cmdinfo, 0, sizeof(cmdinfo));
cmdinfo.verbose = -1;
char *cmdline = NULL;
char *cmdname = NULL;
char **args = NULL;
size_t argc = 0;
String retv = (String)STRING_INIT;
#define OBJ_TO_BOOL(var, value, default, varname) \
do { \
var = api_object_to_bool(value, varname, default, err); \
if (ERROR_SET(err)) { \
goto end; \
} \
} while (0)
#define VALIDATION_ERROR(...) \
do { \
api_set_error(err, kErrorTypeValidation, __VA_ARGS__); \
goto end; \
} while (0)
bool output;
OBJ_TO_BOOL(output, opts->output, false, "'output'");
// First, parse the command name and check if it exists and is valid.
if (!HAS_KEY(cmd->cmd) || cmd->cmd.type != kObjectTypeString
|| cmd->cmd.data.string.data[0] == NUL) {
VALIDATION_ERROR("'cmd' must be a non-empty String");
}
cmdname = string_to_cstr(cmd->cmd.data.string);
ea.cmd = cmdname;
char *p = find_ex_command(&ea, NULL);
// If this looks like an undefined user command and there are CmdUndefined
// autocommands defined, trigger the matching autocommands.
if (p != NULL && ea.cmdidx == CMD_SIZE && ASCII_ISUPPER(*ea.cmd)
&& has_event(EVENT_CMDUNDEFINED)) {
p = xstrdup(cmdname);
int ret = apply_autocmds(EVENT_CMDUNDEFINED, (char_u *)p, (char_u *)p, true, NULL);
xfree(p);
// If the autocommands did something and didn't cause an error, try
// finding the command again.
p = (ret && !aborting()) ? find_ex_command(&ea, NULL) : ea.cmd;
}
if (p == NULL || ea.cmdidx == CMD_SIZE) {
VALIDATION_ERROR("Command not found: %s", cmdname);
}
if (is_cmd_ni(ea.cmdidx)) {
VALIDATION_ERROR("Command not implemented: %s", cmdname);
}
// Get the command flags so that we can know what type of arguments the command uses.
// Not required for a user command since `find_ex_command` already deals with it in that case.
if (!IS_USER_CMDIDX(ea.cmdidx)) {
ea.argt = get_cmd_argt(ea.cmdidx);
}
// Parse command arguments since it's needed to get the command address type.
if (HAS_KEY(cmd->args)) {
if (cmd->args.type != kObjectTypeArray) {
VALIDATION_ERROR("'args' must be an Array");
}
// Check if every argument is valid
for (size_t i = 0; i < cmd->args.data.array.size; i++) {
Object elem = cmd->args.data.array.items[i];
if (elem.type != kObjectTypeString) {
VALIDATION_ERROR("Command argument must be a String");
} else if (string_iswhite(elem.data.string)) {
VALIDATION_ERROR("Command argument must have non-whitespace characters");
}
}
argc = cmd->args.data.array.size;
bool argc_valid;
// Check if correct number of arguments is used.
switch (ea.argt & (EX_EXTRA | EX_NOSPC | EX_NEEDARG)) {
case EX_EXTRA | EX_NOSPC | EX_NEEDARG:
argc_valid = argc == 1;
break;
case EX_EXTRA | EX_NOSPC:
argc_valid = argc <= 1;
break;
case EX_EXTRA | EX_NEEDARG:
argc_valid = argc >= 1;
break;
case EX_EXTRA:
argc_valid = true;
break;
default:
argc_valid = argc == 0;
break;
}
if (!argc_valid) {
argc = 0; // Ensure that args array isn't erroneously freed at the end.
VALIDATION_ERROR("Incorrect number of arguments supplied");
}
if (argc != 0) {
args = xcalloc(argc, sizeof(char *));
for (size_t i = 0; i < argc; i++) {
args[i] = string_to_cstr(cmd->args.data.array.items[i].data.string);
}
}
}
// Simply pass the first argument (if it exists) as the arg pointer to `set_cmd_addr_type()`
// since it only ever checks the first argument.
set_cmd_addr_type(&ea, argc > 0 ? (char_u *)args[0] : NULL);
if (HAS_KEY(cmd->range)) {
if (!(ea.argt & EX_RANGE)) {
VALIDATION_ERROR("Command cannot accept a range");
} else if (cmd->range.type != kObjectTypeArray) {
VALIDATION_ERROR("'range' must be an Array");
} else if (cmd->range.data.array.size > 2) {
VALIDATION_ERROR("'range' cannot contain more than two elements");
}
Array range = cmd->range.data.array;
ea.addr_count = (int)range.size;
for (size_t i = 0; i < range.size; i++) {
Object elem = range.items[i];
if (elem.type != kObjectTypeInteger || elem.data.integer < 0) {
VALIDATION_ERROR("'range' element must be a non-negative Integer");
}
}
if (range.size > 0) {
ea.line1 = range.items[0].data.integer;
ea.line2 = range.items[range.size - 1].data.integer;
}
if (invalid_range(&ea) != NULL) {
VALIDATION_ERROR("Invalid range provided");
}
}
if (ea.addr_count == 0) {
if (ea.argt & EX_DFLALL) {
set_cmd_dflall_range(&ea); // Default range for range=%
} else {
ea.line1 = ea.line2 = get_cmd_default_range(&ea); // Default range.
if (ea.addr_type == ADDR_OTHER) {
// Default is 1, not cursor.
ea.line2 = 1;
}
}
}
if (HAS_KEY(cmd->count)) {
if (!(ea.argt & EX_COUNT)) {
VALIDATION_ERROR("Command cannot accept a count");
} else if (cmd->count.type != kObjectTypeInteger || cmd->count.data.integer < 0) {
VALIDATION_ERROR("'count' must be a non-negative Integer");
}
set_cmd_count(&ea, cmd->count.data.integer, true);
}
if (HAS_KEY(cmd->reg)) {
if (!(ea.argt & EX_REGSTR)) {
VALIDATION_ERROR("Command cannot accept a register");
} else if (cmd->reg.type != kObjectTypeString || cmd->reg.data.string.size != 1) {
VALIDATION_ERROR("'reg' must be a single character");
}
char regname = cmd->reg.data.string.data[0];
if (regname == '=') {
VALIDATION_ERROR("Cannot use register \"=");
} else if (!valid_yank_reg(regname, ea.cmdidx != CMD_put && !IS_USER_CMDIDX(ea.cmdidx))) {
VALIDATION_ERROR("Invalid register: \"%c", regname);
}
ea.regname = (uint8_t)regname;
}
OBJ_TO_BOOL(ea.forceit, cmd->bang, false, "'bang'");
if (ea.forceit && !(ea.argt & EX_BANG)) {
VALIDATION_ERROR("Command cannot accept a bang");
}
if (HAS_KEY(cmd->magic)) {
if (cmd->magic.type != kObjectTypeDictionary) {
VALIDATION_ERROR("'magic' must be a Dictionary");
}
Dict(cmd_magic) magic = { 0 };
if (!api_dict_to_keydict(&magic, KeyDict_cmd_magic_get_field,
cmd->magic.data.dictionary, err)) {
goto end;
}
OBJ_TO_BOOL(cmdinfo.magic.file, magic.file, ea.argt & EX_XFILE, "'magic.file'");
OBJ_TO_BOOL(cmdinfo.magic.bar, magic.bar, ea.argt & EX_TRLBAR, "'magic.bar'");
} else {
cmdinfo.magic.file = ea.argt & EX_XFILE;
cmdinfo.magic.bar = ea.argt & EX_TRLBAR;
}
if (HAS_KEY(cmd->mods)) {
if (cmd->mods.type != kObjectTypeDictionary) {
VALIDATION_ERROR("'mods' must be a Dictionary");
}
Dict(cmd_mods) mods = { 0 };
if (!api_dict_to_keydict(&mods, KeyDict_cmd_mods_get_field, cmd->mods.data.dictionary, err)) {
goto end;
}
if (HAS_KEY(mods.tab)) {
if (mods.tab.type != kObjectTypeInteger || mods.tab.data.integer < 0) {
VALIDATION_ERROR("'mods.tab' must be a non-negative Integer");
}
cmdinfo.cmdmod.tab = (int)mods.tab.data.integer + 1;
}
if (HAS_KEY(mods.verbose)) {
if (mods.verbose.type != kObjectTypeInteger || mods.verbose.data.integer <= 0) {
VALIDATION_ERROR("'mods.verbose' must be a non-negative Integer");
}
cmdinfo.verbose = mods.verbose.data.integer;
}
bool vertical;
OBJ_TO_BOOL(vertical, mods.vertical, false, "'mods.vertical'");
cmdinfo.cmdmod.split |= (vertical ? WSP_VERT : 0);
if (HAS_KEY(mods.split)) {
if (mods.split.type != kObjectTypeString) {
VALIDATION_ERROR("'mods.split' must be a String");
}
if (STRCMP(mods.split.data.string.data, "aboveleft") == 0
|| STRCMP(mods.split.data.string.data, "leftabove") == 0) {
cmdinfo.cmdmod.split |= WSP_ABOVE;
} else if (STRCMP(mods.split.data.string.data, "belowright") == 0
|| STRCMP(mods.split.data.string.data, "rightbelow") == 0) {
cmdinfo.cmdmod.split |= WSP_BELOW;
} else if (STRCMP(mods.split.data.string.data, "topleft") == 0) {
cmdinfo.cmdmod.split |= WSP_TOP;
} else if (STRCMP(mods.split.data.string.data, "botright") == 0) {
cmdinfo.cmdmod.split |= WSP_BOT;
} else {
VALIDATION_ERROR("Invalid value for 'mods.split'");
}
}
OBJ_TO_BOOL(cmdinfo.silent, mods.silent, false, "'mods.silent'");
OBJ_TO_BOOL(cmdinfo.emsg_silent, mods.emsg_silent, false, "'mods.emsg_silent'");
OBJ_TO_BOOL(cmdinfo.sandbox, mods.sandbox, false, "'mods.sandbox'");
OBJ_TO_BOOL(cmdinfo.noautocmd, mods.noautocmd, false, "'mods.noautocmd'");
OBJ_TO_BOOL(cmdinfo.cmdmod.browse, mods.browse, false, "'mods.browse'");
OBJ_TO_BOOL(cmdinfo.cmdmod.confirm, mods.confirm, false, "'mods.confirm'");
OBJ_TO_BOOL(cmdinfo.cmdmod.hide, mods.hide, false, "'mods.hide'");
OBJ_TO_BOOL(cmdinfo.cmdmod.keepalt, mods.keepalt, false, "'mods.keepalt'");
OBJ_TO_BOOL(cmdinfo.cmdmod.keepjumps, mods.keepjumps, false, "'mods.keepjumps'");
OBJ_TO_BOOL(cmdinfo.cmdmod.keepmarks, mods.keepmarks, false, "'mods.keepmarks'");
OBJ_TO_BOOL(cmdinfo.cmdmod.keeppatterns, mods.keeppatterns, false, "'mods.keeppatterns'");
OBJ_TO_BOOL(cmdinfo.cmdmod.lockmarks, mods.lockmarks, false, "'mods.lockmarks'");
OBJ_TO_BOOL(cmdinfo.cmdmod.noswapfile, mods.noswapfile, false, "'mods.noswapfile'");
if (cmdinfo.sandbox && !(ea.argt & EX_SBOXOK)) {
VALIDATION_ERROR("Command cannot be run in sandbox");
}
}
// Finally, build the command line string that will be stored inside ea.cmdlinep.
// This also sets the values of ea.cmd, ea.arg, ea.args and ea.arglens.
if (build_cmdline_str(&cmdline, &ea, &cmdinfo, args, argc) == FAIL) {
goto end;
}
ea.cmdlinep = &cmdline;
garray_T capture_local;
const int save_msg_silent = msg_silent;
garray_T * const save_capture_ga = capture_ga;
if (output) {
ga_init(&capture_local, 1, 80);
capture_ga = &capture_local;
}
try_start();
if (output) {
msg_silent++;
}
WITH_SCRIPT_CONTEXT(channel_id, {
execute_cmd(&ea, &cmdinfo);
});
if (output) {
capture_ga = save_capture_ga;
msg_silent = save_msg_silent;
}
try_end(err);
if (ERROR_SET(err)) {
goto clear_ga;
}
if (output && capture_local.ga_len > 1) {
retv = (String){
.data = capture_local.ga_data,
.size = (size_t)capture_local.ga_len,
};
// redir usually (except :echon) prepends a newline.
if (retv.data[0] == '\n') {
memmove(retv.data, retv.data + 1, retv.size - 1);
retv.data[retv.size - 1] = '\0';
retv.size = retv.size - 1;
}
goto end;
}
clear_ga:
if (output) {
ga_clear(&capture_local);
}
end:
xfree(cmdline);
xfree(cmdname);
xfree(ea.args);
xfree(ea.arglens);
for (size_t i = 0; i < argc; i++) {
xfree(args[i]);
}
xfree(args);
return retv;
#undef OBJ_TO_BOOL
#undef VALIDATION_ERROR
}

View File

@ -175,6 +175,9 @@ enum {
/// Arguments used for Ex commands.
struct exarg {
char *arg; ///< argument of the command
char **args; ///< starting position of command arguments
size_t *arglens; ///< length of command arguments
size_t argc; ///< number of command arguments
char *nextcmd; ///< next command (NULL if none)
char *cmd; ///< the name of the command (except for :make)
char **cmdlinep; ///< pointer to pointer of allocated cmdline

View File

@ -1218,7 +1218,7 @@ static char *skip_colon_white(const char *p, bool skipleadingwhite)
/// Set the addr type for command
///
/// @param p pointer to character after command name in cmdline
static void set_cmd_addr_type(exarg_T *eap, char_u *p)
void set_cmd_addr_type(exarg_T *eap, char_u *p)
{
// ea.addr_type for user commands is set by find_ucmd
if (IS_USER_CMDIDX(eap->cmdidx)) {
@ -1239,8 +1239,48 @@ static void set_cmd_addr_type(exarg_T *eap, char_u *p)
}
}
/// Get default range number for command based on its address type
linenr_T get_cmd_default_range(exarg_T *eap)
{
switch (eap->addr_type) {
case ADDR_LINES:
case ADDR_OTHER:
// Default is the cursor line number. Avoid using an invalid
// line number though.
return MIN(curwin->w_cursor.lnum, curbuf->b_ml.ml_line_count);
break;
case ADDR_WINDOWS:
return CURRENT_WIN_NR;
break;
case ADDR_ARGUMENTS:
return MIN(curwin->w_arg_idx + 1, ARGCOUNT);
break;
case ADDR_LOADED_BUFFERS:
case ADDR_BUFFERS:
return curbuf->b_fnum;
break;
case ADDR_TABS:
return CURRENT_TAB_NR;
break;
case ADDR_TABS_RELATIVE:
case ADDR_UNSIGNED:
return 1;
break;
case ADDR_QUICKFIX:
return (linenr_T)qf_get_cur_idx(eap);
break;
case ADDR_QUICKFIX_VALID:
return qf_get_cur_valid_idx(eap);
break;
default:
return 0;
// Will give an error later if a range is found.
break;
}
}
/// Set default command range for -range=% based on the addr type of the command
static void set_cmd_default_range(exarg_T *eap)
void set_cmd_dflall_range(exarg_T *eap)
{
buf_T *buf;
@ -1320,6 +1360,25 @@ static void parse_register(exarg_T *eap)
}
}
// Change line1 and line2 of Ex command to use count
void set_cmd_count(exarg_T *eap, long count, bool validate)
{
if (eap->addr_type != ADDR_LINES) { // e.g. :buffer 2, :sleep 3
eap->line2 = count;
if (eap->addr_count == 0) {
eap->addr_count = 1;
}
} else {
eap->line1 = eap->line2;
eap->line2 += count - 1;
eap->addr_count++;
// Be vi compatible: no error message for out of range.
if (validate && eap->line2 > curbuf->b_ml.ml_line_count) {
eap->line2 = curbuf->b_ml.ml_line_count;
}
}
}
static int parse_count(exarg_T *eap, char **errormsg, bool validate)
{
// Check for a count. When accepting a EX_BUFNAME, don't use "123foo" as a
@ -1338,25 +1397,19 @@ static int parse_count(exarg_T *eap, char **errormsg, bool validate)
}
return FAIL;
}
if (eap->addr_type != ADDR_LINES) { // e.g. :buffer 2, :sleep 3
eap->line2 = n;
if (eap->addr_count == 0) {
eap->addr_count = 1;
}
} else {
eap->line1 = eap->line2;
eap->line2 += n - 1;
eap->addr_count++;
// Be vi compatible: no error message for out of range.
if (validate && eap->line2 > curbuf->b_ml.ml_line_count) {
eap->line2 = curbuf->b_ml.ml_line_count;
}
}
set_cmd_count(eap, n, validate);
}
return OK;
}
/// Check if command is not implemented
bool is_cmd_ni(cmdidx_T cmdidx)
{
return !IS_USER_CMDIDX(cmdidx) && (cmdnames[cmdidx].cmd_func == ex_ni
|| cmdnames[cmdidx].cmd_func == ex_script_ni);
}
/// Parse command line and return information about the first command.
///
/// @param cmdline Command line string
@ -1394,14 +1447,17 @@ bool parse_cmdline(char *cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, char **er
if (eap->save_msg_silent != -1) {
cmdinfo->silent = !!msg_silent;
msg_silent = eap->save_msg_silent;
eap->save_msg_silent = -1;
}
if (eap->did_esilent) {
cmdinfo->emsg_silent = true;
emsg_silent--;
eap->did_esilent = false;
}
if (eap->did_sandbox) {
cmdinfo->sandbox = true;
sandbox--;
eap->did_sandbox = false;
}
if (cmdmod.save_ei != NULL) {
cmdinfo->noautocmd = true;
@ -1411,6 +1467,7 @@ bool parse_cmdline(char *cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, char **er
if (eap->verbose_save != -1) {
cmdinfo->verbose = p_verbose;
p_verbose = eap->verbose_save;
eap->verbose_save = -1;
} else {
cmdinfo->verbose = -1;
}
@ -1424,7 +1481,7 @@ bool parse_cmdline(char *cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, char **er
if (*eap->cmd == '*') {
eap->cmd = skipwhite(eap->cmd + 1);
}
p = find_command(eap, NULL);
p = find_ex_command(eap, NULL);
// Set command address type and parse command range
set_cmd_addr_type(eap, (char_u *)p);
@ -1494,7 +1551,7 @@ bool parse_cmdline(char *cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, char **er
}
// Set default range for command if required
if ((eap->argt & EX_DFLALL) && eap->addr_count == 0) {
set_cmd_default_range(eap);
set_cmd_dflall_range(eap);
}
// Parse register and count
@ -1519,6 +1576,148 @@ bool parse_cmdline(char *cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, char **er
return true;
}
/// Execute an Ex command using parsed command line information.
/// Does not do any validation of the Ex command arguments.
///
/// @param eap Ex-command arguments
/// @param cmdinfo Command parse information
void execute_cmd(exarg_T *eap, CmdParseInfo *cmdinfo)
{
#define ERROR(msg) \
do { \
emsg(msg); \
goto end; \
} while (0)
char *errormsg = NULL;
cmdmod_T save_cmdmod = cmdmod;
cmdmod = cmdinfo->cmdmod;
// Apply command modifiers
if (cmdinfo->silent) {
eap->save_msg_silent = msg_silent;
msg_silent++;
}
if (cmdinfo->emsg_silent) {
eap->did_esilent = true;
emsg_silent++;
}
if (cmdinfo->sandbox) {
eap->did_sandbox = true;
sandbox++;
}
if (cmdinfo->noautocmd) {
cmdmod.save_ei = (char *)vim_strsave(p_ei);
set_string_option_direct("ei", -1, (char_u *)"all", OPT_FREE, SID_NONE);
}
if (cmdinfo->verbose != -1) {
eap->verbose_save = p_verbose;
p_verbose = cmdinfo->verbose;
}
if (!MODIFIABLE(curbuf) && (eap->argt & EX_MODIFY)
// allow :put in terminals
&& !(curbuf->terminal && eap->cmdidx == CMD_put)) {
ERROR(_(e_modifiable));
}
if (text_locked() && !(eap->argt & EX_CMDWIN)
&& !IS_USER_CMDIDX(eap->cmdidx)) {
ERROR(_(get_text_locked_msg()));
}
// Disallow editing another buffer when "curbuf->b_ro_locked" is set.
// Do allow ":checktime" (it is postponed).
// Do allow ":edit" (check for an argument later).
// Do allow ":file" with no arguments
if (!(eap->argt & EX_CMDWIN)
&& eap->cmdidx != CMD_checktime
&& eap->cmdidx != CMD_edit
&& !(eap->cmdidx == CMD_file && *eap->arg == NUL)
&& !IS_USER_CMDIDX(eap->cmdidx)
&& curbuf_locked()) {
ERROR(_(e_cannot_edit_other_buf));
}
if (((eap->argt & EX_WHOLEFOLD) || eap->addr_count >= 2) && !global_busy
&& eap->addr_type == ADDR_LINES) {
// Put the first line at the start of a closed fold, put the last line
// at the end of a closed fold.
(void)hasFolding(eap->line1, &eap->line1, NULL);
(void)hasFolding(eap->line2, NULL, &eap->line2);
}
// If filename expansion is enabled, expand filenames
if (cmdinfo->magic.file) {
if (expand_filename(eap, (char_u **)eap->cmdlinep, &errormsg) == FAIL) {
ERROR(errormsg);
}
}
// Accept buffer name. Cannot be used at the same time with a buffer
// number. Don't do this for a user command.
if ((eap->argt & EX_BUFNAME) && *eap->arg != NUL && eap->addr_count == 0
&& !IS_USER_CMDIDX(eap->cmdidx)) {
if (eap->args == NULL) {
// If argument positions are not specified, search the argument for the buffer name.
// :bdelete, :bwipeout and :bunload take several arguments, separated by spaces:
// find next space (skipping over escaped characters).
// The others take one argument: ignore trailing spaces.
char *p;
if (eap->cmdidx == CMD_bdelete || eap->cmdidx == CMD_bwipeout
|| eap->cmdidx == CMD_bunload) {
p = (char *)skiptowhite_esc((char_u *)eap->arg);
} else {
p = eap->arg + STRLEN(eap->arg);
while (p > eap->arg && ascii_iswhite(p[-1])) {
p--;
}
}
eap->line2 = buflist_findpat((char_u *)eap->arg, (char_u *)p, (eap->argt & EX_BUFUNL) != 0,
false, false);
eap->addr_count = 1;
eap->arg = skipwhite(p);
} else {
// If argument positions are specified, just use the first argument
eap->line2 = buflist_findpat((char_u *)eap->args[0],
(char_u *)(eap->args[0] + eap->arglens[0]),
(eap->argt & EX_BUFUNL) != 0, false, false);
eap->addr_count = 1;
// Shift each argument by 1
if (eap->args != NULL) {
for (size_t i = 0; i < eap->argc - 1; i++) {
eap->args[i] = eap->args[i + 1];
}
// Make the last argument point to the NUL terminator at the end of string
eap->args[eap->argc - 1] = eap->args[eap->argc - 1] + eap->arglens[eap->argc - 1];
eap->argc -= 1;
}
eap->arg = eap->args[0];
}
if (eap->line2 < 0) { // failed
goto end;
}
}
// Execute the command
if (IS_USER_CMDIDX(eap->cmdidx)) {
// Execute a user-defined command.
do_ucmd(eap);
} else {
// Call the function to execute the command.
eap->errmsg = NULL;
(cmdnames[eap->cmdidx].cmd_func)(eap);
if (eap->errmsg != NULL) {
ERROR(_(eap->errmsg));
}
}
end:
// Undo command modifiers
undo_cmdmod(eap, msg_scroll);
cmdmod = save_cmdmod;
#undef ERROR
}
/// Execute one Ex command.
///
/// If 'sourcing' is TRUE, the command will be included in the error message.
@ -1606,7 +1805,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter
if (*ea.cmd == '*') {
ea.cmd = skipwhite(ea.cmd + 1);
}
p = find_command(&ea, NULL);
p = find_ex_command(&ea, NULL);
// Count this line for profiling if skip is TRUE.
if (do_profiling == PROF_YES
@ -1732,7 +1931,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter
xfree(p);
// If the autocommands did something and didn't cause an error, try
// finding the command again.
p = (ret && !aborting()) ? find_command(&ea, NULL) : ea.cmd;
p = (ret && !aborting()) ? find_ex_command(&ea, NULL) : ea.cmd;
}
if (p == NULL) {
@ -1759,10 +1958,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter
}
// set when Not Implemented
const int ni = !IS_USER_CMDIDX(ea.cmdidx)
&& (cmdnames[ea.cmdidx].cmd_func == ex_ni
|| cmdnames[ea.cmdidx].cmd_func == ex_script_ni);
const int ni = is_cmd_ni(ea.cmdidx);
// Forced commands.
if (*p == '!' && ea.cmdidx != CMD_substitute
@ -1976,7 +2172,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter
}
if ((ea.argt & EX_DFLALL) && ea.addr_count == 0) {
set_cmd_default_range(&ea);
set_cmd_dflall_range(&ea);
}
// Parse register and count
@ -2544,6 +2740,7 @@ static void undo_cmdmod(const exarg_T *eap, int save_msg_scroll)
}
/// Parse the address range, if any, in "eap".
/// May set the last search pattern, unless "silent" is true.
///
@ -2557,47 +2754,7 @@ int parse_cmd_address(exarg_T *eap, char **errormsg, bool silent)
// Repeat for all ',' or ';' separated addresses.
for (;;) {
eap->line1 = eap->line2;
switch (eap->addr_type) {
case ADDR_LINES:
case ADDR_OTHER:
// Default is the cursor line number. Avoid using an invalid
// line number though.
if (curwin->w_cursor.lnum > curbuf->b_ml.ml_line_count) {
eap->line2 = curbuf->b_ml.ml_line_count;
} else {
eap->line2 = curwin->w_cursor.lnum;
}
break;
case ADDR_WINDOWS:
eap->line2 = CURRENT_WIN_NR;
break;
case ADDR_ARGUMENTS:
eap->line2 = curwin->w_arg_idx + 1;
if (eap->line2 > ARGCOUNT) {
eap->line2 = ARGCOUNT;
}
break;
case ADDR_LOADED_BUFFERS:
case ADDR_BUFFERS:
eap->line2 = curbuf->b_fnum;
break;
case ADDR_TABS:
eap->line2 = CURRENT_TAB_NR;
break;
case ADDR_TABS_RELATIVE:
case ADDR_UNSIGNED:
eap->line2 = 1;
break;
case ADDR_QUICKFIX:
eap->line2 = (linenr_T)qf_get_cur_idx(eap);
break;
case ADDR_QUICKFIX_VALID:
eap->line2 = qf_get_cur_valid_idx(eap);
break;
case ADDR_NONE:
// Will give an error later if a range is found.
break;
}
eap->line2 = get_cmd_default_range(eap);
eap->cmd = skipwhite(eap->cmd);
lnum = get_address(eap, &eap->cmd, eap->addr_type, eap->skip, silent,
eap->addr_count == 0, address_count++);
@ -2772,7 +2929,7 @@ static void append_command(char *cmd)
/// "full" is set to TRUE if the whole command name matched.
///
/// @return NULL for an ambiguous user command.
static char *find_command(exarg_T *eap, int *full)
char *find_ex_command(exarg_T *eap, int *full)
FUNC_ATTR_NONNULL_ARG(1)
{
int len;
@ -2859,7 +3016,7 @@ static char *find_command(exarg_T *eap, int *full)
for (; (int)eap->cmdidx < CMD_SIZE;
eap->cmdidx = (cmdidx_T)((int)eap->cmdidx + 1)) {
if (STRNCMP(cmdnames[(int)eap->cmdidx].cmd_name, (char *)eap->cmd,
if (STRNCMP(cmdnames[(int)eap->cmdidx].cmd_name, eap->cmd,
(size_t)len) == 0) {
if (full != NULL
&& cmdnames[(int)eap->cmdidx].cmd_name[len] == NUL) {
@ -3076,7 +3233,7 @@ int cmd_exists(const char *const name)
ea.cmd = (char *)((*name == '2' || *name == '3') ? name + 1 : name);
ea.cmdidx = (cmdidx_T)0;
int full = false;
p = find_command(&ea, &full);
p = find_ex_command(&ea, &full);
if (p == NULL) {
return 3;
}
@ -3108,7 +3265,7 @@ void f_fullcommand(typval_T *argvars, typval_T *rettv, FunPtr fptr)
ea.cmd = (*name == '2' || *name == '3') ? name + 1 : name;
ea.cmdidx = (cmdidx_T)0;
char *p = find_command(&ea, NULL);
char *p = find_ex_command(&ea, NULL);
if (p == NULL || ea.cmdidx == CMD_SIZE) {
return;
}
@ -3431,7 +3588,7 @@ const char *set_one_cmd_context(expand_T *xp, const char *buff)
xp->xp_shell = TRUE;
#endif
// When still after the command name expand executables.
if ((char_u *)xp->xp_pattern == (char_u *)skipwhite(arg)) {
if (xp->xp_pattern == skipwhite(arg)) {
xp->xp_context = EXPAND_SHELLCMD;
}
}
@ -4416,7 +4573,7 @@ static void ex_script_ni(exarg_T *eap)
/// Check range in Ex command for validity.
///
/// @return NULL when valid, error message when invalid.
static char *invalid_range(exarg_T *eap)
char *invalid_range(exarg_T *eap)
{
buf_T *buf;
if (eap->line1 < 0 || eap->line2 < 0 || eap->line1 > eap->line2) {
@ -4535,7 +4692,7 @@ static char *skip_grep_pat(exarg_T *eap)
/// For the ":make" and ":grep" commands insert the 'makeprg'/'grepprg' option
/// in the command line, so that things like % get expanded.
static char *replace_makeprg(exarg_T *eap, char *p, char **cmdlinep)
char *replace_makeprg(exarg_T *eap, char *p, char **cmdlinep)
{
char *new_cmdline;
char *program;
@ -4773,7 +4930,7 @@ int expand_filename(exarg_T *eap, char_u **cmdlinep, char **errormsgp)
return OK;
}
/// Replace part of the command line, keeping eap->cmd, eap->arg and
/// Replace part of the command line, keeping eap->cmd, eap->arg, eap->args and
/// eap->nextcmd correct.
/// "src" points to the part that is to be replaced, of length "srclen".
/// "repl" is the replacement string.
@ -4792,6 +4949,7 @@ static char *repl_cmdline(exarg_T *eap, char *src, size_t srclen, char *repl, ch
i += STRLEN(eap->nextcmd); // add space for next command
}
char *new_cmdline = xmalloc(i);
size_t offset = (size_t)(src - *cmdlinep);
/*
* Copy the stuff before the expanded part.
@ -4799,7 +4957,7 @@ static char *repl_cmdline(exarg_T *eap, char *src, size_t srclen, char *repl, ch
* Copy what came after the expanded part.
* Copy the next commands, if there are any.
*/
i = (size_t)(src - *cmdlinep); // length of part before match
i = offset; // length of part before match
memmove(new_cmdline, *cmdlinep, i);
memmove(new_cmdline + i, repl, len);
@ -4814,6 +4972,19 @@ static char *repl_cmdline(exarg_T *eap, char *src, size_t srclen, char *repl, ch
}
eap->cmd = new_cmdline + (eap->cmd - *cmdlinep);
eap->arg = new_cmdline + (eap->arg - *cmdlinep);
for (size_t j = 0; j < eap->argc; j++) {
if (offset >= (size_t)(eap->args[j] - *cmdlinep)) {
// If replaced text is after or in the same position as the argument,
// the argument's position relative to the beginning of the cmdline stays the same.
eap->args[j] = new_cmdline + (eap->args[j] - *cmdlinep);
} else {
// Otherwise, argument gets shifted alongside the replaced text.
// The amount of the shift is equal to the difference of the old and new string length.
eap->args[j] = new_cmdline + (eap->args[j] - *cmdlinep) + (len - srclen);
}
}
if (eap->do_ecmd_cmd != NULL && eap->do_ecmd_cmd != dollar_command) {
eap->do_ecmd_cmd = new_cmdline + (eap->do_ecmd_cmd - *cmdlinep);
}
@ -6002,7 +6173,7 @@ bool uc_split_args_iter(const char *arg, size_t arglen, size_t *end, char *buf,
}
/// split and quote args for <f-args>
static char *uc_split_args(char *arg, size_t *lenp)
static char *uc_split_args(char *arg, char **args, size_t *arglens, size_t argc, size_t *lenp)
{
char *buf;
char *p;
@ -6010,61 +6181,107 @@ static char *uc_split_args(char *arg, size_t *lenp)
int len;
// Precalculate length
p = arg;
len = 2; // Initial and final quotes
if (args == NULL) {
p = arg;
while (*p) {
if (p[0] == '\\' && p[1] == '\\') {
len += 2;
p += 2;
} else if (p[0] == '\\' && ascii_iswhite(p[1])) {
len += 1;
p += 2;
} else if (*p == '\\' || *p == '"') {
len += 2;
p += 1;
} else if (ascii_iswhite(*p)) {
p = skipwhite(p);
if (*p == NUL) {
break;
while (*p) {
if (p[0] == '\\' && p[1] == '\\') {
len += 2;
p += 2;
} else if (p[0] == '\\' && ascii_iswhite(p[1])) {
len += 1;
p += 2;
} else if (*p == '\\' || *p == '"') {
len += 2;
p += 1;
} else if (ascii_iswhite(*p)) {
p = skipwhite(p);
if (*p == NUL) {
break;
}
len += 3; // ","
} else {
const int charlen = utfc_ptr2len(p);
len += charlen;
p += charlen;
}
len += 3; // ","
} else {
const int charlen = utfc_ptr2len(p);
}
} else {
for (size_t i = 0; i < argc; i++) {
p = args[i];
const char *arg_end = args[i] + arglens[i];
len += charlen;
p += charlen;
while (p < arg_end) {
if (*p == '\\' || *p == '"') {
len += 2;
p += 1;
} else {
const int charlen = utfc_ptr2len(p);
len += charlen;
p += charlen;
}
}
if (i != argc - 1) {
len += 3; // ","
}
}
}
buf = xmalloc((size_t)len + 1);
p = arg;
q = buf;
*q++ = '"';
while (*p) {
if (p[0] == '\\' && p[1] == '\\') {
*q++ = '\\';
*q++ = '\\';
p += 2;
} else if (p[0] == '\\' && ascii_iswhite(p[1])) {
*q++ = p[1];
p += 2;
} else if (*p == '\\' || *p == '"') {
*q++ = '\\';
*q++ = *p++;
} else if (ascii_iswhite(*p)) {
p = skipwhite(p);
if (*p == NUL) {
break;
if (args == NULL) {
p = arg;
while (*p) {
if (p[0] == '\\' && p[1] == '\\') {
*q++ = '\\';
*q++ = '\\';
p += 2;
} else if (p[0] == '\\' && ascii_iswhite(p[1])) {
*q++ = p[1];
p += 2;
} else if (*p == '\\' || *p == '"') {
*q++ = '\\';
*q++ = *p++;
} else if (ascii_iswhite(*p)) {
p = skipwhite(p);
if (*p == NUL) {
break;
}
*q++ = '"';
*q++ = ',';
*q++ = '"';
} else {
mb_copy_char((const char_u **)&p, (char_u **)&q);
}
}
} else {
for (size_t i = 0; i < argc; i++) {
p = args[i];
const char *arg_end = args[i] + arglens[i];
while (p < arg_end) {
if (*p == '\\' || *p == '"') {
*q++ = '\\';
*q++ = *p++;
} else {
mb_copy_char((const char_u **)&p, (char_u **)&q);
}
}
if (i != argc - 1) {
*q++ = '"';
*q++ = ',';
*q++ = '"';
}
*q++ = '"';
*q++ = ',';
*q++ = '"';
} else {
mb_copy_char((const char_u **)&p, (char_u **)&q);
}
}
*q++ = '"';
*q = 0;
@ -6201,7 +6418,7 @@ static size_t uc_check_code(char *code, size_t len, char *buf, ucmd_T *cmd, exar
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);
*split_buf = uc_split_args(eap->arg, eap->args, eap->arglens, eap->argc, split_len);
}
result = *split_len;
@ -9887,7 +10104,7 @@ bool cmd_can_preview(char *cmd)
if (*ea.cmd == '*') {
ea.cmd = skipwhite(ea.cmd + 1);
}
char *end = find_command(&ea, NULL);
char *end = find_ex_command(&ea, NULL);
switch (ea.cmdidx) {
case CMD_substitute:
@ -10278,3 +10495,9 @@ void verify_command(char *cmd)
msg("` `.:.`.,:iii;;;;;;;;iii;;;:` `.`` "
" `nW");
}
/// Get argt of command with id
uint32_t get_cmd_argt(cmdidx_T cmdidx)
{
return cmdnames[(int)cmdidx].cmd_argt;
}

View File

@ -33,6 +33,7 @@
#include "nvim/func_attr.h"
#include "nvim/garray.h"
#include "nvim/getchar.h"
#include "nvim/globals.h"
#include "nvim/highlight.h"
#include "nvim/highlight_defs.h"
#include "nvim/highlight_group.h"
@ -2522,7 +2523,7 @@ char *get_text_locked_msg(void)
bool curbuf_locked(void)
{
if (curbuf->b_ro_locked > 0) {
emsg(_("E788: Not allowed to edit another buffer now"));
emsg(_(e_cannot_edit_other_buf));
return true;
}
return allbuf_locked();

View File

@ -997,6 +997,7 @@ EXTERN char e_listarg[] INIT(= N_("E686: Argument of %s must be a List"));
EXTERN char e_unsupportedoption[] INIT(= N_("E519: Option not supported"));
EXTERN char e_fnametoolong[] INIT(= N_("E856: Filename too long"));
EXTERN char e_float_as_string[] INIT(= N_("E806: using Float as a String"));
EXTERN char e_cannot_edit_other_buf[] INIT(= N_("E788: Not allowed to edit another buffer now"));
EXTERN char e_autocmd_err[] INIT(= N_("E5500: autocmd has thrown an exception: %s"));
EXTERN char e_cmdmap_err[] INIT(= N_("E5520: <Cmd> mapping must end with <CR>"));

View File

@ -1863,8 +1863,8 @@ void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap)
if (cmd->uc_argt & EX_NOSPC) {
// Commands where nargs = 1 or "?" fargs is the same as args
lua_rawseti(lstate, -2, 1);
} else {
// Commands with more than one possible argument we split
} else if (eap->args == NULL) {
// For commands with more than one possible argument, split if argument list isn't available.
lua_pop(lstate, 1); // Pop the reference of opts.args
size_t length = STRLEN(eap->arg);
size_t end = 0;
@ -1881,6 +1881,13 @@ void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap)
}
}
xfree(buf);
} else {
// If argument list is available, just use it.
lua_pop(lstate, 1);
for (size_t i = 0; i < eap->argc; i++) {
lua_pushlstring(lstate, eap->args[i], eap->arglens[i]);
lua_rawseti(lstate, -2, (int)i + 1);
}
}
lua_setfield(lstate, -2, "fargs");

View File

@ -1,5 +1,6 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local lfs = require('lfs')
local fmt = string.format
local assert_alive = helpers.assert_alive
@ -25,6 +26,7 @@ local tmpname = helpers.tmpname
local write_file = helpers.write_file
local exec_lua = helpers.exec_lua
local exc_exec = helpers.exc_exec
local insert = helpers.insert
local pcall_err = helpers.pcall_err
local format_string = helpers.format_string
@ -3473,4 +3475,145 @@ describe('API', function()
pcall_err(meths.parse_cmd, '4,6Fubar', {}))
end)
end)
describe('nvim_cmd', function()
it('works', function ()
meths.cmd({ cmd = "set", args = { "cursorline" } }, {})
eq(true, meths.get_option_value("cursorline", {}))
end)
it('captures output', function()
eq("foo", meths.cmd({ cmd = "echo", args = { '"foo"' } }, { output = true }))
end)
it('sets correct script context', function()
meths.cmd({ cmd = "set", args = { "cursorline" } }, {})
local str = meths.exec([[verbose set cursorline?]], true)
neq(nil, str:find("cursorline\n\tLast set from API client %(channel id %d+%)"))
end)
it('works with range', function()
insert [[
line1
line2
line3
line4
you didn't expect this
line5
line6
]]
meths.cmd({ cmd = "del", range = {2, 4} }, {})
expect [[
line1
you didn't expect this
line5
line6
]]
end)
it('works with count', function()
insert [[
line1
line2
line3
line4
you didn't expect this
line5
line6
]]
meths.cmd({ cmd = "del", range = { 2 }, count = 4 }, {})
expect [[
line1
line5
line6
]]
end)
it('works with register', function()
insert [[
line1
line2
line3
line4
you didn't expect this
line5
line6
]]
meths.cmd({ cmd = "del", range = { 2, 4 }, reg = 'a' }, {})
meths.exec("1put a", false)
expect [[
line1
line2
line3
line4
you didn't expect this
line5
line6
]]
end)
it('works with bang', function ()
meths.create_user_command("Foo", 'echo "<bang>"', { bang = true })
eq("!", meths.cmd({ cmd = "Foo", bang = true }, { output = true }))
eq("", meths.cmd({ cmd = "Foo", bang = false }, { output = true }))
end)
it('works with modifiers', function()
meths.create_user_command("Foo", 'set verbose', {})
eq(" verbose=1", meths.cmd({ cmd = "Foo", mods = { verbose = 1 } }, { output = true }))
eq(0, meths.get_option_value("verbose", {}))
end)
it('works with magic.file', function()
exec_lua([[
vim.api.nvim_create_user_command("Foo", function(opts)
vim.api.nvim_echo({{ opts.fargs[1] }}, false, {})
end, { nargs = 1 })
]])
eq(lfs.currentdir(),
meths.cmd({ cmd = "Foo", args = { '%:p:h' }, magic = { file = true } },
{ output = true }))
end)
it('splits arguments correctly', function()
meths.exec([[
function! FooFunc(...)
echo a:000
endfunction
]], false)
meths.create_user_command("Foo", "call FooFunc(<f-args>)", { nargs = '+' })
eq([=[['a quick', 'brown fox', 'jumps over the', 'lazy dog']]=],
meths.cmd({ cmd = "Foo", args = { "a quick", "brown fox", "jumps over the", "lazy dog"}},
{ output = true }))
eq([=[['test \ \\ \"""\', 'more\ tests\" ']]=],
meths.cmd({ cmd = "Foo", args = { [[test \ \\ \"""\]], [[more\ tests\" ]] } },
{ output = true }))
end)
it('splits arguments correctly for Lua callback', function()
meths.exec_lua([[
local function FooFunc(opts)
vim.pretty_print(opts.fargs)
end
vim.api.nvim_create_user_command("Foo", FooFunc, { nargs = '+' })
]], {})
eq([[{ "a quick", "brown fox", "jumps over the", "lazy dog" }]],
meths.cmd({ cmd = "Foo", args = { "a quick", "brown fox", "jumps over the", "lazy dog"}},
{ output = true }))
eq([[{ 'test \\ \\\\ \\"""\\', 'more\\ tests\\" ' }]],
meths.cmd({ cmd = "Foo", args = { [[test \ \\ \"""\]], [[more\ tests\" ]] } },
{ output = true }))
end)
it('works with buffer names', function()
command("edit foo.txt | edit bar.txt")
meths.cmd({ cmd = "buffer", args = { "foo.txt" } }, {})
eq("foo.txt", funcs.fnamemodify(meths.buf_get_name(0), ":t"))
meths.cmd({ cmd = "buffer", args = { "bar.txt" } }, {})
eq("bar.txt", funcs.fnamemodify(meths.buf_get_name(0), ":t"))
end)
it('triggers CmdUndefined event if command is not found', function()
meths.exec_lua([[
vim.api.nvim_create_autocmd("CmdUndefined",
{ pattern = "Foo",
callback = function()
vim.api.nvim_create_user_command("Foo", "echo 'foo'", {})
end
})
]], {})
eq("foo", meths.cmd({ cmd = "Foo" }, { output = true }))
end)
it('errors if command is not implemented', function()
eq("Command not implemented: popup", pcall_err(meths.cmd, { cmd = "popup" }, {}))
end)
end)
end)