From 496af49bda05ff5f530047ce9fbc919b9d4aef93 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Fri, 26 Jun 2026 20:20:18 +0200 Subject: [PATCH] fix(cmdwin): implement Enter/Ctrl-C as builtins What echasnovski wants, echasnovski gets. --- runtime/lua/vim/_core/cmdwin.lua | 33 ++++++++------------ src/nvim/edit.c | 10 ++++++ src/nvim/ex_getln.c | 42 +++++++++++++++++--------- src/nvim/normal.c | 15 ++++++--- test/functional/editor/cmdwin_spec.lua | 2 +- 5 files changed, 61 insertions(+), 41 deletions(-) diff --git a/runtime/lua/vim/_core/cmdwin.lua b/runtime/lua/vim/_core/cmdwin.lua index a9da04bdb5..a799cf8eb9 100644 --- a/runtime/lua/vim/_core/cmdwin.lua +++ b/runtime/lua/vim/_core/cmdwin.lua @@ -101,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, @@ -146,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 5195e4ad61..07f5a645ce 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.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); } @@ -4620,8 +4632,8 @@ static int open_cmdwin(void) a->pos = ccline.cmdpos; // Capture the cmdline; will append to end of cmdwin. - // Clear the live cmdline so that unwinding it (via Ctrl_C below) does not add it to history. 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; 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 2b3b8a774d..4b79e0b356 100644 --- a/test/functional/editor/cmdwin_spec.lua +++ b/test/functional/editor/cmdwin_spec.lua @@ -126,7 +126,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)