aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFamiu Haque <famiuhaque@protonmail.com>2022-04-23 10:21:59 +0600
committerFamiu Haque <famiuhaque@protonmail.com>2022-04-30 21:04:51 +0600
commit8dbb11ebf633e40cb57568e77c7168deffc8bd7f (patch)
treead5ce9c55bec9b2ee05c28a7c5a9c5262575370b /src
parent3c23100130725bb79c04e933c505bbeda96fb3bb (diff)
downloadrneovim-8dbb11ebf633e40cb57568e77c7168deffc8bd7f.tar.gz
rneovim-8dbb11ebf633e40cb57568e77c7168deffc8bd7f.tar.bz2
rneovim-8dbb11ebf633e40cb57568e77c7168deffc8bd7f.zip
feat(api): add `nvim_parse_cmdline`
Adds an API function to parse a command line string and get command information from it.
Diffstat (limited to 'src')
-rw-r--r--src/nvim/api/vim.c201
-rw-r--r--src/nvim/ex_cmds_defs.h14
-rw-r--r--src/nvim/ex_docmd.c292
3 files changed, 437 insertions, 70 deletions
diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c
index ac2fc09056..3d1b5eade4 100644
--- a/src/nvim/api/vim.c
+++ b/src/nvim/api/vim.c
@@ -28,6 +28,7 @@
#include "nvim/eval/typval.h"
#include "nvim/eval/userfunc.h"
#include "nvim/ex_cmds2.h"
+#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/file_search.h"
#include "nvim/fileio.h"
@@ -2460,3 +2461,203 @@ void nvim_del_user_command(String name, Error *err)
{
nvim_buf_del_user_command(-1, name, err);
}
+
+/// 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.
+/// - line1: (number) Starting line of command range. Only applicable if command can take a
+/// range.
+/// - line2: (number) Final line of command range. Only applicable if command can take a
+/// range.
+/// - 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|.
+/// - silent: (boolean) |:silent|.
+/// - emsg_silent: (boolean) |:silent!|.
+/// - sandbox: (boolean) |:sandbox|.
+/// - noautocmd: (boolean) |:noautocmd|.
+/// - browse: (boolean) |:browse|.
+/// - confirm: (boolean) |:confirm|.
+/// - hide: (boolean) |:hide|.
+/// - keepalt: (boolean) |:keepalt|.
+/// - keepjumps: (boolean) |:keepjumps|.
+/// - keepmarks: (boolean) |:keepmarks|.
+/// - keeppatterns: (boolean) |:keeppatterns|.
+/// - lockmarks: (boolean) |:lockmarks|.
+/// - noswapfile: (boolean) |:noswapfile|.
+/// - tab: (integer) |:tab|.
+/// - verbose: (integer) |:verbose|.
+/// - 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_u *cmdline = vim_strsave((char_u *)str.data);
+
+ if (!parse_cmdline(cmdline, &ea, &cmdinfo)) {
+ 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);
+ }
+
+ if (ea.cmdidx == CMD_USER) {
+ PUT(result, "cmd", CSTR_TO_OBJ((char *)USER_CMD(ea.useridx)->uc_name));
+ } else if (ea.cmdidx == CMD_USER_BUF) {
+ PUT(result, "cmd", CSTR_TO_OBJ((char *)USER_CMD_GA(&curbuf->b_ucmds, ea.useridx)->uc_name));
+ } else {
+ PUT(result, "cmd", CSTR_TO_OBJ((char *)get_command_name(NULL, ea.cmdidx)));
+ }
+ PUT(result, "line1", INTEGER_OBJ(ea.line1));
+ PUT(result, "line2", INTEGER_OBJ(ea.line2));
+ 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;
+ PUT(mods, "silent", BOOLEAN_OBJ(cmdinfo.silent));
+ PUT(mods, "emsg_silent", BOOLEAN_OBJ(cmdinfo.emsg_silent));
+ PUT(mods, "sandbox", BOOLEAN_OBJ(cmdinfo.sandbox));
+ PUT(mods, "noautocmd", BOOLEAN_OBJ(cmdinfo.noautocmd));
+ PUT(mods, "tab", INTEGER_OBJ(cmdmod.tab));
+ PUT(mods, "verbose", INTEGER_OBJ(cmdinfo.verbose));
+ PUT(mods, "browse", BOOLEAN_OBJ(cmdmod.browse));
+ PUT(mods, "confirm", BOOLEAN_OBJ(cmdmod.confirm));
+ PUT(mods, "hide", BOOLEAN_OBJ(cmdmod.hide));
+ PUT(mods, "keepalt", BOOLEAN_OBJ(cmdmod.keepalt));
+ PUT(mods, "keepjumps", BOOLEAN_OBJ(cmdmod.keepjumps));
+ PUT(mods, "keepmarks", BOOLEAN_OBJ(cmdmod.keepmarks));
+ PUT(mods, "keeppatterns", BOOLEAN_OBJ(cmdmod.keeppatterns));
+ PUT(mods, "lockmarks", BOOLEAN_OBJ(cmdmod.lockmarks));
+ PUT(mods, "noswapfile", BOOLEAN_OBJ(cmdmod.noswapfile));
+ PUT(mods, "vertical", BOOLEAN_OBJ(cmdmod.split & WSP_VERT));
+
+ const char *split;
+ if (cmdmod.split & WSP_BOT) {
+ split = "botright";
+ } else if (cmdmod.split & WSP_TOP) {
+ split = "topleft";
+ } else if (cmdmod.split & WSP_BELOW) {
+ split = "belowright";
+ } else if (cmdmod.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));
+end:
+ xfree(cmdline);
+ return result;
+}
diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h
index d8c4544f2e..f3b3e094f5 100644
--- a/src/nvim/ex_cmds_defs.h
+++ b/src/nvim/ex_cmds_defs.h
@@ -261,4 +261,18 @@ typedef struct {
bool filter_force; ///< set for :filter!
} cmdmod_T;
+/// Stores command modifier info used by `nvim_parse_cmd`
+typedef struct {
+ bool silent;
+ bool emsg_silent;
+ bool sandbox;
+ bool noautocmd;
+ long verbose;
+ cmdmod_T cmdmod;
+ struct {
+ bool file;
+ bool bar;
+ } magic;
+} CmdParseInfo;
+
#endif // NVIM_EX_CMDS_DEFS_H
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
index 74963c165e..83cf945608 100644
--- a/src/nvim/ex_docmd.c
+++ b/src/nvim/ex_docmd.c
@@ -1216,6 +1216,226 @@ static char *skip_colon_white(const char *p, bool skipleadingwhite)
return (char *)p;
}
+/// 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)
+{
+ // ea.addr_type for user commands is set by find_ucmd
+ if (IS_USER_CMDIDX(eap->cmdidx)) {
+ return;
+ }
+ if (eap->cmdidx != CMD_SIZE) {
+ eap->addr_type = cmdnames[(int)eap->cmdidx].cmd_addr_type;
+ } else {
+ eap->addr_type = ADDR_LINES;
+ }
+ // :wincmd range depends on the argument
+ if (eap->cmdidx == CMD_wincmd && p != NULL) {
+ get_wincmd_addr_type((char *)skipwhite((char_u *)p), eap);
+ }
+ // :.cc in quickfix window uses line number
+ if ((eap->cmdidx == CMD_cc || eap->cmdidx == CMD_ll) && bt_quickfix(curbuf)) {
+ eap->addr_type = ADDR_OTHER;
+ }
+}
+
+/// Set default command range based on the addr type of the command
+static void set_cmd_default_range(exarg_T *eap)
+{
+ buf_T *buf;
+
+ eap->line1 = 1;
+ switch (eap->addr_type) {
+ case ADDR_LINES:
+ case ADDR_OTHER:
+ eap->line2 = curbuf->b_ml.ml_line_count;
+ break;
+ case ADDR_LOADED_BUFFERS:
+ buf = firstbuf;
+ while (buf->b_next != NULL && buf->b_ml.ml_mfp == NULL) {
+ buf = buf->b_next;
+ }
+ eap->line1 = buf->b_fnum;
+ buf = lastbuf;
+ while (buf->b_prev != NULL && buf->b_ml.ml_mfp == NULL) {
+ buf = buf->b_prev;
+ }
+ eap->line2 = buf->b_fnum;
+ break;
+ case ADDR_BUFFERS:
+ eap->line1 = firstbuf->b_fnum;
+ eap->line2 = lastbuf->b_fnum;
+ break;
+ case ADDR_WINDOWS:
+ eap->line2 = LAST_WIN_NR;
+ break;
+ case ADDR_TABS:
+ eap->line2 = LAST_TAB_NR;
+ break;
+ case ADDR_TABS_RELATIVE:
+ eap->line2 = 1;
+ break;
+ case ADDR_ARGUMENTS:
+ if (ARGCOUNT == 0) {
+ eap->line1 = eap->line2 = 0;
+ } else {
+ eap->line2 = ARGCOUNT;
+ }
+ break;
+ case ADDR_QUICKFIX_VALID:
+ eap->line2 = (linenr_T)qf_get_valid_size(eap);
+ if (eap->line2 == 0) {
+ eap->line2 = 1;
+ }
+ break;
+ case ADDR_NONE:
+ case ADDR_UNSIGNED:
+ case ADDR_QUICKFIX:
+ iemsg(_("INTERNAL: Cannot use EX_DFLALL "
+ "with ADDR_NONE, ADDR_UNSIGNED or ADDR_QUICKFIX"));
+ break;
+ }
+}
+
+/// Parse command line and return information about the first command.
+///
+/// @return Success or failure
+bool parse_cmdline(char_u *cmdline, exarg_T *eap, CmdParseInfo *cmdinfo)
+{
+ char *errormsg = NULL;
+ char *cmd;
+ char *p;
+
+ // Initialize cmdinfo
+ memset(cmdinfo, 0, sizeof(*cmdinfo));
+
+ // Initialize eap
+ memset(eap, 0, sizeof(*eap));
+ eap->line1 = 1;
+ eap->line2 = 1;
+ eap->cmd = (char *)cmdline;
+ eap->cmdlinep = &cmdline;
+ eap->getline = NULL;
+ eap->cookie = NULL;
+
+ // Parse command modifiers
+ if (parse_command_modifiers(eap, &errormsg, false) == FAIL) {
+ return false;
+ }
+ // Revert the side-effects of `parse_command_modifiers`
+ if (eap->save_msg_silent != -1) {
+ cmdinfo->silent = !!msg_silent;
+ msg_silent = eap->save_msg_silent;
+ }
+ if (eap->did_esilent) {
+ cmdinfo->emsg_silent = true;
+ emsg_silent--;
+ }
+ if (eap->did_sandbox) {
+ cmdinfo->sandbox = true;
+ sandbox--;
+ }
+ if (cmdmod.save_ei != NULL) {
+ cmdinfo->noautocmd = true;
+ set_string_option_direct("ei", -1, cmdmod.save_ei, OPT_FREE, SID_NONE);
+ free_string_option(cmdmod.save_ei);
+ }
+ if (eap->verbose_save != -1) {
+ cmdinfo->verbose = p_verbose;
+ p_verbose = eap->verbose_save;
+ }
+ cmdinfo->cmdmod = cmdmod;
+
+ // Save location after command modifiers
+ cmd = eap->cmd;
+ // Skip ranges to find command name since we need the command to know what kind of range it uses
+ eap->cmd = skip_range(eap->cmd, NULL);
+ if (*eap->cmd == '*') {
+ eap->cmd = (char *)skipwhite((char_u *)eap->cmd + 1);
+ }
+ p = find_command(eap, NULL);
+
+ // Set command attribute type and parse command range
+ set_cmd_addr_type(eap, (char_u *)p);
+ eap->cmd = cmd;
+ if (parse_cmd_address(eap, &errormsg, false) == FAIL) {
+ return false;
+ }
+
+ // Skip colon and whitespace
+ eap->cmd = skip_colon_white(eap->cmd, true);
+ // Fail if command is a comment or if command doesn't exist
+ if (*eap->cmd == NUL || *eap->cmd == '"') {
+ return false;
+ }
+ // Fail if command is invalid
+ if (eap->cmdidx == CMD_SIZE) {
+ return false;
+ }
+
+ // Correctly set 'forceit' for commands
+ if (*p == '!' && eap->cmdidx != CMD_substitute
+ && eap->cmdidx != CMD_smagic && eap->cmdidx != CMD_snomagic) {
+ p++;
+ eap->forceit = true;
+ } else {
+ eap->forceit = false;
+ }
+
+ // Parse arguments.
+ if (!IS_USER_CMDIDX(eap->cmdidx)) {
+ eap->argt = cmdnames[(int)eap->cmdidx].cmd_argt;
+ }
+ // Skip to start of argument.
+ // Don't do this for the ":!" command, because ":!! -l" needs the space.
+ if (eap->cmdidx == CMD_bang) {
+ eap->arg = (char_u *)p;
+ } else {
+ eap->arg = skipwhite((char_u *)p);
+ }
+
+ // Don't treat ":r! filter" like a bang
+ if (eap->cmdidx == CMD_read) {
+ if (eap->forceit) {
+ eap->forceit = false; // :r! filter
+ }
+ }
+
+ // Check for '|' to separate commands and '"' to start comments.
+ // Don't do this for ":read !cmd" and ":write !cmd".
+ if ((eap->argt & EX_TRLBAR)) {
+ separate_nextcmd(eap);
+ }
+ // Fail if command doesn't support bang but is used with a bang
+ if (!(eap->argt & EX_BANG) && eap->forceit) {
+ return false;
+ }
+ // Fail if command doesn't support a range but it is given a range
+ if (!(eap->argt & EX_RANGE) && eap->addr_count > 0) {
+ return false;
+ }
+ // Set default range for command if required
+ if ((eap->argt & EX_DFLALL) && eap->addr_count == 0) {
+ set_cmd_default_range(eap);
+ }
+
+ // Remove leading whitespace and colon from next command
+ if (eap->nextcmd) {
+ eap->nextcmd = (char_u *)skip_colon_white((char *)eap->nextcmd, true);
+ }
+
+ // Set the "magic" values (characters that get treated specially)
+ if (eap->argt & EX_XFILE) {
+ cmdinfo->magic.file = true;
+ }
+ if (eap->argt & EX_TRLBAR) {
+ cmdinfo->magic.bar = true;
+ }
+
+ return true;
+}
+
/// Execute one Ex command.
///
/// If 'sourcing' is TRUE, the command will be included in the error message.
@@ -1361,23 +1581,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter
// The ea.cmd pointer is updated to point to the first character following the
// range spec. If an initial address is found, but no second, the upper bound
// is equal to the lower.
-
- // ea.addr_type for user commands is set by find_ucmd
- if (!IS_USER_CMDIDX(ea.cmdidx)) {
- if (ea.cmdidx != CMD_SIZE) {
- ea.addr_type = cmdnames[(int)ea.cmdidx].cmd_addr_type;
- } else {
- ea.addr_type = ADDR_LINES;
- }
- // :wincmd range depends on the argument
- if (ea.cmdidx == CMD_wincmd && p != NULL) {
- get_wincmd_addr_type((char *)skipwhite((char_u *)p), &ea);
- }
- // :.cc in quickfix window uses line number
- if ((ea.cmdidx == CMD_cc || ea.cmdidx == CMD_ll) && bt_quickfix(curbuf)) {
- ea.addr_type = ADDR_OTHER;
- }
- }
+ set_cmd_addr_type(&ea, (char_u *)p);
ea.cmd = cmd;
if (parse_cmd_address(&ea, &errormsg, false) == FAIL) {
@@ -1690,59 +1894,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter
}
if ((ea.argt & EX_DFLALL) && ea.addr_count == 0) {
- buf_T *buf;
-
- ea.line1 = 1;
- switch (ea.addr_type) {
- case ADDR_LINES:
- case ADDR_OTHER:
- ea.line2 = curbuf->b_ml.ml_line_count;
- break;
- case ADDR_LOADED_BUFFERS:
- buf = firstbuf;
- while (buf->b_next != NULL && buf->b_ml.ml_mfp == NULL) {
- buf = buf->b_next;
- }
- ea.line1 = buf->b_fnum;
- buf = lastbuf;
- while (buf->b_prev != NULL && buf->b_ml.ml_mfp == NULL) {
- buf = buf->b_prev;
- }
- ea.line2 = buf->b_fnum;
- break;
- case ADDR_BUFFERS:
- ea.line1 = firstbuf->b_fnum;
- ea.line2 = lastbuf->b_fnum;
- break;
- case ADDR_WINDOWS:
- ea.line2 = LAST_WIN_NR;
- break;
- case ADDR_TABS:
- ea.line2 = LAST_TAB_NR;
- break;
- case ADDR_TABS_RELATIVE:
- ea.line2 = 1;
- break;
- case ADDR_ARGUMENTS:
- if (ARGCOUNT == 0) {
- ea.line1 = ea.line2 = 0;
- } else {
- ea.line2 = ARGCOUNT;
- }
- break;
- case ADDR_QUICKFIX_VALID:
- ea.line2 = (linenr_T)qf_get_valid_size(&ea);
- if (ea.line2 == 0) {
- ea.line2 = 1;
- }
- break;
- case ADDR_NONE:
- case ADDR_UNSIGNED:
- case ADDR_QUICKFIX:
- iemsg(_("INTERNAL: Cannot use EX_DFLALL "
- "with ADDR_NONE, ADDR_UNSIGNED or ADDR_QUICKFIX"));
- break;
- }
+ set_cmd_default_range(&ea);
}
// accept numbered register only when no count allowed (:put)