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

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