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 == '' 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 == '' + 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 == '' 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])) + 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 == '' 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 == '' - 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 == '' 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])) - 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') diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index 0aa56f6f46..48f5e1714d 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -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 echo "foo\nbar" ls]]) 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('') + -- 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('') 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"') + 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"') + 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()