fix(api): open_win fire BufWinEnter for other buffer when !enter && !noautocmd

Problem: BufWinEnter is not fired when not entering a new window, even when a
different buffer is specified and buffer-related autocommands are unblocked
(!noautocmd).

Solution: fire it in the context of the new window and buffer. Do not do it if
the buffer is unchanged, like :{s}buffer.

Be wary of autocommands! For example, it's possible for nvim_win_set_config to
be used in an autocommand to move a window to a different tabpage (in contrast,
things like wincmd T actually create a *new* window, so it may not have been
possible before, meaning other parts of Nvim could assume windows can't do
this... I'd be especially cautious of logic that restores curwin and curtab
without checking if curwin is still valid in curtab, if any such logic exists).

Also, bail early from win_set_buf if setting the temp curwin fails; this
shouldn't be possible, as the callers check that wp is valid, but in case that's
not true, win_set_buf will no longer continue setting a buffer for the wrong
window.

Note that pum_create_float_preview also uses win_set_buf, but from a glance,
doesn't look like it properly checks for autocmds screwing things up (win_enter,
nvim_create_buf...). I haven't addressed that here.

Also adds some test coverage for nvim_open_win autocommands.

Closes #27121.
This commit is contained in:
Sean Dewar 2024-02-04 02:59:26 +00:00 committed by Sean Dewar
parent 233649bc75
commit a873f33993
No known key found for this signature in database
GPG Key ID: 08CC2C83AD41B581
3 changed files with 242 additions and 10 deletions

View File

