commit c7f38e3bc8fb99a490a0575ed7ce0624bd7b975a
parent 4367441213de95b178c16589e80fdbcec10c010b
Author: glepnir <glephunter@gmail.com>
Date: Sun, 15 Jun 2025 01:17:56 +0800
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.
Diffstat:
3 files changed, 89 insertions(+), 3 deletions(-)
diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c
@@ -15,6 +15,7 @@
#include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h"
#include "nvim/buffer_defs.h"
+#include "nvim/charset.h"
#include "nvim/cmdexpand_defs.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
@@ -40,6 +41,31 @@
# include "api/command.c.generated.h"
#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.
///
/// 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;
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) {
+ // 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,
+ // otherwise split arguments by whitespace.
if (*ea.arg != NUL) {
args = arena_array(arena, 1);
ADD_C(args, STRING_OBJ(cstrn_as_string(ea.arg, length)));
diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c
@@ -8187,3 +8187,18 @@ uint32_t get_cmd_argt(cmdidx_T cmdidx)
{
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.
+}
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
@@ -4641,6 +4641,45 @@ describe('API', function()
},
}, api.nvim_parse_cmd('argadd a.txt | argadd b.txt', {}))
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()
command('command -nargs=1 MyCommand echo <q-args>')
eq({