fix(ui2): unable to stop display of very long message #39823

Problem:  Processing of a very long message may take a long time; there
          is no visual feedback of work being done, and no way to abort
          processing.
          Calculating text height for spill indicator inhibits
          performance for very long message.

Solution: Whenever writing part of a message is taking longer than 100ms,
          show the first part of the message, while checking for CTRL-C.
          Calculate ('wrap'-ed) text height accurately until 'lines',
          use line count beyond that.
This commit is contained in:
luukvbaal
2026-05-21 11:24:39 +02:00
committed by GitHub
parent 26df751add
commit 89431707a0

View File

@@ -205,12 +205,13 @@ end
local hlopts = { undo_restore = false, invalidate = true, priority = 1 }
--- Move messages to expanded cmdline, dialog or pager to show in full.
--- Return updated target+buffer in case it differs from 'src'.
function M.expand_msg(src, tgt)
-- Copy and clear message from src to enlarged cmdline that is dismissed by any
-- key press. Append to pager instead if it isn't hidden or we want to enter it
-- after cmdline was entered during expanded cmdline.
local hidden = api.nvim_win_get_config(ui.wins.pager).hide
tgt = tgt or not hidden and 'pager' or 'cmd'
tgt = tgt or not hidden and 'pager' or 'cmd' ---@type 'cmd'|'dialog'|'msg'|'pager'
if tgt ~= src then
local srow = hidden and 0 or api.nvim_buf_line_count(ui.bufs.pager)
local opts = { details = true, type = 'highlight' }
@@ -238,12 +239,12 @@ function M.expand_msg(src, tgt)
end
end
M.set_pos(tgt)
return tgt, ui.bufs[tgt]
end
-- Keep track of the current message column to be able to
-- append or overwrite messages for :echon or carriage returns.
local col = 0
local col = 0 -- Current message column appended to with :echon or reset for carriage return.
local cmd_timer ---@type uv.uv_timer_t? Timer resetting cmdline state next event loop.
---@param tgt 'cmd'|'dialog'|'msg'|'pager'
---@param kind string
---@param content MsgContent
@@ -289,8 +290,46 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
local start_row, width = row, M.msg.width
col = mark[2] or (append and not cr and math.min(col, #curline) or 0)
local start_col, insert = col, false
local stime, lines, code = vim.uv.hrtime(), o.lines, nil
local function set_target_pos()
if tgt == 'msg' then
api.nvim_win_set_width(ui.wins.msg, width)
local texth = api.nvim_win_text_height(ui.wins.msg, { start_row = start_row, end_row = row })
if texth.all > math.ceil(lines * 0.5) then
tgt, buf = M.expand_msg(tgt)
else
M.msg.width = width
M.msg:start_timer(ui.bufs[tgt], id)
M.set_pos(tgt)
end
elseif tgt == 'cmd' and dupe == 0 then
fn.clearmatches(ui.wins.cmd) -- Clear matchparen highlights.
if ui.cmd.srow > 0 then
-- In block mode the cmdheight is already dynamic, so just print the full message
-- regardless of height. Put cmdline below message.
ui.cmd.srow = row + 1
else
api.nvim_win_set_cursor(ui.wins.cmd, { 1, 0 }) -- ensure first line is visible
-- Place [+x] indicator for lines that spill over 'cmdheight'.
local texth = api.nvim_win_text_height(ui.wins.cmd, { max_height = lines })
texth.all = math.max(texth.all, api.nvim_buf_line_count(ui.bufs.cmd))
local spill = texth.all > ui.cmdheight and (' [+%d]'):format(texth.all - ui.cmdheight)
M.virt.cmd[M.virt.idx.spill][1] = spill and { 0, spill } or nil
M.cmd.msg_row = texth.end_row
-- Expand the cmdline for a non-error message that doesn't fit.
local expand = ui.cfg.msg.cmd.height < 1 or ui.cfg.msg.cmd.height > ui.cmdheight
local error_kinds = { rpc_error = 1, emsg = 1, echoerr = 1, lua_error = 1 }
if expand and texth.all > ui.cmdheight and (ui.cmdheight == 0 or not error_kinds[kind]) then
tgt, buf = M.expand_msg(tgt)
end
end
elseif tgt ~= 'cmd' then
M.set_pos(tgt)
end
end
-- Accumulate to be inserted and highlighted message chunks.
for i, chunk in ipairs(content) do
-- Split at newline and write to start of line after carriage return.
for str in (chunk[2] .. '\0'):gmatch('.-[\n\r%z]') do
@@ -314,63 +353,40 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
api.nvim_buf_set_extmark(buf, ui.ns, row, col, hlopts)
end
-- Show progress and check for CTRL-C when taking a while.
local time = vim.uv.hrtime()
if time - stime > 100000000 then
stime, _, code = time, vim.wait(0, nil, 0)
if code == -2 then
break -- CTRL-C
end
set_target_pos()
api.nvim__redraw({ flush = true })
elseif tgt == 'msg' and (pat == '\n' or (i == #content and pat == '\0')) then
width = api.nvim_win_call(ui.wins.msg, function()
return math.max(width, fn.strdisplaywidth(curline))
end)
end
if pat == '\n' then
row, col, insert = row + 1, 0, mark[1] ~= nil
else
col = pat == '\r' and 0 or end_col
end
if tgt == 'msg' and (pat == '\n' or (i == #content and pat == '\0')) then
width = api.nvim_win_call(ui.wins.msg, function()
return math.max(width, fn.strdisplaywidth(curline))
end)
end
end
if code == -2 then
break -- CTRL-C
end
end
if M[tgt] then
-- Keep track of message span to replace by ID.
local opts = { end_row = row, end_col = col, invalidate = true, undo_restore = false }
M[tgt].ids[id] = M[tgt].ids[id] or {}
M[tgt].prev_id, M[tgt].prev_msg, M[tgt].ids[id] = id, msg, M[tgt].ids[id] or {}
M[tgt].ids[id].extid = api.nvim_buf_set_extmark(buf, ui.ns, start_row, start_col, opts)
M[tgt].prev_id, M[tgt].prev_msg, M[tgt].dupe = id, msg, dupe
end
if tgt == 'msg' then
api.nvim_win_set_width(ui.wins.msg, width)
local texth = api.nvim_win_text_height(ui.wins.msg, { start_row = start_row, end_row = row })
if texth.all > math.ceil(o.lines * 0.5) then
M.expand_msg(tgt)
else
M.msg.width = width
M.msg:start_timer(buf, id)
end
elseif tgt == 'cmd' and dupe == 0 then
fn.clearmatches(ui.wins.cmd) -- Clear matchparen highlights.
if ui.cmd.srow > 0 then
-- In block mode the cmdheight is already dynamic, so just print the full message
-- regardless of height. Put cmdline below message.
ui.cmd.srow = row + 1
else
api.nvim_win_set_cursor(ui.wins.cmd, { 1, 0 }) -- ensure first line is visible
-- Place [+x] indicator for lines that spill over 'cmdheight'.
local texth = api.nvim_win_text_height(ui.wins.cmd, {})
local spill = texth.all > ui.cmdheight and (' [+%d]'):format(texth.all - ui.cmdheight)
M.virt.cmd[M.virt.idx.spill][1] = spill and { 0, spill } or nil
M.cmd.msg_row = texth.end_row
-- Expand the cmdline for a non-error message that doesn't fit.
local expand = ui.cfg.msg.cmd.height < 1 or ui.cfg.msg.cmd.height > ui.cmdheight
local error_kinds = { rpc_error = 1, emsg = 1, echoerr = 1, lua_error = 1 }
if expand and texth.all > ui.cmdheight and (ui.cmdheight == 0 or not error_kinds[kind]) then
M.expand_msg(tgt)
end
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
set_target_pos()
if M[tgt] and not M.cmd_on_key and row == api.nvim_buf_line_count(buf) - 1 then
-- Place (x) indicator for repeated messages. Mainly to mitigate unnecessary
@@ -542,7 +558,7 @@ function M.msg_history_show(entries, prev_cmd)
end
local typed_g = false
local cmd_on_key = function(key, typed)
local function cmd_on_key(key, typed)
-- Don't dismiss for non-typed keys and mouse movement. When 'g' is passed (typed
-- or mapped), wait until the next key to avoid flickering when the pager is opened.
if not typed_g and (typed == '' or typed == '<MouseMove>' or typed == 'g' or key == 'g') then
@@ -674,7 +690,7 @@ function M.set_pos(tgt)
and api.nvim_win_is_valid(win)
and api.nvim_win_get_config(win)
if cfg and (tgt or not cfg.hide) then
local texth = api.nvim_win_text_height(win, {})
local texth = api.nvim_win_text_height(win, { max_height = o.lines })
local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' }
cfg = { hide = false, relative = 'laststatus', col = 10000 } ---@type table
cfg.row, cfg.height, cfg.border = win_row_height_border(t, texth.all)
@@ -684,6 +700,7 @@ function M.set_pos(tgt)
if tgt == 'cmd' then
-- Dismiss temporarily expanded cmdline on next keypress and update spill indicator.
texth.all = math.max(texth.all, api.nvim_buf_line_count(ui.bufs.cmd))
local spill = texth.all > cfg.height and (' [+%d]'):format(texth.all - cfg.height)
M.virt.cmd[M.virt.idx.spill][1] = spill and { 0, spill } or nil
set_virttext('cmd', 'cmd')