fix(ui2): wrong condition used to detect active pager

Problem:  Entering the pager is scheduled to avoid errors while in the
          cmdwin. Meanwhile the current window is used as a condition to
          detect an active pager.
Solution: Keep track of when the user is in the pager (or entered the
          cmdwin while the pager was open).
This commit is contained in:
Luuk van Baal
2026-03-17 01:50:07 +01:00
parent dff3028fe7
commit e58a842749
2 changed files with 96 additions and 34 deletions

View File

@@ -371,6 +371,7 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
end
end
local in_pager = false -- Whether the pager is or will be the current window.
--- Route the message to the appropriate sink.
---
---@param kind string
@@ -388,7 +389,7 @@ function M.msg_show(kind, content, replace_last, _, append, id, trigger)
-- 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'
or (trigger == 'typed_cmd' and in_pager and fn.getcmdwintype() == '') and 'pager'
-- Otherwise route to configured target: trigger takes precedence over kind.
or ui.cfg.msg.targets[trigger]
or ui.cfg.msg.targets[kind]
@@ -432,12 +433,12 @@ function M.msg_show(kind, content, replace_last, _, append, id, trigger)
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)
local enter_pager = tgt == 'pager' and not in_pager
M.show_msg(tgt, kind, content, replace_last or enter_pager or ui.cmd.expand > 0, 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
elseif tgt == 'pager' and in_pager and not enter_pager then
api.nvim_win_set_cursor(ui.wins.pager, { api.nvim_buf_line_count(ui.bufs.pager), 0 })
end
end
@@ -570,21 +571,67 @@ local dialog_on_key = function(_, typed)
end
end
---@param min integer Minimum window height.
local function win_row_height(win, min)
if win ~= ui.wins.pager then
return (win == ui.wins.msg and 0 or 1) - ui.cmd.wmnumode,
math.min(min, math.ceil(o.lines * 0.5))
end
local cmdwin = fn.getcmdwintype() ~= '' and api.nvim_win_get_height(0) or 0
local global_stl = (cmdwin > 0 or o.laststatus == 3) and 1 or 0
local row = 1 - cmdwin - global_stl
return row, math.min(min, o.lines - 1 - ui.cmdheight - global_stl - cmdwin)
end
local function enter_pager()
in_pager = true
-- Cmdwin is closed one event iteration later so schedule in case it was open.
vim.schedule(function()
local height, id = api.nvim_win_get_height(ui.wins.pager), 0
api.nvim_win_set_cursor(ui.wins.pager, { 1, 0 })
api.nvim_set_option_value('eiw', '', { scope = 'local', win = ui.wins.pager })
api.nvim_set_current_win(ui.wins.pager)
id = api.nvim_create_autocmd({ 'WinEnter', 'CmdwinEnter', 'WinResized' }, {
group = ui.augroup,
callback = function(ev)
if fn.getcmdtype() ~= '' then
-- WinEnter fires before we can detect cmdwin will be entered: keep open.
return
elseif ev.event == 'WinResized' and fn.getcmdwintype() == '' then
-- Remember height to be restored when cmdwin is closed.
height = api.nvim_win_get_height(ui.wins.pager)
elseif ev.event == 'WinEnter' then
-- Close when no longer current window.
in_pager = api.nvim_get_current_win() == ui.wins.pager
end
in_pager = in_pager and api.nvim_win_is_valid(ui.wins.pager)
local cfg = in_pager and { relative = 'laststatus', col = 0 } or { hide = true }
if in_pager then
cfg.row, cfg.height = win_row_height(ui.wins.pager, height)
end
pcall(api.nvim_win_set_config, ui.wins.pager, cfg)
if not in_pager then
pcall(api.nvim_set_option_value, 'eiw', 'all', { scope = 'local', win = ui.wins.pager })
api.nvim_del_autocmd(id)
end
end,
desc = 'Hide or reposition pager window.',
})
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).
function M.set_pos(tgt)
local function win_set_pos(win)
local cfg = { hide = false, relative = 'laststatus', col = 10000 }
local texth = api.nvim_win_text_height(win, {})
local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' }
local lines = o.lines - (win == ui.wins.pager and ui.cmdheight + (o.ls == 3 and 2 or 0) or 0)
cfg.height = math.min(texth.all, math.ceil(lines * (win == ui.wins.pager and 1 or 0.5)))
local title = { 'f/d/j: screen/page/line down, b/u/k: up, <Esc>: stop paging', 'MsgSeparator' }
local cfg = { hide = false, relative = 'laststatus', col = 10000 }
cfg.row, cfg.height = win_row_height(win, texth.all)
cfg.border = win ~= ui.wins.msg and { '', top, '', '', '', '', '', '' } or nil
cfg.mouse = tgt == 'cmd' or nil
cfg.row = (win == ui.wins.msg and 0 or 1) - ui.cmd.wmnumode
cfg.row = cfg.row - ((win == ui.wins.pager and o.laststatus == 3) and 1 or 0)
local title = { 'f/d/j: screen/page/line down, b/u/k: up, <Esc>: stop paging', 'MsgSeparator' }
cfg.title = tgt == 'dialog' and cfg.height < texth.all and { title } or nil
api.nvim_win_set_config(win, cfg)
@@ -602,36 +649,14 @@ function M.set_pos(tgt)
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')
elseif tgt == 'pager' and api.nvim_get_current_win() ~= ui.wins.pager then
elseif tgt == 'pager' and not in_pager then
if fn.getcmdwintype() ~= '' then
-- Cannot leave the cmdwin to enter the pager, so close it.
-- NOTE: regression w.r.t. the message grid, which allowed this.
-- Resolving that would require somehow bypassing textlock for the pager.
api.nvim_command('quit')
end
-- Cmdwin is actually closed one event iteration later so schedule in case it was open.
vim.schedule(function()
-- Allow events while the user is in the pager.
api.nvim_set_option_value('eiw', '', { scope = 'local', win = ui.wins.pager })
api.nvim_set_current_win(ui.wins.pager)
api.nvim_win_set_cursor(ui.wins.pager, { 1, 0 })
-- Make pager relative to cmdwin when it is opened, restore when it is closed.
api.nvim_create_autocmd({ 'WinEnter', 'CmdwinEnter', 'CmdwinLeave' }, {
callback = function(ev)
if api.nvim_win_is_valid(ui.wins.pager) then
local config = ev.event == 'CmdwinLeave' and cfg
or ev.event == 'WinEnter' and { hide = true }
or { relative = 'win', win = 0, row = 0, col = 0 }
api.nvim_win_set_config(ui.wins.pager, config)
api.nvim_set_option_value('eiw', 'all', { scope = 'local', win = ui.wins.pager })
end
return ev.event == 'WinEnter'
end,
desc = 'Hide or reposition pager window.',
})
end)
enter_pager()
end
end

View File

@@ -175,6 +175,43 @@ describe('messages2', function()
{3:[Pager] 1,1 Top}|
|
]])
feed(':<C-F>')
screen:expect([[
{3: }|
foo |*4
{1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) |
{1::}^ |
{1:~ }|*5
{3:[Command Line] 2,0-1 All}|
|
]])
command('wincmd +')
screen:expect([[
{3: }|
foo |*3
{1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) |
{1::}^ |
{1:~ }|*6
{3:[Command Line] 2,0-1 All}|
|
]])
command('echo "foo"')
screen:expect([[
{3: }|
foo |*3
{1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) |
{1::}^ |
{1:~ }|*6
{3:[Command Line] 2,0-1 All}|
foo |
]])
feed('<C-C>')
screen:expect([[
{3: }|
foo |*11
{3:[Pager] 1,1 Top}|
{16::}^ |
]])
end)
it('new buffer, window and options after closing a buffer or switching tabpage', function()