mirror of
https://github.com/neovim/neovim.git
synced 2026-03-28 19:32:01 +00:00
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()`.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -85,6 +85,13 @@ describe('messages2', function()
|
||||
{1:~ }|*12
|
||||
foo 0,0-1 All|
|
||||
]])
|
||||
command('echo "foo"')
|
||||
-- Ruler still positioned correctly after dupe message.
|
||||
screen:expect([[
|
||||
^ |
|
||||
{1:~ }|*12
|
||||
foo(1) 0,0-1 All|
|
||||
]])
|
||||
-- No error for ruler virt_text msg_row exceeding buffer length.
|
||||
command([[map Q <cmd>echo "foo\nbar" <bar> ls<CR>]])
|
||||
feed('Q')
|
||||
@@ -334,32 +341,59 @@ describe('messages2', function()
|
||||
]])
|
||||
command('echo "foo\nbar"')
|
||||
screen:expect_unchanged()
|
||||
-- Place cmdline and subsequent message below expanded cmdline instead: #37653.
|
||||
feed(':')
|
||||
n.poke_eventloop()
|
||||
feed('echo "baz"')
|
||||
n.poke_eventloop()
|
||||
feed('<CR>')
|
||||
-- Place cmdline below expanded cmdline instead: #37653.
|
||||
feed(':call setline(1, "foo")')
|
||||
screen:expect([[
|
||||
^ |
|
||||
{1:~ }|*8
|
||||
|
|
||||
{1:~ }|*9
|
||||
{3: }|
|
||||
foo |
|
||||
bar |
|
||||
{16::}{15:echo} {26:"baz"} |
|
||||
baz |
|
||||
{16::}{15:call} {25:setline}{16:(}{26:1}{16:,} {26:"foo"}{16:)}^ |
|
||||
]])
|
||||
-- No message closes expanded cmdline.
|
||||
feed(':')
|
||||
n.poke_eventloop()
|
||||
feed('call setline(1, "foo")')
|
||||
n.poke_eventloop()
|
||||
-- No message closes expanded cmdline and keeps the entered command.
|
||||
feed('<CR>')
|
||||
screen:expect([[
|
||||
^foo |
|
||||
{1:~ }|*12
|
||||
{16::}{15:call} {25:setline}{16:(}{26:1}{16:,} {26:"foo"}{16:)} |
|
||||
]])
|
||||
-- If command emits another message we enter the pager to closely mimic useful UI1 behavior.
|
||||
command('echo "foo\nbar"')
|
||||
feed(':echo "baz"<CR>')
|
||||
screen:expect([[
|
||||
foo |
|
||||
{1:~ }|*8
|
||||
{3: }|
|
||||
^foo |
|
||||
bar |
|
||||
baz |
|
||||
{16::}{15:echo} {26:"baz"} |
|
||||
]])
|
||||
-- Subsequent typed commands are appended to the pager.
|
||||
feed(':echo "typed append"<CR>')
|
||||
screen:expect([[
|
||||
foo |
|
||||
{1:~ }|*7
|
||||
{3: }|
|
||||
foo |
|
||||
bar |
|
||||
baz |
|
||||
^typed append |
|
||||
{16::}{15:echo} {26:"typed append"} |
|
||||
]])
|
||||
-- Other messages that fit 'cmdheight' are not.
|
||||
feed('n')
|
||||
screen:expect([[
|
||||
foo |
|
||||
{1:~ }|*7
|
||||
{3: }|
|
||||
foo |
|
||||
bar |
|
||||
baz |
|
||||
^typed append |
|
||||
{9:E35: No previous regular expression} |
|
||||
]])
|
||||
end)
|
||||
|
||||
it('paging prompt dialog #35191', function()
|
||||
|
||||
Reference in New Issue
Block a user