feat(ui): emit "msg_clear" event after clearing the screen (#34035)

Problem:  ext_messages cannot tell when the screen was cleared, which is
          needed to clear visible messages. An empty message is also
          never emitted, but clears messages from the message grid.
Solution: Repurpose the "msg_clear" event to be emitted when the screen
          was cleared. Emit an empty message with the `empty` kind to
          hint to a UI to clear the cmdline area.
This commit is contained in:
luukvbaal
2025-06-27 00:27:21 +02:00
committed by GitHub
parent 6005bc68b2
commit 2b4c1127ad
11 changed files with 227 additions and 139 deletions

View File

@ -67,10 +67,12 @@ EDITOR
EVENTS
• |ui-messages| no longer emits the `msg_show.return_prompt`, `msg_clear` and
`msg_history_clear` events. These events arbitrarily assume a message UI
mimicking the legacy message grid. Benefit: reduced UI event traffic and
more flexibility for UIs.
• |ui-messages| no longer emits the `msg_show.return_prompt`, and
`msg_history_clear` events. The `msg_clear` event was repurposed and is now
emitted after the screen is cleared. These events arbitrarily assumed a
message UI that mimicks the legacy message grid. Benefit: reduced UI event
traffic and more flexibility for UIs.
• A new `empty` message kind is emitted for an empty (e.g. `:echo ""`) message.
HIGHLIGHTS

View File

@ -824,6 +824,9 @@ must handle.
kind
Name indicating the message kind:
"" (empty) Unknown (consider a |feature-request|)
"empty" Empty message (`:echo ""`), with empty `content`.
Should clear messages sharing the 'cmdheight'
area if it is the only message in a batch.
"bufwrite" |:write| message
"confirm" Message preceding a prompt (|:confirm|,
|confirm()|, |inputlist()|, |z=|, …)
@ -872,8 +875,9 @@ must handle.
rather than started on a new line. Is set for |:echon|.
["msg_clear"] ~
Clear all messages currently displayed by "msg_show". (Messages sent
by other "msg_" events below will not be affected).
Clear all messages currently displayed by "msg_show", emitted after
clearing the screen (messages sent by other "msg_" events below should
not be affected).
["msg_showmode", content] ~
Shows 'showmode' and |recording| messages. `content` has the same

View File

@ -56,9 +56,14 @@ 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, #prompt > 0
M.level, M.indent, M.prompt = level, indent, M.prompt or #prompt > 0
-- Only enable TS highlighter for Ex commands (not search or filter commands).
M.highlighter.active[ext.bufs.cmd] = firstc == ':' and M.highlighter or nil
if ext.msg.cmd.msg_row ~= -1 then
ext.msg.msg_clear()
end
ext.msg.virt.last = { {}, {}, {}, {} }
set_text(content, ('%s%s%s'):format(firstc, prompt, (' '):rep(indent)))
if promptlen > 0 and hl_id > 0 then
api.nvim_buf_set_extmark(ext.bufs.cmd, ext.ns, 0, 0, { hl_group = hl_id, end_col = promptlen })
@ -67,14 +72,6 @@ function M.cmdline_show(content, pos, firstc, prompt, indent, level, hl_id)
local height = math.max(ext.cmdheight, api.nvim_win_text_height(ext.wins.cmd, {}).all)
win_config(ext.wins.cmd, false, height)
M.cmdline_pos(pos)
-- Clear message cmdline state; should not be shown during, and reset after cmdline.
if ext.cfg.msg.target == 'cmd' and ext.msg.cmd.msg_row ~= -1 then
ext.msg.prev_msg, ext.msg.dupe, ext.msg.cmd.msg_row = '', 0, -1
api.nvim_buf_clear_namespace(ext.bufs.cmd, ext.ns, 0, -1)
ext.msg.virt.msg = { {}, {} }
end
ext.msg.virt.last = { {}, {}, {}, {} }
end
--- Insert special character at cursor position.

View File

@ -334,18 +334,22 @@ function M.show_msg(tar, content, replace_last, append, pager)
end
end
local replace_bufwrite = false
--- Route the message to the appropriate sink.
---
---@param kind string
---@alias MsgChunk [integer, string, integer]
---@alias MsgContent MsgChunk[]
---@param content MsgContent
--@param replace_last boolean
---@param replace_last boolean
--@param history boolean
---@param append boolean
function M.msg_show(kind, content, _, _, append)
if kind == 'search_count' then
function M.msg_show(kind, content, replace_last, _, append)
if kind == 'empty' then
-- A sole empty message clears the cmdline.
if ext.cfg.msg.target == 'cmd' and M.cmd.count == 0 then
M.msg_clear()
end
elseif kind == 'search_count' then
-- Extract only the search_count, not the entered search command.
-- Match any of search.c:cmdline_search_stat():' [(x | >x | ?)/(y | >y | ??)]'
content = { content[#content] }
@ -353,16 +357,18 @@ function M.msg_show(kind, content, _, _, append)
M.virt.last[M.virt.idx.search] = content
M.virt.last[M.virt.idx.cmd] = { { 0, (' '):rep(11) } }
set_virttext('last')
elseif kind == 'return_prompt' then
-- Bypass hit enter prompt.
vim.api.nvim_feedkeys(vim.keycode('<CR>'), 'n', false)
elseif kind == 'verbose' then
-- Verbose messages are sent too often to be meaningful in the cmdline:
-- Verbose messages are sent too often to be meaningful in the cmdline.
-- always route to message window regardless of cfg.msg.target.
M.show_msg('msg', content, false, append)
elseif ext.cmd.prompt then
elseif ext.cmd.prompt or kind == 'wildlist' then
-- Route to dialog that stays open so long as the cmdline prompt is active.
M.show_msg('dialog', content, api.nvim_win_get_config(ext.wins.dialog).hide, append)
replace_last = api.nvim_win_get_config(ext.wins.dialog).hide or kind == 'wildlist'
if kind == 'wildlist' then
api.nvim_buf_set_lines(ext.bufs.dialog, 0, -1, false, {})
ext.cmd.prompt = true -- Ensure dialog is closed when cmdline is hidden.
end
M.show_msg('dialog', content, replace_last, append)
M.set_pos('dialog')
else
-- Set the entered search command in the cmdline (if available).
@ -381,9 +387,7 @@ function M.msg_show(kind, content, _, _, append)
-- Typed "inspection" messages should be routed to the pager.
local inspect = { 'echo', 'echomsg', 'lua_print' }
local pager = kind == 'list_cmd' or (ext.cmd.level >= 0 and vim.tbl_contains(inspect, kind))
M.show_msg(tar, content, replace_bufwrite, append, pager)
-- Replace message for every second bufwrite message.
replace_bufwrite = not replace_bufwrite and kind == 'bufwrite'
M.show_msg(tar, content, replace_last, append, pager)
-- Don't remember search_cmd message as actual message.
if kind == 'search_cmd' then
M.cmd.lines, M.cmd.count, M.prev_msg = 0, 0, ''
@ -391,7 +395,14 @@ function M.msg_show(kind, content, _, _, append)
end
end
function M.msg_clear() end
---Clear currently visible messages.
function M.msg_clear()
api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {})
api.nvim_buf_set_lines(ext.bufs.msg, 0, -1, false, {})
api.nvim_win_set_config(ext.wins.msg, { hide = true })
M.dupe, M[ext.cfg.msg.target].count, M.cmd.msg_row, M.cmd.lines, M.msg.width = 0, 0, -1, 1, 1
M.prev_msg, M.virt.msg = '', { {}, {} }
end
--- Place the mode text in the cmdline.
---
@ -437,8 +448,6 @@ function M.msg_history_show(entries)
M.set_pos('pager')
end
function M.msg_history_clear() end
--- Adjust dimensions of the message windows after certain events.
---
---@param type? 'cmd'|'dialog'|'msg'|'pager' Type of to be positioned window (nil for all).

View File

@ -264,6 +264,9 @@ void screenclear(void)
msg_grid_invalid = false;
clear_cmdline = true;
}
if (ui_has(kUIMessages)) {
ui_call_msg_clear();
}
}
/// Unlike cmdline "one_key" prompts, the message part of the prompt is not stored

