diff --git a/runtime/lua/vim/_core/cmdwin.lua b/runtime/lua/vim/_core/cmdwin.lua index e223354599..b83fb93472 100644 --- a/runtime/lua/vim/_core/cmdwin.lua +++ b/runtime/lua/vim/_core/cmdwin.lua @@ -18,12 +18,13 @@ local state = nil local cmdwin_types = { [':'] = true, ['/'] = true, ['?'] = true } --- Fills the cmdwin buffer with the cmdline history. +--- @return boolean filled Whether any lines were written. local function fill_history(buf, type) local histname = type == ':' and 'cmd' or (type == '/' or type == '?') and 'search' or nil assert(histname, 'cmdwin: unknown type: ' .. tostring(type)) local n = vim.fn.histnr(histname) if n <= 0 then -- May be -1 if history is empty. - return + return false end local lines = {} --- @type string[] for i = 1, n do @@ -33,9 +34,11 @@ local function fill_history(buf, type) lines[#lines + 1] = (h:gsub('\n', '\0')) end end - if #lines > 0 then - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + if #lines == 0 then + return false end + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + return true end --- Open the command-line window. @@ -71,16 +74,16 @@ function M.open(type, init_line, init_col) vim.bo[buf].buftype = 'nofile' vim.bo[buf].bufhidden = 'wipe' vim.bo[buf].swapfile = false - vim.bo[buf].buflisted = false + vim.bo[buf].buflisted = true -- #40431 vim.wo[win][0].winfixbuf = true vim.wo[win][0].foldenable = false -- Show cmdwin-char via 'statuscolumn'. - vim.wo[win][0].statuscolumn = '%#NonText#' .. type .. ' ' + vim.wo[win][0].statuscolumn = '%#NonText#' .. type - fill_history(buf, type) + local filled = fill_history(buf, type) - -- Append the in-flight cmdline (or empty placeholder) as the last line. - vim.api.nvim_buf_set_lines(buf, -1, -1, false, { init_line or '' }) + -- Append the in-flight cmdline as the last line (or only line if history is empty). + vim.api.nvim_buf_set_lines(buf, filled and -1 or 0, -1, false, { init_line or '' }) local last = vim.api.nvim_buf_line_count(buf) vim.api.nvim_win_set_cursor(win, { last, math.max(0, (init_col or 1) - 1) }) @@ -98,20 +101,6 @@ function M.open(type, init_line, init_col) caller_win = caller, } - -- Buffer-local mappings. - vim.keymap.set( - { 'n', 'i' }, - '', - [[lua require'vim._core.cmdwin'.confirm()]], - { buffer = buf, silent = true, desc = 'cmdwin: execute' } - ) - vim.keymap.set( - { 'n', 'i', 'x' }, - '', - [[lua require'vim._core.cmdwin'.cancel()]], - { buffer = buf, silent = true, desc = 'cmdwin: cancel' } - ) - -- Clean up if the window is closed by other means (`:q`, `:close`, etc.). vim.api.nvim_create_autocmd({ 'WinClosed' }, { buffer = buf, @@ -143,25 +132,30 @@ function M._cleanup() end end -local function _confirm() - if state == nil then - return - end +--- Closes the cmdwin and returns its current line and type. +--- @return string line, string type +local function _close() local line = vim.api.nvim_get_current_line() - local type = state.type + local type = assert(state).type M._cleanup() return line, type end --- Confirm and execute the current line as a cmdline. function M.confirm() - local line, type = _confirm() + if state == nil then -- Not in cmdwin (closed already?). + return + end + local line, type = _close() vim.api.nvim_feedkeys(type .. line .. vim.keycode(''), 'nt', false) end --- Cancel: close the cmdwin and re-enter cmdline mode with the line pre-filled (no execute). function M.cancel() - local line, type = _confirm() + if state == nil then -- Not in cmdwin (closed already?). + return + end + local line, type = _close() vim.api.nvim_feedkeys(type .. line, 'nt', false) end diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 77d0b134ae..1b9320c73a 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -28,6 +28,7 @@ #include "nvim/eval/vars.h" #include "nvim/ex_cmds_defs.h" #include "nvim/ex_docmd.h" +#include "nvim/ex_getln.h" #include "nvim/extmark.h" #include "nvim/extmark_defs.h" #include "nvim/fileio.h" @@ -740,6 +741,11 @@ static int insert_handle_key(InsertState *s) FALLTHROUGH; case Ctrl_C: // End input mode + if (s->c == Ctrl_C && bt_cmdwin(curbuf)) { + got_int = false; // Don't interrupt autocmds etc. + cmdwin_do_action("cancel"); // cmdwin: CTRL-C closes cmdwin, pre-fills cmdline. + return 0; // exit insert mode + } if (s->c == Ctrl_C && bt_prompt(curbuf)) { if (invoke_prompt_interrupt()) { if (!bt_prompt(curbuf)) { @@ -1100,6 +1106,10 @@ static int insert_handle_key(InsertState *s) } break; } + if (s->c == CAR && bt_cmdwin(curbuf)) { + cmdwin_do_action("confirm"); // cmdwin: executes the command under the cursor. + return 0; // exit insert mode + } if ((mod_mask & MOD_MASK_SHIFT) == 0 && bt_prompt(curbuf)) { prompt_invoke_callback(); if (!bt_prompt(curbuf)) { diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index b832f0126b..07f5a645ce 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1487,7 +1487,7 @@ static int command_line_execute(VimState *state, int key) // confirms/cancels cmdwin, it will reopen cmdline with the command pre-filled. if (ex_normal_busy == 0 && got_int == false) { s->c = open_cmdwin(); - s->some_key_typed = true; + s->some_key_typed = true; // Treat c_CTRL-F as "typed" to skip the wait-return prompt. } } else { s->c = do_digraph(s->c); @@ -4572,25 +4572,37 @@ const char *did_set_cedit(optset_T *args) } typedef struct { - int firstc; - char *content; // owned - int pos; + int firstc; ///< ':'/'/'/'?'. + char *content; ///< Initial cmdline (owned). + int pos; ///< Cursor column. } CmdwinOpenArgs; -/// Deferred event: opens cmdwin after the current cmdline-reader exits. (Can't use vim.schedule, -/// where the event could fire _during typeahead_; revisit after #40380). +/// Synchronously calls `vim._core.cmdwin.(...)`. +static void cmdwin_invoke(const char *action, int firstc, char *content, int pos) +{ + char fc[2] = { (char)firstc, 0 }; + typval_T tv_args[] = { + { .v_type = VAR_STRING, .vval.v_string = fc }, + { .v_type = VAR_STRING, .vval.v_string = content ? content : "" }, + { .v_type = VAR_NUMBER, .vval.v_number = pos + 1 }, + { .v_type = VAR_UNKNOWN }, + }; + nlua_call_vimfn("vim._core.cmdwin", action, firstc ? tv_args : tv_args + 3, NULL); + xfree(content); +} + +/// Calls `vim._core.cmdwin.()`, synchronously. +void cmdwin_do_action(const char *action) +{ + cmdwin_invoke(action, 0, NULL, 0); +} + +/// Deferred event: opens cmdwin after the cmdline-reader unwinds. Can't run synchronously (cmdline +/// is still being read), nor via vim.schedule (could fire _during typeahead_; revisit after #40380). static void open_cmdwin_event(void **argv) { CmdwinOpenArgs *a = argv[0]; - char fc[2] = { (char)a->firstc, 0 }; - typval_T tv_args[] = { - { .v_type = VAR_STRING, .vval.v_string = fc }, - { .v_type = VAR_STRING, .vval.v_string = a->content ? a->content : "" }, - { .v_type = VAR_NUMBER, .vval.v_number = a->pos + 1 }, - { .v_type = VAR_UNKNOWN }, - }; - nlua_call_vimfn("vim._core.cmdwin", "open", tv_args, NULL); - xfree(a->content); + cmdwin_invoke("open", a->firstc, a->content, a->pos); // frees a->content xfree(a); } @@ -4617,8 +4629,14 @@ static int open_cmdwin(void) CmdwinOpenArgs *a = xmalloc(sizeof(*a)); a->firstc = ft; - a->content = ccline.cmdbuff ? xstrnsave(ccline.cmdbuff, (size_t)ccline.cmdlen) : NULL; a->pos = ccline.cmdpos; + + // Capture the cmdline; will append to end of cmdwin. + a->content = ccline.cmdbuff ? xstrnsave(ccline.cmdbuff, (size_t)ccline.cmdlen) : NULL; + // Clear cmdline so that unwinding it (via Ctrl_C below) does not add it to history. + ccline.cmdlen = 0; + ccline.cmdpos = 0; + // Intentionally not using vim.scheduled, see note on `open_cmdwin_event`. loop_schedule_deferred(&main_loop, event_create(open_cmdwin_event, a)); return Ctrl_C; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 188bb5dac8..f5feaf195a 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -3859,8 +3859,11 @@ static void nv_down(cmdarg_T *cap) // is page down cap->arg = FORWARD; nv_page(cap); + } else if (bt_cmdwin(curbuf) && cap->cmdchar == CAR) { + // cmdwin: execute the command-line under the cursor. + cmdwin_do_action("confirm"); } else if (bt_quickfix(curbuf) && cap->cmdchar == CAR) { - // Quickfix window only: view the result under the cursor. + // Quickfix window: view the result under the cursor. qf_view_result(false); } else { if (bt_prompt(curbuf) && cap->cmdchar == CAR @@ -6174,9 +6177,13 @@ static void nv_esc(cmdarg_T *cap) && cap->opcount == 0 && cap->count0 == 0 && cap->oap->regname == 0); + bool cmdwin_cancel = cap->arg && bt_cmdwin(curbuf); - if (cap->arg) { // true for CTRL-C - if (restart_edit == 0 && !bt_cmdwin(curbuf) && !VIsual_active && no_reason) { + if (cmdwin_cancel) { + got_int = false; // CTRL-C cancels cmdwin; don't interrupt autocmds etc. + cmdwin_do_action("cancel"); + } else if (cap->arg) { // true for CTRL-C + if (restart_edit == 0 && !VIsual_active && no_reason) { if (anyBufIsChanged()) { msg(_("Type :qa! and press to abandon all changes" " and exit Nvim"), 0); @@ -6197,7 +6204,7 @@ static void nv_esc(cmdarg_T *cap) check_cursor_col(curwin); // make sure cursor is not beyond EOL curwin->w_set_curswant = true; redraw_curbuf_later(UPD_INVERTED); - } else if (no_reason) { + } else if (no_reason && !cmdwin_cancel) { vim_beep(kOptBoFlagEsc); } clearop(cap->oap); diff --git a/test/functional/editor/cmdwin_spec.lua b/test/functional/editor/cmdwin_spec.lua index 347b4edd3b..023686af08 100644 --- a/test/functional/editor/cmdwin_spec.lua +++ b/test/functional/editor/cmdwin_spec.lua @@ -17,11 +17,12 @@ describe('cmdwin', function() eq(':', fn.getcmdwintype()) eq('[Command Line]', vim.fs.basename(api.nvim_buf_get_name(0))) -- cmdwin-char is shown in window-local 'statuscolumn'. - eq('%#NonText#: ', api.nvim_get_option_value('statuscolumn', { win = 0 })) + eq('%#NonText#:', api.nvim_get_option_value('statuscolumn', { win = 0 })) eq('nofile', api.nvim_get_option_value('buftype', { buf = 0 })) eq('wipe', api.nvim_get_option_value('bufhidden', { buf = 0 })) eq(false, api.nvim_get_option_value('swapfile', { buf = 0 })) + eq(true, api.nvim_get_option_value('buflisted', { buf = 0 })) -- #40431 eq(true, api.nvim_get_option_value('winfixbuf', { win = 0 })) -- executes the cmdline @@ -32,15 +33,23 @@ describe('cmdwin', function() end) it('q/ opens cmdwin with search history', function() + fn.histadd('/', 'foo') + fn.histadd('/', 'bar') feed('q/') eq('/', fn.getcmdwintype()) + -- The "/" history is presented, plus an empty editable last line. + eq({ 'foo', 'bar', '' }, api.nvim_buf_get_lines(0, 0, -1, false)) end) it('c_CTRL-F opens cmdwin pre-filled with current cmdline', function() + fn.histadd(':', 'let g:x = 1') + fn.histadd(':', 'let g:y = 2') feed(':echo "hi"') n.poke_eventloop() eq(':', fn.getcmdwintype()) - eq('echo "hi"', api.nvim_get_current_line()) + -- The ":" history is presented. The in-flight cmdline must not be added to history (else it shows up twice). + eq({ 'let g:x = 1', 'let g:y = 2', 'echo "hi"' }, api.nvim_buf_get_lines(0, 0, -1, false)) + eq(2, fn.histnr('cmd')) -- History unchanged (the in-flight cmdline was not added). end) it('history entry with literal newline char', function() @@ -52,10 +61,13 @@ describe('cmdwin', function() it(' in normal mode cancels without executing', function() feed('q:') - feed('iecho "nope"') + feed('ilet g:executed = 1') feed('') eq('', fn.getcmdwintype()) - -- v:errmsg should not contain a successful echo from the cancelled line. + -- The cancelled line is neither executed nor added to history. It is pre-filled into the + -- cmdline; reopening via c_CTRL-F must then not duplicate it. + eq(0, fn.exists('g:executed')) + eq(-1, fn.histnr('cmd')) end) it('async API calls work while cmdwin is open #40312', function() @@ -115,7 +127,7 @@ describe('cmdwin', function() }) ]]) feed('q:') - n.poke_eventloop() -- Ensure q: is processed before the mapping fires. + n.poke_eventloop() -- Ensure q: is processed before . feed('') eq({ 'enter::', 'leave::' }, exec_lua('return _G.events')) end) diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index 934b5e259f..d21fe79c6c 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -1046,9 +1046,8 @@ describe('completion', function() screen:expect([[ | {2:[No Name] }| - {1:: } | - {1:: }foo faa fee f^ | - {1:~ }|*2 + {1::}foo faa fee f^ | + {1:~ }|*3 {3:[Command Line] }| {5:-- INSERT --} | ]]) @@ -1056,11 +1055,11 @@ describe('completion', function() screen:expect([[ | {2:[No Name] }| - {1:: } | - {1:: }foo faa fee foo^ | - {1:~ }{12: foo }{1: }| - {1:~ }{4: faa }{1: }| - {3:[Command Line}{4: fee }{3: }| + {1::}foo faa fee foo^ | + {1:~ }{12: foo }{1: }| + {1:~ }{4: faa }{1: }| + {1:~ }{4: fee }{1: }| + {3:[Command Line] }| {5:-- Keyword Local completion (^N^P) }{6:match 1 of 3} | ]]) feed('') diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua index 5a46dd454b..66489ccba4 100644 --- a/test/functional/ui/cmdline_spec.lua +++ b/test/functional/ui/cmdline_spec.lua @@ -255,7 +255,7 @@ local function test_cmdline(linegrid) screen:expect([[ | {2:[No Name] }| - {1:: }mak^e | + {1::}mak^e | {3:[Command Line] }| | ]]) @@ -266,7 +266,7 @@ local function test_cmdline(linegrid) grid = [[ | {2:[No Name] }| - {1:: }mak^e | + {1::}mak^e | {3:[Command Line] }| | ]], @@ -278,7 +278,7 @@ local function test_cmdline(linegrid) grid = [[ | {2:[No Name] }| - {1:: }mak^e | + {1::}mak^e | {3:[Command Line] }| | ]], @@ -291,7 +291,7 @@ local function test_cmdline(linegrid) grid = [[ | {2:[No Name] }| - {1:: }mak^e | + {1::}mak^e | {3:[Command Line] }| | ]], @@ -900,9 +900,8 @@ describe('cmdline redraw', function() | {1:~ }|*3 {2:[No Name] }| - {1:: } | - {1:: }^a{17:bc} | - {1:~ }| + {1::}^a{17:bc} | + {1:~ }|*2 {3:[Command Line] }| {5:-- VISUAL --} | ]]) diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index f19b4d2d6d..7a611ebadb 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -208,8 +208,8 @@ describe('messages2', function() x | {1:~ }|*3 ─────────────────────────────────────────────────────| - {1:: }echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | - {1:: }^ | + {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | + {1::}^ | {1:~ }|*5 {3:[Command Line] 2,0-1 All}| | @@ -219,8 +219,8 @@ describe('messages2', function() x | {1:~ }|*2 ─────────────────────────────────────────────────────| - {1:: }echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | - {1:: }^ | + {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | + {1::}^ | {1:~ }|*6 {3:[Command Line] 2,0-1 All}| | @@ -230,8 +230,8 @@ describe('messages2', function() x | {1:~ }|*2 ─────────────────────────────────────────────────────| - {1:: }echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | - {1:: }^ | + {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | + {1::}^ | {1:~ }|*6 {3:[Command Line] 2,0-1 All}| foo | @@ -252,8 +252,8 @@ describe('messages2', function() x | {1:~ }|*3 ─────────────────────────────────────────────────────| - {1:: }echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | - {1:: }^ | + {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | + {1::}^ | {1:~ }|*5 {3:[Command Line] 2,0-1 All}| | @@ -272,9 +272,9 @@ describe('messages2', function() x | {1:~ }|*3 ─────────────────────────────────────────────────────| - {1:: }echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | - {1:: }messages | - {1:: }^ | + {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | + {1::}messages | + {1::}^ | {1:~ }|*4 {3:[Command Line] 3,0-1 All}| {16::}{15:messages} | diff --git a/test/functional/ui/statuscolumn_spec.lua b/test/functional/ui/statuscolumn_spec.lua index ee8a81e89e..5fe5a79826 100644 --- a/test/functional/ui/statuscolumn_spec.lua +++ b/test/functional/ui/statuscolumn_spec.lua @@ -970,8 +970,8 @@ describe('statuscolumn', function() {8: 9}aaaaa | {8:10}aaaaa | {2:[No Name] [+] }| - {1:: }set stc=%^l | - {1:: } | + {1::}set stc=%^l | + {1::} | {1:~ }|*5 {3:[Command Line] }| :set stc=%l |