commit 76d0206342a2255fecf035b3d74253632b8cb86d
parent 6f632a8615e03847b05051f0783a768b81a30c3e
Author: glepnir <glephunter@gmail.com>
Date: Mon, 9 Jun 2025 21:50:26 +0800
fix(api): count parameter in nvim_parse_cmd, nvim_cmd #34253
Problem:
- nvim_parse_cmd('copen', {}) returns count: 0, causing nvim_cmd to override default behavior
- nvim_cmd({cmd = 'copen', args = {10}}, {}) fails with "Wrong number of arguments"
Solution:
- Only include count field in parse result when explicitly provided or non-zero
- Interpret single numeric argument as count for count-only commands like copen
Diffstat:
2 files changed, 154 insertions(+), 53 deletions(-)
diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c
@@ -165,7 +165,12 @@ Dict(cmd) nvim_parse_cmd(String str, Dict(empty) *opts, Arena *arena, Error *err
if (ea.argt & EX_COUNT) {
Integer count = ea.addr_count > 0 ? ea.line2 : (cmd != NULL ? cmd->uc_def : 0);
- PUT_KEY(result, cmd, count, count);
+ // For built-in commands, if count is not explicitly provided and the default value is 0,
+ // do not include the count field in the result, so the command uses its built-in default
+ // behavior.
+ if (ea.addr_count > 0 || (cmd != NULL && cmd->uc_def != 0) || count != 0) {
+ PUT_KEY(result, cmd, count, count);
+ }
}
if (ea.argt & EX_REGSTR) {
@@ -377,68 +382,102 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Arena
ea.argt = get_cmd_argt(ea.cmdidx);
}
+ // Track whether the first argument was interpreted as count to avoid conflicts
+ bool count_from_first_arg = false;
// Parse command arguments since it's needed to get the command address type.
if (HAS_KEY(cmd, cmd, args)) {
- // Process all arguments. Convert non-String arguments to String and check if String arguments
- // have non-whitespace characters.
- args = arena_array(arena, cmd->args.size);
- for (size_t i = 0; i < cmd->args.size; i++) {
- Object elem = cmd->args.items[i];
- char *data_str;
-
- switch (elem.type) {
- case kObjectTypeBoolean:
- data_str = arena_alloc(arena, 2, false);
- data_str[0] = elem.data.boolean ? '1' : '0';
- data_str[1] = NUL;
- ADD_C(args, CSTR_AS_OBJ(data_str));
+ // Special handling: for commands that support count but not regular arguments,
+ // if a single numeric argument is provided, interpret it as count
+ if (cmd->args.size == 1 && (ea.argt & EX_COUNT) && !(ea.argt & EX_EXTRA)) {
+ Object first_arg = cmd->args.items[0];
+ bool is_numeric = false;
+ int64_t count_value = 0;
+
+ if (first_arg.type == kObjectTypeInteger) {
+ is_numeric = true;
+ count_value = first_arg.data.integer;
+ } else if (first_arg.type == kObjectTypeString) {
+ // Try to parse string as a number Example: vim.api.nvim_cmd({cmd = 'copen', args = {'10'}}, {})
+ char *endptr;
+ long val = strtol(first_arg.data.string.data, &endptr, 10);
+ // Check if entire string was consumed (valid number) and string is not empty
+ if (*endptr == '\0' && first_arg.data.string.size > 0) {
+ is_numeric = true;
+ count_value = val;
+ }
+ }
+
+ if (is_numeric && count_value >= 0) {
+ // Interpret the argument as count
+ count_from_first_arg = true;
+ ea.addr_count = 1;
+ ea.line1 = ea.line2 = (linenr_T)count_value;
+ args = arena_array(arena, 0);
+ }
+ }
+
+ if (!count_from_first_arg) {
+ // Process all arguments. Convert non-String arguments to String and check if String arguments
+ // have non-whitespace characters.
+ args = arena_array(arena, cmd->args.size);
+ for (size_t i = 0; i < cmd->args.size; i++) {
+ Object elem = cmd->args.items[i];
+ char *data_str;
+
+ switch (elem.type) {
+ case kObjectTypeBoolean:
+ data_str = arena_alloc(arena, 2, false);
+ data_str[0] = elem.data.boolean ? '1' : '0';
+ data_str[1] = NUL;
+ ADD_C(args, CSTR_AS_OBJ(data_str));
+ break;
+ case kObjectTypeBuffer:
+ case kObjectTypeWindow:
+ case kObjectTypeTabpage:
+ case kObjectTypeInteger:
+ data_str = arena_alloc(arena, NUMBUFLEN, false);
+ snprintf(data_str, NUMBUFLEN, "%" PRId64, elem.data.integer);
+ ADD_C(args, CSTR_AS_OBJ(data_str));
+ break;
+ case kObjectTypeString:
+ VALIDATE_EXP(!string_iswhite(elem.data.string), "command arg", "non-whitespace", NULL, {
+ goto end;
+ });
+ ADD_C(args, elem);
+ break;
+ default:
+ VALIDATE_EXP(false, "command arg", "valid type", api_typename(elem.type), {
+ goto end;
+ });
+ break;
+ }
+ }
+
+ bool argc_valid;
+
+ // Check if correct number of arguments is used.
+ switch (ea.argt & (EX_EXTRA | EX_NOSPC | EX_NEEDARG)) {
+ case EX_EXTRA | EX_NOSPC | EX_NEEDARG:
+ argc_valid = args.size == 1;
break;
- case kObjectTypeBuffer:
- case kObjectTypeWindow:
- case kObjectTypeTabpage:
- case kObjectTypeInteger:
- data_str = arena_alloc(arena, NUMBUFLEN, false);
- snprintf(data_str, NUMBUFLEN, "%" PRId64, elem.data.integer);
- ADD_C(args, CSTR_AS_OBJ(data_str));
+ case EX_EXTRA | EX_NOSPC:
+ argc_valid = args.size <= 1;
break;
- case kObjectTypeString:
- VALIDATE_EXP(!string_iswhite(elem.data.string), "command arg", "non-whitespace", NULL, {
- goto end;
- });
- ADD_C(args, elem);
+ case EX_EXTRA | EX_NEEDARG:
+ argc_valid = args.size >= 1;
+ break;
+ case EX_EXTRA:
+ argc_valid = true;
break;
default:
- VALIDATE_EXP(false, "command arg", "valid type", api_typename(elem.type), {
- goto end;
- });
+ argc_valid = args.size == 0;
break;
}
- }
-
- bool argc_valid;
- // Check if correct number of arguments is used.
- switch (ea.argt & (EX_EXTRA | EX_NOSPC | EX_NEEDARG)) {
- case EX_EXTRA | EX_NOSPC | EX_NEEDARG:
- argc_valid = args.size == 1;
- break;
- case EX_EXTRA | EX_NOSPC:
- argc_valid = args.size <= 1;
- break;
- case EX_EXTRA | EX_NEEDARG:
- argc_valid = args.size >= 1;
- break;
- case EX_EXTRA:
- argc_valid = true;
- break;
- default:
- argc_valid = args.size == 0;
- break;
+ VALIDATE(argc_valid, "%s", "Wrong number of arguments", {
+ goto end;
+ });
}
-
- VALIDATE(argc_valid, "%s", "Wrong number of arguments", {
- goto end;
- });
}
// Simply pass the first argument (if it exists) as the arg pointer to `set_cmd_addr_type()`
@@ -485,6 +524,9 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Arena
}
if (HAS_KEY(cmd, cmd, count)) {
+ VALIDATE(!count_from_first_arg, "%s", "Cannot specify both 'count' and numeric argument", {
+ goto end;
+ });
VALIDATE_MOD((ea.argt & EX_COUNT), "count", cmd->cmd.data);
VALIDATE_EXP((cmd->count >= 0), "count", "non-negative Integer", NULL, {
goto end;
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
@@ -4783,6 +4783,14 @@ describe('API', function()
)
eq('', eval('v:errmsg'))
end)
+ it('does not include count field when no count provided for builtin commands', function()
+ local result = api.nvim_parse_cmd('copen', {})
+ eq(nil, result.count)
+ api.nvim_cmd(result, {})
+ eq(10, api.nvim_win_get_height(0))
+ result = api.nvim_parse_cmd('copen 5', {})
+ eq(5, result.count)
+ end)
end)
describe('nvim_cmd', function()
@@ -5396,6 +5404,57 @@ describe('API', function()
-- Clean up
os.remove('Xfile')
end)
+ it('interprets numeric args as count for count-only commands', function()
+ api.nvim_cmd({ cmd = 'copen', args = { 8 } }, {})
+ local height1 = api.nvim_win_get_height(0)
+ command('cclose')
+ api.nvim_cmd({ cmd = 'copen', count = 8 }, {})
+ local height2 = api.nvim_win_get_height(0)
+ command('cclose')
+ eq(height1, height2)
+
+ exec_lua 'vim.cmd.copen(5)'
+ height2 = api.nvim_win_get_height(0)
+ command('cclose')
+ eq(5, height2)
+
+ -- should reject both count and numeric arg
+ eq(
+ "Cannot specify both 'count' and numeric argument",
+ pcall_err(api.nvim_cmd, { cmd = 'copen', args = { 5 }, count = 10 }, {})
+ )
+ end)
+ it('handles string numeric arguments correctly', function()
+ -- Valid string numbers should work
+ api.nvim_cmd({ cmd = 'copen', args = { '6' } }, {})
+ eq(6, api.nvim_win_get_height(0))
+ command('cclose')
+ -- Invalid strings should be rejected
+ eq(
+ 'Wrong number of arguments',
+ pcall_err(api.nvim_cmd, { cmd = 'copen', args = { 'abc' } }, {})
+ )
+ -- Partial numbers should be rejected
+ eq(
+ 'Wrong number of arguments',
+ pcall_err(api.nvim_cmd, { cmd = 'copen', args = { '8abc' } }, {})
+ )
+ -- Empty string should be rejected
+ eq(
+ 'Invalid command arg: expected non-whitespace',
+ pcall_err(api.nvim_cmd, { cmd = 'copen', args = { '' } }, {})
+ )
+ -- Negative string numbers should be rejected
+ eq(
+ 'Wrong number of arguments',
+ pcall_err(api.nvim_cmd, { cmd = 'copen', args = { '-5' } }, {})
+ )
+ -- Leading/trailing spaces should be rejected
+ eq(
+ 'Wrong number of arguments',
+ pcall_err(api.nvim_cmd, { cmd = 'copen', args = { ' 5 ' } }, {})
+ )
+ end)
end)
it('nvim__redraw', function()