diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index 30ef921ecc..526f10ef38 100644 --- a/src/nvim/api/command.c +++ b/src/nvim/api/command.c @@ -148,7 +148,7 @@ Dict(cmd) nvim_parse_cmd(String str, Dict(empty) *opts, Arena *arena, Error *err // Check if this is a mapping command that needs special handling // like mapping commands need special argument parsing to preserve whitespace in RHS: // "map a b c" => { args=["a", "b c"], ... } - if (is_map_cmd(ea.cmdidx) && *ea.arg != NUL) { + if (ea.cmdidx != CMD_SIZE && is_map_cmd(ea.cmdidx) && *ea.arg != NUL) { // For mapping commands, split differently to preserve whitespace args = parse_map_cmd(ea.arg, arena); } else if (ea.argt & EX_NOSPC) { @@ -181,7 +181,10 @@ Dict(cmd) nvim_parse_cmd(String str, Dict(empty) *opts, Arena *arena, Error *err cmd = USER_CMD_GA(&curbuf->b_ucmds, ea.useridx); } - char *name = (cmd != NULL ? cmd->uc_name : get_command_name(NULL, ea.cmdidx)); + // For range-only (:1) or modifier-only (:aboveleft) commands, cmd is empty string. + char *name = ea.cmdidx == CMD_SIZE + ? "" : (cmd != NULL ? cmd->uc_name : get_command_name(NULL, ea.cmdidx)); + PUT_KEY(result, cmd, cmd, cstr_as_string(name)); if ((ea.argt & EX_RANGE) && ea.addr_count > 0) { @@ -373,9 +376,13 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Arena VALIDATE_R(HAS_KEY(cmd, cmd, cmd), "cmd", { goto end; }); - VALIDATE_EXP((cmd->cmd.data[0] != NUL), "cmd", "non-empty String", NULL, { - goto end; - }); + + if (cmd->cmd.data[0] == NUL) { + VALIDATE_EXP((HAS_KEY(cmd, cmd, range) && cmd->range.size > 0) || HAS_KEY(cmd, cmd, mods), + "cmd", "non-empty String", NULL, { + goto end; + }); + } cmdname = arena_string(arena, cmd->cmd).data; ea.cmd = cmdname; @@ -393,22 +400,41 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Arena p = (ret && !aborting()) ? find_ex_command(&ea, NULL) : ea.cmd; } - VALIDATE((p != NULL && ea.cmdidx != CMD_SIZE), "Command not found: %s", cmdname, { + // Commands such as ":1" are "range only" commands. + bool range_only = ea.cmdidx == CMD_SIZE && cmd->cmd.data[0] == NUL + && HAS_KEY(cmd, cmd, range) && cmd->range.size > 0; + + // modifier only + if (ea.cmdidx == CMD_SIZE && cmd->cmd.data[0] == NUL + && (!HAS_KEY(cmd, cmd, range) || cmd->range.size == 0) + && HAS_KEY(cmd, cmd, mods)) { goto end; - }); - VALIDATE(!is_cmd_ni(ea.cmdidx), "Command not implemented: %s", cmdname, { - goto end; - }); - const char *fullname = IS_USER_CMDIDX(ea.cmdidx) - ? get_user_command_name(ea.useridx, ea.cmdidx) - : get_command_name(NULL, ea.cmdidx); - VALIDATE(strncmp(fullname, cmdname, strlen(cmdname)) == 0, "Invalid command: \"%s\"", cmdname, { + } + // Allow CMD_SIZE only for range-only commands (empty cmd with range) + VALIDATE((p != NULL && ea.cmdidx != CMD_SIZE) || range_only, + "Command not found: %s", cmdname, { goto end; }); - // 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)) { + VALIDATE(range_only || !is_cmd_ni(ea.cmdidx), "Command not implemented: %s", cmdname, { + goto end; + }); + + if (!range_only) { + const char *fullname = IS_USER_CMDIDX(ea.cmdidx) + ? get_user_command_name(ea.useridx, ea.cmdidx) + : get_command_name(NULL, ea.cmdidx); + VALIDATE(strncmp(fullname, cmdname, strlen(cmdname)) == 0, + "Invalid command: \"%s\"", cmdname, { + goto end; + }); + } + + if (range_only) { + ea.argt = EX_RANGE | EX_SBOXOK; + } else if (!IS_USER_CMDIDX(ea.cmdidx)) { + // 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. ea.argt = get_cmd_argt(ea.cmdidx); } @@ -510,9 +536,11 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Arena } } - // 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 (!range_only) { + // 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, cmd, range)) { VALIDATE_MOD((ea.argt & EX_RANGE), "range", cmd->cmd.data); diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 22e6b744f6..b49730da78 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -1553,11 +1553,13 @@ bool parse_cmdline(char **cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, const ch .cookie = NULL, }; - // Parse command modifiers - if (parse_command_modifiers(eap, errormsg, &cmdinfo->cmdmod, false) == FAIL) { + char *orig_cmd = eap->cmd; + // If parse command modifiers failed but modifiers were passed, continue + int result = parse_command_modifiers(eap, errormsg, &cmdinfo->cmdmod, false); + after_modifier = eap->cmd; + if (result == FAIL && after_modifier == orig_cmd) { goto end; } - after_modifier = eap->cmd; // We need the command name to know what kind of range it uses. char *p = find_excmd_after_range(eap); @@ -1574,10 +1576,28 @@ bool parse_cmdline(char **cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, const ch // 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 == '"') { + // Fail if command is a comment + if (*eap->cmd == '"') { goto end; } + // Fail only if: empty command AND no range AND no modifier + if (*eap->cmd == NUL && eap->addr_count == 0 && after_modifier == *cmdline) { + goto end; + } + + // Allow range-only (:1) or modifier-only (:aboveleft) commands. + if (*eap->cmd == NUL && eap->cmdidx == CMD_SIZE) { + eap->arg = eap->cmd; + if (eap->addr_count > 0) { + eap->argt = EX_RANGE; + } else { + eap->argt = 0; + eap->addr_type = ADDR_NONE; + } + retval = true; + goto end; + } + // Fail if command is invalid if (eap->cmdidx == CMD_SIZE) { xstrlcpy(IObuff, _(e_not_an_editor_command), IOSIZE); @@ -1823,6 +1843,10 @@ int execute_cmd(exarg_T *eap, CmdParseInfo *cmdinfo, bool preview) } correct_range(eap); + if (eap->cmdidx == CMD_SIZE && eap->addr_count > 0) { + errormsg = ex_range_without_command(eap); + goto end; + } if (((eap->argt & EX_WHOLEFOLD) || eap->addr_count >= 2) && !global_busy && eap->addr_type == ADDR_LINES) { @@ -2517,9 +2541,13 @@ int parse_command_modifiers(exarg_T *eap, const char **errormsg, cmdmod_T *cmod, // typing ":cmdmod cmd" in Visual mode works without having to move the // range to after the modifiers. The command will be "'<,'>cmdmod cmd", // parse "cmdmod cmd" and then put back "'<,'>" before "cmd" below. - eap->cmd += 5; - cmd_start = eap->cmd; - has_visual_range = true; + // Only skip '<,'>' if there's a command after it + const char *p = skipwhite(eap->cmd + 5); + if (*p != NUL && *p != '|') { + eap->cmd += 5; + cmd_start = eap->cmd; + has_visual_range = true; + } } // Repeat until no more command modifiers are found. diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 196165ea90..d69c99b61b 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -4979,6 +4979,66 @@ describe('API', function() result = api.nvim_parse_cmd('copen 5', {}) eq(5, result.count) end) + it('parses range-only command', function() + insert [[ + line1 + line2 + line3 + line4 + ]] + api.nvim_win_set_cursor(0, { 4, 4 }) + local res = api.nvim_parse_cmd('1', {}) + eq({ + addr = 'line', + args = {}, + bang = false, + cmd = '', + magic = { + bar = false, + file = false, + }, + mods = { + browse = false, + confirm = false, + emsg_silent = false, + filter = { + force = false, + pattern = '', + }, + hide = false, + horizontal = false, + keepalt = false, + keepjumps = false, + keepmarks = false, + keeppatterns = false, + lockmarks = false, + noautocmd = false, + noswapfile = false, + sandbox = false, + silent = false, + split = '', + tab = -1, + unsilent = false, + verbose = -1, + vertical = false, + }, + nargs = '0', + nextcmd = '', + range = { 1 }, + }, res) + api.nvim_cmd(res, {}) + eq(1, api.nvim_win_get_cursor(0)[1]) + feed('VG:') + n.poke_eventloop() + res = api.nvim_parse_cmd("'<,'>", {}) + eq({ 1, 5 }, res.range) + end) + it('parses modifier-only command', function() + local res = api.nvim_parse_cmd('aboveleft', {}) + eq('', res.cmd) + eq('aboveleft', res.mods.split) + eq('none', res.addr) + end) end) describe('nvim_cmd', function() @@ -5209,6 +5269,9 @@ describe('API', function() -- error from the next command typed is not suppressed #21420 feed(':call') eq('E471: Argument required', api.nvim_cmd({ cmd = 'messages' }, { output = true })) + + -- modifier only + eq('', api.nvim_cmd({ cmd = '', mods = { noautocmd = true } }, {})) end) it('works with magic.file', function()