diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 1ef687e029..a77e2314d4 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -5165,9 +5165,11 @@ static void ex_pclose(exarg_T *eap) /// Close window "win" and take care of handling closing the last window for a /// modified buffer. /// -/// @param tp NULL or the tab page "win" is in +/// @param tp NULL or the tab page "win" is in, if different from "curtab" void ex_win_close(int forceit, win_T *win, tabpage_T *tp) { + assert(tp == NULL || tp != curtab); + // Never close the autocommand window. if (is_ctx_win(win)) { emsg(_(e_autocmd_close)); @@ -5276,6 +5278,8 @@ static void ex_tabonly(exarg_T *eap) /// Close the current tab page. void tabpage_close(int forceit) { + int done = 0; + if (window_layout_locked(CMD_tabclose)) { return; } @@ -5286,9 +5290,13 @@ void tabpage_close(int forceit) // First close all the windows but the current one. If that worked then // close the last window in this tab, that will close it. - while (curwin->w_floating) { + while (curwin->w_floating && ++done < 1000) { ex_win_close(forceit, curwin, NULL); } + if (done == 1000) { + return; + } + if (!ONE_WINDOW) { close_others(true, forceit, true); } @@ -5311,6 +5319,8 @@ void tabpage_close_other(tabpage_T *tp, int forceit) int done = 0; char prev_idx[NUMBUFLEN]; + assert(tp != curtab); + if (window_layout_locked(CMD_SIZE)) { return; } @@ -5323,12 +5333,20 @@ void tabpage_close_other(tabpage_T *tp, int forceit) while (++done < 1000) { snprintf(prev_idx, sizeof(prev_idx), "%i", tabpage_index(tp)); win_T *wp = tp->tp_lastwin; + + // Autocommands (including TabClosedPre above) may change the current tab page, + // abort a `:tabonly` (etc) if we're now on the tab we're trying to close + if (tp == curtab) { + done = 1000; + break; + } ex_win_close(forceit, wp, tp); // Autocommands may delete the tab page under our fingers. if (!valid_tabpage(tp)) { break; } + // We may fail to close a window with a modified buffer. if (tp->tp_lastwin == wp) { done = 1000; diff --git a/test/functional/editor/tabpage_spec.lua b/test/functional/editor/tabpage_spec.lua index 0eff990c25..80a1612e75 100644 --- a/test/functional/editor/tabpage_spec.lua +++ b/test/functional/editor/tabpage_spec.lua @@ -10,6 +10,7 @@ local neq = t.neq local feed = n.feed local eval = n.eval local exec = n.exec +local exec_lua = n.exec_lua local fn = n.fn local api = n.api local curwin = n.api.nvim_get_current_win @@ -173,4 +174,28 @@ describe('tabpage', function() quit ]]) end) + + it( + 'no crash when :tabclose/:tabonly and WinClosed autocmd wipes buf and switches to closing tab', + function() + exec_lua(function() + local win1 = vim.api.nvim_get_current_win() + vim.cmd('botright new') + local win2 = vim.api.nvim_get_current_win() + local buf2 = vim.api.nvim_get_current_buf() + vim.cmd('tabedit') + vim.api.nvim_create_autocmd('WinClosed', { + pattern = tostring(win2), + callback = function() + -- Wipe the closing window's buffer and switch back to the original window, + -- so `curtab == tp` in tabpage_close_other's loop. + vim.api.nvim_buf_delete(buf2, { force = true }) + vim.api.nvim_set_current_win(win1) + end, + }) + vim.cmd('tabonly') + end) + assert_alive() + end + ) end)