From 77fe01f2005af12c8565712734fa426288dc587a Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 19 Sep 2025 07:40:12 +0800 Subject: [PATCH] vim-patch:9.1.1773: Crash in BufLeave after BufUnload closes other windows (#35830) Problem: Crash in BufLeave/WinLeave/TabLeave when closing window after BufUnload closes all other windows in the tab page. Solution: Avoid duplicate BufLeave/WinLeave events. Trigger TabLeave before removing the buffer (zeertzjq). related: vim/vim#14166 related: neovim/neovim#33603 closes: vim/vim#18330 https://github.com/vim/vim/commit/0c70820015c7a37425c07bf30ad277ee2656d496 (cherry picked from commit c9f62674028d764f8a6faa433baa7ab209956fc8) --- src/nvim/window.c | 14 +++++++-- test/old/testdir/test_autocmd.vim | 48 ++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/nvim/window.c b/src/nvim/window.c index a94e2cc126..646c6d9d81 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -2610,9 +2610,11 @@ static bool close_last_window_tabpage(win_T *win, bool free_buf, tabpage_T *prev // page and then close the window and the tab page. This avoids that // curwin and curtab are invalid while we are freeing memory, they may // be used in GUI events. - // Don't trigger autocommands yet, they may use wrong values, so do + // Don't trigger *Enter autocommands yet, they may use wrong values, so do // that below. - goto_tabpage_tp(alt_tabpage(), false, true); + // Do trigger *Leave autocommands, unless win->w_buffer is NULL, in which + // case they have already been triggered. + goto_tabpage_tp(alt_tabpage(), false, win->w_buffer != NULL); // Safety check: Autocommands may have closed the window when jumping // to the other tab page. @@ -2906,6 +2908,14 @@ int win_close(win_T *win, bool free_buf, bool force) } } + if (ONE_WINDOW && curwin->w_locked && curbuf->b_locked_split + && first_tabpage->tp_next != NULL) { + // The new curwin is the last window in the current tab page, and it is + // already being closed. Trigger TabLeave now, as after its buffer is + // removed it's no longer safe to do that. + apply_autocmds(EVENT_TABLEAVE, NULL, NULL, false, curbuf); + } + split_disallowed--; // After closing the help window, try restoring the window layout from diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim index 381a810045..92f034c31b 100644 --- a/test/old/testdir/test_autocmd.vim +++ b/test/old/testdir/test_autocmd.vim @@ -746,27 +746,49 @@ func Test_WinClosed_switch_tab() %bwipe! endfunc -" This used to trigger WinClosed twice for the same window, and the window's -" buffer was NULL in the second autocommand. -func Test_WinClosed_BufUnload_close_other() - tabnew +" This used to trigger WinClosed/WinLeave/BufLeave twice for the same window, +" and the window's buffer was NULL in the second autocommand. +func Run_test_BufUnload_close_other(extra_cmd) + let oldtab = tabpagenr() + tabnew Xb1 let g:tab = tabpagenr() - let g:buf = bufnr() - new - setlocal bufhidden=wipe - augroup test-WinClosed - autocmd BufUnload * ++once exe g:buf .. 'bwipe!' - autocmd WinClosed * call tabpagebuflist(g:tab) + let g:w1 = win_getid() + new Xb2 + let g:w2 = win_getid() + let g:log = [] + exe a:extra_cmd + + augroup test-BufUnload-close-other + autocmd BufUnload * ++nested ++once bwipe! Xb1 + for event in ['WinClosed', 'BufLeave', 'WinLeave', 'TabLeave'] + exe $'autocmd {event} * call tabpagebuflist(g:tab)' + exe $'autocmd {event} * let g:log += ["{event}:" .. expand("")]' + endfor augroup END + close + " WinClosed is triggered once for each of the 2 closed windows. + " Others are only triggered once. + call assert_equal(['BufLeave:Xb2', 'WinLeave:Xb2', $'WinClosed:{g:w2}', + \ $'WinClosed:{g:w1}', 'TabLeave:Xb2'], g:log) + call assert_equal(oldtab, tabpagenr()) + call assert_equal([0, 0], win_id2tabwin(g:w1)) + call assert_equal([0, 0], win_id2tabwin(g:w2)) unlet g:tab - unlet g:buf - autocmd! test-WinClosed - augroup! test-WinClosed + unlet g:w1 + unlet g:w2 + unlet g:log + autocmd! test-BufUnload-close-other + augroup! test-BufUnload-close-other %bwipe! endfunc +func Test_BufUnload_close_other() + call Run_test_BufUnload_close_other('') + call Run_test_BufUnload_close_other('setlocal bufhidden=wipe') +endfunc + func s:AddAnAutocmd() augroup vimBarTest au BufReadCmd * echo 'hello'