View File

@ -7863,10 +7863,8 @@ void ex_echo(exarg_T *eap)
msg_puts_hl(" ", echo_hl_id, false);
}
char *tofree = encode_tv2echo(&rettv, NULL);
if (*tofree != NUL) {
msg_ext_append = eap->cmdidx == CMD_echon;
msg_multiline(cstr_as_string(tofree), echo_hl_id, true, false, &need_clear);
}
msg_ext_append = eap->cmdidx == CMD_echon;
msg_multiline(cstr_as_string(tofree), echo_hl_id, true, false, &need_clear);
xfree(tofree);
}
tv_clear(&rettv);

View File

@ -1405,10 +1405,16 @@ static void list_one_var(dictitem_T *v, const char *prefix, int *first)
static void list_one_var_a(const char *prefix, const char *name, const ptrdiff_t name_len,
const VarType type, const char *string, int *first)
{
msg_ext_set_kind("list_cmd");
if (*first) {
msg_ext_set_kind("list_cmd");
msg_start();
} else {
msg_putchar('\n');
}
// don't use msg() to avoid overwriting "v:statusmsg"
msg_start();
msg_puts(prefix);
if (*prefix != NUL) {
msg_puts(prefix);
}
if (name != NULL) { // "a:" vars don't have a name stored
msg_puts_len(name, name_len, 0, false);
}

View File

@ -283,9 +283,7 @@ void msg_multiline(String str, int hl_id, bool check_int, bool hist, bool *need_
}
// Print the rest of the message
if (*chunk != NUL) {
msg_outtrans_len(chunk, (int)(str.size - (size_t)(chunk - str.data)), hl_id, hist);
}
msg_outtrans_len(chunk, (int)(str.size - (size_t)(chunk - str.data)), hl_id, hist);
}
// Avoid starting a new message for each chunk and adding message to history in msg_keep().
@ -1632,7 +1630,7 @@ static void msg_home_replace_hl(const char *fname, int hl_id)
/// @return the number of characters it takes on the screen.
int msg_outtrans(const char *str, int hl_id, bool hist)
{
return msg_outtrans_len(str, (int)strlen(str), hl_id, hist);
return *str == NUL ? 0 : msg_outtrans_len(str, (int)strlen(str), hl_id, hist);
}
/// Output one character at "p".
@ -1714,8 +1712,8 @@ int msg_outtrans_len(const char *msgstr, int len, int hl_id, bool hist)
}
}
if (str > plain_start && !got_int) {
// Print the printable chars at the end.
if ((str > plain_start || plain_start == msgstr) && !got_int) {
// Print the printable chars at the end (or emit empty string).
msg_puts_len(plain_start, str - plain_start, hl_id, hist);
}
@ -2155,6 +2153,9 @@ void msg_puts_len(const char *const str, const ptrdiff_t len, int hl_id, bool hi
// Don't print anything when using ":silent cmd" or empty message.
if (msg_silent != 0 || *str == NUL) {
if (*str == NUL && ui_has(kUIMessages)) {
ui_call_msg_show(cstr_as_string("empty"), (Array)ARRAY_DICT_INIT, false, false, false);
}
return;
}

View File

@ -196,7 +196,7 @@ describe('vim.ui_attach', function()
pos = 0,
} },
})
feed('version<CR><CR>v<Esc>')
feed('version<CR>')
screen:expect({
grid = [[
^2 |
@ -208,7 +208,7 @@ describe('vim.ui_attach', function()
screen.messages = {} -- Ignore the build dependent :version content
end,
})
feed([[:call confirm("Save changes?", "&Yes\n&No\n&Cancel")<CR>]])
feed([[v<Esc>:call confirm("Save changes?", "&Yes\n&No\n&Cancel")<CR>]])
screen:expect({
grid = [[
^4 |

View File

@ -7,95 +7,145 @@ local clear, command, exec_lua, feed = n.clear, n.command, n.exec_lua, n.feed
describe('messages2', function()
local screen
describe('target=cmd', function()
before_each(function()
clear()
screen = Screen.new()
screen:add_extra_attr_ids({
[100] = { foreground = Screen.colors.Magenta1, bold = true },
})
exec_lua(function()
require('vim._extui').enable({})
end)
end)
it('multiline messages and pager', function()
command('echo "foo\nbar"')
screen:expect([[
^ |
{1:~ }|*12
foo[+1] |
]])
command('set ruler showcmd noshowmode')
feed('g<lt>')
screen:expect([[
|
{1:~ }|*9
─{100:Pager}───────────────────────────────────────────────|
{4:fo^o }|
{4:bar }|
foo[+1] 1,3 All|
]])
-- New message clears spill indicator.
feed('Q')
screen:expect([[
|
{1:~ }|*9
─{100:Pager}───────────────────────────────────────────────|
{4:fo^o }|
{4:bar }|
{9:E354: Invalid register name: '^@'} 1,3 All|
]])
-- Multiple messages in same event loop iteration are appended.
feed([[q:echo "foo\nbar" | echo "baz"<CR>]])
screen:expect([[
|
{1:~ }|*8
─{100:Pager}───────────────────────────────────────────────|
{4:^foo }|
{4:bar }|
{4:baz }|
1,1 All|
]])
-- No error for ruler virt_text msg_row exceeding buffer length.
command([[map Q <cmd>echo "foo\nbar" <bar> ls<CR>]])
feed('qQ')
screen:expect([[
|
{1:~ }|*7
─{100:Pager}───────────────────────────────────────────────|
{4:^foo }|
{4:bar }|
{4: }|
{4: 1 %a "[No Name]" line 1 }|
1,1 All|
]])
-- edit_unputchar() does not clear already updated screen #34515.
feed('qix<Esc>dwi<C-r>')
screen:expect([[
{18:^"} |
{1:~ }|*12
^R 1,1 All|
]])
feed('-')
screen:expect([[
x^ |
{1:~ }|*12
1,2 All|
]])
end)
it('new buffer, window and options after closing a buffer', function()
command('set nomodifiable | echom "foo" | messages')
screen:expect([[
|
{1:~ }|*10
─{100:Pager}───────────────────────────────────────────────|
{4:fo^o }|
foo |
]])
command('bdelete | messages')
screen:expect_unchanged()
before_each(function()
clear()
screen = Screen.new()
screen:add_extra_attr_ids({
[100] = { foreground = Screen.colors.Magenta1, bold = true },
})
exec_lua(function()
require('vim._extui').enable({})
end)
end)
it('multiline messages and pager', function()
command('echo "foo\nbar"')
screen:expect([[
^ |
{1:~ }|*12
foo[+1] |
]])
command('set ruler showcmd noshowmode')
feed('g<lt>')
screen:expect([[
|
{1:~ }|*9
─{100:Pager}───────────────────────────────────────────────|
{4:fo^o }|
{4:bar }|
foo[+1] 1,3 All|
]])
-- New message clears spill indicator.
feed('Q')
screen:expect([[
|
{1:~ }|*9
─{100:Pager}───────────────────────────────────────────────|
{4:fo^o }|
{4:bar }|
{9:E354: Invalid register name: '^@'} 1,3 All|
]])
-- Multiple messages in same event loop iteration are appended.
feed([[q:echo "foo\nbar" | echo "baz"<CR>]])
screen:expect([[
|
{1:~ }|*8
─{100:Pager}───────────────────────────────────────────────|
{4:^foo }|
{4:bar }|
{4:baz }|
1,1 All|
]])
-- No error for ruler virt_text msg_row exceeding buffer length.
command([[map Q <cmd>echo "foo\nbar" <bar> ls<CR>]])
feed('qQ')
screen:expect([[
|
{1:~ }|*7
─{100:Pager}───────────────────────────────────────────────|
{4:^foo }|
{4:bar }|
{4: }|
{4: 1 %a "[No Name]" line 1 }|
1,1 All|
]])
-- edit_unputchar() does not clear already updated screen #34515.
feed('qix<Esc>dwi<C-r>')
screen:expect([[
{18:^"} |
{1:~ }|*12
^R 1,1 All|
]])
feed('-')
screen:expect([[
x^ |
{1:~ }|*12
1,2 All|
]])
end)
it('new buffer, window and options after closing a buffer', function()
command('set nomodifiable | echom "foo" | messages')
screen:expect([[
|
{1:~ }|*10
─{100:Pager}───────────────────────────────────────────────|
{4:fo^o }|
foo |
]])
command('bdelete | messages')
screen:expect_unchanged()
end)
it('screenclear and empty message clears messages', function()
command('echo "foo"')
screen:expect([[
^ |
{1:~ }|*12
foo |
]])
command('mode')
screen:expect([[
^ |
{1:~ }|*12
|
]])
command('echo "foo"')
screen:expect([[
^ |
{1:~ }|*12
foo |
]])
command('echo ""')
screen:expect([[
^ |
{1:~ }|*12
|
]])
command('set cmdheight=0')
command('echo "foo"')
screen:expect([[
^ |
{1:~ }|*10
{1:~ }┌───┐|
{1:~ }│{4:foo}│|
{1:~ }└───┘|
]])
command('mode')
screen:expect([[
^ |
{1:~ }|*13
]])
-- But not with target='msg'
command('echo "foo"')
screen:expect([[
^ |
{1:~ }|*10
{1:~ }┌───┐|
{1:~ }│{4:foo}│|
{1:~ }└───┘|
]])
command('echo ""')
screen:expect_unchanged()
end)
end)

View File

@ -547,6 +547,22 @@ describe('ui/ext_messages', function()
screen.messages = {}
end,
})
-- Empty messages
feed(':echo "foo" | echo "" | lua print()<CR>')
screen:expect({
grid = [[
line 1 |
^line |
{1:~ }|*3
]],
cmdline = { { abort = false } },
messages = {
{ content = { { 'foo' } }, kind = 'echo' },
{ content = {}, kind = 'empty' },
{ content = {}, kind = 'empty' },
},
})
end)
it(':echoerr', function()
@ -735,17 +751,19 @@ describe('ui/ext_messages', function()
it("doesn't crash with column adjustment #10069", function()
feed(':let [x,y] = [1,2]<cr>')
feed(':let x y<cr>')
screen:expect {
screen:expect({
grid = [[
^ |
{1:~ }|*4
]],
cmdline = { { abort = false } },
messages = {
{ content = { { 'x #1' } }, kind = 'list_cmd' },
{ content = { { 'y #2' } }, kind = 'list_cmd' },
{
content = { { 'x #1\ny #2' } },
kind = 'list_cmd',
},
},
}
})
end)
it('&showmode', function()