diff options
| author | luukvbaal <luukvbaal@gmail.com> | 2026-03-12 23:38:39 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-12 18:38:39 -0400 |
| commit | 52dd62aa6eccd74aab5197ae84136d1ee08fc4f5 (patch) | |
| tree | 877bc1a323f399ddbe717fef8de699f9df58eba1 /runtime/lua/vim | |
| parent | ce1154048b11cdc402d0e5da352a3adca19afb56 (diff) | |
| download | rneovim-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.lua | 20 | ||||
| -rw-r--r-- | runtime/lua/vim/_core/ui2/messages.lua | 177 |
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') |