From e2a0748cb28fb44f6ee42f7470ef50a9dda6e320 Mon Sep 17 00:00:00 2001 From: glepnir Date: Fri, 5 Sep 2025 13:35:27 +0800 Subject: [PATCH] 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> --- src/nvim/api/win_config.c | 111 ++++++++++++++++++++-- test/functional/api/window_spec.lua | 140 +++++++++++++++++++++------- 2 files changed, 209 insertions(+), 42 deletions(-) diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 55e089b133..21b1c98e7f 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -32,6 +32,7 @@ #include "nvim/syntax.h" #include "nvim/types_defs.h" #include "nvim/ui.h" +#include "nvim/ui_compositor.h" #include "nvim/ui_defs.h" #include "nvim/vim_defs.h" #include "nvim/window.h" @@ -381,6 +382,24 @@ static int win_split_flags(WinSplit split, bool toplevel) return flags; } +static bool can_move(win_T *wp, bool switch_tab, Error *err) +{ + if (is_aucmd_win(wp) && switch_tab) { + api_set_error(err, kErrorTypeException, "Cannot move autocmd window to another tabpage"); + return false; + } + // Can't move the cmdwin or its old curwin to a different tabpage. + if ((wp == cmdwin_win || wp == cmdwin_old_curwin) && switch_tab) { + api_set_error(err, kErrorTypeException, "%s", e_cmdwin); + return false; + } + if (wp->w_floating && wp->w_config.external) { + api_set_error(err, kErrorTypeException, "%s", "Cannot move external floating window"); + return false; + } + return true; +} + static bool win_config_split(win_T *win, Dict(win_config) *config, WinConfig *fconfig, Error *err) FUNC_ATTR_NONNULL_ALL { @@ -423,13 +442,7 @@ static bool win_config_split(win_T *win, Dict(win_config) *config, WinConfig *fc api_set_error(err, kErrorTypeException, "Cannot split a floating window"); return false; } - if (is_aucmd_win(win) && win_tp != parent_tp) { - api_set_error(err, kErrorTypeException, "Cannot move autocmd window to another tabpage"); - return false; - } - // Can't move the cmdwin or its old curwin to a different tabpage. - if ((win == cmdwin_win || win == cmdwin_old_curwin) && win_tp != parent_tp) { - api_set_error(err, kErrorTypeException, "%s", e_cmdwin); + if (!can_move(win, win_tp != parent_tp, err)) { return false; } } @@ -652,6 +665,24 @@ void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err) return; } + win_T *parent = NULL; + tabpage_T *parent_tp = NULL; + tabpage_T *win_tp = win_find_tabpage(win); + bool curwin_moving_tp = false; + if (config->win == 0) { + parent = curwin; + parent_tp = curtab; + } else if (config->win > 0) { + parent = find_window_by_handle(fconfig.window, err); + if (!parent) { + return; + } + parent_tp = win_find_tabpage(parent); + if (!parent_tp) { + return; + } + } + if (was_split && !to_split) { if (!win_new_float(win, false, fconfig, err)) { return; @@ -662,6 +693,64 @@ void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err) return; } } else { + if (win->w_floating && HAS_KEY_X(config, win) && parent && parent_tp != win_tp) { + if (!can_move(win, win_tp != parent_tp, err)) { + return; + } + if (win == curwin) { + curwin_moving_tp = true; + win_goto(win_float_find_altwin(win, NULL)); + + // Autocommands may have been a real nuisance and messed things up... + if (curwin == win) { + api_set_error(err, kErrorTypeException, "Failed to switch away from window %d", + win->handle); + return; + } + win_tp = win_find_tabpage(win); + parent_tp = win_find_tabpage(parent); + if (!win_tp || !parent_tp) { + api_set_error(err, kErrorTypeException, "Target windows were closed"); + goto restore_curwin; + } + if (!win->w_floating) { + api_set_error(err, kErrorTypeException, "Window %d was made non-floating", + win->handle); + goto restore_curwin; + } + if (!can_move(win, win_tp != parent_tp, err)) { + goto restore_curwin; + } + } + + if (win_tp != parent_tp) { + win_remove(win, win_tp == curtab ? NULL : win_tp); + win_T *target_after; + if (parent_tp == curtab) { + target_after = lastwin_nofloating(); + } else { + target_after = parent_tp->tp_lastwin; + while (target_after->w_floating) { + target_after = target_after->w_prev; + } + } + + win_append(target_after, win, parent_tp == curtab ? NULL : parent_tp); + // If `win` was the curwin of its old tabpage, select a new curwin for it. + if (win_tp != curtab && win_tp->tp_curwin == win) { + win_tp->tp_curwin = win_float_find_altwin(win, win_tp); + } + + if (parent_tp != curtab) { + if (ui_has(kUIMultigrid)) { + ui_call_win_hide(win->w_grid_alloc.handle); + } + ui_comp_remove_grid(&win->w_grid_alloc); + } else { + win->w_hl_needs_update = true; + } + } + } win_config_float(win, fconfig); } @@ -675,6 +764,14 @@ void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err) } else if (win == cmdline_win && fconfig._cmdline_offset == INT_MAX) { cmdline_win = NULL; } + return; + +restore_curwin: + // If `win` was the original curwin, and autocommands didn't move it outside of curtab, be a + // good citizen and try to return to it. + if (curwin_moving_tp && win_valid(win)) { + win_goto(win); + } #undef HAS_KEY_X } 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)