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).
This commit is contained in:
luukvbaal
2025-06-04 19:59:36 +02:00
committed by GitHub
parent 03832842d5
commit 5e4700152b
4 changed files with 50 additions and 51 deletions

View File

@@ -51,7 +51,7 @@ local function ui_callback(event, ...)
api.nvim__redraw({ api.nvim__redraw({
flush = true, flush = true,
cursor = handler == ext.cmd[event] and true or nil, 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 end
local scheduled_ui_callback = vim.schedule_wrap(ui_callback) local scheduled_ui_callback = vim.schedule_wrap(ui_callback)
@@ -63,10 +63,8 @@ function M.enable(opts)
if ext.cfg.enable == false then if ext.cfg.enable == false then
-- Detach and cleanup windows, buffers and autocommands. -- Detach and cleanup windows, buffers and autocommands.
for _, tab in ipairs(api.nvim_list_tabpages()) do for _, win in pairs(ext.wins) do
for _, win in pairs(ext.wins[tab] or {}) do api.nvim_win_close(win, true)
api.nvim_win_close(win, true)
end
end end
for _, buf in pairs(ext.bufs) do for _, buf in pairs(ext.bufs) do
api.nvim_buf_delete(buf, {}) api.nvim_buf_delete(buf, {})
@@ -100,7 +98,7 @@ function M.enable(opts)
if name == 'cmdheight' then if name == 'cmdheight' then
-- 'cmdheight' set; (un)hide cmdline window and set its height. -- 'cmdheight' set; (un)hide cmdline window and set its height.
local cfg = { height = math.max(value, 1), hide = value == 0 } 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. -- Change message position when 'cmdheight' was or becomes 0.
if value == 0 or ext.cmdheight == 0 then if value == 0 or ext.cmdheight == 0 then
ext.cfg.msg.pos = value == 0 and 'box' or ext.cmdheight == 0 and 'cmd' 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.', 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, group = ext.augroup,
callback = function(ev) callback = function(ev)
ext.tab_check_wins() ext.tab_check_wins()
@@ -133,13 +131,13 @@ function M.enable(opts)
end end
ext.msg.set_pos() ext.msg.set_pos()
end, 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', { api.nvim_create_autocmd('WinEnter', {
callback = function() callback = function()
local win = api.nvim_get_current_win() 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') vim.cmd.wincmd('p')
end end
end, end,

View File

@@ -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 }) api.nvim_buf_set_extmark(ext.bufs.cmd, ext.ns, 0, 0, { hl_group = hl_id, end_col = promptlen })
end end
local height = math.max(ext.cmdheight, api.nvim_win_text_height(ext.wins[ext.tab].cmd, {}).all) local height = math.max(ext.cmdheight, api.nvim_win_text_height(ext.wins.cmd, {}).all)
win_config(ext.wins[ext.tab].cmd, false, height) win_config(ext.wins.cmd, false, height)
M.cmdline_pos(pos) M.cmdline_pos(pos)
-- Clear message cmdline state; should not be shown during, and reset after cmdline. -- Clear message cmdline state; should not be shown during, and reset after cmdline.
@@ -83,7 +83,7 @@ end
---@param shift boolean ---@param shift boolean
--@param level integer --@param level integer
function M.cmdline_special_char(c, shift) 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) api.nvim_put({ c }, shift and '' or 'c', false, false)
end) end)
end end
@@ -99,12 +99,11 @@ function M.cmdline_pos(pos)
curpos[1], curpos[2] = M.row + 1, promptlen + pos curpos[1], curpos[2] = M.row + 1, promptlen + pos
-- Add matchparen highlighting to non-prompt part of cmdline. -- Add matchparen highlighting to non-prompt part of cmdline.
if pos > 0 and fn.exists('#matchparen') then if pos > 0 and fn.exists('#matchparen') then
api.nvim_win_set_cursor(ext.wins[ext.tab].cmd, { curpos[1], curpos[2] - 1 }) vim._with({ win = ext.wins.cmd, wo = { eventignorewin = '' } }, function()
vim.wo[ext.wins[ext.tab].cmd].eventignorewin = '' api.nvim_exec_autocmds('CursorMoved', {})
fn.win_execute(ext.wins[ext.tab].cmd, 'doautocmd CursorMoved') end)
vim.wo[ext.wins[ext.tab].cmd].eventignorewin = 'all'
end end
api.nvim_win_set_cursor(ext.wins[ext.tab].cmd, curpos) api.nvim_win_set_cursor(ext.wins.cmd, curpos)
end end
end end
@@ -117,7 +116,8 @@ function M.cmdline_hide(_, abort)
return -- No need to hide when still in cmdline_block. return -- No need to hide when still in cmdline_block.
end 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 if abort then
-- Clear cmd buffer for aborted command (non-abort is left visible). -- Clear cmd buffer for aborted command (non-abort is left visible).
api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) 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. -- loop iteration. E.g. when a non-choice confirm button is pressed.
if was_prompt and not M.prompt then if was_prompt and not M.prompt then
api.nvim_buf_set_lines(ext.bufs.cmd, 0, -1, false, {}) 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 end
-- Messages emitted as a result of a typed command are treated specially: -- Messages emitted as a result of a typed command are treated specially:
-- remember if the cmdline was used this event loop iteration. -- remember if the cmdline was used this event loop iteration.
@@ -140,7 +140,7 @@ function M.cmdline_hide(_, abort)
clear(M.prompt) clear(M.prompt)
M.prompt, M.level, curpos[1], curpos[2] = false, 0, 0, 0 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 end
--- Set multi-line cmdline buffer text. --- Set multi-line cmdline buffer text.

