commit 32e0d05d53f452a445afad161f6dda31ac054484
parent b40ca5a01c39c2acf10ff8bcca9db18b3336540c
Author: luukvbaal <luukvbaal@gmail.com>
Date: Sat, 28 Feb 2026 14:31:02 +0100
feat(ui2): configure targets per message kind #38091
Problem: Unable to configure message targets based on message kind.
Solution: Add cfg.msg.targets mapping message kinds to "cmd/msg/pager".
Check the configured target when writing the message.
cfg.msg = { target = 'cmd', targets = { progress = 'msg', list_cmd = 'pager' } }
will for example use the 'msg' target for progress messages,
immediately open the pager for 'list_cmd' and use the cmdline
for all other message kinds.
Diffstat:
4 files changed, 93 insertions(+), 45 deletions(-)
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
@@ -5285,9 +5285,11 @@ To enable the experimental UI (default opts shown): >lua
require('vim._core.ui2').enable({
enable = true, -- Whether to enable or disable the UI.
msg = { -- Options related to the message module.
- ---@type 'cmd'|'msg' Where to place regular messages, either in the
+ ---@type 'cmd'|'msg' Default message target, either in the
---cmdline or in a separate ephemeral message window.
- target = 'cmd',
+ ---@type string|table<string, 'cmd'|'msg'|'pager'> Default message target
+ or table mapping |ui-messages| kinds to a target.
+ targets = 'cmd',
timeout = 4000, -- Time a message is visible in the message window.
},
})
diff --git a/runtime/lua/vim/_core/ui2.lua b/runtime/lua/vim/_core/ui2.lua
@@ -8,9 +8,11 @@
---require('vim._core.ui2').enable({
--- enable = true, -- Whether to enable or disable the UI.
--- msg = { -- Options related to the message module.
---- ---@type 'cmd'|'msg' Where to place regular messages, either in the
+--- ---@type 'cmd'|'msg' Default message target, either in the
--- ---cmdline or in a separate ephemeral message window.
---- target = 'cmd',
+--- ---@type string|table<string, 'cmd'|'msg'|'pager'> Default message target
+--- or table mapping |ui-messages| kinds to a target.
+--- targets = 'cmd',
--- timeout = 4000, -- Time a message is visible in the message window.
--- },
---})
@@ -43,9 +45,8 @@ local M = {
cfg = {
enable = true,
msg = { -- Options related to the message module.
- ---@type 'cmd'|'msg' Where to place regular messages, either in the
- ---cmdline or in a separate ephemeral message window.
- target = 'cmd',
+ target = 'cmd', ---@type 'cmd'|'msg' Default message target if not present in targets.
+ targets = {}, ---@type table<string, 'cmd'|'msg'|'pager'> Kind specific message targets.
timeout = 4000, -- Time a message is visible in the message window.
},
},
@@ -160,6 +161,8 @@ local scheduled_ui_callback = vim.schedule_wrap(ui_callback)
function M.enable(opts)
vim.validate('opts', opts, 'table', true)
M.cfg = vim.tbl_deep_extend('keep', opts, M.cfg)
+ M.cfg.msg.target = type(M.cfg.msg.targets) == 'string' and M.cfg.msg.targets or M.cfg.msg.target
+ M.cfg.msg.targets = type(M.cfg.msg.targets) == 'table' and M.cfg.msg.targets or {}
if #vim.api.nvim_list_uis() == 0 then
return -- Don't prevent stdout messaging when no UIs are attached.
end
diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua
@@ -216,8 +216,6 @@ local function expand_msg(src)
if tgt == 'cmd' and ui.cmd.highlighter then
ui.cmd.highlighter.active[ui.bufs.cmd] = nil
- elseif tgt == 'pager' then
- api.nvim_command('norm! G')
end
else
M.virt.msg[M.virt.idx.dupe][1] = nil
@@ -329,7 +327,6 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
if texth.all > math.ceil(o.lines * 0.5) then
expand_msg(tgt)
else
- M.set_pos('msg')
M.msg.width = width
M.msg:start_timer(buf, id)
end
@@ -358,6 +355,11 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
end
end
+ -- Set pager/dialog/msg dimensions unless sent to expanded cmdline.
+ if tgt ~= 'cmd' and (tgt ~= 'msg' or M.msg.ids[id]) then
+ M.set_pos(tgt)
+ end
+
if M[tgt] and (tgt == 'cmd' or row == api.nvim_buf_line_count(buf) - 1) then
-- Place (x) indicator for repeated messages. Mainly to mitigate unnecessary
-- resizing of the message window, but also placed in the cmdline.
@@ -385,7 +387,12 @@ end
---@param append boolean
---@param id integer|string
function M.msg_show(kind, content, replace_last, _, append, id)
- if kind == 'empty' then
+ -- Set the entered search command in the cmdline (if available).
+ local tgt = kind == 'search_cmd' and 'cmd' or ui.cfg.msg.targets[kind] or ui.cfg.msg.target
+ if kind == 'search_cmd' and ui.cmdheight == 0 then
+ -- Blocked by messaging() without ext_messages. TODO: look at other messaging() guards.
+ return
+ elseif kind == 'empty' then
-- A sole empty message clears the cmdline.
if ui.cfg.msg.target == 'cmd' and not next(M.cmd.ids) and ui.cmd.srow == 0 then
M.msg_clear()
@@ -398,9 +405,7 @@ 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 (ui.cmd.level > 0 and ui.cfg.msg.target == 'cmd')) and ui.cmd.srow == 0
- then
+ elseif (ui.cmd.prompt or (ui.cmd.level > 0 and tgt == '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
@@ -408,14 +413,8 @@ function M.msg_show(kind, content, replace_last, _, append, id)
end
ui.cmd.dialog = true -- Ensure dialog is closed when cmdline is hidden.
M.show_msg('dialog', kind, content, replace_last, append, id)
- M.set_pos('dialog')
else
- -- Set the entered search command in the cmdline (if available).
- local tgt = kind == 'search_cmd' and 'cmd' or ui.cfg.msg.target
- if kind == 'search_cmd' and ui.cmdheight == 0 then
- -- Blocked by messaging() without ext_messages. TODO: look at other messaging() guards.
- return
- elseif tgt == 'cmd' then
+ if tgt == 'cmd' then
-- 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.
M.cmd.last_emsg = (kind == 'emsg' or kind == 'wmsg') and os.time() or M.cmd.last_emsg
@@ -423,10 +422,13 @@ function M.msg_show(kind, content, replace_last, _, append, id)
M.virt.last[M.virt.idx.search][1] = nil
end
- M.show_msg(tgt, kind, content, replace_last, append, id)
+ local enter_pager = tgt == 'pager' and api.nvim_get_current_win() ~= ui.wins.pager
+ M.show_msg(tgt, kind, content, replace_last or enter_pager, append, id)
-- Don't remember search_cmd message as actual message.
if kind == 'search_cmd' then
M.cmd.ids, M.prev_msg = {}, ''
+ elseif api.nvim_get_current_win() == ui.wins.pager and not enter_pager then
+ api.nvim_command('norm! G')
end
end
end
@@ -522,7 +524,7 @@ function M.set_pos(tgt)
return
end
vim.on_key(nil, ui.ns)
- cmd_on_key, M[ui.cfg.msg.target].ids = nil, {}
+ cmd_on_key, M.cmd.ids = nil, {}
-- Check if window was entered and reopen with original config.
local entered = typed == '<CR>'
@@ -581,8 +583,7 @@ function M.set_pos(tgt)
end, M.dialog_on_key)
elseif tgt == 'msg' then
-- Ensure last line is visible and first line is at top of window.
- local row = (texth.all > cfg.height and texth.end_row or 0) + 1
- api.nvim_win_set_cursor(ui.wins.msg, { row, 0 })
+ fn.win_execute(ui.wins.msg, 'norm! Gzb')
elseif tgt == 'pager' and api.nvim_get_current_win() ~= ui.wins.pager then
if fn.getcmdwintype() ~= '' then
-- Cannot leave the cmdwin to enter the pager, so close it.
diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua
@@ -4,7 +4,7 @@ local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
-local clear, command, exec_lua, feed = n.clear, n.command, n.exec_lua, n.feed
+local api, clear, command, exec_lua, feed = n.api, n.clear, n.command, n.exec_lua, n.feed
local msg_timeout = 200
local function set_msg_target_zero_ch()
@@ -631,18 +631,14 @@ describe('messages2', function()
baz |
foo |
]])
- exec_lua(function()
- vim.api.nvim_echo({ { 'foo' } }, true, { id = 2 })
- end)
+ api.nvim_echo({ { 'foo' } }, true, { id = 2 })
screen:expect([[
^ |
{1:~ }|*9
{3: }|
foo |*3
]])
- exec_lua(function()
- vim.api.nvim_echo({ { 'bar\nbaz' } }, true, { id = 1 })
- end)
+ api.nvim_echo({ { 'bar\nbaz' } }, true, { id = 1 })
screen:expect([[
^ |
{1:~ }|*8
@@ -653,7 +649,7 @@ describe('messages2', function()
]])
-- Pressing a key immediately dismisses an expanded cmdline, and
-- replacing a multiline, multicolored message doesn't error due
- -- to unneccesarily inserted lines #37994.
+ -- to unnecessarily inserted lines #37994.
feed('Q')
screen:expect([[
^ |
@@ -664,12 +660,11 @@ describe('messages2', function()
]])
feed('Q')
screen:expect_unchanged()
+ feed('<C-L>') -- close expanded cmdline
set_msg_target_zero_ch()
- exec_lua(function()
- vim.api.nvim_echo({ { 'foo' } }, true, { id = 1 })
- vim.api.nvim_echo({ { 'bar\nbaz' } }, true, { id = 2 })
- vim.api.nvim_echo({ { 'foo' } }, true, { id = 3 })
- end)
+ api.nvim_echo({ { 'foo' } }, true, { id = 1 })
+ api.nvim_echo({ { 'bar\nbaz' } }, true, { id = 2 })
+ api.nvim_echo({ { 'foo' } }, true, { id = 3 })
screen:expect([[
^ |
{1:~ }|*9
@@ -678,17 +673,13 @@ describe('messages2', function()
{1:~ }{4:baz}|
{1:~ }{4:foo}|
]])
- exec_lua(function()
- vim.api.nvim_echo({ { 'foo' } }, true, { id = 2 })
- end)
+ api.nvim_echo({ { 'foo' } }, true, { id = 2 })
screen:expect([[
^ |
{1:~ }|*10
{1:~ }{4:foo}|*3
]])
- exec_lua(function()
- vim.api.nvim_echo({ { 'f', 'Conceal' }, { 'oo\nbar' } }, true, { id = 3 })
- end)
+ api.nvim_echo({ { 'f', 'Conceal' }, { 'oo\nbar' } }, true, { id = 3 })
screen:expect([[
^ |
{1:~ }|*9
@@ -704,7 +695,7 @@ describe('messages2', function()
{1:~ }|
{3: }|
foo |*2
- {14:f}oo |
+ {14:f}oo [+6] |
]])
feed('<Esc>')
screen:expect([[
@@ -782,5 +773,56 @@ describe('messages2', function()
{1:~ }|*12
{1:~ }{4:baz}|
]])
+ -- Last message line is at bottom of window after closing it.
+ screen:try_resize(screen._width, 8)
+ command('mode | echo "1\n" | echo "2\n" | echo "3\n" | echo "4\n"')
+ screen:expect([[
+ ^ |
+ {1:~ }|*3
+ {1:~ }{4:3}|
+ {1:~ }{4: }|
+ {1:~ }{4:4}|
+ {1:~ }{4: }|
+ ]])
+ command('fclose!')
+ screen:expect([[
+ ^ |
+ {1:~ }|*7
+ ]])
+ command('echo "5\n"')
+ screen:expect([[
+ ^ |
+ {1:~ }|*3
+ {1:~ }{4:4}|
+ {1:~ }{4: }|
+ {1:~ }{4:5}|
+ {1:~ }{4: }|
+ ]])
+ end)
+
+ it('configured targets per kind', function()
+ exec_lua(function()
+ local cfg = { msg = { targets = { echo = 'msg', list_cmd = 'pager' } } }
+ require('vim._core.ui2').enable(cfg)
+ print('foo') -- "lua_print" kind goes to cmd
+ vim.cmd.echo('"bar"') -- "echo" kind goes to msg
+ vim.cmd.highlight('VisualNC') -- "list_cmd" kind goes to pager
+ end)
+ screen:expect([[
+ |
+ {1:~ }|*10
+ {3: }|
+ ^VisualNC xxx cleared {4:bar}|
+ foo |
+ ]])
+ command('hi VisualNC') -- cursor moved to last message in pager
+ screen:expect([[
+ |
+ {1:~ }|*9
+ {3: }|
+ VisualNC xxx cleared |
+ ^VisualNC xxx cleared {4:bar}|
+ foo |
+ ]])
end)
end)