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.
This commit is contained in:
luukvbaal
2026-02-28 14:31:02 +01:00
committed by GitHub
parent b40ca5a01c
commit 32e0d05d53
4 changed files with 93 additions and 45 deletions

View File

@@ -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.
},
})

View File

@@ -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

View File

@@ -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.

View File

@@ -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)