neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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:
Mruntime/doc/news.txt | 10++++++----
Mruntime/doc/ui.txt | 8++++++--
Mruntime/lua/vim/_extui/cmdline.lua | 15++++++---------
Mruntime/lua/vim/_extui/messages.lua | 41+++++++++++++++++++++++++----------------
Msrc/nvim/drawscreen.c | 3+++
Msrc/nvim/eval.c | 6++----
Msrc/nvim/eval/vars.c | 12+++++++++---
Msrc/nvim/message.c | 13+++++++------
Mtest/functional/lua/ui_event_spec.lua | 4++--
Mtest/functional/ui/messages2_spec.lua | 224++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mtest/functional/ui/messages_spec.lua | 26++++++++++++++++++++++----
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()