fix(cmdwin): implement Enter/Ctrl-C as builtins

What echasnovski wants, echasnovski gets.
This commit is contained in:
Justin M. Keyes
2026-06-26 20:20:18 +02:00
parent 5675c11910
commit 496af49bda
5 changed files with 61 additions and 41 deletions

View File

@@ -101,20 +101,6 @@ function M.open(type, init_line, init_col)
caller_win = caller,
}
-- Buffer-local mappings.
vim.keymap.set(
{ 'n', 'i' },
'<CR>',
[[<C-\><C-N><Cmd>lua require'vim._core.cmdwin'.confirm()<CR>]],
{ buffer = buf, silent = true, desc = 'cmdwin: execute' }
)
vim.keymap.set(
{ 'n', 'i', 'x' },
'<C-C>',
[[<C-\><C-N><Cmd>lua require'vim._core.cmdwin'.cancel()<CR>]],
{ 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('<CR>'), '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

View File

@@ -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: <CR> 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)) {

View File

@@ -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.<action>(...)`.
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.<action>()`, 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;

View File

@@ -3859,8 +3859,11 @@ static void nv_down(cmdarg_T *cap)
// <S-Down> 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 <Enter> 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);

View File

@@ -126,7 +126,7 @@ describe('cmdwin', function()
})
]])
feed('q:')
n.poke_eventloop() -- Ensure q: is processed before the <C-C> mapping fires.
n.poke_eventloop() -- Ensure q: is processed before <C-C>.
feed('<C-C>')
eq({ 'enter::', 'leave::' }, exec_lua('return _G.events'))
end)