View File

@@ -54,8 +54,8 @@ function M.box:start_timer(buf, len)
self.width = 1 self.width = 1
M.prev_msg = ext.cfg.msg.pos == 'box' and '' or M.prev_msg M.prev_msg = ext.cfg.msg.pos == 'box' and '' or M.prev_msg
api.nvim_buf_clear_namespace(ext.bufs.box, -1, 0, -1) api.nvim_buf_clear_namespace(ext.bufs.box, -1, 0, -1)
if api.nvim_win_is_valid(ext.wins[ext.tab].box) then if api.nvim_win_is_valid(ext.wins.box) then
api.nvim_win_set_config(ext.wins[ext.tab].box, { hide = true }) api.nvim_win_set_config(ext.wins.box, { hide = true })
end end
end end
end, ext.cfg.msg.box.timeout) 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 M.cmd.last_col = type == 'last' and o.columns or M.cmd.last_col
elseif #chunks > 0 then elseif #chunks > 0 then
local tar = type == 'msg' and ext.cfg.msg.pos or 'cmd' 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 max = api.nvim_win_get_height(win)
local erow = tar == 'cmd' and M.cmd.msg_row or nil 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 h = api.nvim_win_text_height(win, { start_row = srow, end_row = erow, max_height = max })
local row = h.end_row ---@type integer local row = h.end_row ---@type integer
local col = fn.virtcol2col(win, row + 1, h.end_vcol) 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 end
if tar == 'box' then if tar == 'box' then
api.nvim_win_set_width(ext.wins[ext.tab].box, width) api.nvim_win_set_width(ext.wins.box, width)
local h = api.nvim_win_text_height(ext.wins[ext.tab].box, { start_row = start_row }) local h = api.nvim_win_text_height(ext.wins.box, { start_row = start_row })
if more and h.all > 1 then if more and h.all > 1 then
msg_to_more(tar) 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 return
end 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) M.box:start_timer(ext.bufs.box, row - start_row + 1)
end end
elseif tar == 'cmd' and dupe == 0 then 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 if ext.cmd.row > 0 then
-- In block mode the cmdheight is already dynamic, so just print the full message -- 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. -- regardless of height. Spoof cmdline_show to put cmdline below message.
ext.cmd.row = ext.cmd.row + 1 + row - start_row ext.cmd.row = ext.cmd.row + 1 + row - start_row
ext.cmd.cmdline_show({}, 0, ':', '', ext.cmd.indent, 0, 0) 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 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 if more and h.all > ext.cmdheight then
ext.cmd.highlighter:destroy() ext.cmd.highlighter:destroy()
msg_to_more(tar) msg_to_more(tar)
return return
end 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 ext.cmd.highlighter.active[ext.bufs.cmd] = nil
-- Place [+x] indicator for lines that spill over 'cmdheight'. -- Place [+x] indicator for lines that spill over 'cmdheight'.
M.cmd.lines, M.cmd.msg_row = h.all, h.end_row 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: -- Verbose messages are sent too often to be meaningful in the cmdline:
-- always route to box regardless of cfg.msg.pos. -- always route to box regardless of cfg.msg.pos.
M.show_msg('box', content, false, append) 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. -- Append message to already open 'more' window.
M.msg_history_show({ { 'spill', content } }) M.msg_history_show({ { 'spill', content } })
api.nvim_command('norm! G') api.nvim_command('norm! G')
@@ -411,7 +411,7 @@ function M.msg_history_show(entries)
end end
-- Appending messages while 'more' window is open. -- 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 if not append_more then
api.nvim_buf_set_lines(ext.bufs.more, 0, -1, false, {}) api.nvim_buf_set_lines(ext.bufs.more, 0, -1, false, {})
end end
@@ -436,14 +436,14 @@ function M.set_pos(type)
hide = false, hide = false,
relative = 'laststatus', relative = 'laststatus',
height = height, height = height,
row = win == ext.wins[ext.tab].box and 0 or 1, row = win == ext.wins.box and 0 or 1,
col = 10000, col = 10000,
} }
api.nvim_win_set_config(win, config) api.nvim_win_set_config(win, config)
if type == 'box' then if type == 'box' then
-- Ensure last line is visible and first line is at top of window. -- 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 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 elseif type == 'more' and api.nvim_get_current_win() ~= win then
-- Cannot leave the cmdwin to enter the "more" window, so close it. -- Cannot leave the cmdwin to enter the "more" window, so close it.
-- NOTE: regression w.r.t. the message grid, which allowed this. Resolving -- NOTE: regression w.r.t. the message grid, which allowed this. Resolving
@@ -473,7 +473,7 @@ function M.set_pos(type)
end end
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')) local cfg = (t == type or (type == nil and t ~= 'cmd'))
and api.nvim_win_is_valid(win) and api.nvim_win_is_valid(win)
and api.nvim_win_get_config(win) and api.nvim_win_get_config(win)

