commit 5e4700152b79e090d880d2734e0eaec726f4b8a7
parent 03832842d537f67c6a21d7710465f0206eccd7b0
Author: luukvbaal <luukvbaal@gmail.com>
Date: Wed, 4 Jun 2025 19:59:36 +0200
fix(extui): copy window config to new tabpage (#34308)
Problem: When opening a new tabpage, extui windows are initialized with
their default config. Window visiblity/dimensions on other
tabpages may get out of sync with their buffer content.
Solution: Copy the config of the window to the new tabpage.
No longer keep track of the various windows on each tabpage.
Close windows on inactive tabpages instead (moving them could
be more efficient but is currently not supported in the API).
Diffstat:
4 files changed, 50 insertions(+), 51 deletions(-)
diff --git a/runtime/lua/vim/_extui.lua b/runtime/lua/vim/_extui.lua
@@ -51,7 +51,7 @@ local function ui_callback(event, ...)
api.nvim__redraw({
flush = true,
cursor = handler == ext.cmd[event] and true or nil,
- win = handler == ext.cmd[event] and ext.wins[ext.tab].cmd or nil,
+ win = handler == ext.cmd[event] and ext.wins.cmd or nil,
})
end
local scheduled_ui_callback = vim.schedule_wrap(ui_callback)
@@ -63,10 +63,8 @@ function M.enable(opts)
if ext.cfg.enable == false then
-- Detach and cleanup windows, buffers and autocommands.
- for _, tab in ipairs(api.nvim_list_tabpages()) do
- for _, win in pairs(ext.wins[tab] or {}) do
- api.nvim_win_close(win, true)
- end
+ for _, win in pairs(ext.wins) do
+ api.nvim_win_close(win, true)
end
for _, buf in pairs(ext.bufs) do
api.nvim_buf_delete(buf, {})
@@ -100,7 +98,7 @@ function M.enable(opts)
if name == 'cmdheight' then
-- 'cmdheight' set; (un)hide cmdline window and set its height.
local cfg = { height = math.max(value, 1), hide = value == 0 }
- api.nvim_win_set_config(ext.wins[ext.tab].cmd, cfg)
+ api.nvim_win_set_config(ext.wins.cmd, cfg)
-- Change message position when 'cmdheight' was or becomes 0.
if value == 0 or ext.cmdheight == 0 then
ext.cfg.msg.pos = value == 0 and 'box' or ext.cmdheight == 0 and 'cmd'
@@ -124,7 +122,7 @@ function M.enable(opts)
desc = 'Set cmdline and message window dimensions for changed option values.',
})
- api.nvim_create_autocmd({ 'VimEnter', 'VimResized' }, {
+ api.nvim_create_autocmd({ 'VimEnter', 'VimResized', 'TabEnter' }, {
group = ext.augroup,
callback = function(ev)
ext.tab_check_wins()
@@ -133,13 +131,13 @@ function M.enable(opts)
end
ext.msg.set_pos()
end,
- desc = 'Set cmdline and message window dimensions after startup and shell resize.',
+ desc = 'Set extui window dimensions after startup, shell resize or tabpage change.',
})
api.nvim_create_autocmd('WinEnter', {
callback = function()
local win = api.nvim_get_current_win()
- if vim.tbl_contains(ext.wins[ext.tab] or {}, win) and api.nvim_win_get_config(win).hide then
+ if vim.tbl_contains(ext.wins, win) and api.nvim_win_get_config(win).hide then
vim.cmd.wincmd('p')
end
end,
diff --git a/runtime/lua/vim/_extui/cmdline.lua b/runtime/lua/vim/_extui/cmdline.lua
@@ -64,8 +64,8 @@ function M.cmdline_show(content, pos, firstc, prompt, indent, level, hl_id)
api.nvim_buf_set_extmark(ext.bufs.cmd, ext.ns, 0, 0, { hl_group = hl_id, end_col = promptlen })
end
- local height = math.max(ext.cmdheight, api.nvim_win_text_height(ext.wins[ext.tab].cmd, {}).all)
- win_config(ext.wins[ext.tab].cmd, false, height)
+ local height = math.max(ext.cmdheight, api.nvim_win_text_height(ext.wins.cmd, {}).all)
+ win_config(ext.wins.cmd, false, height)
M.cmdline_pos(pos)
-- Clear message cmdline state; should not be shown during, and reset after cmdline.
@@ -83,7 +83,7 @@ end
---@param shift boolean
--@param level integer
function M.cmdline_special_char(c, shift)
- api.nvim_win_call(ext.wins[ext.tab].cmd, function()
+ api.nvim_win_call(ext.wins.cmd, function()
api.nvim_put({ c }, shift and '' or 'c', false, false)
end)
end
@@ -99,12 +99,11 @@ function M.cmdline_pos(pos)
curpos[1], curpos[2] = M.row + 1, promptlen + pos
-- Add matchparen highlighting to non-prompt part of cmdline.
if pos > 0 and fn.exists('#matchparen') then
- api.nvim_win_set_cursor(ext.wins[ext.tab].cmd, { curpos[1], curpos[2] - 1 })
- vim.wo[ext.wins[ext.tab].cmd].eventignorewin = ''
- fn.win_execute(ext.wins[ext.tab].cmd, 'doautocmd CursorMoved')
- vim.wo[ext.wins[ext.tab].cmd].eventignorewin = 'all'
+ vim._with({ win = ext.wins.cmd, wo = { eventignorewin = '' } }, function()
+ api.nvim_exec_autocmds('CursorMoved', {})
+ end)
end
- api.nvim_win_set_cursor(ext.wins[ext.tab].cmd, curpos)
+ api.nvim_win_set_cursor(ext.wins.cmd, curpos)
end
end
@@ -117,7 +116,8 @@ function M.cmdline_hide(_, abort)
return -- No need to hide when still in cmdline_block.
end
- fn.clearmatches(ext.wins[ext.tab].cmd) -- Clear matchparen highlights.
+ fn.clearmatches(ext.wins.cmd) -- Clear matchparen highlights.
+ api.nvim_win_set_cursor(ext.wins.cmd, { 1, 0 })
if abort then
-- Clear cmd buffer for aborted command (non-abort is left visible).
api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {})
@@ -128,7 +128,7 @@ function M.cmdline_hide(_, abort)
-- loop iteration. E.g. when a non-choice confirm button is pressed.
if was_prompt and not M.prompt then
api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {})
- api.nvim_win_set_config(ext.wins[ext.tab].prompt, { hide = true })
+ api.nvim_win_set_config(ext.wins.prompt, { hide = true })
end
-- Messages emitted as a result of a typed command are treated specially:
-- remember if the cmdline was used this event loop iteration.
@@ -140,7 +140,7 @@ function M.cmdline_hide(_, abort)
clear(M.prompt)
M.prompt, M.level, curpos[1], curpos[2] = false, 0, 0, 0
- win_config(ext.wins[ext.tab].cmd, true, ext.cmdheight)
+ win_config(ext.wins.cmd, true, ext.cmdheight)
end
--- Set multi-line cmdline buffer text.
diff --git a/runtime/lua/vim/_extui/messages.lua b/runtime/lua/vim/_extui/messages.lua
@@ -54,8 +54,8 @@ function M.box:start_timer(buf, len)
self.width = 1
M.prev_msg = ext.cfg.msg.pos == 'box' and '' or M.prev_msg
api.nvim_buf_clear_namespace(ext.bufs.box, -1, 0, -1)
- if api.nvim_win_is_valid(ext.wins[ext.tab].box) then
- api.nvim_win_set_config(ext.wins[ext.tab].box, { hide = true })
+ if api.nvim_win_is_valid(ext.wins.box) then
+ api.nvim_win_set_config(ext.wins.box, { hide = true })
end
end
end, ext.cfg.msg.box.timeout)
@@ -85,10 +85,10 @@ local function set_virttext(type)
M.cmd.last_col = type == 'last' and o.columns or M.cmd.last_col
elseif #chunks > 0 then
local tar = type == 'msg' and ext.cfg.msg.pos or 'cmd'
- local win = ext.wins[ext.tab][tar]
+ local win = ext.wins[tar]
local max = api.nvim_win_get_height(win)
local erow = tar == 'cmd' and M.cmd.msg_row or nil
- local srow = tar == 'box' and fn.line('w0', ext.wins[ext.tab].box) - 1 or nil
+ local srow = tar == 'box' and fn.line('w0', ext.wins.box) - 1 or nil
local h = api.nvim_win_text_height(win, { start_row = srow, end_row = erow, max_height = max })
local row = h.end_row ---@type integer
local col = fn.virtcol2col(win, row + 1, h.end_vcol)
@@ -253,11 +253,11 @@ function M.show_msg(tar, content, replace_last, append, more)
end
if tar == 'box' then
- api.nvim_win_set_width(ext.wins[ext.tab].box, width)
- local h = api.nvim_win_text_height(ext.wins[ext.tab].box, { start_row = start_row })
+ api.nvim_win_set_width(ext.wins.box, width)
+ local h = api.nvim_win_text_height(ext.wins.box, { start_row = start_row })
if more and h.all > 1 then
msg_to_more(tar)
- api.nvim_win_set_width(ext.wins[ext.tab].box, M.box.width)
+ api.nvim_win_set_width(ext.wins.box, M.box.width)
return
end
@@ -271,22 +271,22 @@ function M.show_msg(tar, content, replace_last, append, more)
M.box:start_timer(ext.bufs.box, row - start_row + 1)
end
elseif tar == 'cmd' and dupe == 0 then
- fn.clearmatches(ext.wins[ext.tab].cmd) -- Clear matchparen highlights.
+ fn.clearmatches(ext.wins.cmd) -- Clear matchparen highlights.
if ext.cmd.row > 0 then
-- In block mode the cmdheight is already dynamic, so just print the full message
-- regardless of height. Spoof cmdline_show to put cmdline below message.
ext.cmd.row = ext.cmd.row + 1 + row - start_row
ext.cmd.cmdline_show({}, 0, ':', '', ext.cmd.indent, 0, 0)
- api.nvim__redraw({ flush = true, cursor = true, win = ext.wins[ext.tab].cmd })
+ api.nvim__redraw({ flush = true, cursor = true, win = ext.wins.cmd })
else
- local h = api.nvim_win_text_height(ext.wins[ext.tab].cmd, {})
+ local h = api.nvim_win_text_height(ext.wins.cmd, {})
if more and h.all > ext.cmdheight then
ext.cmd.highlighter:destroy()
msg_to_more(tar)
return
end
- api.nvim_win_set_cursor(ext.wins[ext.tab][tar], { 1, 0 })
+ api.nvim_win_set_cursor(ext.wins[tar], { 1, 0 })
ext.cmd.highlighter.active[ext.bufs.cmd] = nil
-- Place [+x] indicator for lines that spill over 'cmdheight'.
M.cmd.lines, M.cmd.msg_row = h.all, h.end_row
@@ -337,7 +337,7 @@ function M.msg_show(kind, content, _, _, append)
-- Verbose messages are sent too often to be meaningful in the cmdline:
-- always route to box regardless of cfg.msg.pos.
M.show_msg('box', content, false, append)
- elseif ext.cfg.msg.pos == 'cmd' and api.nvim_get_current_win() == ext.wins[ext.tab].more then
+ elseif ext.cfg.msg.pos == 'cmd' and api.nvim_get_current_win() == ext.wins.more then
-- Append message to already open 'more' window.
M.msg_history_show({ { 'spill', content } })
api.nvim_command('norm! G')
@@ -411,7 +411,7 @@ function M.msg_history_show(entries)
end
-- Appending messages while 'more' window is open.
- local append_more = api.nvim_get_current_win() == ext.wins[ext.tab].more
+ local append_more = api.nvim_get_current_win() == ext.wins.more
if not append_more then
api.nvim_buf_set_lines(ext.bufs.more, 0, -1, false, {})
end
@@ -436,14 +436,14 @@ function M.set_pos(type)
hide = false,
relative = 'laststatus',
height = height,
- row = win == ext.wins[ext.tab].box and 0 or 1,
+ row = win == ext.wins.box and 0 or 1,
col = 10000,
}
api.nvim_win_set_config(win, config)
if type == 'box' then
-- Ensure last line is visible and first line is at top of window.
local row = (texth.all > height and texth.end_row or 0) + 1
- api.nvim_win_set_cursor(ext.wins[ext.tab].box, { row, 0 })
+ api.nvim_win_set_cursor(ext.wins.box, { row, 0 })
elseif type == 'more' and api.nvim_get_current_win() ~= win then
-- Cannot leave the cmdwin to enter the "more" window, so close it.
-- NOTE: regression w.r.t. the message grid, which allowed this. Resolving
@@ -473,7 +473,7 @@ function M.set_pos(type)
end
end
- for t, win in pairs(ext.wins[ext.tab] or {}) do
+ for t, win in pairs(ext.wins) do
local cfg = (t == type or (type == nil and t ~= 'cmd'))
and api.nvim_win_is_valid(win)
and api.nvim_win_get_config(win)
diff --git a/runtime/lua/vim/_extui/shared.lua b/runtime/lua/vim/_extui/shared.lua
@@ -5,10 +5,8 @@ local M = {
ns = api.nvim_create_namespace('nvim._ext_ui'),
augroup = api.nvim_create_augroup('nvim._ext_ui', {}),
cmdheight = -1, -- 'cmdheight' option value set by user.
- -- Map of tabpage ID to box/cmd/more/prompt window IDs.
- wins = {}, ---@type { ['box'|'cmd'|'more'|'prompt']: integer }[]
+ wins = { box = -1, cmd = -1, more = -1, prompt = -1 },
bufs = { box = -1, cmd = -1, more = -1, prompt = -1 },
- tab = 0, -- Current tabpage.
cfg = {
enable = true,
msg = { -- Options related to the message module.
@@ -31,13 +29,10 @@ local wincfg = { -- Default cfg for nvim_open_win().
noautocmd = true,
}
+local tab = 0
--- Ensure the various buffers and windows have not been deleted.
function M.tab_check_wins()
- M.tab = api.nvim_get_current_tabpage()
- if not M.wins[M.tab] then
- M.wins[M.tab] = { box = -1, cmd = -1, more = -1, prompt = -1 }
- end
-
+ local curtab = api.nvim_get_current_tabpage()
for _, type in ipairs({ 'box', 'cmd', 'more', 'prompt' }) do
local setopt = not api.nvim_buf_is_valid(M.bufs[type])
if setopt then
@@ -50,8 +45,9 @@ function M.tab_check_wins()
end
if
- not api.nvim_win_is_valid(M.wins[M.tab][type])
- or not api.nvim_win_get_config(M.wins[M.tab][type]).zindex -- no longer floating
+ tab ~= curtab
+ or not api.nvim_win_is_valid(M.wins[type])
+ or not api.nvim_win_get_config(M.wins[type]).zindex -- no longer floating
then
local top = { vim.opt.fcs:get().horiz or o.ambw == 'single' and '─' or '-', 'WinSeparator' }
local border = (type == 'more' or type == 'prompt') and { '', top, '', '', '', '', '', '' }
@@ -66,13 +62,17 @@ function M.tab_check_wins()
zindex = 200 - (type == 'more' and 1 or 0),
_cmdline_offset = type == 'cmd' and 0 or nil,
})
- M.wins[M.tab][type] = api.nvim_open_win(M.bufs[type], false, cfg)
+ if tab ~= curtab and api.nvim_win_is_valid(M.wins[type]) then
+ cfg = api.nvim_win_get_config(M.wins[type])
+ api.nvim_win_close(M.wins[type], true)
+ end
+ M.wins[type] = api.nvim_open_win(M.bufs[type], false, cfg)
if type == 'cmd' then
- api.nvim_win_set_hl_ns(M.wins[M.tab][type], M.ns)
+ api.nvim_win_set_hl_ns(M.wins[type], M.ns)
end
setopt = true
- elseif api.nvim_win_get_buf(M.wins[M.tab][type]) ~= M.bufs[type] then
- api.nvim_win_set_buf(M.wins[M.tab][type], M.bufs[type])
+ elseif api.nvim_win_get_buf(M.wins[type]) ~= M.bufs[type] then
+ api.nvim_win_set_buf(M.wins[type], M.bufs[type])
setopt = true
end
@@ -84,7 +84,7 @@ function M.tab_check_wins()
end
-- Fire a FileType autocommand with window context to let the user reconfigure local options.
- api.nvim_win_call(M.wins[M.tab][type], function()
+ api.nvim_win_call(M.wins[type], function()
api.nvim_set_option_value('wrap', true, { scope = 'local' })
api.nvim_set_option_value('linebreak', false, { scope = 'local' })
api.nvim_set_option_value('smoothscroll', true, { scope = 'local' })
@@ -95,6 +95,7 @@ function M.tab_check_wins()
end)
end
end
+ tab = curtab
end
return M