commit e19803714800e5e139f60f9ed160e240cd944299
parent 7a490d65c57e72342ad91e00788109d991cac6c1
Author: luukvbaal <luukvbaal@gmail.com>
Date: Thu, 5 Feb 2026 20:54:22 +0100
fix(ui2): always route to dialog when cmdline is open #37730
Problem: Messages emitted while cmdline is open are not shown with
cmdline message target.
Solution: Route to dialog window when cmdline is open.
Diffstat:
4 files changed, 72 insertions(+), 38 deletions(-)
diff --git a/runtime/lua/vim/_core/ui2/cmdline.lua b/runtime/lua/vim/_core/ui2/cmdline.lua
@@ -4,10 +4,11 @@ local api, fn = vim.api, vim.fn
local M = {
highlighter = nil, ---@type vim.treesitter.highlighter?
indent = 0, -- Current indent for block event.
- prompt = false, -- Whether a prompt is active; messages are placed in the 'dialog' window.
+ prompt = false, -- Whether a prompt is active; route to dialog regardless of ui.cfg.msg.target.
+ dialog = false, -- Whether a dialog window was opened.
srow = 0, -- Buffer row at which the current cmdline starts; > 0 in block mode.
erow = 0, -- Buffer row at which the current cmdline ends; messages appended here in block mode.
- level = -1, -- Current cmdline level; 0 when inactive, -1 one loop iteration after closing.
+ level = 0, -- Current cmdline level; 0 when inactive.
wmnumode = 0, -- wildmenumode() when not using the pum, dialog position adjusted when toggled.
}
@@ -29,7 +30,7 @@ local function win_config(win, hide, height)
vim.o.cmdheight = height
end)
ui.msg.set_pos()
- elseif M.wmnumode ~= (M.prompt and fn.pumvisible() == 0 and fn.wildmenumode() or 0) then
+ elseif M.wmnumode ~= (M.dialog and fn.pumvisible() == 0 and fn.wildmenumode() or 0) then
M.wmnumode = (M.wmnumode == 1 and 0 or 1)
ui.msg.set_pos()
end
@@ -66,7 +67,7 @@ end
---@param level integer
---@param hl_id integer
function M.cmdline_show(content, pos, firstc, prompt, indent, level, hl_id)
- M.level, M.indent, M.prompt = level, indent, M.prompt or #prompt > 0
+ M.level, M.indent, M.prompt = level, indent, #prompt > 0
if M.highlighter == nil or M.highlighter.bufnr ~= ui.bufs.cmd then
local parser = assert(vim.treesitter.get_parser(ui.bufs.cmd, 'vim', {}))
M.highlighter = vim.treesitter.highlighter.new(parser)
@@ -130,28 +131,21 @@ function M.cmdline_hide(level, abort)
fn.clearmatches(ui.wins.cmd) -- Clear matchparen highlights.
api.nvim_win_set_cursor(ui.wins.cmd, { 1, 0 })
- if abort then
- -- Clear cmd buffer for aborted command (non-abort is left visible).
+ if M.prompt or abort then
+ -- Clear cmd buffer prompt or aborted command (non-abort is left visible).
api.nvim_buf_set_lines(ui.bufs.cmd, 0, -1, false, {})
end
- local clear = vim.schedule_wrap(function(was_prompt)
+ vim.schedule(function()
-- Avoid clearing prompt window when it is re-entered before the next event
-- loop iteration. E.g. when a non-choice confirm button is pressed.
- if was_prompt and not M.prompt then
- api.nvim_buf_set_lines(ui.bufs.cmd, 0, -1, false, {})
+ if M.dialog and M.level == 0 then
api.nvim_buf_set_lines(ui.bufs.dialog, 0, -1, false, {})
api.nvim_win_set_config(ui.wins.dialog, { hide = true })
vim.on_key(nil, ui.msg.dialog_on_key)
+ M.dialog = false
end
- -- Messages emitted as a result of a typed command are treated specially:
- -- remember if the cmdline was used this event loop iteration.
- -- NOTE: Message event callbacks are themselves scheduled, so delay two iterations.
- vim.schedule(function()
- M.level = -1
- end)
end)
- clear(M.prompt)
M.prompt, M.level, curpos[1], curpos[2] = false, 0, 0, 0
win_config(ui.wins.cmd, true, ui.cmdheight)
diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua
@@ -286,7 +286,7 @@ function M.show_msg(tar, content, replace_last, append, id)
local start_col = col
-- Accumulate to be inserted and highlighted message chunks.
- for _, chunk in ipairs(content) do
+ for i, chunk in ipairs(content) do
-- Split at newline and write to start of line after carriage return.
for str in (chunk[2] .. '\0'):gmatch('.-[\n\r%z]') do
local repl, pat = str:sub(1, -2), str:sub(-1)
@@ -302,7 +302,6 @@ function M.show_msg(tar, content, replace_last, append, id)
api.nvim_buf_set_text(buf, row, col, erow, ecol, { repl })
end
curline = api.nvim_buf_get_lines(buf, row, row + 1, false)[1]
- width = tar == 'msg' and math.max(width, api.nvim_strwidth(curline)) or 0
mark[3] = nil
if chunk[3] > 0 then
@@ -315,6 +314,11 @@ function M.show_msg(tar, content, replace_last, append, id)
else
col = pat == '\r' and 0 or end_col
end
+ if tar == 'msg' and (pat == '\n' or (i == #content and pat == '\0')) then
+ width = api.nvim_win_call(ui.wins.msg, function()
+ return math.max(width, fn.strdisplaywidth(curline))
+ end)
+ end
end
end
@@ -398,21 +402,23 @@ function M.msg_show(kind, content, replace_last, _, append, id)
M.virt.last[M.virt.idx.search] = content
M.virt.last[M.virt.idx.cmd] = { { 0, (' '):rep(11) } }
set_virttext('last')
- elseif (ui.cmd.prompt or kind == 'wildlist') and ui.cmd.srow == 0 then
- -- Route to dialog that stays open so long as the cmdline prompt is active.
+ elseif
+ (ui.cmd.prompt or (ui.cmd.level > 0 and ui.cfg.msg.target == 'cmd')) and ui.cmd.srow == 0
+ then
+ -- Route to dialog when a prompt is active, or message would overwrite active cmdline.
replace_last = api.nvim_win_get_config(ui.wins.dialog).hide or kind == 'wildlist'
if kind == 'wildlist' then
api.nvim_buf_set_lines(ui.bufs.dialog, 0, -1, false, {})
- ui.cmd.prompt = true -- Ensure dialog is closed when cmdline is hidden.
end
+ ui.cmd.dialog = true -- Ensure dialog is closed when cmdline is hidden.
M.show_msg('dialog', content, replace_last, append, id)
M.set_pos('dialog')
else
-- Set the entered search command in the cmdline (if available).
local tar = kind == 'search_cmd' and 'cmd' or ui.cfg.msg.target
if tar == 'cmd' then
- if ui.cmdheight == 0 or (ui.cmd.level > 0 and ui.cmd.srow == 0) then
- return -- Do not overwrite an active cmdline unless in block mode.
+ if ui.cmdheight == 0 and ui.cmd.srow == 0 then
+ return
end
-- Store the time when an important message was emitted in order to not overwrite
-- it with 'last' virt_text in the cmdline so that the user has a chance to read it.
@@ -531,7 +537,7 @@ function M.set_pos(type)
if entered then
api.nvim_command('norm! g<') -- User entered the cmdline window: open the pager.
end
- elseif ui.cfg.msg.target == 'cmd' and ui.cmd.level <= 0 then
+ elseif ui.cfg.msg.target == 'cmd' and ui.cmd.level == 0 then
ui.check_targets()
set_virttext('msg')
end
diff --git a/test/functional/ui/cmdline2_spec.lua b/test/functional/ui/cmdline2_spec.lua
@@ -147,7 +147,7 @@ describe('cmdline2', function()
end)
it('highlights after deleting buffer', function()
- feed(':%bw!<CR>:call foo()')
+ feed(':sil %bw!<CR>:call foo()')
screen:expect([[
|
{1:~ }|*12
@@ -209,17 +209,17 @@ describe('cmdline2', function()
{101:Foo()}{3: Fooo() }|
{16::}{15:call} {25:Foo}{16:()}^ |
]])
- feed('()')
+ feed('<BS><BS>')
+ exec('set wildoptions+=pum laststatus=2')
screen:expect([[
|
{1:~ }|*9
{3: }|
Foo() Fooo() |
|
- {16::}{15:call} {25:Foo}{16:()()}^ |
+ {16::}{15:call} Foo^ |
]])
- exec('set wildoptions+=pum laststatus=2')
- feed('<C-U>call Fo<C-Z><C-Z>')
+ feed('<C-Z><C-Z>')
screen:expect([[
|
{1:~ }|*9
@@ -228,15 +228,6 @@ describe('cmdline2', function()
{4: Fooo() } |
{16::}{15:call} {25:Foo}{16:()}^ |
]])
- feed('()')
- screen:expect([[
- |
- {1:~ }|*9
- {3: }|
- Foo() Fooo() |
- |
- {16::}{15:call} {25:Foo}{16:()()}^ |
- ]])
end)
end)
diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua
@@ -612,4 +612,47 @@ describe('messages2', function()
{1:~ }|*5
]])
end)
+
+ it('while cmdline is open', function()
+ command('cnoremap <C-A> <Cmd>lua error("foo")<CR>')
+ feed(':echo "bar"<C-A>')
+ screen:expect([[
+ |
+ {1:~ }|*7
+ {3: }|
+ {9:E5108: Lua: [string ":lua"]:1: foo} |
+ {9:stack traceback:} |
+ {9: [C]: in function 'error'} |
+ {9: [string ":lua"]:1: in main chunk} |
+ {16::}{15:echo} {26:"bar"}^ |
+ ]])
+ feed('<CR>')
+ screen:expect([[
+ ^ |
+ {1:~ }|*12
+ bar |
+ ]])
+ command('set cmdheight=0')
+ feed([[:call confirm("foo\nbar")<C-A>]])
+ screen:expect([[
+ |
+ {1:~ }|*8
+ {1:~ }{9:E5108: Lua: [string ":lua"]:1: foo}{4: }|
+ {1:~ }{9:stack traceback:}{4: }|
+ {1:~ }{9: [C]: in function 'error'}{4: }|
+ {1:~ }{9: [string ":lua"]:1: in main chunk}|
+ {16::}{15:call} {25:confirm}{16:(}{26:"foo\nbar"}{16:)}^ |
+ ]])
+ feed('<CR>')
+ screen:expect([[
+ |
+ {1:~ }|*7
+ {3: }|
+ |
+ {6:foo} |
+ {6:bar} |
+ |
+ {6:[O]k: }^ |
+ ]])
+ end)
end)