fix(autocmd): fire TabClosed after freeing tab page

Problem: TabClosed is fired after close_buffer is called (after b_nwindows is
decremented) and after the tab page is removed from the list, but before it's
freed. This causes inconsistencies such as the removed tabpage having a valid
handle and functions like nvim_tabpage_get_number returning nonsense.

Solution: fire it after free_tabpage. Try to maintain the Nvim-specific
behaviour of setting `<amatch>` to the old tab page number, and the
(undocumented) behaviour of setting `<abuf>` to the buffer it was showing
(close_buffer sets w_buffer to NULL if it was freed, so it should be OK pass it
to apply_autocmds_group, similar to before).
This commit is contained in:
Sean Dewar
2025-07-16 10:18:19 +01:00
committed by zeertzjq
parent 88619e1aaf
commit 46011a4e87
2 changed files with 42 additions and 12 deletions

View File

@@ -3045,15 +3045,11 @@ bool win_close_othertab(win_T *win, int free_buf, tabpage_T *tp, bool force)
goto leave_open;
}
bool free_tp = false;
int free_tp_idx = 0;
// When closing the last window in a tab page remove the tab page.
if (tp->tp_firstwin == tp->tp_lastwin) {
char prev_idx[NUMBUFLEN];
if (has_event(EVENT_TABCLOSED)) {
vim_snprintf(prev_idx, NUMBUFLEN, "%i", tabpage_index(tp));
}
free_tp_idx = tabpage_index(tp);
int h = tabline_height();
if (tp == first_tabpage) {
@@ -3070,23 +3066,25 @@ bool win_close_othertab(win_T *win, int free_buf, tabpage_T *tp, bool force)
}
ptp->tp_next = tp->tp_next;
}
free_tp = true;
redraw_tabline = true;
if (h != tabline_height()) {
win_new_screen_rows();
}
if (has_event(EVENT_TABCLOSED)) {
apply_autocmds(EVENT_TABCLOSED, prev_idx, prev_idx, false, win->w_buffer);
}
}
// Free the memory used for the window.
buf_T *buf = win->w_buffer;
int dir;
win_free_mem(win, &dir, tp);
if (free_tp) {
if (free_tp_idx > 0) {
free_tabpage(tp);
if (has_event(EVENT_TABCLOSED)) {
char prev_idx[NUMBUFLEN];
vim_snprintf(prev_idx, NUMBUFLEN, "%i", free_tp_idx);
apply_autocmds(EVENT_TABCLOSED, prev_idx, prev_idx, false, buf);
}
}
return true;

View File

@@ -4,6 +4,8 @@ local n = require('test.functional.testnvim')()
local clear, eq = n.clear, t.eq
local api = n.api
local command = n.command
local eval = n.eval
local exec = n.exec
describe('TabClosed', function()
before_each(clear)
@@ -48,6 +50,36 @@ describe('TabClosed', function()
eq('tabclosed:2:2:2', api.nvim_exec('bdelete Xtestfile2', true))
eq('Xtestfile1', api.nvim_eval('bufname("")'))
end)
it('triggers after tab page is properly freed', function()
exec([[
let s:tp = nvim_get_current_tabpage()
let g:buf = bufnr()
setlocal bufhidden=wipe
tabnew
au TabClosed * ++once let g:tp_valid = nvim_tabpage_is_valid(s:tp)
\| let g:abuf = expand('<abuf>')
call nvim_buf_delete(g:buf, #{force: 1})
]])
eq(false, eval('g:tp_valid'))
eq(false, eval('nvim_buf_is_valid(g:buf)'))
eq('', eval('g:abuf'))
exec([[
tabnew
let g:buf = bufnr()
let s:win = win_getid()
tabfirst
au TabClosed * ++once let g:abuf = expand('<abuf>')
call nvim_win_close(s:win, 1)
]])
eq(true, eval('nvim_buf_is_valid(g:buf)'))
eq(eval('g:buf'), tonumber(eval('g:abuf')))
end)
end)
describe('with NR as <afile>', function()