From e2a0748cb28fb44f6ee42f7470ef50a9dda6e320 Mon Sep 17 00:00:00 2001 From: glepnir Date: Fri, 5 Sep 2025 13:35:27 +0800 Subject: feat(api): nvim_win_set_config can move floatwin to another tabpage Problem: nvim_win_set_config can't move floating windows to different tab pages. Solution: allow it. Co-authored-by: Sean Dewar <6256228+seandewar@users.noreply.github.com> --- test/functional/api/window_spec.lua | 140 +++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 35 deletions(-) (limited to 'test/functional/api/window_spec.lua') diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 62cc514058..9a8b4db7e1 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -2561,60 +2561,61 @@ describe('API/win', function() eq(win, layout[2][2][2]) end) - it('moves splits to other tabpages', function() - local curtab = api.nvim_get_current_tabpage() + it('moves splits or floats to other tabpages', function() + local first_tab = api.nvim_get_current_tabpage() + local first_win = api.nvim_get_current_win() local win = api.nvim_open_win(0, false, { split = 'left' }) command('tabnew') - local tabnr = api.nvim_get_current_tabpage() - command('tabprev') -- return to the initial tab - - api.nvim_win_set_config(win, { - split = 'right', - win = api.nvim_tabpage_get_win(tabnr), - }) - - eq(tabnr, api.nvim_win_get_tabpage(win)) + local new_tab = api.nvim_get_current_tabpage() + local tab2_win = api.nvim_get_current_win() + api.nvim_set_current_tabpage(first_tab) + -- move new win to new tabpage + api.nvim_win_set_config(win, { split = 'right', win = api.nvim_tabpage_get_win(new_tab) }) + eq(new_tab, api.nvim_win_get_tabpage(win)) -- we are changing the config, the current tabpage should not change - eq(curtab, api.nvim_get_current_tabpage()) + eq(first_tab, api.nvim_get_current_tabpage()) - command('tabnext') -- switch to the new tabpage so we can get the layout + api.nvim_set_current_tabpage(new_tab) local layout = fn.winlayout() - eq({ 'row', { - { 'leaf', api.nvim_tabpage_get_win(tabnr) }, + { 'leaf', api.nvim_tabpage_get_win(new_tab) }, { 'leaf', win }, }, }, layout) + + -- convert new win to float window in new tabpage + api.nvim_win_set_config(win, { relative = 'editor', row = 2, col = 2, height = 2, width = 2 }) + api.nvim_set_current_tabpage(first_tab) + -- move to another tabpage + api.nvim_win_set_config(win, { relative = 'win', win = first_win, row = 2, col = 2 }) + eq(first_tab, api.nvim_win_get_tabpage(win)) + eq({ first_win, win }, api.nvim_tabpage_list_wins(first_tab)) + eq({ tab2_win }, api.nvim_tabpage_list_wins(new_tab)) end) it('correctly moves curwin when moving curwin to a different tabpage', function() - local curtab = api.nvim_get_current_tabpage() + local tab1 = api.nvim_get_current_tabpage() + local tab1_win = api.nvim_get_current_win() command('tabnew') local tab2 = api.nvim_get_current_tabpage() local tab2_win = api.nvim_get_current_win() - - command('tabprev') -- return to the initial tab - - local neighbor = api.nvim_get_current_win() - + api.nvim_set_current_tabpage(tab1) -- return to the initial tab -- create and enter a new split local win = api.nvim_open_win(0, true, { vertical = false, }) - eq(curtab, api.nvim_win_get_tabpage(win)) - - eq({ win, neighbor }, api.nvim_tabpage_list_wins(curtab)) + eq(tab1, api.nvim_win_get_tabpage(win)) + eq({ win, tab1_win }, api.nvim_tabpage_list_wins(tab1)) -- move the current win to a different tabpage api.nvim_win_set_config(win, { split = 'right', win = api.nvim_tabpage_get_win(tab2), }) - - eq(curtab, api.nvim_get_current_tabpage()) + eq(tab1, api.nvim_get_current_tabpage()) -- win should have moved to tab2 eq(tab2, api.nvim_win_get_tabpage(win)) @@ -2622,10 +2623,18 @@ describe('API/win', function() eq(tab2_win, api.nvim_tabpage_get_win(tab2)) -- win lists should be correct eq({ tab2_win, win }, api.nvim_tabpage_list_wins(tab2)) - eq({ neighbor }, api.nvim_tabpage_list_wins(curtab)) - + eq({ tab1_win }, api.nvim_tabpage_list_wins(tab1)) -- current win should have moved to neighboring win - eq(neighbor, api.nvim_tabpage_get_win(curtab)) + eq(tab1_win, api.nvim_tabpage_get_win(tab1)) + + api.nvim_set_current_tabpage(tab2) + -- convert new win to float window + api.nvim_win_set_config(win, { relative = 'editor', row = 2, col = 2, height = 2, width = 2 }) + api.nvim_set_current_win(win) + api.nvim_win_set_config(win, { relative = 'win', win = tab1_win, row = 3, col = 3 }) + eq(tab1, api.nvim_win_get_tabpage(win)) + eq(tab2, api.nvim_get_current_tabpage()) + eq({ tab1_win, win }, api.nvim_tabpage_list_wins(tab1)) end) it('splits windows in non-current tabpage', function() @@ -2834,18 +2843,35 @@ describe('API/win', function() eq({ 'Leave', win, 'Enter', new_curwin }, eval('result')) end) - it('no autocmds when moving window within same tabpage', function() + it('no autocmds when moving window in same or other tabpage', function() local parent = api.nvim_get_current_win() exec([[ split - let result = [] - autocmd WinEnter * let result += ["Enter", win_getid()] - autocmd WinLeave * let result += ["Leave", win_getid()] - autocmd WinNew * let result += ["New", win_getid()] + let g:result = [] + autocmd WinEnter * let g:result += ["Enter", win_getid()] + autocmd WinLeave * let g:result += ["Leave", win_getid()] + autocmd WinNew * let g:result += ["New", win_getid()] ]]) api.nvim_win_set_config(0, { win = parent, split = 'left' }) -- Shouldn't see any of those events, as we remain in the same window. - eq({}, eval('result')) + eq({}, eval('g:result')) + + -- move float window from tab2 to tab1 + command('tabdo only') + local tab1 = api.nvim_get_current_tabpage() + local tab1_win1 = api.nvim_get_current_win() + command('tabnew') + local fwin = api.nvim_open_win(0, false, { + relative = 'editor', + row = 2, + col = 2, + height = 2, + width = 2, + }) + api.nvim_set_current_tabpage(tab1) + api.nvim_set_var('result', {}) + api.nvim_win_set_config(fwin, { relative = 'win', win = tab1_win1, row = 4, col = 4 }) + eq({}, eval('g:result')) end) it('checks if splitting disallowed', function() @@ -3656,5 +3682,49 @@ describe('API/win', function() api.nvim_open_win(0, true, { split = 'below', style = 'minimal' }) command('quit') end) + + it('preserve current floating window when moving fails', function() + local buf = api.nvim_create_buf(false, true) + local tab1_win = api.nvim_get_current_win() + local float_win = api.nvim_open_win(buf, true, { + relative = 'editor', + row = 1, + col = 1, + width = 10, + height = 5, + }) + command('tabnew') + local tab2_win = api.nvim_get_current_win() + command('tabprev') + api.nvim_set_current_win(float_win) + command('autocmd WinLeave * ++once call nvim_win_close(' .. tab2_win .. ', v:true)') + eq( + 'Target windows were closed', + pcall_err( + api.nvim_win_set_config, + float_win, + { relative = 'win', win = tab2_win, row = 0, col = 0 } + ) + ) + eq(float_win, api.nvim_get_current_win()) + + command('tabnew') + local tab3_win = api.nvim_get_current_win() + command('tabprev') + command( + ('autocmd WinLeave * ++once call nvim_win_set_config(%d, {"split": "left", "win": %d})'):format( + float_win, + tab1_win + ) + ) + eq( + ('Window %d was made non-floating'):format(float_win), + pcall_err( + api.nvim_win_set_config, + float_win, + { relative = 'win', win = tab3_win, row = 0, col = 0 } + ) + ) + end) end) end) -- cgit From e5240b35c3b78e34ca16ccc8369d0aeb4e1add8e Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:05:27 +0000 Subject: refactor(api): cleanup, more comments, more tests, news - Factor out logic to keep nvim_win_set_config clean. - Clean up a few things, remove redundant logic, reflow some lines. - Add some more comments where appropriate. - Don't consider negative "win", as that's only relevant for splits. - Add more test coverage. - Add news.txt entry. --- test/functional/api/window_spec.lua | 80 ++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 11 deletions(-) (limited to 'test/functional/api/window_spec.lua') diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 9a8b4db7e1..1c48c51921 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -2561,7 +2561,7 @@ describe('API/win', function() eq(win, layout[2][2][2]) end) - it('moves splits or floats to other tabpages', function() + it('moves windows to other tabpages', function() local first_tab = api.nvim_get_current_tabpage() local first_win = api.nvim_get_current_win() local win = api.nvim_open_win(0, false, { split = 'left' }) @@ -2585,14 +2585,19 @@ describe('API/win', function() }, }, layout) - -- convert new win to float window in new tabpage + -- convert new win to float in new tabpage api.nvim_win_set_config(win, { relative = 'editor', row = 2, col = 2, height = 2, width = 2 }) api.nvim_set_current_tabpage(first_tab) - -- move to another tabpage + -- move to other tabpage api.nvim_win_set_config(win, { relative = 'win', win = first_win, row = 2, col = 2 }) eq(first_tab, api.nvim_win_get_tabpage(win)) eq({ first_win, win }, api.nvim_tabpage_list_wins(first_tab)) eq({ tab2_win }, api.nvim_tabpage_list_wins(new_tab)) + -- unlike splits, negative win is invalid + eq( + 'Invalid window id: -1', + pcall_err(api.nvim_win_set_config, win, { relative = 'win', win = -1, row = 2, col = 2 }) + ) end) it('correctly moves curwin when moving curwin to a different tabpage', function() @@ -2628,7 +2633,7 @@ describe('API/win', function() eq(tab1_win, api.nvim_tabpage_get_win(tab1)) api.nvim_set_current_tabpage(tab2) - -- convert new win to float window + -- convert new win to float api.nvim_win_set_config(win, { relative = 'editor', row = 2, col = 2, height = 2, width = 2 }) api.nvim_set_current_win(win) api.nvim_win_set_config(win, { relative = 'win', win = tab1_win, row = 3, col = 3 }) @@ -3568,20 +3573,29 @@ describe('API/win', function() end) it('cannot move autocmd window between tabpages', function() - local win_type, split_ok, err = exec_lua(function() + local win_type, split_ok, split_err, float_ok, float_err = exec_lua(function() local other_tp_win = vim.api.nvim_get_current_win() vim.cmd.tabnew() - local win_type, split_ok, err + local win_type, split_ok, split_err, float_ok, float_err vim.api.nvim_buf_call(vim.api.nvim_create_buf(true, true), function() win_type = vim.fn.win_gettype() - split_ok, err = + + split_ok, split_err = pcall(vim.api.nvim_win_set_config, 0, { win = other_tp_win, split = 'right' }) + + float_ok, float_err = pcall( + vim.api.nvim_win_set_config, + 0, + { relative = 'win', win = other_tp_win, row = 0, col = 0 } + ) end) - return win_type, split_ok, err + return win_type, split_ok, split_err, float_ok, float_err end) + eq('autocmd', win_type) - eq({ false, 'Cannot move autocmd window to another tabpage' }, { split_ok, err }) + eq({ false, 'Cannot move autocmd window to another tabpage' }, { split_ok, split_err }) + eq({ false, 'Cannot move autocmd window to another tabpage' }, { float_ok, float_err }) end) it('cannot move cmdwin between tabpages', function() @@ -3710,9 +3724,19 @@ describe('API/win', function() command('tabnew') local tab3_win = api.nvim_get_current_win() - command('tabprev') + command('tabprev | autocmd WinEnter * ++once wincmd p') + eq( + ('Failed to switch away from window %d'):format(float_win), + pcall_err( + api.nvim_win_set_config, + float_win, + { relative = 'win', win = tab3_win, row = 0, col = 0 } + ) + ) + eq(float_win, api.nvim_get_current_win()) + command( - ('autocmd WinLeave * ++once call nvim_win_set_config(%d, {"split": "left", "win": %d})'):format( + ('autocmd WinLeave * ++once call nvim_win_set_config(%d, #{split: "left", win: %d})'):format( float_win, tab1_win ) @@ -3725,6 +3749,40 @@ describe('API/win', function() { relative = 'win', win = tab3_win, row = 0, col = 0 } ) ) + eq(float_win, api.nvim_get_current_win()) + + -- Need multigrid for external windows. + Screen.new(20, 9, { ext_multigrid = true }) + api.nvim_win_set_config(float_win, { external = true, width = 5, height = 5 }) + eq(true, api.nvim_win_get_config(float_win).external) + eq( + ('Cannot move external window to another tabpage'):format(float_win), + pcall_err( + api.nvim_win_set_config, + float_win, + { relative = 'win', win = tab3_win, row = 0, col = 0 } + ) + ) + eq(float_win, api.nvim_get_current_win()) + + -- Error if made external by autocommand when attempting to move. + api.nvim_win_set_config( + float_win, + { relative = 'editor', row = 0, col = 0, width = 5, height = 5 } + ) + eq(false, api.nvim_win_get_config(float_win).external) + command( + ('autocmd WinLeave * ++once call nvim_win_set_config(%d, #{external: 1})'):format(float_win) + ) + eq( + ('Cannot move external window to another tabpage'):format(float_win), + pcall_err( + api.nvim_win_set_config, + float_win, + { relative = 'win', win = tab3_win, row = 0, col = 0 } + ) + ) + eq(float_win, api.nvim_get_current_win()) end) end) end) -- cgit From 7be4ae796faa2066044b6cafe266ebe6354c55fe Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:59:48 +0000 Subject: fix(api): relax config validation for "win" Problem: only possible to move floats between tabpages if relative=win, which has the restrictive effect of also anchoring it to the target window. Solution: allow "win" without "relative" or "split"/"vertical". Only assume missing "win" is 0 if relative=win is given to maintain that behaviour. (or when configuring a new window) Also add an error when attempting to change a split into a float that's in another tabpage, as this isn't actually supported yet. (until the next commit) Maybe this could do with some bikeshedding. Unclear if "win" should require "relative" to be given, like with "row"/"col"; this can be annoying though as specifying "relative" requires other fields to be given too. --- test/functional/api/window_spec.lua | 49 ++++++++++++++----------------------- 1 file changed, 18 insertions(+), 31 deletions(-) (limited to 'test/functional/api/window_spec.lua') diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 1c48c51921..579eb88663 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -2585,19 +2585,26 @@ describe('API/win', function() }, }, layout) + -- converting split into a float for a different tabpage is not yet supported + eq( + 'Cannot configure split into float in another tabpage', + pcall_err( + api.nvim_win_set_config, + win, + { relative = 'editor', row = 0, col = 0, width = 1, height = 1, win = first_win } + ) + ) + -- convert new win to float in new tabpage api.nvim_win_set_config(win, { relative = 'editor', row = 2, col = 2, height = 2, width = 2 }) api.nvim_set_current_tabpage(first_tab) -- move to other tabpage - api.nvim_win_set_config(win, { relative = 'win', win = first_win, row = 2, col = 2 }) + api.nvim_win_set_config(win, { win = first_win }) eq(first_tab, api.nvim_win_get_tabpage(win)) eq({ first_win, win }, api.nvim_tabpage_list_wins(first_tab)) eq({ tab2_win }, api.nvim_tabpage_list_wins(new_tab)) -- unlike splits, negative win is invalid - eq( - 'Invalid window id: -1', - pcall_err(api.nvim_win_set_config, win, { relative = 'win', win = -1, row = 2, col = 2 }) - ) + eq('Invalid window id: -1', pcall_err(api.nvim_win_set_config, win, { win = -1 })) end) it('correctly moves curwin when moving curwin to a different tabpage', function() @@ -2875,7 +2882,7 @@ describe('API/win', function() }) api.nvim_set_current_tabpage(tab1) api.nvim_set_var('result', {}) - api.nvim_win_set_config(fwin, { relative = 'win', win = tab1_win1, row = 4, col = 4 }) + api.nvim_win_set_config(fwin, { win = tab1_win1 }) eq({}, eval('g:result')) end) @@ -3584,11 +3591,7 @@ describe('API/win', function() split_ok, split_err = pcall(vim.api.nvim_win_set_config, 0, { win = other_tp_win, split = 'right' }) - float_ok, float_err = pcall( - vim.api.nvim_win_set_config, - 0, - { relative = 'win', win = other_tp_win, row = 0, col = 0 } - ) + float_ok, float_err = pcall(vim.api.nvim_win_set_config, 0, { win = other_tp_win }) end) return win_type, split_ok, split_err, float_ok, float_err end) @@ -3714,11 +3717,7 @@ describe('API/win', function() command('autocmd WinLeave * ++once call nvim_win_close(' .. tab2_win .. ', v:true)') eq( 'Target windows were closed', - pcall_err( - api.nvim_win_set_config, - float_win, - { relative = 'win', win = tab2_win, row = 0, col = 0 } - ) + pcall_err(api.nvim_win_set_config, float_win, { win = tab2_win }) ) eq(float_win, api.nvim_get_current_win()) @@ -3727,11 +3726,7 @@ describe('API/win', function() command('tabprev | autocmd WinEnter * ++once wincmd p') eq( ('Failed to switch away from window %d'):format(float_win), - pcall_err( - api.nvim_win_set_config, - float_win, - { relative = 'win', win = tab3_win, row = 0, col = 0 } - ) + pcall_err(api.nvim_win_set_config, float_win, { win = tab3_win }) ) eq(float_win, api.nvim_get_current_win()) @@ -3743,11 +3738,7 @@ describe('API/win', function() ) eq( ('Window %d was made non-floating'):format(float_win), - pcall_err( - api.nvim_win_set_config, - float_win, - { relative = 'win', win = tab3_win, row = 0, col = 0 } - ) + pcall_err(api.nvim_win_set_config, float_win, { win = tab3_win }) ) eq(float_win, api.nvim_get_current_win()) @@ -3776,11 +3767,7 @@ describe('API/win', function() ) eq( ('Cannot move external window to another tabpage'):format(float_win), - pcall_err( - api.nvim_win_set_config, - float_win, - { relative = 'win', win = tab3_win, row = 0, col = 0 } - ) + pcall_err(api.nvim_win_set_config, float_win, { win = tab3_win }) ) eq(float_win, api.nvim_get_current_win()) end) -- cgit From 3325536150eda0de4dc69f25beca416c0a0b3e3a Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:19:06 +0000 Subject: fix(winfloat): last_status when changing split to floatwin Problem: converting a split to a floatwin may not remove the last statusline when needed. (e.g: 'ls' is 1) Solution: call last_status/win_comp_pos in win_new_float, after win_remove. Also fix float_pos formatting for screen snapshots so it doesn't give a nil error for external windows. Not an issue from this PR. --- test/functional/api/window_spec.lua | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'test/functional/api/window_spec.lua') diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 579eb88663..d75dc78142 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -3241,6 +3241,46 @@ describe('API/win', function() pcall_err(api.nvim_win_set_config, win, cfg) ) end) + + it('removes last statusline if needed', function() + local screen = Screen.new(30, 9) + command('set laststatus=1 | botright split') + screen:expect([[ + | + {1:~ }|*2 + {2:[No Name] }| + ^ | + {1:~ }|*2 + {3:[No Name] }| + | + ]]) + api.nvim_win_set_config(0, { relative = 'editor', row = 0, col = 0, width = 4, height = 4 }) + screen:expect([[ + {4:^ } | + {11:~ }{1: }|*3 + {1:~ }|*4 + | + ]]) + command('quit | set laststatus=2 | botright split') + screen:expect([[ + | + {1:~ }|*2 + {2:[No Name] }| + ^ | + {1:~ }|*2 + {3:[No Name] }| + | + ]]) + api.nvim_win_set_config(0, { relative = 'editor', row = 1, col = 5, width = 4, height = 4 }) + screen:expect([[ + | + {1:~ }{4:^ }{1: }| + {1:~ }{11:~ }{1: }|*3 + {1:~ }|*2 + {2:[No Name] }| + | + ]]) + end) end) describe('get_config', function() -- cgit From 094b297a3b33f6336a825818bfd6a53b25273ac8 Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:06:47 +0000 Subject: feat(api): nvim_win_set_config can move split to other tp as floatwin Problem: not possible for nvim_win_set_config to convert a split to a floatwin, then move it to another tabpage in one call. Solution: allow it. --- test/functional/api/window_spec.lua | 96 +++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 35 deletions(-) (limited to 'test/functional/api/window_spec.lua') diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index d75dc78142..9937c5270b 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -2585,15 +2585,17 @@ describe('API/win', function() }, }, layout) - -- converting split into a float for a different tabpage is not yet supported - eq( - 'Cannot configure split into float in another tabpage', - pcall_err( - api.nvim_win_set_config, - win, - { relative = 'editor', row = 0, col = 0, width = 1, height = 1, win = first_win } - ) + -- directly convert split into a float for a different tabpage + local win2 = api.nvim_open_win(0, true, { split = 'below' }) + eq('', api.nvim_win_get_config(win2).relative) + api.nvim_win_set_config( + win2, + { relative = 'editor', row = 0, col = 0, width = 1, height = 1, win = first_win } ) + eq(first_tab, api.nvim_win_get_tabpage(win2)) + eq('editor', api.nvim_win_get_config(win2).relative) + eq({ first_win, win2 }, api.nvim_tabpage_list_wins(first_tab)) + eq({ tab2_win, win }, api.nvim_tabpage_list_wins(new_tab)) -- convert new win to float in new tabpage api.nvim_win_set_config(win, { relative = 'editor', row = 2, col = 2, height = 2, width = 2 }) @@ -2601,10 +2603,23 @@ describe('API/win', function() -- move to other tabpage api.nvim_win_set_config(win, { win = first_win }) eq(first_tab, api.nvim_win_get_tabpage(win)) - eq({ first_win, win }, api.nvim_tabpage_list_wins(first_tab)) + eq({ first_win, win, win2 }, api.nvim_tabpage_list_wins(first_tab)) eq({ tab2_win }, api.nvim_tabpage_list_wins(new_tab)) -- unlike splits, negative win is invalid eq('Invalid window id: -1', pcall_err(api.nvim_win_set_config, win, { win = -1 })) + + -- can't convert only window in other tabpage to float + command('tabnew') + local only_win = api.nvim_get_current_win() + command('tabprevious') + eq( + 'Cannot change last window into float', + pcall_err( + api.nvim_win_set_config, + only_win, + { relative = 'editor', width = 5, height = 5, row = 0, col = 0 } + ) + ) end) it('correctly moves curwin when moving curwin to a different tabpage', function() @@ -2773,7 +2788,6 @@ describe('API/win', function() it('messing with "win" or "parent" when moving "win" to other tabpage', function() command('split | tabnew') - local t2 = api.nvim_get_current_tabpage() local t2_win1 = api.nvim_get_current_win() command('split') local t2_win2 = api.nvim_get_current_win() @@ -2818,23 +2832,19 @@ describe('API/win', function() eq('', api.nvim_win_get_config(0).relative) eq(cur_win, api.nvim_get_current_win()) - -- Try to make "parent" floating. This should give the same error as before, but because - -- changing a split from another tabpage into a float isn't supported yet, check for that - -- error instead for now. - -- Use ":silent!" to avoid the one second delay from printing the error message. + -- Try to make "parent" floating. This should give the same error as before. exec(([[ - autocmd WinLeave * ++once silent! + autocmd WinLeave * ++once \ call nvim_win_set_config(%d, #{relative:'editor', row:0, col:0, width:5, height:5}) ]]):format(t2_win3)) cur_win = api.nvim_get_current_win() - api.nvim_win_set_config(0, { win = t2_win3, split = 'left' }) - matches( - 'Cannot change window from different tabpage into float$', - api.nvim_get_vvar('errmsg') + eq( + 'Floating state of windows to split changed', + pcall_err(api.nvim_win_set_config, 0, { win = t2_win3, split = 'left' }) ) - -- The error doesn't abort moving the window (or maybe it should, if that's wanted?) - neq(cur_win, api.nvim_get_current_win()) - eq(t2, api.nvim_win_get_tabpage(cur_win)) + eq('editor', api.nvim_win_get_config(t2_win3).relative) + eq('', api.nvim_win_get_config(0).relative) + eq(cur_win, api.nvim_get_current_win()) end) it('expected autocmds when moving window to other tabpage', function() @@ -3228,6 +3238,35 @@ describe('API/win', function() api.nvim_win_set_config(t2_cur_win, { split = 'left', win = 0 }) eq(t2_alt_win, api.nvim_tabpage_get_win(t2)) eq(t1, api.nvim_win_get_tabpage(t2_cur_win)) + + -- Very fun: move curwin between tabpages, converting from split to float, but with an autocmd + -- that deletes altwin after we're bumped to it, re-enters curwin, then switches to a 3rd + -- tabpage. tp_curwin of the window's old tabpage shouldn't be set to the freed altwin! + command('tablast | tab split | tabprevious | split') + command('autocmd WinEnter * ++once quit | let expect_alt = win_getid() | wincmd p | tabnext') + api.nvim_win_set_config(0, { + relative = 'editor', + win = api.nvim_tabpage_get_win(t1), + row = 0, + col = 0, + width = 5, + height = 5, + }) + eq(eval('g:expect_alt'), api.nvim_tabpage_get_win(t2)) + + -- Same, but for float -> float. + command('tabprevious | split') + api.nvim_open_win(0, true, { relative = 'editor', row = 0, col = 0, width = 1, height = 1 }) + command('autocmd WinEnter * ++once quit | let expect_alt = win_getid() | wincmd p | tabnext') + api.nvim_win_set_config(0, { + relative = 'editor', + win = api.nvim_tabpage_get_win(t1), + row = 0, + col = 0, + width = 5, + height = 5, + }) + eq(eval('g:expect_alt'), api.nvim_tabpage_get_win(t2)) end) it('set_config cannot change "noautocmd" #36409', function() @@ -3742,7 +3781,6 @@ describe('API/win', function() it('preserve current floating window when moving fails', function() local buf = api.nvim_create_buf(false, true) - local tab1_win = api.nvim_get_current_win() local float_win = api.nvim_open_win(buf, true, { relative = 'editor', row = 1, @@ -3770,18 +3808,6 @@ describe('API/win', function() ) eq(float_win, api.nvim_get_current_win()) - command( - ('autocmd WinLeave * ++once call nvim_win_set_config(%d, #{split: "left", win: %d})'):format( - float_win, - tab1_win - ) - ) - eq( - ('Window %d was made non-floating'):format(float_win), - pcall_err(api.nvim_win_set_config, float_win, { win = tab3_win }) - ) - eq(float_win, api.nvim_get_current_win()) - -- Need multigrid for external windows. Screen.new(20, 9, { ext_multigrid = true }) api.nvim_win_set_config(float_win, { external = true, width = 5, height = 5 }) -- cgit From 853eea859fd31e7ffcbe6d09e1281f77f279f7a0 Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:31:37 +0000 Subject: 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? --- test/functional/api/window_spec.lua | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'test/functional/api/window_spec.lua') diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 9937c5270b..f7a50bfa02 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -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 @ 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() -- cgit From 3115e3d0d11e0e02ba9040ac1a7bb032afa762bb Mon Sep 17 00:00:00 2001 From: Sean Dewar <6256228+seandewar@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:52:01 +0000 Subject: fix(api): improve external window validation Problem: "win" is allowed in external window configs in some cases. External window converted to normal float can't move tabpages in one nvim_win_set_config call. External window can't be turned into a normal split. Solution: disallow setting "win" for external windows. Allow external window to move tabpages, which turns it non-external. Allow external window to be turned into a (non-external) split. parse_win_config has more validation issues from not considering the window's existing config enough (not from this PR). For example, zindex can be set for an existing split if "split"/"vertical" isn't given, despite intending for that to be an error. Plus the logic is confusing. It could do with a refactor at some point... --- test/functional/api/window_spec.lua | 47 ++++++++++++++----------------------- 1 file changed, 18 insertions(+), 29 deletions(-) (limited to 'test/functional/api/window_spec.lua') diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index f7a50bfa02..bd7189e084 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -3354,6 +3354,24 @@ describe('API/win', function() | ]]) end) + + it('can convert external window to non-external', function() + Screen.new(20, 7, { ext_multigrid = true }) -- multigrid needed for external windows + api.nvim_open_win(0, true, { external = true, width = 5, height = 5 }) + eq(true, api.nvim_win_get_config(0).external) + api.nvim_win_set_config(0, { split = 'below', win = fn.win_getid(1) }) + eq(false, api.nvim_win_get_config(0).external) + + api.nvim_win_set_config(0, { external = true, width = 5, height = 5 }) + eq(true, api.nvim_win_get_config(0).external) + api.nvim_win_set_config(0, { relative = 'editor', row = 3, col = 3 }) + eq(false, api.nvim_win_get_config(0).external) + + api.nvim_win_set_config(0, { external = true, width = 5, height = 5 }) + eq(true, api.nvim_win_get_config(0).external) + api.nvim_win_set_config(0, { external = false }) + eq(false, api.nvim_win_get_config(0).external) + end) end) describe('get_config', function() @@ -3841,35 +3859,6 @@ describe('API/win', function() pcall_err(api.nvim_win_set_config, float_win, { win = tab3_win }) ) eq(float_win, api.nvim_get_current_win()) - - -- Need multigrid for external windows. - Screen.new(20, 9, { ext_multigrid = true }) - api.nvim_win_set_config(float_win, { external = true, width = 5, height = 5 }) - eq(true, api.nvim_win_get_config(float_win).external) - eq( - ('Cannot move external window to another tabpage'):format(float_win), - pcall_err( - api.nvim_win_set_config, - float_win, - { relative = 'win', win = tab3_win, row = 0, col = 0 } - ) - ) - eq(float_win, api.nvim_get_current_win()) - - -- Error if made external by autocommand when attempting to move. - api.nvim_win_set_config( - float_win, - { relative = 'editor', row = 0, col = 0, width = 5, height = 5 } - ) - eq(false, api.nvim_win_get_config(float_win).external) - command( - ('autocmd WinLeave * ++once call nvim_win_set_config(%d, #{external: 1})'):format(float_win) - ) - eq( - ('Cannot move external window to another tabpage'):format(float_win), - pcall_err(api.nvim_win_set_config, float_win, { win = tab3_win }) - ) - eq(float_win, api.nvim_get_current_win()) end) end) end) -- cgit