Merge #40484 from justinmk/cmdwinchaos

This commit is contained in:
Justin M. Keyes
2026-06-29 17:57:49 -04:00
committed by GitHub
8 changed files with 75 additions and 87 deletions

View File

@@ -1205,9 +1205,13 @@ There are two ways to open the command-line window:
When the window opens it is filled with the command-line history. The editor
opens in Normal mode. The last line contains the command as typed so far. The
|cmdwin-char| is shown in 'statuscolumn', indicating the type of command-line.
The window is full width, positioned just above the command-line; its height
is decided by 'cmdwinheight' (if there is room).
The window height is decided by 'cmdwinheight' (if there is room). The window
is full width and is positioned just above the command-line.
Window and tabpage commands work as usual while cmdwin is open: you can
navigate, resize, close and open windows/tabpages. The mouse works as in any
window: clicks focus other windows, and statuslines and vertical separators
can be dragged to resize.
*E1292*
Only one command-line window can be open at a time.
@@ -1223,17 +1227,17 @@ but it's not possible to open another cmdwin from there. There is no
nesting.
*E11*
|CTRL-W| navigation/split commands and some operations (e.g. |:terminal|) emit
E11. But mouse clicks may focus other windows. The cmdwin buffer is pinned by
'winfixbuf', so the window cannot switch to a different buffer.
The cmdwin buffer cannot be used as a |:terminal|. Its window cannot switch to
a different buffer ('winfixbuf').
CLOSE
There are several ways to leave the command-line window:
<CR> Execute the command-line under the cursor. Works both in
Insert and in Normal mode.
<Enter> Execute the command-line under the cursor, then close the
window(s) where cmdwin is visible. Works both in Insert and
in Normal mode.
CTRL-C Continue in Command-line mode. The command-line under the
cursor is pre-filled in the command-line. Works both in
Insert and in Normal mode.
@@ -1245,8 +1249,8 @@ CTRL-C Continue in Command-line mode. The command-line under the
When the cmdwin closes, focus returns to the window where the command-line
was started from (if possible). The cmdwin scratch buffer is destroyed
(`bufhidden=wipe`). Any text edits other than the line executed by
<CR> are lost.
(`bufhidden=wipe`). Any text edits other than the line executed by <Enter>
are lost.
If you would like to execute the command under the cursor and then have the
command-line window open again, you may find this mapping useful: >
@@ -1281,9 +1285,6 @@ If you don't want these mappings, disable them with: >
au CmdwinEnter [:>] nunmap <buffer> <Tab>
You could put these lines in your vimrc file.
The mouse works as in any window: clicks focus other windows, and statuslines
and vertical separators can be dragged to resize.
The |getcmdwintype()| function returns the type of the command-line being
edited as described in |cmdwin-char|.

View File

@@ -188,11 +188,11 @@ EDITOR
kind/menu/info/abbr for the popup menu.
• |cmdwin| (|q:|, |q/|, |q?|, |c_CTRL-F|) is implemented as a "normal"
buffer+window instead of a nested-state modal loop:
• You can create/navigate windows/tabpages while cmdwin is active. Chaos!
• 'inccommand' works in cmdwin!
• API calls (e.g. |nvim_buf_delete()|) that previously failed with
"E11: Invalid in command-line window" while cmdwin was open, now work
normally. Async plugins no longer need special |CmdwinLeave| workarounds.
• Removed most cmdwin-specific |E11| guards, except for window navigation.
• The |cmdwin-char| is shown via 'statuscolumn'.
• |gf| and |<cfile>| support `file://…` URIs.
• |:log| opens log files.

View File

