diff --git a/runtime/doc/message.txt b/runtime/doc/message.txt index 23668a85ad..bfea1e90ee 100644 --- a/runtime/doc/message.txt +++ b/runtime/doc/message.txt @@ -852,7 +852,10 @@ This message is given when the |ui2| modal dialog is active. Only when the 'more' option is on. Highlighted with the |hl-MoreMsg| group. -With |ui2|: use "g<" to show messages in the pager. +With |ui2|: use "g<" to show messages in the pager. When a command emits a +message while a previous message is still displayed, the messages are +collected in the pager window without focusing it: use "g<" to enter the +pager, any other typed key dismisses it. Type Effect ~ or or j or one more line diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 3ee7493e6b..e93eed2da3 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -413,6 +413,12 @@ function M.show_msg(tgt, kind, content, replace_last, append, id) end local in_pager = false -- Whether the pager is or will be the current window. +local pager_focus = false -- Whether the pager was explicitly requested (|g<|, :messages). + +local function pager_shown() + return not api.nvim_win_get_config(ui.wins.pager).hide +end + --- Route the message to the appropriate sink. --- ---@param kind string @@ -437,7 +443,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 in_pager and fn.getcmdwintype() == '') and 'pager' + or (trigger == 'typed_cmd' and pager_shown() and fn.getcmdwintype() == '') and 'pager' -- Otherwise route to configured target: or (trigger ~= '' and ui.cfg.msg.targets[trigger]) or id_target @@ -486,8 +492,8 @@ 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 not in_pager - M.show_msg(tgt, kind, content, replace_last or enter_pager, append, id) + local open_pager = tgt == 'pager' and not pager_shown() + M.show_msg(tgt, kind, content, replace_last or open_pager, append, id) if kind == 'search_cmd' then -- Don't remember search_cmd message as actual message. M.cmd.ids, M.cmd.prev_msg = {}, '' @@ -495,7 +501,7 @@ function M.msg_show(kind, content, replace_last, _, append, id, trigger) -- Position cursor at start of first or last message at bottom of window. -- Keep Normal commands: nvim_win_set_cursor() only ensures visibility, while zb -- reliably places the final message at the bottom. :noautocmd avoids #40780. - fn.win_execute(ui.wins.pager, 'noautocmd norm! ' .. (enter_pager and 'gg0' or 'G0zb')) + fn.win_execute(ui.wins.pager, 'noautocmd norm! ' .. (open_pager and 'gg0' or 'G0zb')) end end end @@ -567,6 +573,8 @@ function M.msg_history_show(entries, prev_cmd) M.show_msg('pager', entry[1], entry[2], i == 1, entry[3], 0) end + -- Message history was explicitly requested (e.g. |g<| or |:messages|): enter the pager. + pager_focus = true M.set_pos('pager') end @@ -574,22 +582,29 @@ local typed_g = false local function cmd_on_key(key, typed) -- Don't dismiss for non-typed keys and mouse movement. When 'g' is passed (typed -- or mapped), wait until the next key to avoid flickering when the pager is opened. - if not typed_g and (typed == '' or typed == '' or typed == 'g' or key == 'g') then + if typed == '' or (not typed_g and (typed == '' or typed == 'g' or key == 'g')) then typed_g = typed == 'g' or key == 'g' return end - vim.on_key(nil, ui.ns) - if typed == ':' or ui.cmd.level > 0 then - return -- Keep expanded messages open until cmdline closes. + if typed == ':' or ui.cmd.level > 0 or fn.getcmdtype() ~= '' then + -- Keep expanded messages open until the cmdline closes, including for the + -- key that executes it. Stay armed: a command that emits no message would + -- otherwise leave an unfocused pager without a handler. + return end + vim.on_key(nil, ui.ns) typed = fn.keytrans(typed) - -- Check if window was entered and reopen with original config. - local mode = not api.nvim_get_mode().mode:match('[it]') + -- Check if window was entered and reopen with original config. An already open + -- pager is dismissed instead; "g<" passes through to reopen and enter it. + local mode = not api.nvim_get_mode().mode:match('[it]') and not pager_shown() local enter = mode and (typed == '' or typed_g and (typed == '' or key == '<')) or (typed:find('LeftMouse') and fn.getmousepos().winid == ui.wins.cmd) if enter then + pager_focus = true M.expand_msg('cmd', 'pager') + elseif not in_pager then + pcall(api.nvim_win_set_config, ui.wins.pager, { hide = true }) end pcall(api.nvim_win_close, ui.wins.cmd, true) ui.check_targets() @@ -732,7 +747,13 @@ function M.set_pos(tgt) -- reliably places the final message at the bottom. :noautocmd avoids #40780. fn.win_execute(ui.wins.msg, 'noautocmd norm! Gzb') elseif tgt == 'pager' and not in_pager then - enter_pager() + if pager_focus then + pager_focus = false + enter_pager() + else + -- Reuse only the handler: M.cmd_on_key would mark the cmdline as expanded. + vim.on_key(cmd_on_key, ui.ns) + end end end end diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index 56f20f37cd..d0f80a6ba6 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -514,45 +514,41 @@ describe('messages2', function() {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. + -- If command emits another message it is opened in the pager without focusing + -- it, to closely mimic useful UI1 behavior. #41061 command('echo "foo\nbar"') feed(':echo "baz"') screen:expect([[ - foo | + ^foo | {1:~ }|*8 {3: }| foo | bar | - ^baz | + baz | {16::}{15:echo} {26:"baz"} | ]]) -- Subsequent typed commands are appended to the pager. feed(':echo "typed append"') screen:expect([[ - foo | + ^foo | {1:~ }|*7 {3: }| foo | bar | baz | - ^typed append | + typed append | {16::}{15:echo} {26:"typed append"} | ]]) - -- Other messages that fit 'cmdheight' are not. + -- Any other typed key dismisses the pager. feed('n') screen:expect([[ - foo | - {1:~ }|*7 - {3: }| - foo | - bar | - baz | - ^typed append | + ^foo | + {1:~ }|*12 {9:E35: No previous regular expression} | ]]) -- Non-typed key doesn't dismiss expanded cmdline #39221 command('nnoremap b :ls!:b') - feed('qb') + feed('b') screen:expect([[ foo | {1:~ }|*6 @@ -566,6 +562,54 @@ describe('messages2', function() ]]) end) + it('pager for consecutive command messages is not focused #41061', function() + local win = api.nvim_get_current_win() + command('echo "foo\nbar"') + feed(':echo "baz"') + screen:expect([[ + ^ | + {1:~ }|*8 + {3: }| + foo | + bar | + baz | + {16::}{15:echo} {26:"baz"} | + ]]) + t.eq(win, api.nvim_get_current_win()) + -- "g<" enters the pager (showing the previous command output). + feed('g') + screen:expect([[ + | + {1:~ }|*10 + {3: }| + ^baz | + | + ]]) + t.neq(win, api.nvim_get_current_win()) + -- "q" closes the entered pager. + feed('q') + t.eq(win, api.nvim_get_current_win()) + -- A typed command that emits no message keeps the pager; the next key dismisses it. + command('echo "foo\nbar"') + feed(':echo "baz"') + feed(':let g:x = 1') + screen:expect([[ + ^ | + {1:~ }|*8 + {3: }| + foo | + bar | + baz | + {16::}{15:let} {25:g:x} {15:=} {26:1} | + ]]) + feed('j') + screen:expect([[ + ^ | + {1:~ }|*12 + {16::}{15:let} {25:g:x} {15:=} {26:1} | + ]]) + end) + it('paging prompt dialog #35191', function() screen:try_resize(71, screen._height) -- Don't consume when paging is not necessary. @@ -1045,27 +1089,30 @@ describe('messages2', function() vim.cmd.highlight('VisualNC') -- "list_cmd" kind goes to pager end) screen:expect([[ - | + ^ | {1:~ }|*10 {3: }| - ^VisualNC xxx cleared {4:bar}| + VisualNC xxx cleared {4:bar}| foo | ]]) - command('hi VisualNC') -- cursor moved to last message in pager + command('hi VisualNC') -- appended to the pager without focusing it screen:expect([[ - | + ^ | {1:~ }|*9 {3: }| VisualNC xxx cleared | - ^VisualNC xxx cleared {4:bar}| + VisualNC xxx cleared {4:bar}| foo | ]]) + -- Any typed key dismisses the unfocused pager. + feed('') -- Duplicate indicator in msg and cmd target simultaneously - command('close | echo "bar" | lua print("foo")') + command('echo "bar" | lua print("foo")') + command('echo "bar" | lua print("foo")') screen:expect([[ ^ | {1:~ }|*11 - {1:~ }{4:bar(1)}| + {1:~ }{4:bar(2)}| foo(1) | ]]) finally(function() @@ -1076,7 +1123,7 @@ describe('messages2', function() screen:expect([[ ^ | {1:~ }|*11 - {1:~ }{4:bar(1)}| + {1:~ }{4:bar(2)}| "Xfile" [New] 0L, 0B written | ]]) end)