fix(api): nvim_parse_cmd parses :map incorrectly #34068

Problem: nvim_parse_cmd() incorrectly splits mapping commands like
into three arguments instead of preserving whitespace in the RHS.

Solution: Add special handling for mapping commands to parse them as exactly
two arguments - the LHS and the RHS with all whitespace preserved.
This commit is contained in:
glepnir
2025-06-15 01:17:56 +08:00
committed by GitHub
parent 4367441213
commit c7f38e3bc8
3 changed files with 89 additions and 3 deletions

View File

@@ -15,6 +15,7 @@
#include "nvim/autocmd.h" #include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h" #include "nvim/autocmd_defs.h"
#include "nvim/buffer_defs.h" #include "nvim/buffer_defs.h"
#include "nvim/charset.h"
#include "nvim/cmdexpand_defs.h" #include "nvim/cmdexpand_defs.h"
#include "nvim/ex_cmds_defs.h" #include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h" #include "nvim/ex_docmd.h"
@@ -40,6 +41,31 @@
# include "api/command.c.generated.h" # include "api/command.c.generated.h"
#endif #endif
/// Parse arguments for :map/:abbrev commands, preserving whitespace in RHS.
/// @param arg_str The argument string to parse
/// @param arena Arena allocator
/// @return Array with at most 2 elements: [lhs, rhs]
static Array parse_map_cmd(const char *arg_str, Arena *arena)
{
Array args = arena_array(arena, 2);
char *lhs_start = (char *)arg_str;
char *lhs_end = skiptowhite(lhs_start);
size_t lhs_len = (size_t)(lhs_end - lhs_start);
// Add the LHS (first argument)
ADD_C(args, STRING_OBJ(cstrn_as_string(lhs_start, lhs_len)));
// Add the RHS (second argument) if it exists, preserving all whitespace
char *rhs_start = skipwhite(lhs_end);
if (*rhs_start != NUL) {
size_t rhs_len = strlen(rhs_start);
ADD_C(args, STRING_OBJ(cstrn_as_string(rhs_start, rhs_len)));
}
return args;
}
/// Parse command line. /// Parse command line.
/// ///
/// Doesn't check the validity of command arguments. /// Doesn't check the validity of command arguments.
@@ -121,9 +147,15 @@ Dict(cmd) nvim_parse_cmd(String str, Dict(empty) *opts, Arena *arena, Error *err
Array args = ARRAY_DICT_INIT; Array args = ARRAY_DICT_INIT;
size_t length = strlen(ea.arg); size_t length = strlen(ea.arg);
// 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) {
// For mapping commands, split differently to preserve whitespace
args = parse_map_cmd(ea.arg, arena);
} else if (ea.argt & EX_NOSPC) {
// For nargs = 1 or '?', pass the entire argument list as a single argument, // For nargs = 1 or '?', pass the entire argument list as a single argument,
// otherwise split arguments by whitespace. // otherwise split arguments by whitespace.
if (ea.argt & EX_NOSPC) {
if (*ea.arg != NUL) { if (*ea.arg != NUL) {
args = arena_array(arena, 1); args = arena_array(arena, 1);
ADD_C(args, STRING_OBJ(cstrn_as_string(ea.arg, length))); ADD_C(args, STRING_OBJ(cstrn_as_string(ea.arg, length)));

View File

@@ -8187,3 +8187,18 @@ uint32_t get_cmd_argt(cmdidx_T cmdidx)
{ {
return cmdnames[(int)cmdidx].cmd_argt; return cmdnames[(int)cmdidx].cmd_argt;
} }
/// Check if a command is a :map/:abbrev command.
bool is_map_cmd(cmdidx_T cmdidx)
{
if (IS_USER_CMDIDX(cmdidx)) {
return false;
}
ex_func_T func = cmdnames[cmdidx].cmd_func;
return func == ex_map // :map, :nmap, :noremap, etc.
|| func == ex_unmap // :unmap, :nunmap, etc.
|| func == ex_mapclear // :mapclear, :nmapclear, etc.
|| func == ex_abbreviate // :abbreviate, :iabbrev, etc.
|| func == ex_abclear; // :abclear, :iabclear, etc.
}

View File

@@ -4641,6 +4641,45 @@ describe('API', function()
}, },
}, api.nvim_parse_cmd('argadd a.txt | argadd b.txt', {})) }, api.nvim_parse_cmd('argadd a.txt | argadd b.txt', {}))
end) end)
it('parses :map commands with space in RHS', function()
eq({
addr = 'none',
args = { 'a', 'b c' },
bang = false,
cmd = 'map',
magic = {
bar = true,
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 = '*',
nextcmd = '',
}, api.nvim_parse_cmd('map a b c', {}))
end)
it('works for nargs=1', function() it('works for nargs=1', function()
command('command -nargs=1 MyCommand echo <q-args>') command('command -nargs=1 MyCommand echo <q-args>')
eq({ eq({