fix(api): disallow moving window between tabpages in more cases

Problem: more cases where it may not be safe to move a window between tabpages.

Solution: check them.

Rather speculative... I haven't spend much time looking, but I didn't find
existing code that sets these locks to skip checking win_valid. (what I did find
called it anyway, like in win_close) Still, I think it's a good precaution for
what future code might do.

If the fact that nvim_win_set_config *actually* moves windows between tabpages
causes unforeseen issues, "faking" it like ":wincmd T" may be an alternative:
split a new window, close the old one, but instead also block autocmds, copy the
old window's config, and give it its handle?
This commit is contained in:
Sean Dewar
2026-03-12 17:31:37 +00:00
parent 094b297a3b
commit 853eea859f
3 changed files with 67 additions and 4 deletions

View File

@@ -18,6 +18,8 @@
#include "nvim/drawscreen.h"
#include "nvim/errors.h"
#include "nvim/eval/window.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/globals.h"
#include "nvim/highlight_group.h"
#include "nvim/macros_defs.h"
@@ -391,6 +393,20 @@ static bool win_can_move_tp(win_T *wp, tabpage_T *tp, Error *err)
api_set_error(err, kErrorTypeException, "Cannot move last non-floating window");
return false;
}
// Like closing, moving windows between tabpages makes win_valid return false. Helpful when e.g:
// walking the window list, as w_next/w_prev can unexpectedly refer to windows in another tabpage!
// Check related locks, in case they were set to avoid checking win_valid.
if (win_locked(wp)) {
api_set_error(err, kErrorTypeException, "Cannot move window to another tabpage whilst in use");
return false;
}
if (window_layout_locked_err(CMD_SIZE, err)) {
return false; // error already set
}
if (textlock || expr_map_locked()) {
api_set_error(err, kErrorTypeException, "%s", e_textlock);
return false;
}
if (is_aucmd_win(wp)) {
api_set_error(err, kErrorTypeException, "Cannot move autocmd window to another tabpage");
return false;

View File

@@ -28,7 +28,6 @@
#include "nvim/eval/window.h"
#include "nvim/ex_cmds.h"
#include "nvim/ex_cmds2.h"
#include "nvim/ex_cmds_defs.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_eval.h"
#include "nvim/ex_getln.h"
@@ -143,12 +142,26 @@ bool frames_locked(void)
/// error message. When closing window(s) and the command isn't easy to know,
/// passing CMD_SIZE will also work.
bool window_layout_locked(cmdidx_T cmd)
{
Error err = ERROR_INIT;
const bool locked = window_layout_locked_err(cmd, &err);
if (ERROR_SET(&err)) {
emsg(_(err.msg));
api_clear_error(&err);
}
return locked;
}
/// Like `window_layout_locked`, but set `err` to the (untranslated) error message when locked.
/// @see window_layout_locked
bool window_layout_locked_err(cmdidx_T cmd, Error *err)
{
if (split_disallowed > 0 || close_disallowed > 0) {
if (close_disallowed == 0 && cmd == CMD_tabnew) {
emsg(_(e_cannot_split_window_when_closing_buffer));
api_set_error(err, kErrorTypeException, "%s", e_cannot_split_window_when_closing_buffer);
} else {
emsg(_(e_not_allowed_to_change_window_layout_in_this_autocmd));
api_set_error(err, kErrorTypeException, "%s",
e_not_allowed_to_change_window_layout_in_this_autocmd);
}
return true;
}

View File

@@ -3168,7 +3168,6 @@ describe('API/win', function()
)
command('quit!')
-- Can't switch away from window before moving it to a different tabpage during textlock.
exec(([[
new
call setline(1, 'foo')
@@ -3180,6 +3179,41 @@ describe('API/win', function()
pcall_err(command, 'normal! ==')
)
eq(cur_win, api.nvim_get_current_win())
exec(([[
wincmd p
call setline(1, 'bar')
setlocal indentexpr=nvim_win_set_config(win_getid(winnr('#')),#{split:'left',win:%d})
]]):format(t2_win))
neq(cur_win, api.nvim_get_current_win())
matches(
'E565: Not allowed to change text or change window$',
pcall_err(command, 'normal! ==')
)
-- expr_map_lock
exec(([[
nnoremap <expr> @ nvim_win_set_config(win_getid(winnr('#')),#{split:'left',win:%d})
]]):format(t2_win))
neq(cur_win, api.nvim_get_current_win())
matches(
'E565: Not allowed to change text or change window$',
pcall_err(fn.feedkeys, '@', 'x')
)
exec(([[
wincmd p
autocmd WinNewPre * ++once call nvim_win_set_config(0, #{relative:'editor', win:%d, row:0, col:0, width:1, height:1})
]]):format(t2_win))
matches(
'E1312: Not allowed to change the window layout in this autocmd$',
pcall_err(command, 'split')
)
eq(cur_win, api.nvim_get_current_win()) -- :split didn't enter new window due to error
exec(([[
autocmd WinLeave * ++once call nvim_win_set_config(0, #{relative:'editor', win:%d, row:0, col:0, width:1, height:1})
]]):format(t2_win))
matches('Cannot move window to another tabpage whilst in use$', pcall_err(command, 'quit'))
eq(cur_win, api.nvim_get_current_win()) -- :quit didn't close window due to error
end)
it('updates statusline when moving bottom split', function()