messages.lua (26784B)
1 local api, fn, o = vim.api, vim.fn, vim.o 2 local ui = require('vim._core.ui2') 3 4 ---@alias Msg { extid: integer, timer: uv.uv_timer_t? } 5 ---@class vim._core.ui2.messages 6 local M = { 7 -- Message window. Used for regular messages with cfg.msg.target == 'msg'. 8 -- Automatically resizes to the text dimensions up to a point, at which point 9 -- only the most recent messages will fit and be shown. A timer is started for 10 -- each message whose callback will remove the message from the window again. 11 msg = { 12 ids = {}, ---@type table<string|integer, Msg> List of visible messages. 13 width = 1, -- Current width of the message window. 14 }, 15 -- Cmdline message window. Used for regular messages with cfg.msg.target == 'cmd'. 16 -- Also contains 'ruler', 'showcmd' and search_cmd/count messages as virt_text. 17 -- Messages that don't fit the 'cmdheight' are first shown in an expanded cmdline. 18 -- Otherwise, or after an expanded cmdline is closed upon the first keypress, the 19 -- cmdline contains the messages with spilled and duplicate lines indicators. 20 cmd = { 21 ids = {}, ---@type table<string|integer, Msg> List of visible messages. 22 msg_row = -1, -- Last row of message to distinguish for placing virt_text. 23 last_col = o.columns, -- Crop text to start column of 'last' virt_text. 24 last_emsg = 0, -- Time an error was printed that should not be overwritten. 25 }, 26 dupe = 0, -- Number of times message is repeated. 27 prev_id = 0, ---@type string|integer Message id of the previous message. 28 prev_msg = '', -- Concatenated content of the previous message. 29 virt = { -- Stored virt_text state. 30 last = { {}, {}, {}, {} }, ---@type MsgContent[] status in last cmdline row. 31 msg = { {}, {} }, ---@type MsgContent[] [(x)] indicators in msg window. 32 top = { {} }, ---@type MsgContent[] [+x] top indicator in dialog window. 33 bot = { {} }, ---@type MsgContent[] [+x] bottom indicator in dialog window. 34 idx = { mode = 1, search = 2, cmd = 3, ruler = 4, spill = 1, dupe = 2 }, 35 ids = {}, ---@type { ['last'|'msg'|'top'|'bot']: integer? } Table of mark IDs. 36 delayed = false, -- Whether placement of 'last' virt_text is delayed. 37 }, 38 dialog_on_key = nil, ---@type integer? vim.on_key namespace for paging in the dialog window. 39 } 40 41 local cmd_on_key ---@type integer? Set to vim.on_key namespace while cmdline is expanded. 42 -- An external redraw indicates the start of a new batch of messages in the cmdline. 43 api.nvim_set_decoration_provider(ui.ns, { 44 on_start = function() 45 M.cmd.ids = (ui.redrawing or cmd_on_key) and M.cmd.ids or {} 46 end, 47 }) 48 49 --- Start a timer whose callback will remove the message from the message window. 50 --- 51 ---@param buf integer Buffer the message was written to. 52 ---@param id integer|string Message ID. 53 function M.msg:start_timer(buf, id) 54 if self.ids[id].timer then 55 self.ids[id].timer:stop() 56 end 57 self.ids[id].timer = vim.defer_fn(function() 58 local extid = api.nvim_buf_is_valid(buf) and self.ids[id] and self.ids[id].extid 59 local mark = extid and api.nvim_buf_get_extmark_by_id(buf, ui.ns, extid, { details = true }) 60 self.ids[id] = nil 61 if not mark or not mark[1] then 62 return 63 end 64 -- Clear prev_msg when line that may have dupe marker is removed. 65 local erow = api.nvim_buf_line_count(buf) - 1 66 M.prev_msg = ui.cfg.msg.target == 'msg' and mark[3].end_row == erow and '' or M.prev_msg 67 68 -- Remove message (including potentially leftover empty line). 69 api.nvim_buf_set_text(buf, mark[1], mark[2], mark[3].end_row, mark[3].end_col, {}) 70 if api.nvim_buf_get_lines(ui.bufs.msg, mark[1], mark[1] + 1, false)[1] == '' then 71 api.nvim_buf_set_lines(buf, mark[1], mark[1] + 1, false, {}) 72 end 73 74 -- Resize or hide message window for removed message. 75 if next(self.ids) then 76 M.set_pos('msg') 77 else 78 pcall(api.nvim_win_set_config, ui.wins.msg, { hide = true }) 79 self.width, M.virt.msg[M.virt.idx.dupe][1] = 1, nil 80 end 81 end, ui.cfg.msg.timeout) 82 end 83 84 --- Place or delete a virtual text mark in the cmdline or message window. 85 --- 86 ---@param type 'last'|'msg'|'top'|'bot' 87 ---@param tgt? 'cmd'|'msg'|'dialog' 88 local function set_virttext(type, tgt) 89 if (type == 'last' and (ui.cmdheight == 0 or M.virt.delayed)) or cmd_on_key then 90 return -- Don't show virtual text while cmdline is expanded or delaying for error. 91 end 92 93 -- Concatenate the components of M.virt[type] and calculate the concatenated width. 94 local width, chunks = 0, {} ---@type integer, [string, integer|string][] 95 local contents = M.virt[type] ---@type MsgContent[] 96 for _, content in ipairs(contents) do 97 for _, chunk in ipairs(content) do 98 chunks[#chunks + 1] = { chunk[2], chunk[3] } 99 width = width + api.nvim_strwidth(chunk[2]) 100 end 101 end 102 tgt = tgt or type == 'msg' and ui.cfg.msg.target or 'cmd' 103 104 if M.virt.ids[type] and #chunks == 0 then 105 api.nvim_buf_del_extmark(ui.bufs[tgt], ui.ns, M.virt.ids[type]) 106 M.cmd.last_col = type == 'last' and o.columns or M.cmd.last_col 107 M.virt.ids[type] = nil 108 elseif #chunks > 0 then 109 local win = ui.wins[tgt] 110 local line = (tgt == 'msg' or type == 'top') and 'w0' or type == 'bot' and 'w$' 111 local srow = line and fn.line(line, ui.wins.dialog) - 1 112 local erow = tgt == 'cmd' and math.min(M.cmd.msg_row, api.nvim_buf_line_count(ui.bufs.cmd) - 1) 113 local texth = api.nvim_win_text_height(win, { 114 max_height = (type == 'top' or type == 'bot') and 1 or api.nvim_win_get_height(win), 115 start_row = srow or nil, 116 end_row = erow or nil, 117 }) 118 local row = texth.end_row 119 local col = fn.virtcol2col(win, row + 1, texth.end_vcol) 120 local scol = fn.screenpos(win, row + 1, col).col ---@type integer 121 122 if type ~= 'last' then 123 -- Calculate at which column to place the virt_text such that it is at the end 124 -- of the last visible message line, overlapping the message text if necessary, 125 -- but not overlapping the 'last' virt_text. 126 local offset = tgt ~= 'msg' and 0 127 or api.nvim_win_get_position(win)[2] 128 + (api.nvim_win_get_config(win).border ~= 'none' and 1 or 0) 129 130 -- Check if adding the virt_text on this line will exceed the current window width. 131 local maxwidth = math.max(M.msg.width, math.min(o.columns, scol - offset + width)) 132 if tgt == 'msg' and api.nvim_win_get_width(win) < maxwidth then 133 api.nvim_win_set_width(win, maxwidth) 134 M.msg.width = maxwidth 135 end 136 137 local mwidth = tgt == 'msg' and M.msg.width or tgt == 'dialog' and o.columns or M.cmd.last_col 138 if scol - offset + width > mwidth then 139 col = fn.virtcol2col(win, row + 1, texth.end_vcol - (scol - offset + width - mwidth)) 140 end 141 142 -- Give virt_text the same highlight as the message tail. 143 local pos, opts = { row, col }, { details = true, overlap = true, type = 'highlight' } 144 local hl = api.nvim_buf_get_extmarks(ui.bufs[tgt], ui.ns, pos, pos, opts) 145 for _, chunk in ipairs(hl[1] and chunks or {}) do 146 chunk[2] = hl[1][4].hl_group 147 end 148 else 149 local mode = #M.virt.last[M.virt.idx.mode] 150 local pad = o.columns - width ---@type integer 151 local newlines = math.max(0, ui.cmdheight - texth.all) 152 row = row + newlines 153 M.cmd.last_col = mode > 0 and 0 or o.columns - (newlines > 0 and 0 or width) 154 155 if newlines > 0 then 156 -- Add empty lines to place virt_text on the last screen row. 157 api.nvim_buf_set_lines(ui.bufs.cmd, -1, -1, false, fn['repeat']({ '' }, newlines)) 158 col = 0 159 else 160 if scol > M.cmd.last_col then 161 -- Give the user some time to read an important message. 162 if os.time() - M.cmd.last_emsg < 2 then 163 M.virt.delayed = true 164 vim.defer_fn(function() 165 M.virt.delayed = false 166 set_virttext('last') 167 end, 2000) 168 return 169 end 170 171 -- Crop text on last screen row and find byte offset to place mark at. 172 local vcol = texth.end_vcol - (scol - M.cmd.last_col) 173 col = vcol <= 0 and 0 or fn.virtcol2col(win, row + 1, vcol) 174 M.prev_msg = mode > 0 and '' or M.prev_msg 175 M.virt.msg = mode > 0 and { {}, {} } or M.virt.msg 176 api.nvim_buf_set_text(ui.bufs.cmd, row, col, row, -1, { mode > 0 and ' ' or '' }) 177 end 178 179 pad = pad - ((mode > 0 or col == 0) and 0 or math.min(M.cmd.last_col, scol)) 180 end 181 table.insert(chunks, mode + 1, { (' '):rep(pad) }) 182 set_virttext('msg') -- Readjust to new M.cmd.last_col or clear for mode. 183 end 184 185 M.virt.ids[type] = api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, row, col, { 186 virt_text = chunks, 187 virt_text_pos = 'overlay', 188 right_gravity = false, 189 undo_restore = false, 190 invalidate = true, 191 id = M.virt.ids[type], 192 priority = type == 'msg' and 2 or 1, 193 }) 194 end 195 end 196 197 local hlopts = { undo_restore = false, invalidate = true, priority = 1 } 198 --- Move messages to expanded cmdline or pager to show in full. 199 local function expand_msg(src) 200 -- Copy and clear message from src to enlarged cmdline that is dismissed by any 201 -- key press, or append to pager in case that is already open (not hidden). 202 local hidden = api.nvim_win_get_config(ui.wins.pager).hide 203 local tgt = hidden and 'cmd' or 'pager' 204 if tgt ~= src then 205 local srow = hidden and 0 or api.nvim_buf_line_count(ui.bufs.pager) 206 local opts = { details = true, type = 'highlight' } 207 local marks = api.nvim_buf_get_extmarks(ui.bufs[src], -1, 0, -1, opts) 208 local lines = api.nvim_buf_get_lines(ui.bufs[src], 0, -1, false) 209 M.msg_clear() 210 211 api.nvim_buf_set_lines(ui.bufs[tgt], srow, -1, false, lines) 212 for _, mark in ipairs(marks) do 213 hlopts.end_col, hlopts.hl_group = mark[4].end_col, mark[4].hl_group 214 api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, srow + mark[2], mark[3], hlopts) 215 end 216 217 if tgt == 'cmd' and ui.cmd.highlighter then 218 ui.cmd.highlighter.active[ui.bufs.cmd] = nil 219 end 220 else 221 M.virt.msg[M.virt.idx.dupe][1] = nil 222 for _, id in pairs(M.virt.ids) do 223 api.nvim_buf_del_extmark(ui.bufs.cmd, ui.ns, id) 224 end 225 end 226 M.set_pos(tgt) 227 end 228 229 -- Keep track of the current message column to be able to 230 -- append or overwrite messages for :echon or carriage returns. 231 local col = 0 232 local cmd_timer ---@type uv.uv_timer_t? Timer resetting cmdline state next event loop. 233 ---@param tgt 'cmd'|'dialog'|'msg'|'pager' 234 ---@param kind string 235 ---@param content MsgContent 236 ---@param replace_last boolean 237 ---@param append boolean 238 ---@param id integer|string 239 function M.show_msg(tgt, kind, content, replace_last, append, id) 240 local mark, msg, cr, dupe, buf = {}, '', false, 0, ui.bufs[tgt] 241 242 if M[tgt] then -- tgt == 'cmd'|'msg' 243 local extid = M[tgt].ids[id] and M[tgt].ids[id].extid 244 if tgt == ui.cfg.msg.target then 245 -- Save the concatenated message to identify repeated messages. 246 for _, chunk in ipairs(content) do 247 msg = msg .. chunk[2] 248 end 249 local reset = extid or append or msg ~= M.prev_msg or ui.cmd.srow > 0 250 dupe = (reset and 0 or M.dupe + 1) 251 end 252 253 cr = next(M[tgt].ids) ~= nil and msg:sub(1, 1) == '\r' 254 replace_last = next(M[tgt].ids) ~= nil and not extid and (replace_last or dupe > 0) 255 extid = extid or replace_last and M[tgt].ids[M.prev_id] and M[tgt].ids[M.prev_id].extid 256 mark = extid and api.nvim_buf_get_extmark_by_id(buf, ui.ns, extid, { details = true }) or {} 257 258 -- Ensure cmdline is clear when writing the first message. 259 if tgt == 'cmd' and dupe == 0 and not next(M.cmd.ids) and ui.cmd.srow == 0 then 260 api.nvim_buf_set_lines(buf, 0, -1, false, {}) 261 end 262 end 263 264 -- Filter out empty newline messages. TODO: don't emit them. 265 if msg == '\n' then 266 return 267 end 268 269 local line_count = api.nvim_buf_line_count(buf) 270 ---@type integer Start row after last line in the target buffer, unless 271 ---this is the first message, or in case of a repeated or replaced message. 272 local row = mark[1] 273 or (M[tgt] and not next(M[tgt].ids) and ui.cmd.srow == 0 and 0) 274 or (line_count - ((replace_last or cr or append) and 1 or 0)) 275 local curline = (cr or append) and api.nvim_buf_get_lines(buf, row, row + 1, false)[1] 276 local start_row, width = row, M.msg.width 277 col = mark[2] or (append and not cr and math.min(col, #curline) or 0) 278 local start_col, insert = col, false 279 280 -- Accumulate to be inserted and highlighted message chunks. 281 for i, chunk in ipairs(content) do 282 -- Split at newline and write to start of line after carriage return. 283 for str in (chunk[2] .. '\0'):gmatch('.-[\n\r%z]') do 284 local repl, pat = str:sub(1, -2), str:sub(-1) 285 local end_col = col + #repl ---@type integer 286 287 -- Insert new line at end of buffer or when inserting lines for a replaced message. 288 if line_count < row + 1 or insert then 289 api.nvim_buf_set_lines(buf, row, row > start_row and row or -1, false, { repl }) 290 insert, line_count = false, line_count + 1 291 else 292 local erow = mark[3] and mark[3].end_row or row 293 local ecol = mark[3] and mark[3].end_col or curline and math.min(end_col, #curline) or -1 294 api.nvim_buf_set_text(buf, row, col, erow, ecol, { repl }) 295 end 296 curline = api.nvim_buf_get_lines(buf, row, row + 1, false)[1] 297 mark[3] = nil 298 299 if chunk[3] > 0 then 300 hlopts.end_col, hlopts.hl_group = end_col, chunk[3] 301 api.nvim_buf_set_extmark(buf, ui.ns, row, col, hlopts) 302 end 303 304 if pat == '\n' then 305 row, col, insert = row + 1, 0, mark[1] ~= nil 306 else 307 col = pat == '\r' and 0 or end_col 308 end 309 if tgt == 'msg' and (pat == '\n' or (i == #content and pat == '\0')) then 310 width = api.nvim_win_call(ui.wins.msg, function() 311 return math.max(width, fn.strdisplaywidth(curline)) 312 end) 313 end 314 end 315 end 316 317 if M[tgt] then 318 -- Keep track of message span to replace by ID. 319 local opts = { end_row = row, end_col = col, invalidate = true, undo_restore = false } 320 M[tgt].ids[id] = M[tgt].ids[id] or {} 321 M[tgt].ids[id].extid = api.nvim_buf_set_extmark(buf, ui.ns, start_row, start_col, opts) 322 end 323 324 if tgt == 'msg' then 325 api.nvim_win_set_width(ui.wins.msg, width) 326 local texth = api.nvim_win_text_height(ui.wins.msg, { start_row = start_row, end_row = row }) 327 if texth.all > math.ceil(o.lines * 0.5) then 328 expand_msg(tgt) 329 else 330 M.msg.width = width 331 M.msg:start_timer(buf, id) 332 end 333 elseif tgt == 'cmd' and dupe == 0 then 334 fn.clearmatches(ui.wins.cmd) -- Clear matchparen highlights. 335 if ui.cmd.srow > 0 then 336 -- In block mode the cmdheight is already dynamic, so just print the full message 337 -- regardless of height. Put cmdline below message. 338 ui.cmd.srow = row + 1 339 else 340 api.nvim_win_set_cursor(ui.wins.cmd, { 1, 0 }) -- ensure first line is visible 341 if ui.cmd.highlighter then 342 ui.cmd.highlighter.active[buf] = nil 343 end 344 -- Place [+x] indicator for lines that spill over 'cmdheight'. 345 local texth = api.nvim_win_text_height(ui.wins.cmd, {}) 346 local spill = texth.all > ui.cmdheight and (' [+%d]'):format(texth.all - ui.cmdheight) 347 M.virt.msg[M.virt.idx.spill][1] = spill and { 0, spill } or nil 348 M.cmd.msg_row = texth.end_row 349 350 -- Expand the cmdline for a non-error message that doesn't fit. 351 local error_kinds = { rpc_error = 1, emsg = 1, echoerr = 1, lua_error = 1 } 352 if texth.all > ui.cmdheight and (ui.cmdheight == 0 or not error_kinds[kind]) then 353 expand_msg(tgt) 354 end 355 end 356 end 357 358 -- Set pager/dialog/msg dimensions unless sent to expanded cmdline. 359 if tgt ~= 'cmd' and (tgt ~= 'msg' or M.msg.ids[id]) then 360 M.set_pos(tgt) 361 end 362 363 if M[tgt] and (tgt == 'cmd' or row == api.nvim_buf_line_count(buf) - 1) then 364 -- Place (x) indicator for repeated messages. Mainly to mitigate unnecessary 365 -- resizing of the message window, but also placed in the cmdline. 366 M.virt.msg[M.virt.idx.dupe][1] = dupe > 0 and { 0, ('(%d)'):format(dupe) } or nil 367 M.prev_id, M.prev_msg, M.dupe = id, msg, dupe 368 set_virttext('msg') 369 end 370 371 -- Reset message state the next event loop iteration. 372 if not cmd_timer and (col > 0 or next(M.cmd.ids) ~= nil) then 373 cmd_timer = vim.defer_fn(function() 374 M.cmd.ids, cmd_timer, col = cmd_on_key and M.cmd.ids or {}, nil, 0 375 end, 0) 376 end 377 end 378 379 --- Route the message to the appropriate sink. 380 --- 381 ---@param kind string 382 ---@alias MsgChunk [integer, string, integer] 383 ---@alias MsgContent MsgChunk[] 384 ---@param content MsgContent 385 ---@param replace_last boolean 386 --@param history boolean 387 ---@param append boolean 388 ---@param id integer|string 389 function M.msg_show(kind, content, replace_last, _, append, id) 390 -- Set the entered search command in the cmdline (if available). 391 local tgt = kind == 'search_cmd' and 'cmd' or ui.cfg.msg.targets[kind] or ui.cfg.msg.target 392 if kind == 'search_cmd' and ui.cmdheight == 0 then 393 -- Blocked by messaging() without ext_messages. TODO: look at other messaging() guards. 394 return 395 elseif kind == 'empty' then 396 -- A sole empty message clears the cmdline. 397 if ui.cfg.msg.target == 'cmd' and not next(M.cmd.ids) and ui.cmd.srow == 0 then 398 M.msg_clear() 399 end 400 elseif kind == 'search_count' then 401 -- Extract only the search_count, not the entered search command. 402 -- Match any of search.c:cmdline_search_stat():' [(x | >x | ?)/(y | >y | ??)]' 403 content = { content[#content] } 404 content[1][2] = content[1][2]:match('W? %[>?%d*%??/>?%d*%?*%]') .. ' ' 405 M.virt.last[M.virt.idx.search] = content 406 M.virt.last[M.virt.idx.cmd] = { { 0, (' '):rep(11) } } 407 set_virttext('last') 408 elseif (ui.cmd.prompt or (ui.cmd.level > 0 and tgt == 'cmd')) and ui.cmd.srow == 0 then 409 -- Route to dialog when a prompt is active, or message would overwrite active cmdline. 410 replace_last = api.nvim_win_get_config(ui.wins.dialog).hide or kind == 'wildlist' 411 if kind == 'wildlist' then 412 api.nvim_buf_set_lines(ui.bufs.dialog, 0, -1, false, {}) 413 end 414 ui.cmd.dialog = true -- Ensure dialog is closed when cmdline is hidden. 415 M.show_msg('dialog', kind, content, replace_last, append, id) 416 else 417 if tgt == 'cmd' then 418 -- Store the time when an important message was emitted in order to not overwrite 419 -- it with 'last' virt_text in the cmdline so that the user has a chance to read it. 420 M.cmd.last_emsg = (kind == 'emsg' or kind == 'wmsg') and os.time() or M.cmd.last_emsg 421 -- Should clear the search count now, mark itself is cleared by invalidate. 422 M.virt.last[M.virt.idx.search][1] = nil 423 end 424 425 local enter_pager = tgt == 'pager' and api.nvim_get_current_win() ~= ui.wins.pager 426 M.show_msg(tgt, kind, content, replace_last or enter_pager, append, id) 427 -- Don't remember search_cmd message as actual message. 428 if kind == 'search_cmd' then 429 M.cmd.ids, M.prev_msg = {}, '' 430 elseif api.nvim_get_current_win() == ui.wins.pager and not enter_pager then 431 api.nvim_command('norm! G') 432 end 433 end 434 end 435 436 ---Clear currently visible messages. 437 function M.msg_clear() 438 api.nvim_buf_set_lines(ui.bufs.cmd, 0, -1, false, {}) 439 api.nvim_buf_set_lines(ui.bufs.msg, 0, -1, false, {}) 440 api.nvim_win_set_config(ui.wins.msg, { hide = true }) 441 M[ui.cfg.msg.target].ids, M.dupe, M.cmd.msg_row, M.msg.width = {}, 0, -1, 1 442 M.prev_msg, M.virt.msg = '', { {}, {} } 443 end 444 445 --- Place the mode text in the cmdline. 446 --- 447 ---@param content MsgContent 448 function M.msg_showmode(content) 449 M.virt.last[M.virt.idx.mode] = ui.cmd.level > 0 and {} or content 450 M.virt.last[M.virt.idx.search] = {} 451 set_virttext('last') 452 end 453 454 --- Place text from the 'showcmd' buffer in the cmdline. 455 --- 456 ---@param content MsgContent 457 function M.msg_showcmd(content) 458 local str = content[1] and content[1][2]:sub(-10) or '' 459 M.virt.last[M.virt.idx.cmd][1] = (content[1] or M.virt.last[M.virt.idx.search][1]) 460 and { 0, str .. (' '):rep(11 - #str) } 461 set_virttext('last') 462 end 463 464 --- Place the 'ruler' text in the cmdline window. 465 --- 466 ---@param content MsgContent 467 function M.msg_ruler(content) 468 M.virt.last[M.virt.idx.ruler] = ui.cmd.level > 0 and {} or content 469 set_virttext('last') 470 end 471 472 ---@alias MsgHistory [string, MsgContent, boolean] 473 --- Open the message history in the pager. 474 --- 475 ---@param entries MsgHistory[] 476 ---@param prev_cmd boolean 477 function M.msg_history_show(entries, prev_cmd) 478 if #entries == 0 then 479 return 480 end 481 482 -- Showing output of previous command, clear in case still visible. 483 if cmd_on_key or prev_cmd then 484 M.msg_clear() 485 api.nvim_feedkeys(vim.keycode('<Esc>'), 'n', false) 486 end 487 488 api.nvim_buf_set_lines(ui.bufs.pager, 0, -1, false, {}) 489 for i, entry in ipairs(entries) do 490 M.show_msg('pager', entry[1], entry[2], i == 1, entry[3], 0) 491 end 492 493 M.set_pos('pager') 494 end 495 496 --- Adjust visibility and dimensions of the message windows after certain events. 497 --- 498 ---@param tgt? 'cmd'|'dialog'|'msg'|'pager' Target window to be positioned (nil for all). 499 function M.set_pos(tgt) 500 local function win_set_pos(win) 501 local cfg = { hide = false, relative = 'laststatus', col = 10000 } 502 local texth = api.nvim_win_text_height(win, {}) 503 local top = { vim.opt.fcs:get().msgsep or ' ', 'MsgSeparator' } 504 local lines = o.lines - (win == ui.wins.pager and ui.cmdheight + (o.ls == 3 and 2 or 0) or 0) 505 cfg.height = math.min(texth.all, math.ceil(lines * (win == ui.wins.pager and 1 or 0.5))) 506 cfg.border = win ~= ui.wins.msg and { '', top, '', '', '', '', '', '' } or nil 507 cfg.focusable = tgt == 'cmd' or nil 508 cfg.row = (win == ui.wins.msg and 0 or 1) - ui.cmd.wmnumode 509 cfg.row = cfg.row - ((win == ui.wins.pager and o.laststatus == 3) and 1 or 0) 510 local title = { 'f/d/j: screen/page/line down, b/u/k: up, <Esc>: stop paging', 'MsgSeparator' } 511 cfg.title = tgt == 'dialog' and cfg.height < texth.all and { title } or nil 512 api.nvim_win_set_config(win, cfg) 513 514 if tgt == 'cmd' and not cmd_on_key then 515 -- Temporarily expand the cmdline, until next key press. 516 local save_spill = M.virt.msg[M.virt.idx.spill][1] 517 local spill = texth.all > cfg.height and (' [+%d]'):format(texth.all - cfg.height) 518 M.virt.msg[M.virt.idx.spill][1] = spill and { 0, spill } or nil 519 set_virttext('msg', 'cmd') 520 M.virt.msg[M.virt.idx.spill][1] = save_spill 521 cmd_on_key = vim.on_key(function(_, typed) 522 typed = typed and fn.keytrans(typed) 523 if not typed or typed == '<MouseMove>' then 524 return 525 end 526 vim.on_key(nil, ui.ns) 527 cmd_on_key, M.cmd.ids = nil, {} 528 529 -- Check if window was entered and reopen with original config. 530 local entered = typed == '<CR>' 531 or typed:find('LeftMouse') and fn.getmousepos().winid == ui.wins.cmd 532 pcall(api.nvim_win_close, ui.wins.cmd, true) 533 ui.check_targets() 534 535 -- Show or clear the message depending on if the pager was opened. 536 if entered then 537 api.nvim_command('norm! g<') 538 end 539 set_virttext('msg') 540 end, ui.ns) 541 elseif tgt == 'dialog' then 542 -- Add virtual [+x] text to indicate scrolling is possible. 543 local function set_top_bot_spill() 544 local topspill = fn.line('w0', ui.wins.dialog) - 1 545 local botspill = api.nvim_buf_line_count(ui.bufs.dialog) - fn.line('w$', ui.wins.dialog) 546 M.virt.top[1][1] = topspill > 0 and { 0, (' [+%d]'):format(topspill) } or nil 547 set_virttext('top', 'dialog') 548 M.virt.bot[1][1] = botspill > 0 and { 0, (' [+%d]'):format(botspill) } or nil 549 set_virttext('bot', 'dialog') 550 api.nvim__redraw({ flush = true }) 551 end 552 set_top_bot_spill() 553 554 -- Allow paging in the dialog window, consume the key if the topline changes. 555 M.dialog_on_key = vim.on_key(function(_, typed) 556 typed = typed and fn.keytrans(typed) 557 if not typed then 558 return 559 elseif typed == '<Esc>' then 560 -- Stop paging, redraw empty title to reflect paging is no longer active. 561 api.nvim_win_set_config(ui.wins.dialog, { title = '' }) 562 api.nvim__redraw({ flush = true }) 563 vim.on_key(nil, M.dialog_on_key) 564 return '' 565 end 566 567 local page_keys = { 568 g = 'gg', 569 G = 'G', 570 j = 'Lj', 571 k = 'Hk', 572 d = [[\<C-D>]], 573 u = [[\<C-U>]], 574 f = [[\<C-F>]], 575 b = [[\<C-B>]], 576 } 577 local info = page_keys[typed] and fn.getwininfo(ui.wins.dialog)[1] 578 if info and (typed ~= 'f' or info.botline < api.nvim_buf_line_count(ui.bufs.dialog)) then 579 fn.win_execute(ui.wins.dialog, ('exe "norm! %s"'):format(page_keys[typed])) 580 set_top_bot_spill() 581 return fn.getwininfo(ui.wins.dialog)[1].topline ~= info.topline and '' or nil 582 end 583 end, M.dialog_on_key) 584 elseif tgt == 'msg' then 585 -- Ensure last line is visible and first line is at top of window. 586 fn.win_execute(ui.wins.msg, 'norm! Gzb') 587 elseif tgt == 'pager' and api.nvim_get_current_win() ~= ui.wins.pager then 588 if fn.getcmdwintype() ~= '' then 589 -- Cannot leave the cmdwin to enter the pager, so close it. 590 -- NOTE: regression w.r.t. the message grid, which allowed this. 591 -- Resolving that would require somehow bypassing textlock for the pager. 592 api.nvim_command('quit') 593 end 594 595 -- Cmdwin is actually closed one event iteration later so schedule in case it was open. 596 vim.schedule(function() 597 -- Allow events while the user is in the pager. 598 api.nvim_set_option_value('eiw', '', { scope = 'local', win = ui.wins.pager }) 599 api.nvim_set_current_win(ui.wins.pager) 600 api.nvim_win_set_cursor(ui.wins.pager, { 1, 0 }) 601 602 -- Make pager relative to cmdwin when it is opened, restore when it is closed. 603 api.nvim_create_autocmd({ 'WinEnter', 'CmdwinEnter', 'CmdwinLeave' }, { 604 callback = function(ev) 605 if api.nvim_win_is_valid(ui.wins.pager) then 606 local config = ev.event == 'CmdwinLeave' and cfg 607 or ev.event == 'WinEnter' and { hide = true } 608 or { relative = 'win', win = 0, row = 0, col = 0 } 609 api.nvim_win_set_config(ui.wins.pager, config) 610 api.nvim_set_option_value('eiw', 'all', { scope = 'local', win = ui.wins.pager }) 611 end 612 return ev.event == 'WinEnter' 613 end, 614 desc = 'Hide or reposition pager window.', 615 }) 616 end) 617 end 618 end 619 620 for t, win in pairs(ui.wins) do 621 local cfg = (t == tgt or (tgt == nil and t ~= 'cmd')) 622 and api.nvim_win_is_valid(win) 623 and api.nvim_win_get_config(win) 624 if cfg and (tgt or not cfg.hide) then 625 win_set_pos(win) 626 end 627 end 628 end 629 630 return M