mirror of
https://github.com/neovim/neovim.git
synced 2026-03-28 03:12:00 +00:00
fix(api): nvim_parse_cmd on range-only, modifier-only commands #36665
Problem: nvim_parse_cmd rejects valid commands like `:1` (range-only) or `aboveleft` (modifier-only). Solution: allow empty command when range or modifiers exist, and handle execution using existing range command logic.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<CR><CR>')
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user