commit 05a83265f9a4c2909b76630a977bfa0c95224819
parent 3a36df9b13a57fc97a2fc411e59201ffd1ab30a3
Author: Sean Dewar <6256228+seandewar@users.noreply.github.com>
Date: Wed, 16 Jul 2025 10:18:19 +0100
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).
Diffstat:
2 files changed, 42 insertions(+), 12 deletions(-)
diff --git a/src/nvim/window.c b/src/nvim/window.c
@@ -3040,15 +3040,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) {
@@ -3065,23 +3061,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;
diff --git a/test/functional/autocmd/tabclose_spec.lua b/test/functional/autocmd/tabclose_spec.lua
@@ -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()