@ -261,8 +261,8 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Err
switchwin_T switchwin;
// `parent` is valid in `tp`, so switch_win should not fail.
const int result = switch_win(&switchwin, parent, tp, true);
(void)result;
assert(result == OK);
(void)result;
wp = win_split_ins(0, flags, NULL, 0);
restore_win(&switchwin, true);
}
@ -276,18 +276,41 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Err
api_set_error(err, kErrorTypeException, "Failed to create window");
return 0;
}
// Autocommands may close `wp` or move it to another tabpage, so update and check `tp` after each
// event. In each case, `wp` should already be valid in `tp`, so switch_win should not fail.
switchwin_T switchwin;
if (switch_win_noblock(&switchwin, wp, tp, true) == OK) {
apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf);
{
const int result = switch_win_noblock(&switchwin, wp, tp, true);
assert(result == OK);
(void)result;
if (apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf)) {
tp = win_find_tabpage(wp);
}
restore_win_noblock(&switchwin, true);
}
restore_win_noblock(&switchwin, true);
if (enter) {
if (tp && enter) {
goto_tabpage_win(tp, wp);
tp = win_find_tabpage(wp);
}
if (win_valid_any_tab(wp) && buf != wp->w_buffer) {
win_set_buf(wp, buf, !enter || fconfig.noautocmd, err);
if (tp && buf != wp->w_buffer) {
const bool noautocmd = !enter || fconfig.noautocmd;
win_set_buf(wp, buf, noautocmd, err);
if (!noautocmd) {
tp = win_find_tabpage(wp);
}
// win_set_buf autocommands were blocked if we didn't enter, but we still want BufWinEnter.
if (noautocmd && !fconfig.noautocmd && wp->w_buffer == buf) {
const int result = switch_win_noblock(&switchwin, wp, tp, true);
assert(result == OK);
(void)result;
if (apply_autocmds(EVENT_BUFWINENTER, NULL, NULL, false, buf)) {
tp = win_find_tabpage(wp);
}
restore_win_noblock(&switchwin, true);
}
}
if (!win_valid_any_tab(wp)) {
if (!tp) {
api_set_error(err, kErrorTypeException, "Window was closed immediately");
return 0;
}

View File

@ -718,6 +718,7 @@ void win_set_buf(win_T *win, buf_T *buf, bool noautocmd, Error *err)
kErrorTypeException,
"Failed to switch to window %d",
win->handle);
goto cleanup;
}
try_start();
@ -729,10 +730,11 @@ void win_set_buf(win_T *win, buf_T *buf, bool noautocmd, Error *err)
buf->handle);
}
// If window is not current, state logic will not validate its cursor.
// So do it now.
// If window is not current, state logic will not validate its cursor. So do it now.
// Still needed if do_buffer returns FAIL (e.g: autocmds abort script after buffer was set).
validate_cursor();
cleanup:
restore_win_noblock(&switchwin, true);
if (noautocmd) {
unblock_autocmds();

View File

@ -1364,6 +1364,213 @@ describe('API/win', function()
},
}, layout)
end)
local function setup_tabbed_autocmd_test()
local info = {}
info.orig_buf = api.nvim_get_current_buf()
info.other_buf = api.nvim_create_buf(true, true)
info.tab1_curwin = api.nvim_get_current_win()
info.tab1 = api.nvim_get_current_tabpage()
command('tab split | split')
info.tab2_curwin = api.nvim_get_current_win()
info.tab2 = api.nvim_get_current_tabpage()
exec([=[
tabfirst
let result = []
autocmd TabEnter * let result += [["TabEnter", nvim_get_current_tabpage()]]
autocmd TabLeave * let result += [["TabLeave", nvim_get_current_tabpage()]]
autocmd WinEnter * let result += [["WinEnter", win_getid()]]
autocmd WinLeave * let result += [["WinLeave", win_getid()]]
autocmd WinNew * let result += [["WinNew", win_getid()]]
autocmd WinClosed * let result += [["WinClosed", str2nr(expand("<afile>"))]]
autocmd BufEnter * let result += [["BufEnter", win_getid(), bufnr()]]
autocmd BufLeave * let result += [["BufLeave", win_getid(), bufnr()]]
autocmd BufWinEnter * let result += [["BufWinEnter", win_getid(), bufnr()]]
autocmd BufWinLeave * let result += [["BufWinLeave", win_getid(), bufnr()]]
]=])
return info
end
it('fires expected autocmds when creating splits without entering', function()
local info = setup_tabbed_autocmd_test()
-- For these, don't want BufWinEnter if visiting the same buffer, like :{s}buffer.
-- Same tabpage, same buffer.
local new_win = api.nvim_open_win(0, false, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
-- Other tabpage, same buffer.
command('let result = []')
new_win = api.nvim_open_win(0, false, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
-- Same tabpage, other buffer.
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, false, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
-- Other tabpage, other buffer.
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, false, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
end)
it('fires expected autocmds when creating and entering splits', function()
local info = setup_tabbed_autocmd_test()
-- Same tabpage, same buffer.
local new_win = api.nvim_open_win(0, true, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'WinEnter', new_win },
}, eval('result'))
-- Same tabpage, other buffer.
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, true, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'WinEnter', new_win },
{ 'BufLeave', new_win, info.orig_buf },
{ 'BufEnter', new_win, info.other_buf },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
-- For these, the other tabpage's prevwin and curwin will change like we switched from its old
-- curwin to the new window, so the extra events near TabEnter reflect that.
-- Other tabpage, same buffer.
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(0, true, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'TabLeave', info.tab1 },
{ 'WinEnter', info.tab2_curwin },
{ 'TabEnter', info.tab2 },
{ 'WinLeave', info.tab2_curwin },
{ 'WinEnter', new_win },
}, eval('result'))
-- Other tabpage, other buffer.
api.nvim_set_current_win(info.tab2_curwin)
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, true, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'TabLeave', info.tab1 },
{ 'WinEnter', info.tab2_curwin },
{ 'TabEnter', info.tab2 },
{ 'WinLeave', info.tab2_curwin },
{ 'WinEnter', new_win },
{ 'BufLeave', new_win, info.orig_buf },
{ 'BufEnter', new_win, info.other_buf },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
-- Other tabpage, other buffer; but other tabpage's curwin has a new buffer active.
api.nvim_set_current_win(info.tab2_curwin)
local new_buf = api.nvim_create_buf(true, true)
api.nvim_set_current_buf(new_buf)
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, true, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'BufLeave', info.tab1_curwin, info.orig_buf },
{ 'WinLeave', info.tab1_curwin },
{ 'TabLeave', info.tab1 },
{ 'WinEnter', info.tab2_curwin },
{ 'TabEnter', info.tab2 },
{ 'BufEnter', info.tab2_curwin, new_buf },
{ 'WinLeave', info.tab2_curwin },
{ 'WinEnter', new_win },
{ 'BufLeave', new_win, new_buf },
{ 'BufEnter', new_win, info.other_buf },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
end)
it('OK when new window is moved to other tabpage by autocommands', function()
-- Use nvim_win_set_config in the autocommands, as other methods of moving a window to a
-- different tabpage (e.g: wincmd T) actually creates a new window.
local tab0 = api.nvim_get_current_tabpage()
local tab0_win = api.nvim_get_current_win()
command('tabnew')
local new_buf = api.nvim_create_buf(true, true)
local tab1 = api.nvim_get_current_tabpage()
local tab1_parent = api.nvim_get_current_win()
command(
'tabfirst | autocmd WinNew * ++once call nvim_win_set_config(0, #{split: "left", win: '
.. tab1_parent
.. '})'
)
local new_win = api.nvim_open_win(new_buf, true, { split = 'left' })
eq(tab1, api.nvim_get_current_tabpage())
eq(new_win, api.nvim_get_current_win())
eq(new_buf, api.nvim_get_current_buf())
-- nvim_win_set_config called after entering. It doesn't follow a curwin that is moved to a
-- different tabpage, but instead moves to the win filling the space, which is tab0_win.
command(
'tabfirst | autocmd WinEnter * ++once call nvim_win_set_config(0, #{split: "left", win: '
.. tab1_parent
.. '})'
)
new_win = api.nvim_open_win(new_buf, true, { split = 'left' })
eq(tab0, api.nvim_get_current_tabpage())
eq(tab0_win, api.nvim_get_current_win())
eq(tab1, api.nvim_win_get_tabpage(new_win))
eq(new_buf, api.nvim_win_get_buf(new_win))
command(
'tabfirst | autocmd BufEnter * ++once call nvim_win_set_config(0, #{split: "left", win: '
.. tab1_parent
.. '})'
)
new_win = api.nvim_open_win(new_buf, true, { split = 'left' })
eq(tab0, api.nvim_get_current_tabpage())
eq(tab0_win, api.nvim_get_current_win())
eq(tab1, api.nvim_win_get_tabpage(new_win))
eq(new_buf, api.nvim_win_get_buf(new_win))
end)
it('does not fire BufWinEnter if win_set_buf fails', function()
exec([[
set nohidden modified
autocmd WinNew * ++once only!
let fired = v:false
autocmd BufWinEnter * ++once let fired = v:true
]])
eq(
'Failed to set buffer 2',
pcall_err(api.nvim_open_win, api.nvim_create_buf(true, true), false, { split = 'left' })
)
eq(false, eval('fired'))
end)
end)
describe('set_config', function()