From b08c289a459332df2a854a94cc475ae12e350026 Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Thu, 16 Apr 2026 22:32:08 +0200 Subject: [PATCH] fix(ui2): dialog paging is inconsistent #39128 Problem: - Paging keys in the dialog window consume input when the user may not expect it. The dismissable title hint intended to mitigate that results in having to press Escape twice to abandon the prompt. - Mimicked "msgsep" float border is taking up unnecessary space when window takes up the entire screen. Solution: - Use (conventional, albeit less convenient) keys intended for scrolling to page the dialog window: <(Mousewheel/Page)Up/Down>, . - Only set the float top border when separation is actually necessary, i.e. window does not reach the first row. (cherry picked from commit f0a8e6f3377c5393859c85cd44c1d2c57e8fe2d2) --- runtime/lua/vim/_core/ui2/messages.lua | 53 ++++++--------- test/functional/ui/messages2_spec.lua | 89 ++++++-------------------- 2 files changed, 40 insertions(+), 102 deletions(-) diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 5c4356d995..33e25aa4f4 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -527,6 +527,7 @@ local cmd_on_key = function(key, typed) or (typed:find('LeftMouse') and fn.getmousepos().winid == ui.wins.cmd) if entered then M.expand_msg('cmd', 'pager') + api.nvim_win_set_cursor(ui.wins.pager, { 1, 0 }) end pcall(api.nvim_win_close, ui.wins.cmd, true) ui.check_targets() @@ -554,28 +555,19 @@ local dialog_on_key = function(_, typed) typed = typed and fn.keytrans(typed) if not typed then return - elseif typed == '' 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 = [[\]], - u = [[\]], - f = [[\]], - 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])) + -- TODO: no hint anymore, so should at least be documented at some point. + local map, eob = {}, typed:match('PageDown') + map[''], map[''] = 'gg', 'G' + map[''], map[''] = 'Hk', 'Hk' + map[''], map[''] = 'Lj', 'Lj' + map[''], map[''] = [[\]], [[\]] + map[''], map[''] = [[\]], [[\]] + + local info = map[typed] and fn.getwininfo(ui.wins.dialog)[1] + if info and (not eob or info.botline < api.nvim_buf_line_count(ui.bufs.dialog)) then + fn.win_execute(ui.wins.dialog, ('exe "norm! %s"'):format(map[typed])) set_top_bot_spill() return fn.getwininfo(ui.wins.dialog)[1].topline ~= info.topline and '' or nil end @@ -583,16 +575,17 @@ end local was_cmdwin = '' ---@param min integer Minimum window height. -local function win_row_height(tgt, min) +local function win_row_height_border(tgt, min) local cfgmin = ui.cfg.msg[tgt].height --[[@as number]] - cfgmin = cfgmin > 1 and cfgmin or math.ceil(o.lines * cfgmin) + min = math.min(min, cfgmin > 1 and cfgmin or math.ceil(o.lines * cfgmin)) if tgt ~= 'pager' then - return (tgt == 'msg' and 0 or 1) - ui.cmd.wmnumode, math.min(min, cfgmin) + return (tgt == 'msg' and 0 or 1) - ui.cmd.wmnumode, min, min < o.lines - ui.cmdheight end local cmdwin = fn.getcmdwintype() ~= was_cmdwin 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(math.min(cfgmin, min), o.lines - 1 - ui.cmdheight - global_stl - cmdwin) + local top = min < o.lines - ui.cmdheight - global_stl - cmdwin + return row, math.min(min, o.lines - (top and 1 or 0) - ui.cmdheight - global_stl - cmdwin), top end local function enter_pager() @@ -624,7 +617,7 @@ local function enter_pager() 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('pager', height) + cfg.row, cfg.height, cfg.border = win_row_height_border('pager', height) else pcall(api.nvim_set_option_value, 'eiw', 'all', { scope = 'local', win = ui.wins.pager }) api.nvim_del_autocmd(id) @@ -651,14 +644,10 @@ function M.set_pos(tgt) if cfg and (tgt or not cfg.hide) then local texth = api.nvim_win_text_height(win, {}) local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' } - local hint = 'f/d/j: screen/page/line down, b/u/k: up, : stop paging' cfg = { hide = false, relative = 'laststatus', col = 10000 } ---@type table - cfg.row, cfg.height = win_row_height(t, texth.all) - cfg.border = t ~= 'msg' and { '', top, '', '', '', '', '', '' } or nil + cfg.row, cfg.height, cfg.border = win_row_height_border(t, texth.all) + cfg.border = cfg.border and t ~= 'msg' and { '', top, '', '', '', '', '', '' } or nil cfg.mouse = tgt == 'cmd' or nil - cfg.title = tgt == 'dialog' - and { { ui.cmd.expand == 0 and cfg.height < texth.all and hint or '', 'MsgMore' } } - or nil api.nvim_win_set_config(win, cfg) if tgt == 'cmd' then @@ -668,7 +657,7 @@ function M.set_pos(tgt) set_virttext('msg', 'cmd') M.virt.msg[M.virt.idx.spill][1] = { 0, (' [+%d]'):format(texth.all - ui.cmdheight) } M.cmd_on_key = vim.on_key(cmd_on_key, ui.ns) - elseif tgt == 'dialog' and set_top_bot_spill() and #cfg.title[1][1] > 0 then + elseif tgt == 'dialog' and set_top_bot_spill() then M.dialog_on_key = vim.on_key(dialog_on_key, M.dialog_on_key) elseif tgt == 'msg' then -- Ensure last line is visible and first line is at top of window. diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index 9350b877e4..c3cf060bc9 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -182,25 +182,22 @@ describe('messages2', function() -- Do enter the pager in normal mode. feed('') screen:expect([[ - {3: }| ^foo | - foo |*11 + foo |*12 1,1 Top| ]]) -- Changing 'laststatus' reveals the global statusline with a pager height -- exceeding the available lines: #38008. command('set laststatus=3') screen:expect([[ - {3: }| ^foo | - foo |*10 + foo |*11 {3:[Pager] 1,1 Top}| | ]]) feed(':') screen:expect([[ - {3: }| - foo |*4 + foo |*5 {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | {1::}^ | {1:~ }|*5 @@ -209,8 +206,7 @@ describe('messages2', function() ]]) command('wincmd +') screen:expect([[ - {3: }| - foo |*3 + foo |*4 {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | {1::}^ | {1:~ }|*6 @@ -219,8 +215,7 @@ describe('messages2', function() ]]) command('echo "foo"') screen:expect([[ - {3: }| - foo |*3 + foo |*4 {1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines) | {1::}^ | {1:~ }|*6 @@ -229,8 +224,7 @@ describe('messages2', function() ]]) feed('') screen:expect([[ - {3: }| - foo |*11 + foo |*12 {3:[Pager] 1,1 Top}| {16::}^ | ]]) @@ -248,9 +242,8 @@ describe('messages2', function() ]]) feed(':messages') screen:expect([[ - {3: }| ^foo | - foo |*10 + foo |*11 {3:[Pager] 1,1 Top}| | ]]) @@ -527,7 +520,7 @@ describe('messages2', function() local top = [[ | {1:~ }|*4 - {3: }f/d/j: screen/page/line down, b/u/k: up, : stop paging{3: }| + {3: }| 0 | 1 | 2 | @@ -539,11 +532,11 @@ describe('messages2', function() ]] feed(':call inputlist(range(100))') screen:expect(top) - feed('j') + feed('') screen:expect([[ | {1:~ }|*4 - {3: }f/d/j: screen/page/line down, b/u/k: up, : stop paging{3: }| + {3: }| 1 [+1] | 2 | 3 | @@ -553,29 +546,13 @@ describe('messages2', function() 7 [+92] | Type number and or click with the mouse (q or empty cancels): ^ | ]]) - feed('k') + feed('') screen:expect(top) - feed('d') + feed('') screen:expect([[ | {1:~ }|*4 - {3: }f/d/j: screen/page/line down, b/u/k: up, : stop paging{3: }| - 3 [+3] | - 4 | - 5 | - 6 | - 7 | - 8 | - 9 [+90] | - Type number and or click with the mouse (q or empty cancels): ^ | - ]]) - feed('u') - screen:expect(top) - feed('f') - screen:expect([[ - | - {1:~ }|*4 - {3: }f/d/j: screen/page/line down, b/u/k: up, : stop paging{3: }| + {3: }| 5 [+5] | 6 | 7 | @@ -585,13 +562,13 @@ describe('messages2', function() 11 [+88] | Type number and or click with the mouse (q or empty cancels): ^ | ]]) - feed('b') + feed('') screen:expect(top) - feed('G') + feed('') screen:expect([[ | {1:~ }|*4 - {3: }f/d/j: screen/page/line down, b/u/k: up, : stop paging{3: }| + {3: }| 93 [+93] | 94 | 95 | @@ -602,38 +579,10 @@ describe('messages2', function() Type number and or click with the mouse (q or empty cancels): ^ | ]]) -- No scrolling beyond end of buffer #36114 - feed('f') - screen:expect([[ - | - {1:~ }|*3 - {3: }f/d/j: screen/page/line down, b/u/k: up, : stop paging{3: }| - 93 [+93] | - 94 | - 95 | - 96 | - 97 | - 98 | - 99 | - Type number and or click with the mouse (q or empty cancels): f| - ^ | - ]]) - feed('g') + feed('') + screen:expect_unchanged() + feed('') screen:expect(top) - feed('f') - screen:expect([[ - | - {1:~ }|*3 - {3: }| - 0 | - 1 | - 2 | - 3 | - 4 | - 5 | - 6 [+93] | - Type number and or click with the mouse (q or empty cancels): f| - ^ | - ]]) end) it('FileType is fired after default options are set', function()