commit 2b4c1127ad1c8cff38f562d71f411c35ec6ba8d6
parent 6005bc68b29ea8c061183a05fcac489a54fe40c2
Author: luukvbaal <luukvbaal@gmail.com>
Date: Fri, 27 Jun 2025 00:27:21 +0200
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.
Diffstat:
11 files changed, 225 insertions(+), 137 deletions(-)
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -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
diff --git a/runtime/doc/ui.txt b/runtime/doc/ui.txt
@@ -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
diff --git a/runtime/lua/vim/_extui/cmdline.lua b/runtime/lua/vim/_extui/cmdline.lua
@@ -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.
diff --git a/runtime/lua/vim/_extui/messages.lua b/runtime/lua/vim/_extui/messages.lua
@@ -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).
diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c
@@ -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
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
@@ -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);
diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c
@@ -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);
}
diff --git a/src/nvim/message.c b/src/nvim/message.c
@@ -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;
}
diff --git a/test/functional/lua/ui_event_spec.lua b/test/functional/lua/ui_event_spec.lua
@@ -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 |
diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua
@@ -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)
+ 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('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('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)
diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua
@@ -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()