diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index 67f8f98fcf..a79e8e6f32 100644 --- 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; + // 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; - 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; + 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); } } - bool argc_valid; + 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; - // 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; + 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 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 index 0567aa411c..8f5b5aa2e1 100644 --- 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()