From e19803714800e5e139f60f9ed160e240cd944299 Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Thu, 5 Feb 2026 20:54:22 +0100 Subject: [PATCH] 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. --- runtime/lua/vim/_core/ui2/cmdline.lua | 26 ++++++---------- runtime/lua/vim/_core/ui2/messages.lua | 22 ++++++++----- test/functional/ui/cmdline2_spec.lua | 19 +++--------- test/functional/ui/messages2_spec.lua | 43 ++++++++++++++++++++++++++ 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 index 4f648de2ae..78eccf20f4 100644 --- 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 index 4c6991f0fd..fb4b6a766b 100644 --- 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 index a2f31e8115..66cdce8dbc 100644 --- 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!:call foo()') + feed(':sil %bw!: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('') + 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('call Fo') + feed('') 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 index 78c4c3b569..86e056631a 100644 --- 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 lua error("foo")') + feed(':echo "bar"') + 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('') + screen:expect([[ + ^ | + {1:~ }|*12 + bar | + ]]) + command('set cmdheight=0') + feed([[:call confirm("foo\nbar")]]) + 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('') + screen:expect([[ + | + {1:~ }|*7 + {3: }| + | + {6:foo} | + {6:bar} | + | + {6:[O]k: }^ | + ]]) + end) end)