mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 03:18:16 +00:00

Adds a `name` key to the opts dict passed to Lua command callbacks created using `nvim_create_user_command()`. This is useful for when multiple commands use the same callback. Note that this kind of behavior is not as strange as one might think, even some internal Neovim commands reuse the same internal C function, differing their behavior by checking the command name. `substitute`, `smagic` and `snomagic` are examples of that. This will also be useful for generalized Lua command preview functions that can preview a wide range of commands, in which case knowing the command name is necessary for the preview function to actually be able to execute the command that it's supposed to preview.
1232 lines
42 KiB
C
1232 lines
42 KiB
C
// This is an open source non-commercial project. Dear PVS-Studio, please check
|
|
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "nvim/api/command.h"
|
|
#include "nvim/api/private/converter.h"
|
|
#include "nvim/api/private/defs.h"
|
|
#include "nvim/api/private/helpers.h"
|
|
#include "nvim/autocmd.h"
|
|
#include "nvim/ex_docmd.h"
|
|
#include "nvim/ex_eval.h"
|
|
#include "nvim/lua/executor.h"
|
|
#include "nvim/ops.h"
|
|
#include "nvim/regexp.h"
|
|
#include "nvim/usercmd.h"
|
|
#include "nvim/window.h"
|
|
|
|
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
|
# include "api/command.c.generated.h"
|
|
#endif
|
|
|
|
/// Parse command line.
|
|
///
|
|
/// Doesn't check the validity of command arguments.
|
|
///
|
|
/// @param str Command line string to parse. Cannot contain "\n".
|
|
/// @param opts Optional parameters. Reserved for future use.
|
|
/// @param[out] err Error details, if any.
|
|
/// @return Dictionary containing command information, with these keys:
|
|
/// - cmd: (string) Command name.
|
|
/// - range: (array) (optional) Command range (|<line1>| |<line2>|).
|
|
/// Omitted if command doesn't accept a range.
|
|
/// Otherwise, has no elements if no range was specified, one element if
|
|
/// only a single range item was specified, or two elements if both range
|
|
/// items were specified.
|
|
/// - count: (number) (optional) Command |<count>|.
|
|
/// Omitted if command cannot take a count.
|
|
/// - reg: (string) (optional) Command |<register>|.
|
|
/// Omitted if command cannot take a register.
|
|
/// - bang: (boolean) Whether command contains a |<bang>| (!) modifier.
|
|
/// - args: (array) Command arguments.
|
|
/// - addr: (string) Value of |:command-addr|. Uses short name.
|
|
/// - nargs: (string) Value of |:command-nargs|.
|
|
/// - nextcmd: (string) Next command if there are multiple commands separated by a |:bar|.
|
|
/// Empty if there isn't a next command.
|
|
/// - magic: (dictionary) Which characters have special meaning in the command arguments.
|
|
/// - file: (boolean) The command expands filenames. Which means characters such as "%",
|
|
/// "#" and wildcards are expanded.
|
|
/// - bar: (boolean) The "|" character is treated as a command separator and the double
|
|
/// quote character (\") is treated as the start of a comment.
|
|
/// - mods: (dictionary) |:command-modifiers|.
|
|
/// - filter: (dictionary) |:filter|.
|
|
/// - pattern: (string) Filter pattern. Empty string if there is no filter.
|
|
/// - force: (boolean) Whether filter is inverted or not.
|
|
/// - silent: (boolean) |:silent|.
|
|
/// - emsg_silent: (boolean) |:silent!|.
|
|
/// - unsilent: (boolean) |:unsilent|.
|
|
/// - sandbox: (boolean) |:sandbox|.
|
|
/// - noautocmd: (boolean) |:noautocmd|.
|
|
/// - browse: (boolean) |:browse|.
|
|
/// - confirm: (boolean) |:confirm|.
|
|
/// - hide: (boolean) |:hide|.
|
|
/// - horizontal: (boolean) |:horizontal|.
|
|
/// - keepalt: (boolean) |:keepalt|.
|
|
/// - keepjumps: (boolean) |:keepjumps|.
|
|
/// - keepmarks: (boolean) |:keepmarks|.
|
|
/// - keeppatterns: (boolean) |:keeppatterns|.
|
|
/// - lockmarks: (boolean) |:lockmarks|.
|
|
/// - noswapfile: (boolean) |:noswapfile|.
|
|
/// - tab: (integer) |:tab|. -1 when omitted.
|
|
/// - verbose: (integer) |:verbose|. -1 when omitted.
|
|
/// - vertical: (boolean) |:vertical|.
|
|
/// - split: (string) Split modifier string, is an empty string when there's no split
|
|
/// modifier. If there is a split modifier it can be one of:
|
|
/// - "aboveleft": |:aboveleft|.
|
|
/// - "belowright": |:belowright|.
|
|
/// - "topleft": |:topleft|.
|
|
/// - "botright": |:botright|.
|
|
Dictionary nvim_parse_cmd(String str, Dictionary opts, Error *err)
|
|
FUNC_API_SINCE(10) FUNC_API_FAST
|
|
{
|
|
Dictionary result = ARRAY_DICT_INIT;
|
|
|
|
if (opts.size > 0) {
|
|
api_set_error(err, kErrorTypeValidation, "opts dict isn't empty");
|
|
return result;
|
|
}
|
|
|
|
// Parse command line
|
|
exarg_T ea;
|
|
CmdParseInfo cmdinfo;
|
|
char *cmdline = string_to_cstr(str);
|
|
char *errormsg = NULL;
|
|
|
|
if (!parse_cmdline(cmdline, &ea, &cmdinfo, &errormsg)) {
|
|
if (errormsg != NULL) {
|
|
api_set_error(err, kErrorTypeException, "Error while parsing command line: %s", errormsg);
|
|
} else {
|
|
api_set_error(err, kErrorTypeException, "Error while parsing command line");
|
|
}
|
|
goto end;
|
|
}
|
|
|
|
// Parse arguments
|
|
Array args = ARRAY_DICT_INIT;
|
|
size_t length = strlen(ea.arg);
|
|
|
|
// For nargs = 1 or '?', pass the entire argument list as a single argument,
|
|
// otherwise split arguments by whitespace.
|
|
if (ea.argt & EX_NOSPC) {
|
|
if (*ea.arg != NUL) {
|
|
ADD(args, STRING_OBJ(cstrn_to_string((char *)ea.arg, length)));
|
|
}
|
|
} else {
|
|
size_t end = 0;
|
|
size_t len = 0;
|
|
char *buf = xcalloc(length, sizeof(char));
|
|
bool done = false;
|
|
|
|
while (!done) {
|
|
done = uc_split_args_iter(ea.arg, length, &end, buf, &len);
|
|
if (len > 0) {
|
|
ADD(args, STRING_OBJ(cstrn_to_string(buf, len)));
|
|
}
|
|
}
|
|
|
|
xfree(buf);
|
|
}
|
|
|
|
ucmd_T *cmd = NULL;
|
|
if (ea.cmdidx == CMD_USER) {
|
|
cmd = USER_CMD(ea.useridx);
|
|
} else if (ea.cmdidx == CMD_USER_BUF) {
|
|
cmd = USER_CMD_GA(&curbuf->b_ucmds, ea.useridx);
|
|
}
|
|
|
|
if (cmd != NULL) {
|
|
PUT(result, "cmd", CSTR_TO_OBJ((char *)cmd->uc_name));
|
|
} else {
|
|
PUT(result, "cmd", CSTR_TO_OBJ((char *)get_command_name(NULL, ea.cmdidx)));
|
|
}
|
|
|
|
if (ea.argt & EX_RANGE) {
|
|
Array range = ARRAY_DICT_INIT;
|
|
if (ea.addr_count > 0) {
|
|
if (ea.addr_count > 1) {
|
|
ADD(range, INTEGER_OBJ(ea.line1));
|
|
}
|
|
ADD(range, INTEGER_OBJ(ea.line2));
|
|
}
|
|
PUT(result, "range", ARRAY_OBJ(range));
|
|
}
|
|
|
|
if (ea.argt & EX_COUNT) {
|
|
if (ea.addr_count > 0) {
|
|
PUT(result, "count", INTEGER_OBJ(ea.line2));
|
|
} else if (cmd != NULL) {
|
|
PUT(result, "count", INTEGER_OBJ(cmd->uc_def));
|
|
} else {
|
|
PUT(result, "count", INTEGER_OBJ(0));
|
|
}
|
|
}
|
|
|
|
if (ea.argt & EX_REGSTR) {
|
|
char reg[2] = { (char)ea.regname, NUL };
|
|
PUT(result, "reg", CSTR_TO_OBJ(reg));
|
|
}
|
|
|
|
PUT(result, "bang", BOOLEAN_OBJ(ea.forceit));
|
|
PUT(result, "args", ARRAY_OBJ(args));
|
|
|
|
char nargs[2];
|
|
if (ea.argt & EX_EXTRA) {
|
|
if (ea.argt & EX_NOSPC) {
|
|
if (ea.argt & EX_NEEDARG) {
|
|
nargs[0] = '1';
|
|
} else {
|
|
nargs[0] = '?';
|
|
}
|
|
} else if (ea.argt & EX_NEEDARG) {
|
|
nargs[0] = '+';
|
|
} else {
|
|
nargs[0] = '*';
|
|
}
|
|
} else {
|
|
nargs[0] = '0';
|
|
}
|
|
nargs[1] = '\0';
|
|
PUT(result, "nargs", CSTR_TO_OBJ(nargs));
|
|
|
|
const char *addr;
|
|
switch (ea.addr_type) {
|
|
case ADDR_LINES:
|
|
addr = "line";
|
|
break;
|
|
case ADDR_ARGUMENTS:
|
|
addr = "arg";
|
|
break;
|
|
case ADDR_BUFFERS:
|
|
addr = "buf";
|
|
break;
|
|
case ADDR_LOADED_BUFFERS:
|
|
addr = "load";
|
|
break;
|
|
case ADDR_WINDOWS:
|
|
addr = "win";
|
|
break;
|
|
case ADDR_TABS:
|
|
addr = "tab";
|
|
break;
|
|
case ADDR_QUICKFIX:
|
|
addr = "qf";
|
|
break;
|
|
case ADDR_NONE:
|
|
addr = "none";
|
|
break;
|
|
default:
|
|
addr = "?";
|
|
break;
|
|
}
|
|
PUT(result, "addr", CSTR_TO_OBJ(addr));
|
|
PUT(result, "nextcmd", CSTR_TO_OBJ((char *)ea.nextcmd));
|
|
|
|
Dictionary mods = ARRAY_DICT_INIT;
|
|
|
|
Dictionary filter = ARRAY_DICT_INIT;
|
|
PUT(filter, "pattern", cmdinfo.cmdmod.cmod_filter_pat
|
|
? CSTR_TO_OBJ(cmdinfo.cmdmod.cmod_filter_pat)
|
|
: STRING_OBJ(STATIC_CSTR_TO_STRING("")));
|
|
PUT(filter, "force", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_filter_force));
|
|
PUT(mods, "filter", DICTIONARY_OBJ(filter));
|
|
|
|
PUT(mods, "silent", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_SILENT));
|
|
PUT(mods, "emsg_silent", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_ERRSILENT));
|
|
PUT(mods, "unsilent", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_UNSILENT));
|
|
PUT(mods, "sandbox", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_SANDBOX));
|
|
PUT(mods, "noautocmd", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_NOAUTOCMD));
|
|
PUT(mods, "tab", INTEGER_OBJ(cmdinfo.cmdmod.cmod_tab - 1));
|
|
PUT(mods, "verbose", INTEGER_OBJ(cmdinfo.cmdmod.cmod_verbose - 1));
|
|
PUT(mods, "browse", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_BROWSE));
|
|
PUT(mods, "confirm", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_CONFIRM));
|
|
PUT(mods, "hide", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_HIDE));
|
|
PUT(mods, "keepalt", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPALT));
|
|
PUT(mods, "keepjumps", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPJUMPS));
|
|
PUT(mods, "keepmarks", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPMARKS));
|
|
PUT(mods, "keeppatterns", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPPATTERNS));
|
|
PUT(mods, "lockmarks", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_LOCKMARKS));
|
|
PUT(mods, "noswapfile", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_NOSWAPFILE));
|
|
PUT(mods, "vertical", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_split & WSP_VERT));
|
|
PUT(mods, "horizontal", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_split & WSP_HOR));
|
|
|
|
const char *split;
|
|
if (cmdinfo.cmdmod.cmod_split & WSP_BOT) {
|
|
split = "botright";
|
|
} else if (cmdinfo.cmdmod.cmod_split & WSP_TOP) {
|
|
split = "topleft";
|
|
} else if (cmdinfo.cmdmod.cmod_split & WSP_BELOW) {
|
|
split = "belowright";
|
|
} else if (cmdinfo.cmdmod.cmod_split & WSP_ABOVE) {
|
|
split = "aboveleft";
|
|
} else {
|
|
split = "";
|
|
}
|
|
PUT(mods, "split", CSTR_TO_OBJ(split));
|
|
|
|
PUT(result, "mods", DICTIONARY_OBJ(mods));
|
|
|
|
Dictionary magic = ARRAY_DICT_INIT;
|
|
PUT(magic, "file", BOOLEAN_OBJ(cmdinfo.magic.file));
|
|
PUT(magic, "bar", BOOLEAN_OBJ(cmdinfo.magic.bar));
|
|
PUT(result, "magic", DICTIONARY_OBJ(magic));
|
|
|
|
undo_cmdmod(&cmdinfo.cmdmod);
|
|
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. Command arguments may also be Number, Boolean or String.
|
|
///
|
|
/// The first argument may also be used instead of count for commands that support it in order to
|
|
/// make their usage simpler with |vim.cmd()|. For example, instead of
|
|
/// `vim.cmd.bdelete{ count = 2 }`, you may do `vim.cmd.bdelete(2)`.
|
|
///
|
|
/// On execution error: fails with VimL error, updates v:errmsg.
|
|
///
|
|
/// @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;
|
|
CLEAR_FIELD(ea);
|
|
|
|
CmdParseInfo cmdinfo;
|
|
CLEAR_FIELD(cmdinfo);
|
|
|
|
char *cmdline = NULL;
|
|
char *cmdname = NULL;
|
|
ArrayOf(String) args = ARRAY_DICT_INIT;
|
|
|
|
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 OBJ_TO_CMOD_FLAG(flag, value, default, varname) \
|
|
do { \
|
|
if (api_object_to_bool(value, varname, default, err)) { \
|
|
cmdinfo.cmdmod.cmod_flags |= (flag); \
|
|
} \
|
|
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, p, 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");
|
|
}
|
|
|
|
// Process all arguments. Convert non-String arguments to String and check if String arguments
|
|
// have non-whitespace characters.
|
|
for (size_t i = 0; i < cmd->args.data.array.size; i++) {
|
|
Object elem = cmd->args.data.array.items[i];
|
|
char *data_str;
|
|
|
|
switch (elem.type) {
|
|
case kObjectTypeBoolean:
|
|
data_str = xcalloc(2, sizeof(char));
|
|
data_str[0] = elem.data.boolean ? '1' : '0';
|
|
data_str[1] = '\0';
|
|
break;
|
|
case kObjectTypeBuffer:
|
|
case kObjectTypeWindow:
|
|
case kObjectTypeTabpage:
|
|
case kObjectTypeInteger:
|
|
data_str = xcalloc(NUMBUFLEN, sizeof(char));
|
|
snprintf(data_str, NUMBUFLEN, "%" PRId64, elem.data.integer);
|
|
break;
|
|
case kObjectTypeString:
|
|
if (string_iswhite(elem.data.string)) {
|
|
VALIDATION_ERROR("String command argument must have at least one non-whitespace "
|
|
"character");
|
|
}
|
|
data_str = xstrndup(elem.data.string.data, elem.data.string.size);
|
|
break;
|
|
default:
|
|
VALIDATION_ERROR("Invalid type for command argument");
|
|
break;
|
|
}
|
|
|
|
ADD(args, STRING_OBJ(cstr_as_string(data_str)));
|
|
}
|
|
|
|
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 = args.size == 1;
|
|
break;
|
|
case EX_EXTRA | EX_NOSPC:
|
|
argc_valid = args.size <= 1;
|
|
break;
|
|
case EX_EXTRA | EX_NEEDARG:
|
|
argc_valid = args.size >= 1;
|
|
break;
|
|
case EX_EXTRA:
|
|
argc_valid = true;
|
|
break;
|
|
default:
|
|
argc_valid = args.size == 0;
|
|
break;
|
|
}
|
|
|
|
if (!argc_valid) {
|
|
VALIDATION_ERROR("Incorrect number of arguments supplied");
|
|
}
|
|
}
|
|
|
|
// 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, args.size > 0 ? args.items[0].data.string.data : 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 = (linenr_T)range.items[0].data.integer;
|
|
ea.line2 = (linenr_T)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'");
|
|
if (cmdinfo.magic.file) {
|
|
ea.argt |= EX_XFILE;
|
|
} else {
|
|
ea.argt &= ~EX_XFILE;
|
|
}
|
|
} 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.filter)) {
|
|
if (mods.filter.type != kObjectTypeDictionary) {
|
|
VALIDATION_ERROR("'mods.filter' must be a Dictionary");
|
|
}
|
|
|
|
Dict(cmd_mods_filter) filter = { 0 };
|
|
|
|
if (!api_dict_to_keydict(&filter, KeyDict_cmd_mods_filter_get_field,
|
|
mods.filter.data.dictionary, err)) {
|
|
goto end;
|
|
}
|
|
|
|
if (HAS_KEY(filter.pattern)) {
|
|
if (filter.pattern.type != kObjectTypeString) {
|
|
VALIDATION_ERROR("'mods.filter.pattern' must be a String");
|
|
}
|
|
|
|
OBJ_TO_BOOL(cmdinfo.cmdmod.cmod_filter_force, filter.force, false, "'mods.filter.force'");
|
|
|
|
// "filter! // is not no-op, so add a filter if either the pattern is non-empty or if filter
|
|
// is inverted.
|
|
if (*filter.pattern.data.string.data != NUL || cmdinfo.cmdmod.cmod_filter_force) {
|
|
cmdinfo.cmdmod.cmod_filter_pat = string_to_cstr(filter.pattern.data.string);
|
|
cmdinfo.cmdmod.cmod_filter_regmatch.regprog = vim_regcomp(cmdinfo.cmdmod.cmod_filter_pat,
|
|
RE_MAGIC);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (HAS_KEY(mods.tab)) {
|
|
if (mods.tab.type != kObjectTypeInteger) {
|
|
VALIDATION_ERROR("'mods.tab' must be an Integer");
|
|
} else if ((int)mods.tab.data.integer >= 0) {
|
|
// Silently ignore negative integers to allow mods.tab to be set to -1.
|
|
cmdinfo.cmdmod.cmod_tab = (int)mods.tab.data.integer + 1;
|
|
}
|
|
}
|
|
|
|
if (HAS_KEY(mods.verbose)) {
|
|
if (mods.verbose.type != kObjectTypeInteger) {
|
|
VALIDATION_ERROR("'mods.verbose' must be an Integer");
|
|
} else if ((int)mods.verbose.data.integer >= 0) {
|
|
// Silently ignore negative integers to allow mods.verbose to be set to -1.
|
|
cmdinfo.cmdmod.cmod_verbose = (int)mods.verbose.data.integer + 1;
|
|
}
|
|
}
|
|
|
|
bool vertical;
|
|
OBJ_TO_BOOL(vertical, mods.vertical, false, "'mods.vertical'");
|
|
cmdinfo.cmdmod.cmod_split |= (vertical ? WSP_VERT : 0);
|
|
|
|
bool horizontal;
|
|
OBJ_TO_BOOL(horizontal, mods.horizontal, false, "'mods.horizontal'");
|
|
cmdinfo.cmdmod.cmod_split |= (horizontal ? WSP_HOR : 0);
|
|
|
|
if (HAS_KEY(mods.split)) {
|
|
if (mods.split.type != kObjectTypeString) {
|
|
VALIDATION_ERROR("'mods.split' must be a String");
|
|
}
|
|
|
|
if (*mods.split.data.string.data == NUL) {
|
|
// Empty string, do nothing.
|
|
} else if (strcmp(mods.split.data.string.data, "aboveleft") == 0
|
|
|| strcmp(mods.split.data.string.data, "leftabove") == 0) {
|
|
cmdinfo.cmdmod.cmod_split |= WSP_ABOVE;
|
|
} else if (strcmp(mods.split.data.string.data, "belowright") == 0
|
|
|| strcmp(mods.split.data.string.data, "rightbelow") == 0) {
|
|
cmdinfo.cmdmod.cmod_split |= WSP_BELOW;
|
|
} else if (strcmp(mods.split.data.string.data, "topleft") == 0) {
|
|
cmdinfo.cmdmod.cmod_split |= WSP_TOP;
|
|
} else if (strcmp(mods.split.data.string.data, "botright") == 0) {
|
|
cmdinfo.cmdmod.cmod_split |= WSP_BOT;
|
|
} else {
|
|
VALIDATION_ERROR("Invalid value for 'mods.split'");
|
|
}
|
|
}
|
|
|
|
OBJ_TO_CMOD_FLAG(CMOD_SILENT, mods.silent, false, "'mods.silent'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_ERRSILENT, mods.emsg_silent, false, "'mods.emsg_silent'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_UNSILENT, mods.unsilent, false, "'mods.unsilent'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_SANDBOX, mods.sandbox, false, "'mods.sandbox'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_NOAUTOCMD, mods.noautocmd, false, "'mods.noautocmd'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_BROWSE, mods.browse, false, "'mods.browse'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_CONFIRM, mods.confirm, false, "'mods.confirm'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_HIDE, mods.hide, false, "'mods.hide'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_KEEPALT, mods.keepalt, false, "'mods.keepalt'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_KEEPJUMPS, mods.keepjumps, false, "'mods.keepjumps'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_KEEPMARKS, mods.keepmarks, false, "'mods.keepmarks'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_KEEPPATTERNS, mods.keeppatterns, false, "'mods.keeppatterns'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_LOCKMARKS, mods.lockmarks, false, "'mods.lockmarks'");
|
|
OBJ_TO_CMOD_FLAG(CMOD_NOSWAPFILE, mods.noswapfile, false, "'mods.noswapfile'");
|
|
|
|
if ((cmdinfo.cmdmod.cmod_flags & CMOD_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.
|
|
build_cmdline_str(&cmdline, &ea, &cmdinfo, args);
|
|
ea.cmdlinep = &cmdline;
|
|
|
|
garray_T capture_local;
|
|
const int save_msg_silent = msg_silent;
|
|
garray_T * const save_capture_ga = capture_ga;
|
|
const int save_msg_col = msg_col;
|
|
|
|
if (output) {
|
|
ga_init(&capture_local, 1, 80);
|
|
capture_ga = &capture_local;
|
|
}
|
|
|
|
TRY_WRAP({
|
|
try_start();
|
|
if (output) {
|
|
msg_silent++;
|
|
msg_col = 0; // prevent leading spaces
|
|
}
|
|
|
|
WITH_SCRIPT_CONTEXT(channel_id, {
|
|
execute_cmd(&ea, &cmdinfo, false);
|
|
});
|
|
|
|
if (output) {
|
|
capture_ga = save_capture_ga;
|
|
msg_silent = save_msg_silent;
|
|
// Put msg_col back where it was, since nothing should have been written.
|
|
msg_col = save_msg_col;
|
|
}
|
|
|
|
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:
|
|
api_free_array(args);
|
|
xfree(cmdline);
|
|
xfree(cmdname);
|
|
xfree(ea.args);
|
|
xfree(ea.arglens);
|
|
|
|
return retv;
|
|
|
|
#undef OBJ_TO_BOOL
|
|
#undef OBJ_TO_CMOD_FLAG
|
|
#undef VALIDATION_ERROR
|
|
}
|
|
|
|
/// Check if a string contains only whitespace characters.
|
|
static 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 occurrence of a NUL character
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Build cmdline string for command, used by `nvim_cmd()`.
|
|
static void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdinfo,
|
|
ArrayOf(String) args)
|
|
{
|
|
size_t argc = args.size;
|
|
StringBuilder cmdline = KV_INITIAL_VALUE;
|
|
kv_resize(cmdline, 32); // Make it big enough to handle most typical commands
|
|
|
|
// Add command modifiers
|
|
if (cmdinfo->cmdmod.cmod_tab != 0) {
|
|
kv_printf(cmdline, "%dtab ", cmdinfo->cmdmod.cmod_tab - 1);
|
|
}
|
|
if (cmdinfo->cmdmod.cmod_verbose > 0) {
|
|
kv_printf(cmdline, "%dverbose ", cmdinfo->cmdmod.cmod_verbose - 1);
|
|
}
|
|
|
|
if (cmdinfo->cmdmod.cmod_flags & CMOD_ERRSILENT) {
|
|
kv_concat(cmdline, "silent! ");
|
|
} else if (cmdinfo->cmdmod.cmod_flags & CMOD_SILENT) {
|
|
kv_concat(cmdline, "silent ");
|
|
}
|
|
|
|
if (cmdinfo->cmdmod.cmod_flags & CMOD_UNSILENT) {
|
|
kv_concat(cmdline, "unsilent ");
|
|
}
|
|
|
|
switch (cmdinfo->cmdmod.cmod_split & (WSP_ABOVE | WSP_BELOW | WSP_TOP | WSP_BOT)) {
|
|
case WSP_ABOVE:
|
|
kv_concat(cmdline, "aboveleft ");
|
|
break;
|
|
case WSP_BELOW:
|
|
kv_concat(cmdline, "belowright ");
|
|
break;
|
|
case WSP_TOP:
|
|
kv_concat(cmdline, "topleft ");
|
|
break;
|
|
case WSP_BOT:
|
|
kv_concat(cmdline, "botright ");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
#define CMDLINE_APPEND_IF(cond, str) \
|
|
do { \
|
|
if (cond) { \
|
|
kv_concat(cmdline, str); \
|
|
} \
|
|
} while (0)
|
|
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_split & WSP_VERT, "vertical ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_split & WSP_HOR, "horizontal ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_SANDBOX, "sandbox ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_NOAUTOCMD, "noautocmd ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_BROWSE, "browse ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_CONFIRM, "confirm ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_HIDE, "hide ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPALT, "keepalt ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPJUMPS, "keepjumps ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPMARKS, "keepmarks ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPPATTERNS, "keeppatterns ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_LOCKMARKS, "lockmarks ");
|
|
CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_NOSWAPFILE, "noswapfile ");
|
|
#undef CMDLINE_APPEND_IF
|
|
|
|
// Command range / count.
|
|
if (eap->argt & EX_RANGE) {
|
|
if (eap->addr_count == 1) {
|
|
kv_printf(cmdline, "%" PRIdLINENR, eap->line2);
|
|
} else if (eap->addr_count > 1) {
|
|
kv_printf(cmdline, "%" PRIdLINENR ",%" PRIdLINENR, 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 = cmdline.size;
|
|
kv_concat(cmdline, eap->cmd);
|
|
|
|
// Command bang.
|
|
if (eap->argt & EX_BANG && eap->forceit) {
|
|
kv_concat(cmdline, "!");
|
|
}
|
|
|
|
// Command register.
|
|
if (eap->argt & EX_REGSTR && eap->regname) {
|
|
kv_printf(cmdline, " %c", eap->regname);
|
|
}
|
|
|
|
eap->argc = argc;
|
|
eap->arglens = eap->argc > 0 ? xcalloc(argc, sizeof(size_t)) : NULL;
|
|
size_t argstart_idx = cmdline.size;
|
|
for (size_t i = 0; i < argc; i++) {
|
|
String s = args.items[i].data.string;
|
|
eap->arglens[i] = s.size;
|
|
kv_concat(cmdline, " ");
|
|
kv_concat_len(cmdline, s.data, s.size);
|
|
}
|
|
|
|
// Done appending to cmdline, ensure it is NUL terminated
|
|
kv_push(cmdline, NUL);
|
|
|
|
// Now that all the arguments are appended, use the command index and argument indices to set the
|
|
// values of eap->cmd, eap->arg and eap->args.
|
|
eap->cmd = cmdline.items + cmdname_idx;
|
|
eap->args = eap->argc > 0 ? xcalloc(argc, sizeof(char *)) : NULL;
|
|
size_t offset = argstart_idx;
|
|
for (size_t i = 0; i < argc; i++) {
|
|
offset++; // Account for space
|
|
eap->args[i] = cmdline.items + offset;
|
|
offset += eap->arglens[i];
|
|
}
|
|
// If there isn't an argument, make eap->arg point to end of cmdline.
|
|
eap->arg = argc > 0 ? eap->args[0] :
|
|
cmdline.items + cmdline.size - 1; // Subtract 1 to account for NUL
|
|
|
|
// Finally, make cmdlinep point to the cmdline string.
|
|
*cmdlinep = cmdline.items;
|
|
|
|
// 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 eap->arg pointer.
|
|
eap->arg = p;
|
|
// This cannot be a user command, so eap->args will not be used.
|
|
XFREE_CLEAR(eap->args);
|
|
XFREE_CLEAR(eap->arglens);
|
|
eap->argc = 0;
|
|
}
|
|
}
|
|
|
|
/// Create a new user command |user-commands|
|
|
///
|
|
/// {name} is the name of the new command. The name must begin with an uppercase letter.
|
|
///
|
|
/// {command} is the replacement text or Lua function to execute.
|
|
///
|
|
/// Example:
|
|
/// <pre>
|
|
/// :call nvim_create_user_command('SayHello', 'echo "Hello world!"', {})
|
|
/// :SayHello
|
|
/// Hello world!
|
|
/// </pre>
|
|
///
|
|
/// @param name Name of the new user command. Must begin with an uppercase letter.
|
|
/// @param command Replacement command to execute when this user command is executed. When called
|
|
/// from Lua, the command can also be a Lua function. The function is called with a
|
|
/// single table argument that contains the following keys:
|
|
/// - name: (string) Command name
|
|
/// - args: (string) The args passed to the command, if any |<args>|
|
|
/// - fargs: (table) The args split by unescaped whitespace (when more than one
|
|
/// argument is allowed), if any |<f-args>|
|
|
/// - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>|
|
|
/// - line1: (number) The starting line of the command range |<line1>|
|
|
/// - line2: (number) The final line of the command range |<line2>|
|
|
/// - range: (number) The number of items in the command range: 0, 1, or 2 |<range>|
|
|
/// - count: (number) Any count supplied |<count>|
|
|
/// - reg: (string) The optional register, if specified |<reg>|
|
|
/// - mods: (string) Command modifiers, if any |<mods>|
|
|
/// - smods: (table) Command modifiers in a structured format. Has the same
|
|
/// structure as the "mods" key of |nvim_parse_cmd()|.
|
|
/// @param opts Optional command attributes. See |command-attributes| for more details. To use
|
|
/// boolean attributes (such as |:command-bang| or |:command-bar|) set the value to
|
|
/// "true". In addition to the string options listed in |:command-complete|, the
|
|
/// "complete" key also accepts a Lua function which works like the "customlist"
|
|
/// completion mode |:command-completion-customlist|. Additional parameters:
|
|
/// - desc: (string) Used for listing the command when a Lua function is used for
|
|
/// {command}.
|
|
/// - force: (boolean, default true) Override any previous definition.
|
|
/// - preview: (function) Preview callback for 'inccommand' |:command-preview|
|
|
/// @param[out] err Error details, if any.
|
|
void nvim_create_user_command(String name, Object command, Dict(user_command) *opts, Error *err)
|
|
FUNC_API_SINCE(9)
|
|
{
|
|
create_user_command(name, command, opts, 0, err);
|
|
}
|
|
|
|
/// Delete a user-defined command.
|
|
///
|
|
/// @param name Name of the command to delete.
|
|
/// @param[out] err Error details, if any.
|
|
void nvim_del_user_command(String name, Error *err)
|
|
FUNC_API_SINCE(9)
|
|
{
|
|
nvim_buf_del_user_command(-1, name, err);
|
|
}
|
|
|
|
/// Create a new user command |user-commands| in the given buffer.
|
|
///
|
|
/// @param buffer Buffer handle, or 0 for current buffer.
|
|
/// @param[out] err Error details, if any.
|
|
/// @see nvim_create_user_command
|
|
void nvim_buf_create_user_command(Buffer buffer, String name, Object command,
|
|
Dict(user_command) *opts, Error *err)
|
|
FUNC_API_SINCE(9)
|
|
{
|
|
buf_T *target_buf = find_buffer_by_handle(buffer, err);
|
|
if (ERROR_SET(err)) {
|
|
return;
|
|
}
|
|
|
|
buf_T *save_curbuf = curbuf;
|
|
curbuf = target_buf;
|
|
create_user_command(name, command, opts, UC_BUFFER, err);
|
|
curbuf = save_curbuf;
|
|
}
|
|
|
|
/// Delete a buffer-local user-defined command.
|
|
///
|
|
/// Only commands created with |:command-buffer| or
|
|
/// |nvim_buf_create_user_command()| can be deleted with this function.
|
|
///
|
|
/// @param buffer Buffer handle, or 0 for current buffer.
|
|
/// @param name Name of the command to delete.
|
|
/// @param[out] err Error details, if any.
|
|
void nvim_buf_del_user_command(Buffer buffer, String name, Error *err)
|
|
FUNC_API_SINCE(9)
|
|
{
|
|
garray_T *gap;
|
|
if (buffer == -1) {
|
|
gap = &ucmds;
|
|
} else {
|
|
buf_T *buf = find_buffer_by_handle(buffer, err);
|
|
gap = &buf->b_ucmds;
|
|
}
|
|
|
|
for (int i = 0; i < gap->ga_len; i++) {
|
|
ucmd_T *cmd = USER_CMD_GA(gap, i);
|
|
if (!strcmp(name.data, cmd->uc_name)) {
|
|
free_ucmd(cmd);
|
|
|
|
gap->ga_len -= 1;
|
|
|
|
if (i < gap->ga_len) {
|
|
memmove(cmd, cmd + 1, (size_t)(gap->ga_len - i) * sizeof(ucmd_T));
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
api_set_error(err, kErrorTypeException, "No such user-defined command: %s", name.data);
|
|
}
|
|
|
|
void create_user_command(String name, Object command, Dict(user_command) *opts, int flags,
|
|
Error *err)
|
|
{
|
|
uint32_t argt = 0;
|
|
long def = -1;
|
|
cmd_addr_T addr_type_arg = ADDR_NONE;
|
|
int compl = EXPAND_NOTHING;
|
|
char *compl_arg = NULL;
|
|
const char *rep = NULL;
|
|
LuaRef luaref = LUA_NOREF;
|
|
LuaRef compl_luaref = LUA_NOREF;
|
|
LuaRef preview_luaref = LUA_NOREF;
|
|
|
|
if (!uc_validate_name(name.data)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid command name");
|
|
goto err;
|
|
}
|
|
|
|
if (mb_islower(name.data[0])) {
|
|
api_set_error(err, kErrorTypeValidation, "'name' must begin with an uppercase letter");
|
|
goto err;
|
|
}
|
|
|
|
if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) {
|
|
api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->nargs.type == kObjectTypeInteger) {
|
|
switch (opts->nargs.data.integer) {
|
|
case 0:
|
|
// Default value, nothing to do
|
|
break;
|
|
case 1:
|
|
argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG;
|
|
break;
|
|
default:
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
} else if (opts->nargs.type == kObjectTypeString) {
|
|
if (opts->nargs.data.string.size > 1) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
|
|
switch (opts->nargs.data.string.data[0]) {
|
|
case '*':
|
|
argt |= EX_EXTRA;
|
|
break;
|
|
case '?':
|
|
argt |= EX_EXTRA | EX_NOSPC;
|
|
break;
|
|
case '+':
|
|
argt |= EX_EXTRA | EX_NEEDARG;
|
|
break;
|
|
default:
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
} else if (HAS_KEY(opts->nargs)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'");
|
|
goto err;
|
|
}
|
|
|
|
if (HAS_KEY(opts->complete) && !argt) {
|
|
api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->range.type == kObjectTypeBoolean) {
|
|
if (opts->range.data.boolean) {
|
|
argt |= EX_RANGE;
|
|
addr_type_arg = ADDR_LINES;
|
|
}
|
|
} else if (opts->range.type == kObjectTypeString) {
|
|
if (opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1) {
|
|
argt |= EX_RANGE | EX_DFLALL;
|
|
addr_type_arg = ADDR_LINES;
|
|
} else {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'");
|
|
goto err;
|
|
}
|
|
} else if (opts->range.type == kObjectTypeInteger) {
|
|
argt |= EX_RANGE | EX_ZEROR;
|
|
def = opts->range.data.integer;
|
|
addr_type_arg = ADDR_LINES;
|
|
} else if (HAS_KEY(opts->range)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->count.type == kObjectTypeBoolean) {
|
|
if (opts->count.data.boolean) {
|
|
argt |= EX_COUNT | EX_ZEROR | EX_RANGE;
|
|
addr_type_arg = ADDR_OTHER;
|
|
def = 0;
|
|
}
|
|
} else if (opts->count.type == kObjectTypeInteger) {
|
|
argt |= EX_COUNT | EX_ZEROR | EX_RANGE;
|
|
addr_type_arg = ADDR_OTHER;
|
|
def = opts->count.data.integer;
|
|
} else if (HAS_KEY(opts->count)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'count'");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->addr.type == kObjectTypeString) {
|
|
if (parse_addr_type_arg(opts->addr.data.string.data, (int)opts->addr.data.string.size,
|
|
&addr_type_arg) != OK) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'");
|
|
goto err;
|
|
}
|
|
|
|
if (addr_type_arg != ADDR_LINES) {
|
|
argt |= EX_ZEROR;
|
|
}
|
|
} else if (HAS_KEY(opts->addr)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'");
|
|
goto err;
|
|
}
|
|
|
|
if (api_object_to_bool(opts->bang, "bang", false, err)) {
|
|
argt |= EX_BANG;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
if (api_object_to_bool(opts->bar, "bar", false, err)) {
|
|
argt |= EX_TRLBAR;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
if (api_object_to_bool(opts->register_, "register", false, err)) {
|
|
argt |= EX_REGSTR;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
if (api_object_to_bool(opts->keepscript, "keepscript", false, err)) {
|
|
argt |= EX_KEEPSCRIPT;
|
|
} else if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
bool force = api_object_to_bool(opts->force, "force", true, err);
|
|
if (ERROR_SET(err)) {
|
|
goto err;
|
|
}
|
|
|
|
if (opts->complete.type == kObjectTypeLuaRef) {
|
|
compl = EXPAND_USER_LUA;
|
|
compl_luaref = api_new_luaref(opts->complete.data.luaref);
|
|
} else if (opts->complete.type == kObjectTypeString) {
|
|
if (parse_compl_arg(opts->complete.data.string.data,
|
|
(int)opts->complete.data.string.size, &compl, &argt,
|
|
&compl_arg) != OK) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'");
|
|
goto err;
|
|
}
|
|
} else if (HAS_KEY(opts->complete)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'");
|
|
goto err;
|
|
}
|
|
|
|
if (opts->preview.type == kObjectTypeLuaRef) {
|
|
argt |= EX_PREVIEW;
|
|
preview_luaref = api_new_luaref(opts->preview.data.luaref);
|
|
} else if (HAS_KEY(opts->preview)) {
|
|
api_set_error(err, kErrorTypeValidation, "Invalid value for 'preview'");
|
|
goto err;
|
|
}
|
|
|
|
switch (command.type) {
|
|
case kObjectTypeLuaRef:
|
|
luaref = api_new_luaref(command.data.luaref);
|
|
if (opts->desc.type == kObjectTypeString) {
|
|
rep = opts->desc.data.string.data;
|
|
} else {
|
|
rep = "";
|
|
}
|
|
break;
|
|
case kObjectTypeString:
|
|
rep = command.data.string.data;
|
|
break;
|
|
default:
|
|
api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function");
|
|
goto err;
|
|
}
|
|
|
|
if (uc_add_command(name.data, name.size, rep, argt, def, flags, compl, compl_arg, compl_luaref,
|
|
preview_luaref, addr_type_arg, luaref, force) != OK) {
|
|
api_set_error(err, kErrorTypeException, "Failed to create user command");
|
|
// Do not goto err, since uc_add_command now owns luaref, compl_luaref, and compl_arg
|
|
}
|
|
|
|
return;
|
|
|
|
err:
|
|
NLUA_CLEAR_REF(luaref);
|
|
NLUA_CLEAR_REF(compl_luaref);
|
|
xfree(compl_arg);
|
|
}
|
|
/// Gets a map of global (non-buffer-local) Ex commands.
|
|
///
|
|
/// Currently only |user-commands| are supported, not builtin Ex commands.
|
|
///
|
|
/// @param opts Optional parameters. Currently only supports
|
|
/// {"builtin":false}
|
|
/// @param[out] err Error details, if any.
|
|
///
|
|
/// @returns Map of maps describing commands.
|
|
Dictionary nvim_get_commands(Dict(get_commands) *opts, Error *err)
|
|
FUNC_API_SINCE(4)
|
|
{
|
|
return nvim_buf_get_commands(-1, opts, err);
|
|
}
|
|
|
|
/// Gets a map of buffer-local |user-commands|.
|
|
///
|
|
/// @param buffer Buffer handle, or 0 for current buffer
|
|
/// @param opts Optional parameters. Currently not used.
|
|
/// @param[out] err Error details, if any.
|
|
///
|
|
/// @returns Map of maps describing commands.
|
|
Dictionary nvim_buf_get_commands(Buffer buffer, Dict(get_commands) *opts, Error *err)
|
|
FUNC_API_SINCE(4)
|
|
{
|
|
bool global = (buffer == -1);
|
|
bool builtin = api_object_to_bool(opts->builtin, "builtin", false, err);
|
|
if (ERROR_SET(err)) {
|
|
return (Dictionary)ARRAY_DICT_INIT;
|
|
}
|
|
|
|
if (global) {
|
|
if (builtin) {
|
|
api_set_error(err, kErrorTypeValidation, "builtin=true not implemented");
|
|
return (Dictionary)ARRAY_DICT_INIT;
|
|
}
|
|
return commands_array(NULL);
|
|
}
|
|
|
|
buf_T *buf = find_buffer_by_handle(buffer, err);
|
|
if (builtin || !buf) {
|
|
return (Dictionary)ARRAY_DICT_INIT;
|
|
}
|
|
return commands_array(buf);
|
|
}
|