neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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:
Mruntime/doc/autocmd.txt | 10+++++++---
Mruntime/doc/news.txt | 1+
Mruntime/doc/options.txt | 1+
Mruntime/lua/vim/_meta/api_keysets.lua | 1+
Mruntime/lua/vim/_meta/options.lua | 1+
Mruntime/syntax/vim.vim | 2+-
Msrc/nvim/auevents.lua | 3++-
Msrc/nvim/window.c | 37+++++++++++++++++++++++++++++++++++++
Mtest/old/testdir/test_autocmd.vim | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 = ''