From 72b0bfa1fb7e897e5126aabae718a5480f466b9e Mon Sep 17 00:00:00 2001 From: glepnir Date: Mon, 13 Oct 2025 07:36:06 +0800 Subject: [PATCH] fix(api): nvim_parse_cmd handle nextcmd for commands without EX_TRLBAR (#36055) Problem: nvim_parse_cmd('exe "ls"|edit foo', {}) fails to separate nextcmd, returning args as { '"ls"|edit', 'foo' } instead of { '"ls"' } with nextcmd='edit foo'. Solution: Skip expressions before checking for '|' separator. --- src/nvim/eval.c | 6 +----- src/nvim/ex_docmd.c | 27 +++++++++++++++++++++++++++ test/functional/api/vim_spec.lua | 6 ++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index b7737f74d3..feae0a7ef0 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -1986,11 +1986,7 @@ void set_context_for_expression(expand_T *xp, char *arg, cmdidx_T cmdidx) } // ":exe one two" completes "two" - if ((cmdidx == CMD_execute - || cmdidx == CMD_echo - || cmdidx == CMD_echon - || cmdidx == CMD_echomsg) - && xp->xp_context == EXPAND_EXPRESSION) { + if (cmd_has_expr_args(cmdidx) && xp->xp_context == EXPAND_EXPRESSION) { while (true) { char *const n = skiptowhite(arg); diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index cc2d87b90e..9eb82b7065 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -13,6 +13,7 @@ #include "auto/config.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" #include "nvim/api/ui.h" #include "nvim/api/vimscript.h" @@ -1507,6 +1508,16 @@ static bool parse_bang(const exarg_T *eap, char **p) return false; } +/// Check if command expects expression arguments that need special parsing +bool cmd_has_expr_args(cmdidx_T cmdidx) +{ + return cmdidx == CMD_execute + || cmdidx == CMD_echo + || cmdidx == CMD_echon + || cmdidx == CMD_echomsg + || cmdidx == CMD_echoerr; +} + /// Parse command line and return information about the first command. /// If parsing is done successfully, need to free cmod_filter_pat and cmod_filter_regmatch.regprog /// after calling, usually done using undo_cmdmod() or execute_cmd(). @@ -1596,6 +1607,22 @@ bool parse_cmdline(char **cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, const ch // Don't do this for ":read !cmd" and ":write !cmd". if ((eap->argt & EX_TRLBAR)) { separate_nextcmd(eap); + } else if (cmd_has_expr_args(eap->cmdidx)) { + // For commands without EX_TRLBAR, check for '|' separator + // by skipping over expressions (including string literals) + char *arg = eap->arg; + while (*arg != NUL && *arg != '|' && *arg != '\n') { + char *start = arg; + skip_expr(&arg, NULL); + // If skip_expr didn't advance, move forward to avoid infinite loop + if (arg == start) { + arg++; + } + } + if (*arg == '|' || *arg == '\n') { + eap->nextcmd = check_nextcmd(arg); + *arg = NUL; + } } // Fail if command doesn't support bang but is used with a bang if (!(eap->argt & EX_BANG) && eap->forceit) { diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 81f1da8109..35b4c21a76 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -4849,6 +4849,12 @@ describe('API', function() result = api.nvim_parse_cmd('copen 5', {}) eq(5, result.count) end) + it('parses nextcmd for commands #36029', function() + local result = api.nvim_parse_cmd('exe "ls"|edit foo', {}) + eq({ '"ls"' }, result.args) + eq('execute', result.cmd) + eq('edit foo', result.nextcmd) + end) end) describe('nvim_cmd', function()