@@ -101,15 +101,22 @@ function M.open(type, init_line, init_col)
caller_win = caller,
}
-- Clean up if the window is closed by other means (`:q`, `:close`, etc.).
-- Clean up when the (last-visible) cmdwin is closed by other means (`:q`, `:close`, etc.).
vim.api.nvim_create_autocmd({ 'WinClosed' }, {
buffer = buf,
once = true,
nested = true,
callback = function()
if state ~= nil then
M._cleanup()
callback = function(ev)
if state == nil then
return
end
local closing = tonumber(ev.match)
for _, w in ipairs(vim.fn.win_findbuf(buf)) do
if w ~= closing then
return -- Still visible elsewhere; keep cmdwin (and this autocmd) active.
end
end
M._cleanup()
return true -- Last cmdwin window gone; delete this autocmd.
end,
})
@@ -125,8 +132,8 @@ function M._cleanup()
state = nil
pcall(vim.api.nvim__cmdwin_set, '', 0) -- Clear the C-side globals.
pcall(vim.api.nvim_exec_autocmds, 'CmdwinLeave', { pattern = s.type, modeline = false })
if vim.api.nvim_win_is_valid(s.win) then
pcall(vim.api.nvim_win_close, s.win, true)
if vim.api.nvim_buf_is_valid(s.buf) then
pcall(vim.api.nvim_buf_delete, s.buf, { force = true })
end
if vim.api.nvim_win_is_valid(s.caller_win) then
pcall(vim.api.nvim_set_current_win, s.caller_win)

View File

