From 1e7406fa38eef8cb9812272196a97cf530218c4e Mon Sep 17 00:00:00 2001 From: glepnir Date: Mon, 5 May 2025 20:58:36 +0800 Subject: [PATCH] feat(api): nvim_cmd supports plus ("+cmd", "++opt") flags #30103 Problem: nvim_cmd does not handle plus flags. Solution: In nvim_cmd, parse the flags and set the relevant `ea` fields. --- src/nvim/api/command.c | 16 +++ src/nvim/ex_docmd.c | 4 +- test/functional/api/vim_spec.lua | 163 +++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 2 deletions(-) diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index bd54cf7d19..67f8f98fcf 100644 --- a/src/nvim/api/command.c +++ b/src/nvim/api/command.c @@ -634,6 +634,22 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Arena build_cmdline_str(&cmdline, &ea, &cmdinfo, args); ea.cmdlinep = &cmdline; + // Check for "++opt=val" argument. + if (ea.argt & EX_ARGOPT) { + while (ea.arg[0] == '+' && ea.arg[1] == '+') { + char *orig_arg = ea.arg; + int result = getargopt(&ea); + VALIDATE_S(result != FAIL || is_cmd_ni(ea.cmdidx), "argument ", orig_arg, { + goto end; + }); + } + } + + // Check for "+command" argument. + if ((ea.argt & EX_CMDARG) && !ea.usefilter) { + ea.do_ecmd_cmd = getargcmd(&ea.arg); + } + garray_T capture_local; const int save_msg_silent = msg_silent; garray_T * const save_capture_ga = capture_ga; diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index acce909d2a..0629357bdf 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -4147,7 +4147,7 @@ void separate_nextcmd(exarg_T *eap) } /// get + command from ex argument -static char *getargcmd(char **argp) +char *getargcmd(char **argp) { char *arg = *argp; char *command = NULL; @@ -4222,7 +4222,7 @@ static char *get_bad_name(expand_T *xp FUNC_ATTR_UNUSED, int idx) /// Get "++opt=arg" argument. /// /// @return FAIL or OK. -static int getargopt(exarg_T *eap) +int getargopt(exarg_T *eap) { char *arg = eap->arg + 2; int *pp = NULL; diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 3e18fb357b..7dae67c723 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -5233,6 +5233,169 @@ describe('API', function() assert_alive() eq(false, exec_lua('return _G.success')) end) + + it('handles +flags correctly', function() + -- Write a file for testing +flags + t.write_file('testfile', 'Line 1\nLine 2\nLine 3', false, false) + + -- Test + command (go to the last line) + local result = exec_lua([[ + local parsed = vim.api.nvim_parse_cmd('edit + testfile', {}) + vim.cmd(parsed) + return { vim.fn.line('.'), parsed.args, parsed.cmd } + ]]) + eq({ 3, { '+ testfile' }, 'edit' }, result) + + -- Test +{num} command (go to line number) + result = exec_lua([[ + vim.cmd(vim.api.nvim_parse_cmd('edit +1 testfile', {})) + return vim.fn.line('.') + ]]) + eq(1, result) + + -- Test +/{pattern} command (go to line with pattern) + result = exec_lua([[ + local parsed = vim.api.nvim_parse_cmd('edit +/Line\\ 2 testfile', {}) + vim.cmd(parsed) + return {vim.fn.line('.'), parsed.args} + ]]) + eq({ 2, { '+/Line\\ 2 testfile' } }, result) + + -- Test +{command} command (execute a command after opening the file) + result = exec_lua([[ + vim.cmd(vim.api.nvim_parse_cmd('edit +set\\ nomodifiable testfile', {})) + return vim.bo.modifiable + ]]) + eq(false, result) + + -- Test ++ flags structure in parsed command + result = exec_lua([[ + local parsed = vim.api.nvim_parse_cmd('botright edit + testfile', {}) + vim.cmd(parsed) + return { vim.fn.line('.'), parsed.cmd, parsed.args, parsed.mods.split } + ]]) + eq({ 3, 'edit', { '+ testfile' }, 'botright' }, result) + + -- Clean up + os.remove('testfile') + end) + + it('handles various ++ flags correctly', function() + -- Test ++ff flag + local result = exec_lua [[ + local parsed = vim.api.nvim_parse_cmd('edit ++ff=mac test_ff_mac.txt', {}) + vim.cmd(parsed) + return parsed.args + ]] + eq({ '++ff=mac test_ff_mac.txt' }, result) + eq('mac', api.nvim_get_option_value('fileformat', {})) + eq('test_ff_mac.txt', fn.fnamemodify(api.nvim_buf_get_name(0), ':t')) + + exec_lua [[ + vim.cmd(vim.api.nvim_parse_cmd('edit ++fileformat=unix test_ff_unix.txt', {})) + ]] + eq('unix', api.nvim_get_option_value('fileformat', {})) + eq('test_ff_unix.txt', fn.fnamemodify(api.nvim_buf_get_name(0), ':t')) + + -- Test ++enc flag + exec_lua [[ + vim.cmd(vim.api.nvim_parse_cmd('edit ++enc=utf-32 test_enc.txt', {})) + ]] + eq('ucs-4', api.nvim_get_option_value('fileencoding', {})) + eq('test_enc.txt', fn.fnamemodify(api.nvim_buf_get_name(0), ':t')) + + -- Test ++bin and ++nobin flags + exec_lua [[ + vim.cmd(vim.api.nvim_parse_cmd('edit ++bin test_bin.txt', {})) + ]] + eq(true, api.nvim_get_option_value('binary', {})) + eq('test_bin.txt', fn.fnamemodify(api.nvim_buf_get_name(0), ':t')) + + exec_lua [[ + vim.cmd(vim.api.nvim_parse_cmd('edit ++nobin test_nobin.txt', {})) + ]] + eq(false, api.nvim_get_option_value('binary', {})) + eq('test_nobin.txt', fn.fnamemodify(api.nvim_buf_get_name(0), ':t')) + + -- Test multiple flags together + exec_lua [[ + vim.cmd(vim.api.nvim_parse_cmd('edit ++ff=mac ++enc=utf-32 ++bin test_multi.txt', {})) + ]] + eq(true, api.nvim_get_option_value('binary', {})) + eq('mac', api.nvim_get_option_value('fileformat', {})) + eq('ucs-4', api.nvim_get_option_value('fileencoding', {})) + eq('test_multi.txt', fn.fnamemodify(api.nvim_buf_get_name(0), ':t')) + end) + + it('handles invalid and incorrect ++ flags gracefully', function() + -- Test invalid ++ff flag + local result = exec_lua [[ + local cmd = vim.api.nvim_parse_cmd('edit ++ff=invalid test_invalid_ff.txt', {}) + local _, err = pcall(vim.cmd, cmd) + return err + ]] + matches("Invalid argument : '%+%+ff=invalid'$", result) + + -- Test incorrect ++ syntax + result = exec_lua [[ + local cmd = vim.api.nvim_parse_cmd('edit ++unknown=test_unknown.txt', {}) + local _, err = pcall(vim.cmd, cmd) + return err + ]] + matches("Invalid argument : '%+%+unknown=test_unknown.txt'$", result) + + -- Test invalid ++bin flag + result = exec_lua [[ + local cmd = vim.api.nvim_parse_cmd('edit ++binabc test_invalid_bin.txt', {}) + local _, err = pcall(vim.cmd, cmd) + return err + ]] + matches("Invalid argument : '%+%+binabc test_invalid_bin.txt'$", result) + end) + + it('handles ++p for creating parent directory', function() + exec_lua [[ + vim.cmd('edit flags_dir/test_create.txt') + vim.cmd(vim.api.nvim_parse_cmd('write! ++p', {})) + ]] + eq(true, fn.isdirectory('flags_dir') == 1) + fn.delete('flags_dir', 'rf') + end) + + it('tests editing files with bad utf8 sequences', function() + -- Write a file with bad utf8 sequences + local file = io.open('Xfile', 'wb') + file:write('[\255][\192][\226\137\240][\194\194]') + file:close() + + exec_lua([[ + vim.cmd(vim.api.nvim_parse_cmd('edit! ++enc=utf8 Xfile', {})) + ]]) + eq('[?][?][???][??]', api.nvim_get_current_line()) + + exec_lua([[ + vim.cmd(vim.api.nvim_parse_cmd('edit! ++enc=utf8 ++bad=_ Xfile', {})) + ]]) + eq('[_][_][___][__]', api.nvim_get_current_line()) + + exec_lua([[ + vim.cmd(vim.api.nvim_parse_cmd('edit! ++enc=utf8 ++bad=drop Xfile', {})) + ]]) + eq('[][][][]', api.nvim_get_current_line()) + + exec_lua([[ + vim.cmd(vim.api.nvim_parse_cmd('edit! ++enc=utf8 ++bad=keep Xfile', {})) + ]]) + eq('[\255][\192][\226\137\240][\194\194]', api.nvim_get_current_line()) + + local result = exec_lua([[ + local _, err = pcall(vim.cmd, vim.api.nvim_parse_cmd('edit ++enc=utf8 ++bad=foo Xfile', {})) + return err + ]]) + matches("Invalid argument : '%+%+bad=foo'$", result) + -- Clean up + os.remove('Xfile') + end) end) it('nvim__redraw', function()