diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index a79e8e6f32..37d98afd58 100644 --- 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 index 0bdf19300d..b4726f1120 100644 --- 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 index bddea7e4f5..beb3d0f34d 100644 --- 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 ') eq({