diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2026-03-14 17:44:06 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-14 17:44:06 -0400 |
| commit | 8c2b6d6062cbfe01acd8cfc396dab56bac09253e (patch) | |
| tree | 12bf4e87e36da984a3d3168046a330aac671a860 | |
| parent | 0a9d6dc9aae61374ed6ec91c68e05b27e19cb108 (diff) | |
| parent | 3115e3d0d11e0e02ba9040ac1a7bb032afa762bb (diff) | |
| download | rneovim-8c2b6d6062cbfe01acd8cfc396dab56bac09253e.tar.gz rneovim-8c2b6d6062cbfe01acd8cfc396dab56bac09253e.tar.bz2 rneovim-8c2b6d6062cbfe01acd8cfc396dab56bac09253e.zip | |
Merge #35816 nvim_win_set_config can move floatwin to another tabpage
| -rw-r--r-- | runtime/doc/api.txt | 8 | ||||
| -rw-r--r-- | runtime/doc/news.txt | 1 | ||||
| -rw-r--r-- | runtime/lua/vim/_meta/api.lua | 5 | ||||
| -rw-r--r-- | src/nvim/api/win_config.c | 264 | ||||
| -rw-r--r-- | src/nvim/arglist.c | 2 | ||||
| -rw-r--r-- | src/nvim/buffer.c | 2 | ||||
| -rw-r--r-- | src/nvim/ex_getln.c | 2 | ||||
| -rw-r--r-- | src/nvim/statusline.c | 2 | ||||
| -rw-r--r-- | src/nvim/window.c | 35 | ||||
| -rw-r--r-- | src/nvim/winfloat.c | 40 | ||||
| -rw-r--r-- | test/functional/api/window_spec.lua | 312 | ||||
| -rw-r--r-- | test/functional/ui/float_spec.lua | 514 | ||||
| -rw-r--r-- | test/functional/ui/messages_spec.lua | 3 | ||||
| -rw-r--r-- | test/functional/ui/screen.lua | 11 |
14 files changed, 1024 insertions, 177 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 318ba96605..3813792418 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3921,9 +3921,11 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* Default is `"left"`. • vertical: Split vertically |:vertical|. • width: Window width (in character cells). Minimum of 1. - • win: |window-ID| window to split, or relative window when - creating a float (relative="win"). When splitting, - negative value works like |:topleft|, |:botright|. + • win: |window-ID| target window. Can be in a different tab + page. Determines the window to split (negative values act + like |:topleft|, |:botright|), the relative window for a + `relative="win"` float, or just the target tab page + (inferred from the window) for others. • zindex: Stacking order. floats with higher `zindex` go on top on floats with lower indices. Must be larger than zero. The following screen elements have hard-coded diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index e8c52a8ca9..7e5f7565b1 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -167,6 +167,7 @@ API they were so specified in `nvim_create_user_command()`. • |nvim_open_win()| floating windows can show a 'statusline'. Plugins can use `style='minimal'` or `:setlocal statusline=` to hide the statusline. +• |nvim_win_set_config()| can move windows to other tab pages as floats. • Added experimental |nvim__exec_lua_fast()| to allow remote API clients to execute code while nvim is blocking for input. • |vim.secure.trust()| accepts `path` for the `allow` action. diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index 975db4ac3b..0510e2bef9 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -1848,8 +1848,9 @@ function vim.api.nvim_open_term(buffer, opts) end --- Default is `"left"`. --- - vertical: Split vertically `:vertical`. --- - width: Window width (in character cells). Minimum of 1. ---- - win: `window-ID` window to split, or relative window when creating a float (relative="win"). ---- When splitting, negative value works like `:topleft`, `:botright`. +--- - win: `window-ID` target window. Can be in a different tab page. Determines the window to +--- split (negative values act like `:topleft`, `:botright`), the relative window for a +--- `relative="win"` float, or just the target tab page (inferred from the window) for others. --- - zindex: Stacking order. floats with higher `zindex` go on top on --- floats with lower indices. Must be larger than zero. The --- following screen elements have hard-coded z-indices: diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 55e089b133..921a622c6e 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -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" @@ -32,6 +34,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" @@ -39,6 +42,8 @@ #include "api/win_config.c.generated.h" +#define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) + /// Opens a new split window, floating window, or external window. /// /// - Specify `relative` to create a floating window. Floats are drawn over the split layout, @@ -177,8 +182,9 @@ /// Default is `"left"`. /// - vertical: Split vertically |:vertical|. /// - width: Window width (in character cells). Minimum of 1. -/// - win: |window-ID| window to split, or relative window when creating a float (relative="win"). -/// When splitting, negative value works like |:topleft|, |:botright|. +/// - win: |window-ID| target window. Can be in a different tab page. Determines the window to +/// split (negative values act like |:topleft|, |:botright|), the relative window for a +/// `relative="win"` float, or just the target tab page (inferred from the window) for others. /// - zindex: Stacking order. floats with higher `zindex` go on top on /// floats with lower indices. Must be larger than zero. The /// following screen elements have hard-coded z-indices: @@ -197,7 +203,6 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Error *err) FUNC_API_SINCE(6) FUNC_API_TEXTLOCK_ALLOW_CMDWIN { -#define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) buf_T *buf = find_buffer_by_handle(buffer, err); if (!buf) { return 0; @@ -348,7 +353,6 @@ cleanup: unblock_autocmds(); } return rv; -#undef HAS_KEY_X } static WinSplit win_split_dir(win_T *win) @@ -381,10 +385,56 @@ static int win_split_flags(WinSplit split, bool toplevel) return flags; } -static bool win_config_split(win_T *win, Dict(win_config) *config, WinConfig *fconfig, Error *err) +/// Checks if window `wp` can be moved to tabpage `tp`. +static bool win_can_move_tp(win_T *wp, tabpage_T *tp, Error *err) + FUNC_ATTR_NONNULL_ALL +{ + if (one_window(wp, tp == curtab ? NULL : tp)) { + 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; + } + // Can't move the cmdwin or its old curwin to a different tabpage. + if (wp == cmdwin_win || wp == cmdwin_old_curwin) { + api_set_error(err, kErrorTypeException, "%s", e_cmdwin); + return false; + } + return true; +} + +static win_T *win_find_altwin(win_T *win, tabpage_T *tp) + FUNC_ATTR_NONNULL_ALL +{ + if (win->w_floating) { + return win_float_find_altwin(win, tp == curtab ? NULL : tp); + } else { + int dir; + return winframe_find_altwin(win, &dir, tp == curtab ? NULL : tp, NULL); + } +} + +/// Configures `win` into a split, also moving it to another tabpage if requested. +static bool win_config_split(win_T *win, const Dict(win_config) *config, WinConfig *fconfig, + Error *err) FUNC_ATTR_NONNULL_ALL { -#define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) bool was_split = !win->w_floating; bool has_split = HAS_KEY_X(config, split); bool has_vertical = HAS_KEY_X(config, vertical); @@ -423,14 +473,8 @@ 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); - return false; + if (win_tp != parent_tp && !win_can_move_tp(win, win_tp, err)) { + return false; // error already set } } @@ -443,24 +487,13 @@ static bool win_config_split(win_T *win, Dict(win_config) *config, WinConfig *fc // window list or remove its frame (if non-floating), so it's valid for autocommands. const bool curwin_moving_tp = win == curwin && parent && win_tp != parent_tp; if (curwin_moving_tp) { - if (was_split) { - int dir; - win_T *altwin = winframe_find_altwin(win, &dir, NULL, NULL); - // Autocommands may still make this the last non-float after this check. - // That case will be caught later when trying to move the window. - if (!altwin) { - api_set_error(err, kErrorTypeException, "Cannot move last non-floating window"); - return false; - } - win_goto(altwin); - } else { - win_goto(win_float_find_altwin(win, NULL)); - } + win_T *altwin = win_find_altwin(win, win_tp); + assert(altwin); // win_can_move_tp ensures `win` is not the only window + win_goto(altwin); // 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); + api_set_error(err, kErrorTypeException, "Failed to switch away from window %d", win->handle); return false; } win_tp = win_find_tabpage(win); @@ -608,7 +641,98 @@ resize: } merge_win_config(&win->w_config, *fconfig); return true; -#undef HAS_KEY_X +} + +/// Configures `win` into a float, also moving it to another tabpage if requested. +static bool win_config_float_tp(win_T *win, const Dict(win_config) *config, + const WinConfig *fconfig, Error *err) + FUNC_ATTR_NONNULL_ALL +{ + tabpage_T *win_tp = win_find_tabpage(win); + win_T *parent = win; + tabpage_T *parent_tp = win_tp; + if (HAS_KEY_X(config, win)) { + parent = find_window_by_handle(fconfig->window, err); + if (!parent) { + return false; // error already set + } + parent_tp = win_find_tabpage(parent); + } + + bool curwin_moving_tp = false; + win_T *altwin = NULL; + + if (win_tp != parent_tp) { + if (!win_can_move_tp(win, win_tp, err)) { + return false; // error already set + } + altwin = win_find_altwin(win, win_tp); + assert(altwin); // win_can_move_tp ensures `win` is not the only window + + // If we are moving curwin to another tabpage, switch windows *before* we remove it from the + // window list or remove its frame (if non-floating), so it's valid for autocommands. + if (curwin == win) { + curwin_moving_tp = true; + win_goto(altwin); + + // 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 false; + } + 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_tp != parent_tp && !win_can_move_tp(win, win_tp, err)) { + goto restore_curwin; // error already set + } + altwin = win_find_altwin(win, win_tp); + assert(altwin); // win_can_move_tp ensures `win` is not the only window + } + } + + // Convert the window to a float if needed. + if (!win->w_floating) { + if (!win_new_float(win, false, *fconfig, err)) { +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); + } + return false; + } + redraw_later(win, UPD_NOT_VALID); + } + + if (win_tp != parent_tp) { + win_remove(win, win_tp == curtab ? NULL : win_tp); + tabpage_T *append_tp = parent_tp == curtab ? NULL : parent_tp; + win_append(lastwin_nofloating(append_tp), win, append_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 = altwin; + } + + // Remove grid if present. More reliable than checking curtab, as tabpage_check_windows may not + // run when temporarily switching tabpages, meaning grids may be stale from another tabpage! + // (e.g: switch_win_noblock with no_display=true) + ui_comp_remove_grid(&win->w_grid_alloc); + + // Redraw tabline, update window's hl attribs, etc. Set must_redraw here, as redraw_later might + // not if w_redr_type >= UPD_NOT_VALID was set in the old tabpage. + redraw_later(win, UPD_NOT_VALID); + set_must_redraw(UPD_NOT_VALID); + } + + win_config_float(win, *fconfig); + return true; } /// Reconfigures the layout and properties of a window. @@ -631,7 +755,6 @@ resize: void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err) FUNC_API_SINCE(6) { -#define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) win_T *win = find_window_by_handle(window, err); if (!win) { return; @@ -645,24 +768,21 @@ void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err) WinConfig fconfig = win->w_config; bool to_split = config->relative.size == 0 - && !(HAS_KEY_X(config, external) ? config->external : fconfig.external) + && !(HAS_KEY_X(config, external) && config->external) && (has_split || has_vertical || was_split); if (!parse_win_config(win, config, &fconfig, !was_split || to_split, err)) { return; } - if (was_split && !to_split) { - if (!win_new_float(win, false, fconfig, err)) { - return; - } - redraw_later(win, UPD_NOT_VALID); - } else if (to_split) { + if (to_split) { if (!win_config_split(win, config, &fconfig, err)) { return; } } else { - win_config_float(win, fconfig); + if (!win_config_float_tp(win, config, &fconfig, err)) { + return; + } } if (fconfig.style == kWinStyleMinimal && old_style != fconfig.style) { @@ -675,7 +795,6 @@ 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; } -#undef HAS_KEY_X } #define PUT_KEY_X(d, key, value) PUT_KEY(d, win_config, key, value) @@ -1144,7 +1263,6 @@ bool parse_winborder(WinConfig *fconfig, char *border_opt, Error *err) static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fconfig, bool reconf, Error *err) { -#define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) bool has_relative = false, relative_is_win = false, is_split = false; if (config->relative.size > 0) { if (!parse_float_relative(config->relative, &fconfig->relative)) { @@ -1167,6 +1285,7 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco } else if (!config->external) { if (HAS_KEY_X(config, vertical) || HAS_KEY_X(config, split)) { is_split = true; + fconfig->external = false; } else if (wp == NULL) { // new win api_set_error(err, kErrorTypeValidation, "Must specify 'relative' or 'external' when creating a float"); @@ -1258,36 +1377,6 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco goto fail; } - if (relative_is_win || is_split) { - if (reconf && relative_is_win) { - win_T *target_win = find_window_by_handle(config->win, err); - if (!target_win) { - goto fail; - } - - if (target_win == wp) { - api_set_error(err, kErrorTypeException, "floating window cannot be relative to itself"); - goto fail; - } - } - fconfig->window = curwin->handle; - if (HAS_KEY_X(config, win)) { - if (config->win > 0) { - fconfig->window = config->win; - } - } - } else if (HAS_KEY_X(config, win)) { - if (has_relative) { - api_set_error(err, kErrorTypeValidation, - "'win' key is only valid with relative='win' and relative=''"); - goto fail; - } else if (!is_split) { - api_set_error(err, kErrorTypeValidation, - "non-float with 'win' requires at least 'split' or 'vertical'"); - goto fail; - } - } - if (HAS_KEY_X(config, external)) { fconfig->external = config->external; if (has_relative && fconfig->external) { @@ -1301,6 +1390,38 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco } } + if (HAS_KEY_X(config, win) && fconfig->external) { + api_set_error(err, kErrorTypeValidation, "external window cannot have 'win'"); + goto fail; + } + if (relative_is_win || (HAS_KEY_X(config, win) && !is_split && wp && wp->w_floating + && fconfig->relative == kFloatRelativeWindow)) { + // When relative=win is given, missing win field means win=0. + win_T *target_win = find_window_by_handle(config->win, err); + if (!target_win) { + goto fail; + } + if (target_win == wp) { + api_set_error(err, kErrorTypeException, "floating window cannot be relative to itself"); + goto fail; + } + fconfig->window = target_win->handle; + } else { + // Handle is not validated here, as win_config_split can accept negative values. + if (HAS_KEY_X(config, win)) { + if (!is_split && !has_relative && (!wp || !wp->w_floating)) { + api_set_error(err, kErrorTypeValidation, + "non-float with 'win' requires at least 'split' or 'vertical'"); + goto fail; + } + fconfig->window = config->win; + } + // Resolve, but skip validating. E.g: win_config_split accepts negative "win". + if (fconfig->window == 0) { + fconfig->window = curwin->handle; + } + } + if (HAS_KEY_X(config, focusable)) { fconfig->focusable = config->focusable; fconfig->mouse = config->focusable; @@ -1422,5 +1543,4 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco fail: merge_win_config(fconfig, wp != NULL ? wp->w_config : WIN_CONFIG_INIT); return false; -#undef HAS_KEY_X } diff --git a/src/nvim/arglist.c b/src/nvim/arglist.c index 1d5903aefc..7d12c92555 100644 --- a/src/nvim/arglist.c +++ b/src/nvim/arglist.c @@ -1116,7 +1116,7 @@ static void do_arg_all(int count, int forceit, int keep_tabs) last_curwin = curwin; last_curtab = curtab; // lastwin may be aucmd_win - win_enter(lastwin_nofloating(), false); + win_enter(lastwin_nofloating(NULL), false); // Open up to "count" windows. arg_all_open_windows(&aall, count); diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 373cc6263b..94b670b6dc 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -3692,7 +3692,7 @@ void ex_buffer_all(exarg_T *eap) // Don't execute Win/Buf Enter/Leave autocommands here. autocmd_no_enter++; // lastwin may be aucmd_win - win_enter(lastwin_nofloating(), false); + win_enter(lastwin_nofloating(NULL), false); autocmd_no_leave++; for (buf_T *buf = firstbuf; buf != NULL && open_wins < count; buf = buf->b_next) { // Check if this buffer needs a window diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 08d01700d6..9433957762 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -4062,7 +4062,7 @@ void compute_cmdrow(void) if (exmode_active || msg_scrolled != 0) { cmdline_row = Rows - 1; } else { - win_T *wp = lastwin_nofloating(); + win_T *wp = lastwin_nofloating(NULL); cmdline_row = wp->w_winrow + wp->w_height + wp->w_hsep_height + wp->w_status_height + global_stl_height(); } diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index 005a52b675..82febeb6c5 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -440,7 +440,7 @@ void win_redr_winbar(win_T *wp) void redraw_ruler(void) { static int did_ruler_col = -1; - win_T *wp = curwin->w_status_height == 0 ? curwin : lastwin_nofloating(); + win_T *wp = curwin->w_status_height == 0 ? curwin : lastwin_nofloating(NULL); bool is_stl_global = global_stl_height() > 0; // Check if ruler should be drawn, clear if it was drawn before. diff --git a/src/nvim/window.c b/src/nvim/window.c index bdc3234532..da4f74fc76 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -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" @@ -144,11 +143,25 @@ bool frames_locked(void) /// 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; } @@ -505,7 +518,7 @@ newwindow: // cursor to bottom-right window case 'b': case Ctrl_B: - win_goto(lastwin_nofloating()); + win_goto(lastwin_nofloating(NULL)); break; // cursor to last accessed (previous) window @@ -1158,7 +1171,7 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir, frame_T *to_fl oldwin = firstwin; } else if (flags & WSP_BOT || curwin->w_floating) { // can't split float, use last nonfloating window instead - oldwin = lastwin_nofloating(); + oldwin = lastwin_nofloating(NULL); } else { oldwin = curwin; } @@ -4800,7 +4813,7 @@ static void tabpage_check_windows(tabpage_T *old_curtab) if (wp->w_floating) { if (wp->w_config.external) { win_remove(wp, old_curtab); - win_append(lastwin_nofloating(), wp, NULL); + win_append(lastwin_nofloating(NULL), wp, NULL); } else { ui_comp_remove_grid(&wp->w_grid_alloc); } @@ -6237,7 +6250,7 @@ static void frame_setheight(frame_T *curfrp, int height) if (curfrp->fr_width != Columns) { room_cmdline = 0; } else { - win_T *wp = lastwin_nofloating(); + win_T *wp = lastwin_nofloating(NULL); room_cmdline = Rows - (int)p_ch - global_stl_height() - (wp->w_winrow + wp->w_height + wp->w_hsep_height + wp->w_status_height); room_cmdline = MAX(room_cmdline, 0); @@ -7051,7 +7064,7 @@ void command_height(void) int old_p_ch = (int)curtab->tp_ch_used; // Find bottom frame with width of screen. - frame_T *frp = lastwin_nofloating()->w_frame; + frame_T *frp = lastwin_nofloating(NULL)->w_frame; while (frp->fr_width != Columns && frp->fr_parent != NULL) { frp = frp->fr_parent; } @@ -7804,9 +7817,11 @@ void win_ui_flush(bool validate) msg_ui_flush(); } -win_T *lastwin_nofloating(void) +/// @return last non-floating window in `tp`, or NULL for current tabpage. +win_T *lastwin_nofloating(tabpage_T *tp) { - win_T *res = lastwin; + assert(tp != curtab || !tp); + win_T *res = tp ? tp->tp_lastwin : lastwin; while (res->w_floating) { res = res->w_prev; } diff --git a/src/nvim/winfloat.c b/src/nvim/winfloat.c index 27d4b859d9..f222f6b17e 100644 --- a/src/nvim/winfloat.c +++ b/src/nvim/winfloat.c @@ -35,10 +35,9 @@ #include "winfloat.c.generated.h" -/// Create a new float. +/// Creates a new float, or transforms an existing window to a float. /// /// @param wp if NULL, allocate a new window, otherwise turn existing window into a float. -/// It must then already belong to the current tabpage! /// @param last make the window the last one in the window list. /// Only used when allocating the autocommand window. /// @param config must already have been validated! @@ -46,7 +45,7 @@ win_T *win_new_float(win_T *wp, bool last, WinConfig fconfig, Error *err) { if (wp == NULL) { tabpage_T *tp = NULL; - win_T *tp_last = last ? lastwin : lastwin_nofloating(); + win_T *tp_last = last ? lastwin : lastwin_nofloating(NULL); if (fconfig.window != 0) { assert(!last); win_T *parent_wp = find_window_by_handle(fconfig.window, err); @@ -57,10 +56,7 @@ win_T *win_new_float(win_T *wp, bool last, WinConfig fconfig, Error *err) if (!tp) { return NULL; } - tp_last = tp == curtab ? lastwin : tp->tp_lastwin; - while (tp_last->w_floating && tp_last->w_prev) { - tp_last = tp_last->w_prev; - } + tp_last = lastwin_nofloating(tp == curtab ? NULL : tp); } wp = win_alloc(tp_last, false); win_init(wp, curwin, 0); @@ -77,19 +73,19 @@ win_T *win_new_float(win_T *wp, bool last, WinConfig fconfig, Error *err) } else { assert(!last); assert(!wp->w_floating); - if (firstwin == wp && lastwin_nofloating() == wp) { - // last non-float - api_set_error(err, kErrorTypeException, - "Cannot change last window into float"); - return NULL; - } else if (!win_valid(wp)) { - api_set_error(err, kErrorTypeException, - "Cannot change window from different tabpage into float"); + tabpage_T *win_tp = win_find_tabpage(wp); + assert(win_tp); + if ((win_tp == curtab && firstwin == wp && lastwin_nofloating(NULL) == wp) + || (win_tp != curtab && win_tp->tp_firstwin == wp && lastwin_nofloating(win_tp) == wp)) { + api_set_error(err, kErrorTypeException, "Cannot change last window into float"); return NULL; } else if (cmdwin_win != NULL && !cmdwin_win->w_floating) { // cmdwin can't become the only non-float. Check for others. bool other_nonfloat = false; - for (win_T *wp2 = firstwin; wp2 != NULL && !wp2->w_floating; wp2 = wp2->w_next) { + FOR_ALL_WINDOWS_IN_TAB(wp2, win_tp) { + if (wp2->w_floating) { + break; + } if (wp2 != wp && wp2 != cmdwin_win) { other_nonfloat = true; break; @@ -100,12 +96,16 @@ win_T *win_new_float(win_T *wp, bool last, WinConfig fconfig, Error *err) return NULL; } } + tabpage_T *tp = win_tp == curtab ? NULL : win_tp; int dir; - winframe_remove(wp, &dir, NULL, NULL); + winframe_remove(wp, &dir, tp, NULL); XFREE_CLEAR(wp->w_frame); - win_comp_pos(); // recompute window positions - win_remove(wp, NULL); - win_append(lastwin_nofloating(), wp, NULL); + win_remove(wp, tp); + if (win_tp == curtab) { + last_status(false); // may need to remove last status line + win_comp_pos(); // recompute window positions + } + win_append(lastwin_nofloating(tp), wp, tp); } wp->w_floating = true; wp->w_status_height = wp->w_p_stl && *wp->w_p_stl != NUL diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 62cc514058..bd7189e084 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -2561,60 +2561,88 @@ 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 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' }) 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) + + -- 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 }) + api.nvim_set_current_tabpage(first_tab) + -- 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, 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() - 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_lis |