From 7e787f6a4f16ddccfc3017e44f9cb9c9d1615dc3 Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Fri, 9 May 2025 12:17:26 +0200 Subject: [PATCH] fix(extui): route interactive messages to more-window (#33885) Problem: Extui does not route messages emitted as a result of a typed command to the "more" window. Command message leading shell messages is missing a kind. Messages not routed to 'cmdheight' area after it was 0. Solution: Route messages that were emitted in the same event loop as an entered command to the "more" window. Also append multiple messages in an already open more-window. Assign it the `shell_cmd` kind. Change message position when 'cmdheight' changes from 0. --- runtime/doc/ui.txt | 1 + runtime/lua/vim/_extui.lua | 10 +++++++--- runtime/lua/vim/_extui/cmdline.lua | 23 ++++++++++++++--------- runtime/lua/vim/_extui/messages.lua | 22 ++++++++++++++++------ src/nvim/ex_cmds.c | 1 + test/functional/ui/messages_spec.lua | 2 +- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/runtime/doc/ui.txt b/runtime/doc/ui.txt index c28f1c8111..fe2fbad0cd 100644 --- a/runtime/doc/ui.txt +++ b/runtime/doc/ui.txt @@ -832,6 +832,7 @@ must handle. "quickfix" Quickfix navigation message "search_cmd" Entered search command "search_count" Search count message ("S" flag of 'shortmess') + "shell_cmd" |:!cmd| executed command "shell_err" |:!cmd| shell stderr output "shell_out" |:!cmd| shell stdout output "shell_ret" |:!cmd| shell return code diff --git a/runtime/lua/vim/_extui.lua b/runtime/lua/vim/_extui.lua index 75adcfa7c5..8b3f67076e 100644 --- a/runtime/lua/vim/_extui.lua +++ b/runtime/lua/vim/_extui.lua @@ -82,10 +82,14 @@ function M.enable(opts) local function check_opt(name, value) if name == 'cmdheight' then -- 'cmdheight' set; (un)hide cmdline window and set its height. - ext.cmdheight = value - ext.cfg.msg.pos = ext.cmdheight == 0 and 'box' or ext.cfg.msg.pos - local cfg = { height = math.max(ext.cmdheight, 1), hide = ext.cmdheight == 0 } + local cfg = { height = math.max(value, 1), hide = value == 0 } api.nvim_win_set_config(ext.wins[ext.tab].cmd, cfg) + -- Change message position when 'cmdheight' was or becomes 0. + if value == 0 or ext.cmdheight == 0 then + ext.cfg.msg.pos = value == 0 and 'box' or ext.cmdheight == 0 and 'cmd' + ext.msg.prev_msg = '' + end + ext.cmdheight = value elseif name == 'termguicolors' then -- 'termguicolors' toggled; add or remove border and set 'winblend' for box windows. for _, tab in ipairs(api.nvim_list_tabpages()) do diff --git a/runtime/lua/vim/_extui/cmdline.lua b/runtime/lua/vim/_extui/cmdline.lua index 61785a8a1a..23f6be0872 100644 --- a/runtime/lua/vim/_extui/cmdline.lua +++ b/runtime/lua/vim/_extui/cmdline.lua @@ -6,7 +6,7 @@ local M = { indent = 0, -- Current indent for block event. prompt = false, -- Whether a prompt is active; messages are placed in the 'prompt' window. row = 0, -- Current row in the cmdline buffer, > 0 for block events. - level = 0, -- Current cmdline level, 0 when inactive (otherwise unused). + level = -1, -- Current cmdline level, 0 when inactive, -1 one loop iteration after closing. } --- Set the 'cmdheight' and cmdline window height. Reposition message windows. @@ -121,16 +121,21 @@ function M.cmdline_hide(_, abort) api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) end - -- 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 M.prompt then + local clear = vim.schedule_wrap(function(was_prompt) + -- 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(ext.bufs.cmd, 0, -1, false, {}) + api.nvim_win_set_config(ext.wins[ext.tab].prompt, { hide = true }) + 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() - if not M.prompt then - api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) - api.nvim_win_set_config(ext.wins[ext.tab].prompt, { hide = true }) - end + M.level = -1 end) - end + end) + clear(M.prompt) M.prompt, M.level, curpos[1], curpos[2] = false, 0, 0, 0 win_config(ext.wins[ext.tab].cmd, true, ext.cmdheight) diff --git a/runtime/lua/vim/_extui/messages.lua b/runtime/lua/vim/_extui/messages.lua index b5ffe3f2d4..d14955aeaa 100644 --- a/runtime/lua/vim/_extui/messages.lua +++ b/runtime/lua/vim/_extui/messages.lua @@ -219,8 +219,9 @@ function M.show_msg(tar, content, replace_last, more) local srow, scol = row, col -- Split at newline and concatenate first and last message chunks. for str in (chunk[2] .. '\0'):gmatch('.-[\n%z]') do - local idx = i > 1 and row == srow and 0 or 1 - lines[#lines + idx] = idx > 0 and str:sub(1, -2) or lines[#lines] .. str:sub(1, -2) + local idx = #lines + (i > 1 and row == srow and 0 or 1) + -- Filter out NL, CRs and appended NUL. TODO: actually handle carriage return? + lines[idx] = (lines[idx] or '') .. str:gsub('[\n\r%z]', '') col = #lines[#lines] row = row + (str:sub(-1) == '\0' and 0 or 1) if tar == 'box' then @@ -279,6 +280,7 @@ function M.show_msg(tar, content, replace_last, more) end end +local append_more = 0 local replace_bufwrite = false --- Route the message to the appropriate sink. --- @@ -302,7 +304,7 @@ function M.msg_show(kind, content) elseif kind == 'return_prompt' then -- Bypass hit enter prompt. vim.api.nvim_feedkeys(vim.keycode(''), 'n', false) - elseif kind == 'verbose' then + elseif kind == 'verbose' and append_more == 0 then -- Verbose messages are sent too often to be meaningful in the cmdline: -- always route to box regardless of cfg.msg.pos. M.show_msg('box', content, false) @@ -323,7 +325,9 @@ function M.msg_show(kind, content) M.virt.last[M.virt.idx.search][1] = nil end - M.show_msg(tar, content, replace_bufwrite, kind == 'list_cmd') + -- Messages sent as a result of a typed command should be routed to the more window. + local more = ext.cmd.level >= 0 or kind == 'list_cmd' + M.show_msg(tar, content, replace_bufwrite, more) -- Replace message for every second bufwrite message. replace_bufwrite = not replace_bufwrite and kind == 'bufwrite' end @@ -367,9 +371,14 @@ function M.msg_history_show(entries) return end - api.nvim_buf_set_lines(ext.bufs.more, 0, -1, false, {}) + -- Appending messages while 'more' window is open. + append_more = entries[1][1] == 'spill' and append_more + 1 or 0 + if append_more < 2 then + api.nvim_buf_set_lines(ext.bufs.more, 0, -1, false, {}) + end + for i, entry in ipairs(entries) do - M.show_msg('more', entry[2], i == 1) + M.show_msg('more', entry[2], i == 1 and append_more < 2) end M.set_pos('more') @@ -402,6 +411,7 @@ function M.set_pos(type) if api.nvim_win_is_valid(win) then api.nvim_win_set_config(win, { hide = true }) end + append_more = 0 end, desc = 'Hide inactive more window.', }) diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index a34eaeea17..025bb9f2de 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -1025,6 +1025,7 @@ void do_bang(int addr_count, exarg_T *eap, bool forceit, bool do_in, bool do_out if (addr_count == 0) { // :! // echo the command msg_start(); + msg_ext_set_kind("shell_cmd"); msg_putchar(':'); msg_putchar('!'); msg_outtrans(newcmd, 0, false); diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index 8f1005d52a..9deb306cd4 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -463,7 +463,7 @@ describe('ui/ext_messages', function() { content = { { (':!%s\r\n[No write since last change]\n'):format(cmd) } }, history = false, - kind = '', + kind = 'shell_cmd', }, { content = { { ('stdout%s\n'):format(t.is_os('win') and '\r' or '') } },