commit e3990f8643c9199817c3f2c0a48a9244c9e1d09a
parent df65f87fd7711d4e8699a7625aa5435839c02d09
Author: zeertzjq <zeertzjq@outlook.com>
Date: Mon, 19 Jan 2026 10:09:08 +0800
vim-patch:9.1.1202: Missing TabClosedPre autocommand
Problem: Missing TabClosedPre autocommand
(zoumi)
Solution: Add the TabClosedPre autcommand (Jim Zhou).
fixes: vim/vim#16518
closes: vim/vim#16855
https://github.com/vim/vim/commit/5606ca5349982fe53cc6a2ec6345aa66f0613d40
Co-authored-by: Jim Zhou <jimzhouzzy@gmail.com>
Diffstat:
9 files changed, 210 insertions(+), 5 deletions(-)
diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt
@@ -1041,6 +1041,13 @@ Syntax When the 'syntax' option has been set. The
this option was set. <amatch> expands to the
new value of 'syntax'.
See |:syn-on|.
+ *TabClosed*
+TabClosed After closing a tab page. <afile> expands to
+ the tab page number.
+ *TabClosedPre*
+TabClosedPre Before closing a tab page. The window layout
+ is locked, thus opening and closing of windows
+ is prohibited.
*TabEnter*
TabEnter Just after entering a tab page. |tab-page|
After WinEnter.
@@ -1055,9 +1062,6 @@ TabNew When creating a new tab page. |tab-page|
*TabNewEntered*
TabNewEntered After entering a new tab page. |tab-page|
After BufEnter.
- *TabClosed*
-TabClosed After closing a tab page. <afile> expands to
- the tab page number.
*TermOpen*
TermOpen When a |terminal| job is starting. Can be
used to configure the terminal buffer. To get
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -233,6 +233,7 @@ EVENTS
• Creating or updating a progress message with |nvim_echo()| triggers a |Progress| event.
• |MarkSet| is triggered after a |mark| is set by the user (currently doesn't
support implicit marks like |'[| or |'<|, …).
+• |TabClosedPre| is triggered before closing a |tabpage|.
• New `terminator` parameter for |TermRequest| event.
HIGHLIGHTS
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -2561,6 +2561,7 @@ A jump table for the options with a short description can be found at |Q_op|.
|SwapExists|,
|Syntax|,
|TabClosed|,
+ |TabClosedPre|,
|TabEnter|,
|TabLeave|,
|TabNew|,
diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua
@@ -189,6 +189,7 @@ error('Cannot require a meta file')
--- |'SwapExists'
--- |'Syntax'
--- |'TabClosed'
+--- |'TabClosedPre'
--- |'TabEnter'
--- |'TabLeave'
--- |'TabNew'
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -2239,6 +2239,7 @@ vim.go.ei = vim.go.eventignore
--- `SwapExists`,
--- `Syntax`,
--- `TabClosed`,
+--- `TabClosedPre`,
--- `TabEnter`,
--- `TabLeave`,
--- `TabNew`,
diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim
@@ -1890,7 +1890,7 @@ syn keyword vimSyncCcomment contained ccomment skipwhite nextgroup=vimGroupName
syn keyword vimSyncClear contained clear skipwhite nextgroup=vimSyncGroupName
syn keyword vimSyncFromstart contained fromstart
syn keyword vimSyncMatch contained match skipwhite nextgroup=vimSyncGroupName
-syn keyword vimSyncRegion contained region skipwhite nextgroup=vimSynRegion
+syn keyword vimSyncRegion contained region skipwhite nextgroup=vimSynReg
syn match vimSyncLinebreak contained "\<linebreaks=" nextgroup=vimNumber
syn keyword vimSyncLinecont contained linecont skipwhite nextgroup=vimSynRegPat
syn match vimSyncLines contained "\<lines=" nextgroup=vimNumber
diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua
@@ -109,7 +109,8 @@ return {
StdinReadPre = false, -- before reading from stdin
SwapExists = false, -- found existing swap file
Syntax = false, -- syntax selected
- TabClosed = false, -- a tab has closed
+ TabClosed = false, -- after closing a tab page
+ TabClosedPre = false, -- before closing a tab page
TabEnter = false, -- after entering a tab page
TabLeave = false, -- before leaving a tab page
TabNew = false, -- when creating a new tab
diff --git a/src/nvim/window.c b/src/nvim/window.c
@@ -3102,6 +3102,35 @@ static void do_autocmd_winclosed(win_T *win)
recursive = false;
}
+static void trigger_tabclosedpre(tabpage_T *tp)
+{
+ static bool recursive = false;
+ tabpage_T *ptp = curtab;
+
+ // Quickly return when no TabClosedPre autocommands to be executed or
+ // already executing
+ if (!has_event(EVENT_TABCLOSEDPRE) || recursive) {
+ return;
+ }
+
+ if (valid_tabpage(tp)) {
+ goto_tabpage_tp(tp, false, false);
+ }
+ recursive = true;
+ window_layout_lock();
+ apply_autocmds(EVENT_TABCLOSEDPRE, NULL, NULL, false, NULL);
+ window_layout_unlock();
+ recursive = false;
+ // tabpage may have been modified or deleted by autocmds
+ if (valid_tabpage(ptp)) {
+ // try to recover the tappage first
+ goto_tabpage_tp(ptp, false, false);
+ } else {
+ // fall back to the first tappage
+ goto_tabpage_tp(first_tabpage, false, false);
+ }
+}
+
// Close window "win" in tab page "tp", which is not the current tab page.
// This may be the last window in that tab page and result in closing the tab,
// thus "tp" may become invalid!
@@ -3157,6 +3186,14 @@ bool win_close_othertab(win_T *win, int free_buf, tabpage_T *tp, bool force)
}
}
+ if (tp->tp_firstwin == tp->tp_lastwin) {
+ trigger_tabclosedpre(tp);
+ // autocmd may have freed the window already.
+ if (!win_valid_any_tab(win)) {
+ return false;
+ }
+ }
+
bufref_T bufref;
set_bufref(&bufref, win->w_buffer);
diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim
@@ -4636,6 +4636,165 @@ func Test_WinScrolled_Resized_eiw()
call StopVimInTerminal(buf)
endfunc
+" Test that TabClosedPre and TabClosed are triggered when closing a tab.
+func Test_autocmd_tabclosedpre()
+ augroup testing
+ au TabClosedPre * call add(g:tabpagenr_pre, t:testvar)
+ au TabClosed * call add(g:tabpagenr_post, t:testvar)
+ augroup END
+
+ " Test 'tabclose' triggering
+ let g:tabpagenr_pre = []
+ let g:tabpagenr_post = []
+ let t:testvar = 1
+ tabnew
+ let t:testvar = 2
+ tabnew
+ let t:testvar = 3
+ tabnew
+ let t:testvar = 4
+ tabnext
+ tabclose
+ tabclose
+ tabclose
+ call assert_equal([1, 2, 3], g:tabpagenr_pre)
+ call assert_equal([2, 3, 4], g:tabpagenr_post)
+
+ " Test 'tabclose {count}' triggering
+ let g:tabpagenr_pre = []
+ let g:tabpagenr_post = []
+ let t:testvar = 1
+ tabnew
+ let t:testvar = 2
+ tabnew
+ let t:testvar = 3
+ tabclose 2
+ tabclose 2
+ call assert_equal([2, 3], g:tabpagenr_pre)
+ call assert_equal([3, 1], g:tabpagenr_post)
+
+ " Test 'tabonly' triggering
+ let g:tabpagenr_pre = []
+ let g:tabpagenr_post = []
+ let t:testvar = 1
+ tabnew
+ let t:testvar = 2
+ tabonly
+ call assert_equal([1], g:tabpagenr_pre)
+ call assert_equal([2], g:tabpagenr_post)
+
+ " Test 'q' and 'close' triggering (closing the last window in a tab)
+ let g:tabpagenr_pre = []
+ let g:tabpagenr_post = []
+ split
+ let t:testvar = 1
+ tabnew
+ let t:testvar = 2
+ split
+ vsplit
+ tabnew
+ let t:testvar = 3
+ tabnext
+ only
+ quit
+ quit
+ close
+ close
+ call assert_equal([1, 2], g:tabpagenr_pre)
+ call assert_equal([2, 3], g:tabpagenr_post)
+
+ func ClearAutomcdAndCreateTabs()
+ au! TabClosedPre
+ bw!
+ e Z
+ tabonly
+ tabnew A
+ tabnew B
+ tabnew C
+ endfunc
+
+ func GetTabs()
+ redir => tabsout
+ tabs
+ redir END
+ let tabsout = substitute(tabsout, '\n', '', 'g')
+ let tabsout = substitute(tabsout, 'Tab page ', '', 'g')
+ let tabsout = substitute(tabsout, '#', '', 'g') " Nvim: remove '#'
+ let tabsout = substitute(tabsout, ' ', '', 'g')
+ return tabsout
+ endfunc
+
+ call CleanUpTestAuGroup()
+
+ " Close tab in TabClosedPre autocmd
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabclose
+ call assert_fails('tabclose', 'E1312')
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabclose
+ call assert_fails('tabclose 2', 'E1312')
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabclose 1
+ call assert_fails('tabclose', 'E1312')
+
+ " Close other (all) tabs in TabClosedPre autocmd
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabonly
+ call assert_fails('tabclose', 'E1312')
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabonly
+ call assert_fails('tabclose 2', 'E1312')
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabclose 4
+ call assert_fails('tabclose 2', 'E1312')
+
+ " Open new tabs in TabClosedPre autocmd
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabnew D
+ call assert_fails('tabclose', 'E1312')
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabnew D
+ call assert_fails('tabclose 1', 'E1312')
+
+ " Moving the tab page in TabClosedPre autocmd
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabmove 0
+ tabclose
+ call assert_equal('1Z2A3>B', GetTabs())
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabmove 0
+ tabclose 1
+ call assert_equal('1A2B3>C', GetTabs())
+ tabonly
+ call assert_equal('1>C', GetTabs())
+
+ " Switching tab page in TabClosedPre autocmd
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabnext | e Y
+ tabclose
+ call assert_equal('1Y2A3>B', GetTabs())
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * tabnext | e Y
+ tabclose 1
+ call assert_equal('1Y2B3>C', GetTabs())
+ tabonly
+ call assert_equal('1>Y', GetTabs())
+
+ " Create new windows in TabClosedPre autocmd
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * split | e X| vsplit | e Y | split | e Z
+ call assert_fails('tabclose', 'E242')
+ call ClearAutomcdAndCreateTabs()
+ au TabClosedPre * new X | new Y | new Z
+ call assert_fails('tabclose 1', 'E242')
+
+ " Clean up
+ au!
+ only
+ tabonly
+ bw!
+endfunc
+
func Test_eventignorewin_non_current()
defer CleanUpTestAuGroup()
let s:triggered = ''