Merge #40434 from justinmk/fixcmdwin

This commit is contained in:
Justin M. Keyes
2026-06-26 17:40:41 -04:00
committed by GitHub
9 changed files with 121 additions and 82 deletions

View File

@@ -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' },
'<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,
@@ -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('<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

@@ -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.<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);
}
@@ -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;

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

@@ -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 }))
-- <CR> 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"<C-F>')
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('<C-C> in normal mode cancels without executing', function()
feed('q:')
feed('iecho "nope"<Esc>')
feed('ilet g:executed = 1<Esc>')
feed('<C-C>')
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 <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)

View File

@@ -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('<c-c>')

View File

@@ -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 --} |
]])

View File

@@ -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} |

View File

@@ -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 |