fix(cmdwin): handle split/tabpage

Problem:
- If cmdwin window is split, ENTER in one does not close the others.
- If cmdwin is put into a different tabpage via <c-w>T, it stops working
  (ENTER does not execute the cmd).

Solution:
- Close the buffer instead of the window.
- In the WinClosed handler, skip `M._cleanup()` unless this is the last
  cmdwin window.
This commit is contained in:
Justin M. Keyes
2026-06-29 23:19:52 +02:00
parent 2a4bb3eb37
commit 684371ba5d
3 changed files with 50 additions and 14 deletions

View File

@@ -1205,9 +1205,8 @@ 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 height is decided by 'cmdwinheight' (if there is room). The window
is full width and is positioned just above the command-line.
The window is full width, positioned just above the command-line; its height
is decided by 'cmdwinheight' (if there is room).
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
@@ -1236,8 +1235,9 @@ 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.
@@ -1249,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: >

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

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