@@ -271,20 +271,11 @@ void do_window(int nchar, int Prenum, int xchar)
int Prenum1 = Prenum == 0 ? 1 : Prenum;
#define CHECK_CMDWIN \
do { \
if (cmdwin_buf != NULL) { \
emsg(_(e_cmdwin)); \
return; \
} \
} while (0)
switch (nchar) {
// split current window in two parts, horizontally
case 'S':
case Ctrl_S:
case 's':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
// When splitting the quickfix window open a new buffer in it,
// don't replicate the quickfix buffer.
@@ -297,7 +288,6 @@ void do_window(int nchar, int Prenum, int xchar)
// split current window in two parts, vertically
case Ctrl_V:
case 'v':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
// When splitting the quickfix window open a new buffer in it,
// don't replicate the quickfix buffer.
@@ -310,7 +300,6 @@ void do_window(int nchar, int Prenum, int xchar)
// split current window and edit alternate file
case Ctrl_HAT:
case '^':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
if (buflist_findnr(Prenum == 0 ? curwin->w_alt_fnum : Prenum) == NULL) {
@@ -331,7 +320,6 @@ void do_window(int nchar, int Prenum, int xchar)
// open new window
case Ctrl_N:
case 'n':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
newwindow:
if (Prenum) {
@@ -366,7 +354,6 @@ newwindow:
// close preview window
case Ctrl_Z:
case 'z':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
do_cmdline_cmd("pclose");
break;
@@ -391,7 +378,6 @@ newwindow:
// close all but current window
case Ctrl_O:
case 'o':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
cmd_with_count("only", cbuf, sizeof(cbuf), Prenum);
do_cmdline_cmd(cbuf);
@@ -402,7 +388,6 @@ newwindow:
case 'w':
// cursor to previous window with wrap around
case 'W':
CHECK_CMDWIN;
if (ONE_WINDOW && Prenum != 1) { // just one window
beep_flush();
} else {
@@ -454,7 +439,6 @@ newwindow:
case 'j':
case K_DOWN:
case Ctrl_J:
CHECK_CMDWIN;
win_goto_ver(false, Prenum1);
break;
@@ -462,7 +446,6 @@ newwindow:
case 'k':
case K_UP:
case Ctrl_K:
CHECK_CMDWIN;
win_goto_ver(true, Prenum1);
break;
@@ -471,7 +454,6 @@ newwindow:
case K_LEFT:
case Ctrl_H:
case K_BS:
CHECK_CMDWIN;
win_goto_hor(true, Prenum1);
break;
@@ -479,13 +461,11 @@ newwindow:
case 'l':
case K_RIGHT:
case Ctrl_L:
CHECK_CMDWIN;
win_goto_hor(false, Prenum1);
break;
// move window to new tab page
case 'T':
CHECK_CMDWIN;
if (one_window(curwin, NULL)) {
msg(_(m_onlyone), 0);
} else {
@@ -533,21 +513,18 @@ newwindow:
// exchange current and next window
case 'x':
case Ctrl_X:
CHECK_CMDWIN;
win_exchange(Prenum);
break;
// rotate windows downwards
case Ctrl_R:
case 'r':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
win_rotate(false, Prenum1); // downwards
break;
// rotate windows upwards
case 'R':
CHECK_CMDWIN;
reset_VIsual_and_resel(); // stop Visual mode
win_rotate(true, Prenum1); // upwards
break;
@@ -557,7 +534,6 @@ newwindow:
case 'J':
case 'H':
case 'L':
CHECK_CMDWIN;
if (one_window(curwin, NULL)) {
beep_flush();
} else {
@@ -608,7 +584,6 @@ newwindow:
// jump to tag and split window if tag exists (in preview window)
case '}':
CHECK_CMDWIN;
if (Prenum) {
g_do_tagpreview = Prenum;
} else {
@@ -617,7 +592,6 @@ newwindow:
FALLTHROUGH;
case ']':
case Ctrl_RSB:
CHECK_CMDWIN;
// Keep visual mode, can select words to use as a tag.
if (Prenum) {
postponed_split = Prenum;
@@ -640,7 +614,6 @@ newwindow:
case 'F':
case Ctrl_F: {
wingotofile:
CHECK_CMDWIN;
if (check_text_or_curbuf_locked(NULL)) {
break;
}
@@ -698,7 +671,6 @@ wingotofile:
FALLTHROUGH;
case 'd': // Go to definition, using 'define'
case Ctrl_D: {
CHECK_CMDWIN;
size_t len;
char *ptr;
if ((len = find_ident_under_cursor(&ptr, FIND_IDENT, NULL)) == 0) {
@@ -726,7 +698,6 @@ wingotofile:
// CTRL-W g extended commands
case 'g':
case Ctrl_G:
CHECK_CMDWIN;
no_mapping++;
allow_keys++; // no mapping for xchar, but allow key codes
if (xchar == NUL) {
@@ -4498,10 +4469,6 @@ tabpage_T *win_new_tabpage(int after, char *filename, bool enter, win_T **first)
{
tabpage_T *old_curtab = curtab;
if (enter && cmdwin_buf != NULL) {
emsg(_(e_cmdwin));
return NULL;
}
if (window_layout_locked(CMD_tabnew)) {
return NULL;
}
@@ -4936,10 +4903,6 @@ void goto_tabpage(int n)
/// @param trigger_leave_autocmds when true trigger *Leave autocommands.
void goto_tabpage_tp(tabpage_T *tp, bool trigger_enter_autocmds, bool trigger_leave_autocmds)
{
if (trigger_enter_autocmds || trigger_leave_autocmds) {
CHECK_CMDWIN;
}
// Don't repeat a message in another tab page.
set_keep_msg(NULL, 0);

View File

@@ -618,7 +618,7 @@ describe('tabpage/previous', function()
-- it('does not switch to previous via :tabn #<CR> after entering operator pending',
-- does_not_switch_to_previous_after_entering_operator_pending(':tabn #<CR>'))
local function cmdwin_prevents_tab_switch(characters, completion_visible)
local function cmdwin_allows_tab_switch(characters)
return function()
-- Add three tabs for a total of four
command('tabnew')
@@ -633,25 +633,19 @@ describe('tabpage/previous', function()
local cmdline_win_id = eval('win_getid()')
-- At this point switching to the previous tab should have no effect.
-- Switching to the previous tab now works.
feed(characters)
-- Attempting to switch tabs maintains the current window.
eq(cmdline_win_id, eval('win_getid()'))
eq(completion_visible, eval('complete_info().pum_visible'))
-- The current tab is still the fourth.
eq(4, eval('tabpagenr()'))
-- The previous tab is still the third.
eq(3, eval("tabpagenr('#')"))
-- Focus moved out of the cmdwin to the previous (third) tab.
t.neq(cmdline_win_id, eval('win_getid()'))
eq(3, eval('tabpagenr()'))
end
end
it('cmdwin prevents tab switch via g<Tab>', cmdwin_prevents_tab_switch('g<Tab>', 0))
it('cmdwin prevents tab switch via <C-W>g<Tab>', cmdwin_prevents_tab_switch('<C-W>g<Tab>', 0))
it('cmdwin prevents tab switch via <C-Tab>', cmdwin_prevents_tab_switch('<C-Tab>', 0))
it('cmdwin prevents tab switch via :tabn #<CR>', cmdwin_prevents_tab_switch(':tabn #<CR>', 0))
it('cmdwin allows tab switch via g<Tab>', cmdwin_allows_tab_switch('g<Tab>'))
it('cmdwin allows tab switch via <C-W>g<Tab>', cmdwin_allows_tab_switch('<C-W>g<Tab>'))
it('cmdwin allows tab switch via <C-Tab>', cmdwin_allows_tab_switch('<C-Tab>'))
it('cmdwin allows tab switch via :tabn #<CR>', cmdwin_allows_tab_switch(':tabn #<CR>'))
it(':tabs indicates correct prevtab curwin', function()
-- Add three tabs for a total of four

View File

@@ -32,6 +32,35 @@ describe('cmdwin', function()
eq('', fn.getcmdwintype())
end)
it('<CR> closes all cmdwin (split) windows #40484', function()
feed('q:')
feed('ilet g:cmdwin_result = 7<Esc>')
feed('<C-w>s') -- split: two windows now show the cmdwin buffer.
local cmdwin_buf = api.nvim_get_current_buf()
eq(2, #fn.win_findbuf(cmdwin_buf)) -- sanity: the split happened
feed('<CR>')
eq(7, api.nvim_get_var('cmdwin_result')) -- the cmdline executed
eq('', fn.getcmdwintype())
eq(0, #fn.win_findbuf(cmdwin_buf)) -- All cmdwin windows are closed.
eq(1, #api.nvim_list_wins()) -- Back to the single original window.
end)
it('<CR> executes when cmdwin was moved to another tabpage #40484', function()
feed('q:')
feed('ilet g:cmdwin_result = 9<Esc>')
feed('<C-w>T') -- Move the cmdwin to its own new tabpage.
eq(2, fn.tabpagenr('$')) -- sanity: the new tabpage exists
local cmdwin_buf = api.nvim_get_current_buf()
feed('<CR>')
eq(9, api.nvim_get_var('cmdwin_result')) -- the cmdline executed.
eq('', fn.getcmdwintype())
eq(0, #fn.win_findbuf(cmdwin_buf)) -- cmdwin window closed.
eq(1, fn.tabpagenr('$')) -- cmdwin's tabpage is gone.
eq(1, #api.nvim_list_wins())
end)
it('q/ opens cmdwin with search history', function()
fn.histadd('/', 'foo')
fn.histadd('/', 'bar')

View File

@@ -60,7 +60,7 @@ describe('eval-API', function()
eq('Vim(call):E5555: API call: Invalid buffer id: 17', err)
end)
it('cannot change text or window if textlocked', function()
it('cannot change text or window if textlock', function()
command('autocmd TextYankPost <buffer> ++once call nvim_buf_set_lines(0, 0, -1, v:false, [])')
matches(
'Vim%(call%):E5555: API call: E565: Not allowed to change text or change window$',
@@ -90,19 +90,16 @@ describe('eval-API', function()
api.nvim_get_vvar('errmsg')
)
-- cmdwin behavior (changed since #40312).
-- cmdwin (#40312, #40484): E11 "Invalid in command-line window" restriction was removed.
local old_win = api.nvim_get_current_win()
feed('q:')
local cmdwin_win = api.nvim_get_current_win()
eq(
'Vim:E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
pcall_err(api.nvim_set_current_win, old_win)
)
neq(old_win, cmdwin_win)
-- Switching to another window from the cmdwin now works (previously E11).
api.nvim_set_current_win(old_win)
eq(old_win, api.nvim_get_current_win())
-- TODO(justinmk): awkward: E11 is raised but the focus switch happens anyway; inherited bug in
-- goto_tabpage_win (calls win_enter unconditionally even when goto_tabpage_tp's CHECK_CMDWIN
-- emsg'd). We should probably just remove CHECK_CMDWIN from goto_tabpage_tp. #40407
pcall(api.nvim_set_current_win, cmdwin_win)
-- ...and back into the cmdwin.
api.nvim_set_current_win(cmdwin_win)
eq(cmdwin_win, api.nvim_get_current_win())
-- nvim_buf_set_lines() in the cmdwin buffer is OK.

View File

@@ -3242,11 +3242,8 @@ func Test_normal50_commandline()
func! DoTimerWork(id)
call assert_equal(1, getbufinfo('')[0].command)
" should fail, with E11, but does fail with E23?
"call feedkeys("\<c-^>", 'tm')
" should fail with E11 - "Invalid in command-line window"
call assert_fails(":wincmd p", 'E11')
" Nvim removed the E11 "Invalid in command-line window" restriction (#40312, #40484).
"call assert_fails(":wincmd p", 'E11')
" Return from commandline window.
call feedkeys("\<CR>", 't')