View File

@@ -5,10 +5,8 @@ local M = {
ns = api.nvim_create_namespace('nvim._ext_ui'), ns = api.nvim_create_namespace('nvim._ext_ui'),
augroup = api.nvim_create_augroup('nvim._ext_ui', {}), augroup = api.nvim_create_augroup('nvim._ext_ui', {}),
cmdheight = -1, -- 'cmdheight' option value set by user. cmdheight = -1, -- 'cmdheight' option value set by user.
-- Map of tabpage ID to box/cmd/more/prompt window IDs. wins = { box = -1, cmd = -1, more = -1, prompt = -1 },
wins = {}, ---@type { ['box'|'cmd'|'more'|'prompt']: integer }[]
bufs = { box = -1, cmd = -1, more = -1, prompt = -1 }, bufs = { box = -1, cmd = -1, more = -1, prompt = -1 },
tab = 0, -- Current tabpage.
cfg = { cfg = {
enable = true, enable = true,
msg = { -- Options related to the message module. msg = { -- Options related to the message module.
@@ -31,13 +29,10 @@ local wincfg = { -- Default cfg for nvim_open_win().
noautocmd = true, noautocmd = true,
} }
local tab = 0
--- Ensure the various buffers and windows have not been deleted. --- Ensure the various buffers and windows have not been deleted.
function M.tab_check_wins() function M.tab_check_wins()
M.tab = api.nvim_get_current_tabpage() local curtab = 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
for _, type in ipairs({ 'box', 'cmd', 'more', 'prompt' }) do for _, type in ipairs({ 'box', 'cmd', 'more', 'prompt' }) do
local setopt = not api.nvim_buf_is_valid(M.bufs[type]) local setopt = not api.nvim_buf_is_valid(M.bufs[type])
if setopt then if setopt then
@@ -50,8 +45,9 @@ function M.tab_check_wins()
end end
if if
not api.nvim_win_is_valid(M.wins[M.tab][type]) tab ~= curtab
or not api.nvim_win_get_config(M.wins[M.tab][type]).zindex -- no longer floating 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 then
local top = { vim.opt.fcs:get().horiz or o.ambw == 'single' and '' or '-', 'WinSeparator' } local top = { vim.opt.fcs:get().horiz or o.ambw == 'single' and '' or '-', 'WinSeparator' }
local border = (type == 'more' or type == 'prompt') and { '', top, '', '', '', '', '', '' } 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), zindex = 200 - (type == 'more' and 1 or 0),
_cmdline_offset = type == 'cmd' and 0 or nil, _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 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 end
setopt = true setopt = true
elseif api.nvim_win_get_buf(M.wins[M.tab][type]) ~= M.bufs[type] then elseif api.nvim_win_get_buf(M.wins[type]) ~= M.bufs[type] then
api.nvim_win_set_buf(M.wins[M.tab][type], M.bufs[type]) api.nvim_win_set_buf(M.wins[type], M.bufs[type])
setopt = true setopt = true
end end
@@ -84,7 +84,7 @@ function M.tab_check_wins()
end end
-- Fire a FileType autocommand with window context to let the user reconfigure local options. -- 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('wrap', true, { scope = 'local' })
api.nvim_set_option_value('linebreak', false, { scope = 'local' }) api.nvim_set_option_value('linebreak', false, { scope = 'local' })
api.nvim_set_option_value('smoothscroll', true, { scope = 'local' }) api.nvim_set_option_value('smoothscroll', true, { scope = 'local' })
@@ -95,6 +95,7 @@ function M.tab_check_wins()
end) end)
end end
end end
tab = curtab
end end
return M return M