fix(window): crash closing split if autocmds close other splits (#37233)

Problem:  Crash when closing a split window if autocmds close other
          split windows but there are still floating windows.
Solution: Bail out and give the window back its buffer.
This commit is contained in:
zeertzjq
2026-01-05 11:17:00 +08:00
parent a9ffdca528
commit f7e2554bfb
2 changed files with 94 additions and 17 deletions

View File

@@ -2650,7 +2650,9 @@ static bool close_last_window_tabpage(win_T *win, bool free_buf, tabpage_T *prev
/// "action" can also be zero (do nothing).
/// "abort_if_last" is passed to close_buffer(): abort closing if all other
/// windows are closed.
static void win_close_buffer(win_T *win, int action, bool abort_if_last)
///
/// @return whether close_buffer() decremented b_nwindows
static bool win_close_buffer(win_T *win, int action, bool abort_if_last)
FUNC_ATTR_NONNULL_ALL
{
// Free independent synblock before the buffer is freed.
@@ -2665,12 +2667,13 @@ static void win_close_buffer(win_T *win, int action, bool abort_if_last)
win->w_buffer->b_p_bl = false;
}
bool retval = false;
// Close the link to the buffer.
if (win->w_buffer != NULL) {
bufref_T bufref;
set_bufref(&bufref, curbuf);
win->w_locked = true;
close_buffer(win, win->w_buffer, action, abort_if_last, true);
retval = close_buffer(win, win->w_buffer, action, abort_if_last, true);
if (win_valid_any_tab(win)) {
win->w_locked = false;
}
@@ -2681,6 +2684,27 @@ static void win_close_buffer(win_T *win, int action, bool abort_if_last)
curbuf = firstbuf;
}
}
return retval;
}
/// When failing to close a window after already calling close_buffer() on it,
/// call this to make the window have a buffer again.
///
/// @param bufref reference to win->w_buffer before calling close_buffer()
/// @param did_decrement whether close_buffer() decremented b_nwindows
static void win_unclose_buffer(win_T *win, bufref_T *bufref, bool did_decrement)
{
if (win->w_buffer == NULL) {
// If the buffer was removed from the window we have to give it any buffer.
win->w_buffer = firstbuf;
firstbuf->b_nwindows++;
win_init_empty(win);
} else if (did_decrement && win->w_buffer == bufref->br_buf && bufref_valid(bufref)) {
// close_buffer() decremented the window count, but we're keeping the window.
// As the window is still viewing the buffer, increment the count.
win->w_buffer->b_nwindows++;
}
}
// Close window "win". Only works for the current tab page.
@@ -2804,7 +2828,10 @@ int win_close(win_T *win, bool free_buf, bool force)
return OK;
}
win_close_buffer(win, free_buf ? DOBUF_UNLOAD : 0, true);
bufref_T bufref;
set_bufref(&bufref, win->w_buffer);
bool did_decrement = win_close_buffer(win, free_buf ? DOBUF_UNLOAD : 0, true);
if (win_valid(win) && win->w_buffer == NULL
&& !win->w_floating && last_window(win)) {
@@ -2825,8 +2852,17 @@ int win_close(win_T *win, bool free_buf, bool force)
// Autocommands may have closed the window already, or closed the only
// other window or moved to another tab page.
if (!win_valid(win) || (!win->w_floating && last_window(win))
|| close_last_window_tabpage(win, free_buf, prev_curtab)) {
if (!win_valid(win)) {
return FAIL;
}
if (one_window(win, NULL) && (first_tabpage->tp_next == NULL || lastwin->w_floating)) {
if (first_tabpage->tp_next != NULL) {
emsg(e_floatonly);
}
win_unclose_buffer(win, &bufref, did_decrement);
return FAIL;
}
if (close_last_window_tabpage(win, free_buf, prev_curtab)) {
return FAIL;
}
@@ -3105,16 +3141,7 @@ bool win_close_othertab(win_T *win, int free_buf, tabpage_T *tp, bool force)
leave_open:
if (win_valid_any_tab(win)) {
if (win->w_buffer == NULL) {
// If the buffer was removed from the window we have to give it any buffer.
win->w_buffer = firstbuf;
firstbuf->b_nwindows++;
win_init_empty(win);
} else if (did_decrement && win->w_buffer == bufref.br_buf && bufref_valid(&bufref)) {
// close_buffer decremented the window count, but we're keeping the window.
// As the window is still viewing the buffer, increment the count.
win->w_buffer->b_nwindows++;
}
win_unclose_buffer(win, &bufref, did_decrement);
}
return false;
}

View File

@@ -1005,7 +1005,7 @@ describe('float window', function()
assert_alive()
end)
pending('does not crash if BufUnload makes it the only non-float in tabpage', function()
it('does not crash if BufUnload makes it the only non-float in tabpage', function()
exec([[
tabnew
let g:buf = bufnr()
@@ -1017,10 +1017,60 @@ describe('float window', function()
\ #{relative: 'editor', row: 5, col: 5, width: 5, height: 5})
autocmd BufUnload * ++once exe g:buf .. 'bwipe!'
]])
command('close')
eq('Vim(close):E5601: Cannot close window, only floating window would remain', pcall_err(command, 'close'))
assert_alive()
end)
describe('does not crash if WinClosed makes it the only non-float', function()
before_each(function()
exec([[
let g:buf = bufnr()
new
setlocal bufhidden=wipe
autocmd WinClosed * ++once exe g:buf .. 'bwipe!'
]])
end)
local opts = { relative = 'editor', row = 5, col = 5, width = 5, height = 5 }
local floatwin
describe('and there is a float window with the same buffer', function()
before_each(function()
floatwin = api.nvim_open_win(0, false, opts)
end)
it('with multiple tabpages', function()
command('tabnew | tabprev')
eq('Vim(close):E5601: Cannot close window, only floating window would remain', pcall_err(command, 'close'))
api.nvim_win_close(floatwin, true)
assert_alive()
end)
it('with only one tabpage', function()
command('close')
api.nvim_win_close(floatwin, true)
assert_alive()
end)
end)
describe('and there is a float with a different buffer', function()
before_each(function()
floatwin = api.nvim_open_win(api.nvim_create_buf(true, false), false, opts)
end)
it('with multiple tabpages', function()
command('tabnew | tabprev')
eq('Vim(close):E855: Autocommands caused command to abort', pcall_err(command, 'close'))
assert_alive()
end)
it('with only one tabpage', function()
eq('Vim(close):E855: Autocommands caused command to abort', pcall_err(command, 'close'))
assert_alive()
end)
end)
end)
it('does not crash if WinClosed from floating window closes it', function()
exec([[
tabnew