aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
authorluukvbaal <luukvbaal@gmail.com>2026-03-12 23:38:39 +0100
committerGitHub <noreply@github.com>2026-03-12 18:38:39 -0400
commit52dd62aa6eccd74aab5197ae84136d1ee08fc4f5 (patch)
tree877bc1a323f399ddbe717fef8de699f9df58eba1 /runtime/lua/vim
parentce1154048b11cdc402d0e5da352a3adca19afb56 (diff)
downloadrneovim-52dd62aa6eccd74aab5197ae84136d1ee08fc4f5.tar.gz
rneovim-52dd62aa6eccd74aab5197ae84136d1ee08fc4f5.tar.bz2
rneovim-52dd62aa6eccd74aab5197ae84136d1ee08fc4f5.zip
fix(ui2): use pager to list consecutively typed commands #38272
Problem: Mimicked block mode for cmdline entered while expanded does not work intuitively for repeated commands yielding messages exceeding the screen height. The expanded cmdline resizes and scrolls to bottom/top when appending a message and entering the cmdline. Also includes the entered command, as opposed to the UI1 behavior. Crash when scrolling to bottom of pager due to recursive uv_run after shell message callback executes `nvim_command()` with 'showcmd'. Solution: Still mimic block mode when entering the expanded cmdline, but when the entered command emits a message open the pager with the current message content in the expanded cmdline. Always route typed commands to the pager when it is open. Use `nvim_buf_set_cursor()` instead of `nvim_command()`.
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/_core/ui2/cmdline.lua20
-rw-r--r--runtime/lua/vim/_core/ui2/messages.lua177
2 files changed, 108 insertions, 89 deletions
diff --git a/runtime/lua/vim/_core/ui2/cmdline.lua b/runtime/lua/vim/_core/ui2/cmdline.lua
index cd2dc12152..6532074181 100644
--- a/runtime/lua/vim/_core/ui2/cmdline.lua
+++ b/runtime/lua/vim/_core/ui2/cmdline.lua
@@ -9,6 +9,8 @@ local M = {
erow = 0, -- Buffer row at which the current cmdline ends; messages appended here in block mode.
level = 0, -- Current cmdline level; 0 when inactive.
wmnumode = 0, -- wildmenumode() when not using the pum, dialog position adjusted when toggled.
+ -- Non-zero for entered expanded cmdline, incremented for each message emitted as a result of entered command to move and open messages in the pager.
+ expand = 0,
}
--- Set the 'cmdheight' and cmdline window height. Reposition message windows.
@@ -93,8 +95,8 @@ function M.cmdline_show(content, pos, firstc, prompt, indent, level, hl_id)
-- When entering the cmdline while it is expanded, place cmdline below messages.
if M.level == 0 and ui.msg.cmd_on_key then
M.srow = api.nvim_buf_line_count(ui.bufs.cmd)
- vim.on_key(nil, ui.msg.cmd_on_key)
- elseif ui.msg.cmd.msg_row ~= -1 and not ui.msg.cmd_on_key then
+ M.expand, ui.msg.cmd_on_key = 1, nil
+ elseif ui.msg.cmd.msg_row ~= -1 and M.expand == 0 then
ui.msg.msg_clear()
end
@@ -143,15 +145,13 @@ end
---@param level integer
---@param abort boolean
function M.cmdline_hide(level, abort)
- if ui.msg.cmd_on_key then
- ui.msg.cmd_on_key, M.srow = nil, 0
- -- Close expanded cmdline if command did not emit a message, keep last line.
+ if M.expand > 0 then
+ -- Close expanded cmdline, keep last line.
vim.schedule(function()
- if ui.msg.cmd_on_key == nil then
- api.nvim_win_close(ui.wins.cmd, true)
- api.nvim_buf_set_lines(ui.bufs.cmd, 0, M.erow, false, {})
- ui.check_targets()
- end
+ api.nvim_win_close(ui.wins.cmd, true)
+ api.nvim_buf_set_lines(ui.bufs.cmd, 0, M.erow, false, {})
+ ui.check_targets()
+ M.expand, M.srow = 0, 0
end)
elseif M.srow > 0 or level > (fn.getcmdwintype() == '' and 1 or 2) then
return -- No need to hide when still in nested cmdline or cmdline_block.
diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua
index 1a34561988..d357e653ea 100644
--- a/runtime/lua/vim/_core/ui2/messages.lua
+++ b/runtime/lua/vim/_core/ui2/messages.lua
@@ -182,15 +182,11 @@ local function set_virttext(type, tgt)
set_virttext('msg') -- Readjust to new M.cmd.last_col or clear for mode.
end
- M.virt.ids[type] = api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, row, col, {
- virt_text = chunks,
- virt_text_pos = 'overlay',
- right_gravity = false,
- undo_restore = false,
- invalidate = true,
- id = M.virt.ids[type],
- priority = type == 'msg' and 2 or 1,
- })
+ local opts = { undo_restore = false, invalidate = true, id = M.virt.ids[type] }
+ opts.priority = type == 'msg' and 2 or 1
+ opts.virt_text_pos = 'overlay'
+ opts.virt_text = chunks
+ M.virt.ids[type] = api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, row, col, opts)
end
end
@@ -198,15 +194,20 @@ local hlopts = { undo_restore = false, invalidate = true, priority = 1 }
--- Move messages to expanded cmdline or pager to show in full.
local function expand_msg(src)
-- Copy and clear message from src to enlarged cmdline that is dismissed by any
- -- key press, or append to pager in case that is already open (not hidden).
+ -- 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
- local tgt = hidden and 'cmd' or 'pager'
+ local tgt = (ui.cmd.expand > 0 or not hidden) and 'pager' or 'cmd'
if tgt ~= src then
local srow = hidden and 0 or api.nvim_buf_line_count(ui.bufs.pager)
local opts = { details = true, type = 'highlight' }
local marks = api.nvim_buf_get_extmarks(ui.bufs[src], -1, 0, -1, opts)
local lines = api.nvim_buf_get_lines(ui.bufs[src], 0, -1, false)
- M.msg_clear()
+ -- Clear unless we want to keep the entered command.
+ if ui.cmd.expand == 0 then
+ M.msg_clear()
+ end
+ M.virt.msg = { {}, {} } -- Clear msg virtual text regardless.
api.nvim_buf_set_lines(ui.bufs[tgt], srow, -1, false, lines)
for _, mark in ipairs(marks) do
@@ -328,9 +329,10 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
end
elseif tgt == 'cmd' and dupe == 0 then
fn.clearmatches(ui.wins.cmd) -- Clear matchparen highlights.
- if ui.cmd.srow > 0 then
+ if ui.cmd.srow > 0 and ui.cmd.expand == 0 then
-- In block mode the cmdheight is already dynamic, so just print the full message
- -- regardless of height. Put cmdline below message.
+ -- regardless of height. Put cmdline below message. Don't do this if the block mode
+ -- was simulated for a cmdline entered while expanded, will open pager instead.
ui.cmd.srow = row + 1
else
api.nvim_win_set_cursor(ui.wins.cmd, { 1, 0 }) -- ensure first line is visible
@@ -381,9 +383,13 @@ end
---@param id integer|string
---@param trigger string
function M.msg_show(kind, content, replace_last, _, append, id, trigger)
- -- Set the entered search command in the cmdline (if available). Otherwise route
- -- to configured target: 'trigger' takes precedence over 'kind.'
+ -- Set the entered search command in the cmdline (if available).
local tgt = kind == 'search_cmd' and 'cmd'
+ -- When the pager is open always route typed commands there. This better simulates
+ -- the UI1 behavior after opening the cmdline below a previous multiline message,
+ -- and seems useful enough even when the pager was entered manually.
+ or (trigger == 'typed_cmd' and api.nvim_get_current_win() == ui.wins.pager) and 'pager'
+ -- Otherwise route to configured target: trigger takes precedence over kind.
or ui.cfg.msg.targets[trigger]
or ui.cfg.msg.targets[kind]
or ui.cfg.msg.target
@@ -419,6 +425,12 @@ function M.msg_show(kind, content, replace_last, _, append, id, trigger)
-- Should clear the search count now, mark itself is cleared by invalidate.
M.virt.last[M.virt.idx.search][1] = nil
end
+ -- When message was emitted below an already expanded cmdline, move and route to pager.
+ tgt = ui.cmd.expand > 0 and 'pager' or tgt
+ if ui.cmd.expand == 1 then
+ expand_msg('cmd')
+ end
+ ui.cmd.expand = ui.cmd.expand + (ui.cmd.expand > 0 and 1 or 0)
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)
@@ -426,7 +438,7 @@ function M.msg_show(kind, content, replace_last, _, append, id, trigger)
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')
+ api.nvim_win_set_cursor(ui.wins.pager, { api.nvim_buf_line_count(ui.bufs.pager), 0 })
end
end
end
@@ -491,6 +503,73 @@ function M.msg_history_show(entries, prev_cmd)
M.set_pos('pager')
end
+local cmd_on_key = function(_, typed)
+ typed = typed and fn.keytrans(typed)
+ if not typed or typed == '<MouseMove>' or typed == ':' then
+ if typed == ':' then
+ vim.on_key(nil, ui.ns)
+ end
+ return
+ end
+ vim.on_key(nil, ui.ns)
+ M.cmd_on_key, M.cmd.ids = nil, {}
+
+ -- Check if window was entered and reopen with original config.
+ local entered = typed == '<CR>'
+ or typed:find('LeftMouse') and fn.getmousepos().winid == ui.wins.cmd
+ pcall(api.nvim_win_close, ui.wins.cmd, true)
+ ui.check_targets()
+
+ -- Show or clear the message depending on if the pager was opened.
+ if entered then
+ api.nvim_command('norm! g<')
+ end
+ set_virttext('msg')
+end
+
+--- Add virtual [+x] text to indicate scrolling is possible.
+local function set_top_bot_spill()
+ local topspill = fn.line('w0', ui.wins.dialog) - 1
+ local botspill = api.nvim_buf_line_count(ui.bufs.dialog) - fn.line('w$', ui.wins.dialog)
+ M.virt.top[1][1] = topspill > 0 and { 0, (' [+%d]'):format(topspill) } or nil
+ set_virttext('top', 'dialog')
+ M.virt.bot[1][1] = botspill > 0 and { 0, (' [+%d]'):format(botspill) } or nil
+ set_virttext('bot', 'dialog')
+ api.nvim__redraw({ flush = true })
+end
+
+--- Allow paging in the dialog window, consume the key if the topline changes.
+local dialog_on_key = function(_, typed)
+ typed = typed and fn.keytrans(typed)
+ if not typed then
+ return
+ elseif typed == '<Esc>' then
+ -- Stop paging, redraw empty title to reflect paging is no longer active.
+ api.nvim_win_set_config(ui.wins.dialog, { title = '' })
+ api.nvim__redraw({ flush = true })
+ vim.on_key(nil, M.dialog_on_key)
+ M.dialog_on_key = nil
+ return ''
+ end
+
+ local page_keys = {
+ g = 'gg',
+ G = 'G',
+ j = 'Lj',
+ k = 'Hk',
+ d = [[\<C-D>]],
+ u = [[\<C-U>]],
+ f = [[\<C-F>]],
+ b = [[\<C-B>]],
+ }
+ local info = page_keys[typed] and fn.getwininfo(ui.wins.dialog)[1]
+ if info and (typed ~= 'f' or info.botline < api.nvim_buf_line_count(ui.bufs.dialog)) then
+ fn.win_execute(ui.wins.dialog, ('exe "norm! %s"'):format(page_keys[typed]))
+ set_top_bot_spill()
+ return fn.getwininfo(ui.wins.dialog)[1].topline ~= info.topline and '' or nil
+ end
+end
+
--- Adjust visibility and dimensions of the message windows after certain events.
---
---@param tgt? 'cmd'|'dialog'|'msg'|'pager' Target window to be positioned (nil for all).
@@ -516,70 +595,10 @@ function M.set_pos(tgt)
M.virt.msg[M.virt.idx.spill][1] = spill and { 0, spill } or nil
set_virttext('msg', 'cmd')
M.virt.msg[M.virt.idx.spill][1] = save_spill
- M.cmd_on_key = vim.on_key(function(_, typed)
- typed = typed and fn.keytrans(typed)
- if not typed or typed == '<MouseMove>' or typed == ':' then
- return
- end
- vim.on_key(nil, ui.ns)
- M.cmd_on_key, M.cmd.ids = nil, {}
-
- -- Check if window was entered and reopen with original config.
- local entered = typed == '<CR>'
- or typed:find('LeftMouse') and fn.getmousepos().winid == ui.wins.cmd
- pcall(api.nvim_win_close, ui.wins.cmd, true)
- ui.check_targets()
-
- -- Show or clear the message depending on if the pager was opened.
- if entered then
- api.nvim_command('norm! g<')
- end
- set_virttext('msg')
- end, ui.ns)
+ M.cmd_on_key = vim.on_key(cmd_on_key, ui.ns)
elseif tgt == 'dialog' then
- -- Add virtual [+x] text to indicate scrolling is possible.
- local function set_top_bot_spill()
- local topspill = fn.line('w0', ui.wins.dialog) - 1
- local botspill = api.nvim_buf_line_count(ui.bufs.dialog) - fn.line('w$', ui.wins.dialog)
- M.virt.top[1][1] = topspill > 0 and { 0, (' [+%d]'):format(topspill) } or nil
- set_virttext('top', 'dialog')
- M.virt.bot[1][1] = botspill > 0 and { 0, (' [+%d]'):format(botspill) } or nil
- set_virttext('bot', 'dialog')
- api.nvim__redraw({ flush = true })
- end
+ M.dialog_on_key = vim.on_key(dialog_on_key, M.dialog_on_key)
set_top_bot_spill()
-
- -- Allow paging in the dialog window, consume the key if the topline changes.
- M.dialog_on_key = vim.on_key(function(_, typed)
- typed = typed and fn.keytrans(typed)
- if not typed then
- return
- elseif typed == '<Esc>' then
- -- Stop paging, redraw empty title to reflect paging is no longer active.
- api.nvim_win_set_config(ui.wins.dialog, { title = '' })
- api.nvim__redraw({ flush = true })
- vim.on_key(nil, M.dialog_on_key)
- M.dialog_on_key = nil
- return ''
- end
-
- local page_keys = {
- g = 'gg',
- G = 'G',
- j = 'Lj',
- k = 'Hk',
- d = [[\<C-D>]],
- u = [[\<C-U>]],
- f = [[\<C-F>]],
- b = [[\<C-B>]],
- }
- local info = page_keys[typed] and fn.getwininfo(ui.wins.dialog)[1]
- if info and (typed ~= 'f' or info.botline < api.nvim_buf_line_count(ui.bufs.dialog)) then
- fn.win_execute(ui.wins.dialog, ('exe "norm! %s"'):format(page_keys[typed]))
- set_top_bot_spill()
- return fn.getwininfo(ui.wins.dialog)[1].topline ~= info.topline and '' or nil
- end
- end, M.dialog_on_key)
elseif tgt == 'msg' then
-- Ensure last line is visible and first line is at top of window.
fn.win_execute(ui.wins.msg, 'norm! Gzb')