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" @@ -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; } @@ -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_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 +2650,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 + 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() @@ -2752,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() @@ -2797,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() @@ -2834,18 +2865,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, { win = tab1_win1 }) + eq({}, eval('g:result')) end) it('checks if splitting disallowed', function() @@ -3120,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') @@ -3132,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() @@ -3190,6 +3272,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() @@ -3203,6 +3314,64 @@ 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) + + 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() @@ -3542,20 +3711,25 @@ 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, { win = other_tp_win }) 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() @@ -3656,5 +3830,35 @@ 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 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, { win = tab2_win }) + ) + eq(float_win, api.nvim_get_current_win()) + + command('tabnew') + local tab3_win = api.nvim_get_current_win() + 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, { win = tab3_win }) + ) + eq(float_win, api.nvim_get_current_win()) + end) end) end) diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index 15b7004f7e..c9453e2b32 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -3782,10 +3782,7 @@ describe('float window', function() it('API has proper error messages', function() local buf = api.nvim_create_buf(false, false) eq("Invalid key: 'bork'", pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, bork = true })) - eq( - "'win' key is only valid with relative='win' and relative=''", - pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, win = 0 }) - ) + eq("Must specify 'relative' or 'external' when creating a float", pcall_err(api.nvim_open_win, buf, false, { win = 0 })) eq( "floating windows cannot have 'vertical'", pcall_err(api.nvim_open_win, buf, false, { width = 20, height = 2, relative = 'editor', row = 0, col = 0, vertical = true }) @@ -3824,6 +3821,18 @@ describe('float window', function() ) eq("Must specify 'width'", pcall_err(api.nvim_open_win, buf, false, { relative = 'editor', row = 0, col = 0 })) eq("Must specify 'height'", pcall_err(api.nvim_open_win, buf, false, { relative = 'editor', row = 0, col = 0, width = 2 })) + + if multigrid then + eq( + "external window cannot have 'win'", + pcall_err(api.nvim_open_win, buf, false, { external = true, win = 0, width = 10, height = 10 }) + ) + api.nvim_open_win(buf, true, { external = true, width = 10, height = 10 }) + eq("external window cannot have 'win'", pcall_err(api.nvim_win_set_config, 0, { win = 0 })) + -- OK to include "win" if external window is also reconfigured to a normal float. + api.nvim_win_set_config(0, { relative = 'editor', win = 0, row = 0, col = 0, width = 5, height = 5 }) + eq('editor', api.nvim_win_get_config(0).relative) + end end) it('can be placed relative window or cursor', function() @@ -7758,12 +7767,11 @@ describe('float window', function() screen:expect { grid = [[ ## grid 1 - [2:----------------------------------------]|*5 - {5:[No Name] [+] }| + [2:----------------------------------------]|*6 [3:----------------------------------------]| ## grid 2 x | - {0:~ }|*4 + {0:~ }|*5 ## grid 3 | ## grid 4 @@ -11191,6 +11199,19 @@ describe('float window', function() local winid = api.nvim_open_win(buf, false, config) api.nvim_set_current_win(winid) eq('floating window cannot be relative to itself', pcall_err(api.nvim_win_set_config, winid, config)) + eq('floating window cannot be relative to itself', pcall_err(api.nvim_win_set_config, winid, { win = winid })) + -- Don't assume win=0 if no win given for existing relative=win float; so no error. + api.nvim_win_set_config(winid, { width = 7 }) + eq(7, api.nvim_win_get_config(winid).width) + -- Don't expect the error when configuring to something other than relative=win, as win=self + -- is fine in those cases. (though maybe pointless) Other errors might be expected, though. + eq('Cannot split a floating window', pcall_err(api.nvim_win_set_config, winid, { split = 'above', win = winid })) + eq('win', api.nvim_win_get_config(winid).relative) + api.nvim_win_set_config(winid, { relative = 'editor', win = winid, row = 3, col = 3 }) + eq('editor', api.nvim_win_get_config(winid).relative) + -- An error when configuring split into relative=win float. + command('split') + eq('floating window cannot be relative to itself', pcall_err(api.nvim_win_set_config, 0, config)) end) it('bufpos out of range', function() @@ -11869,6 +11890,485 @@ describe('float window', function() ]]) end end) + + it('redrawn after moving tabpages via nvim_win_set_config()', function() + local tab1_win = api.nvim_get_current_win() + fn.setline(1, 'hello') + command('tab split') + local tab2_win = api.nvim_get_current_win() + -- Schedule an UPD_NOT_VALID redraw, but in one event move the float out of curtab before it's + -- handled. Do not flush before then. + local float = exec_lua(function() + local float = vim.api.nvim_open_win(0, true, { relative = 'editor', width = 10, height = 5, row = 1, col = 1 }) + vim.api.nvim__redraw({ valid = false, flush = false }) + vim.api.nvim_win_set_config(float, { win = tab1_win }) + return float + end) + + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: }{10:2}{9:+ [No Name] }{3: + [No Name] }{5: }{9:X}| + [4:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + hello | + {0:~ }|*5 + ## grid 3 + | + ## grid 4 + ^hello | + {0:~ }|*4 + ]], + }) + else + screen:expect([[ + {9: }{10:2}{9:+ [No Name] }{3: + [No Name] }{5: }{9:X}| + ^hello | + {0:~ }|*4 + | + ]]) + end + + -- Importantly, want tabline redrawn and float's hl attribs to be correct here. + api.nvim_win_set_config(float, { win = 0 }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: + [No Name] }{3: }{11:2}{3:+ [No Name] }{5: }{9:X}| + [4:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + hello | + {0:~ }|*5 + ## grid 3 + | + ## grid 4 + ^hello | + {0:~ }|*4 + ## grid 5 + {1:hello }| + {2:~ }|*4 + ]], + float_pos = { + [5] = { 1002, 'NW', 1, 1, 1, true, 50, 1, 1, 1 }, + }, + }) + else + screen:expect([[ + {9: + [No Name] }{3: }{11:2}{3:+ [No Name] }{5: }{9:X}| + ^h{1:hello } | + {0:~}{2:~ }{0: }|*4 + | + ]]) + end + + -- Autocommand runs within the first tabpage, but won't refresh grids when switching to it due + -- to switch_win_noblock having no_display set. + command( + ('autocmd OptionSet rightleft ++once call nvim_win_set_config(%d, #{relative: "win", win: %d, row: 0, col: 0})'):format( + float, + tab1_win + ) + ) + api.nvim_set_option_value('rightleft', true, { win = tab1_win }) + -- Stale grids should not have caused issues with removing the float's grid. + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: }{10:2}{9:+ [No Name] }{3: + [No Name] }{5: }{9:X}| + [4:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + hello | + {0:~ }|*5 + ## grid 3 + | + ## grid 4 + ^hello | + {0:~ }|*4 + ## grid 5 (hidden) + {1:hello }| + {2:~ }|*4 + ]], + }) + else + screen:expect([[ + {9: }{10:2}{9:+ [No Name] }{3: + [No Name] }{5: }{9:X}| + ^hello | + {0:~ }|*4 + | + ]]) + end + + command('tabfirst') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {3: }{11:2}{3:+ [No Name] }{9: + [No Name] }{5: }{9:X}| + [2:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 + olle^h| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 (hidden) + hello | + {0:~ }|*4 + ## grid 5 + {1:hello }| + {2:~ }|*4 + ]], + float_pos = { + [5] = { 1002, 'NW', 2, 0, 0, true, 50, 1, 1, 0 }, + }, + }) + else + screen:expect([[ + {3: }{11:2}{3:+ [No Name] }{9: + [No Name] }{5: }{9:X}| + {1:hello } olle^h| + {2:~ }{0: ~}|*4 + | + ]]) + end + + -- Check tablines are redrawn even when moving floats between two non-current tabpages. + command('tabnew') + local tab3_win = api.nvim_get_current_win() + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: }{10:2}{9:+ No Name] }{3: [No Name] }{9: + [No Name] }{5: }{9:X}| + [6:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + olleh| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 (hidden) + hello | + {0:~ }|*4 + ## grid 5 (hidden) + {1:hello }| + {2:~ }|*4 + ## grid 6 + ^ | + {0: ~}|*4 + ]], + }) + else + screen:expect([[ + {9: }{10:2}{9:+ No Name] }{3: [No Name] }{9: + [No Name] }{5: }{9:X}| + ^ | + {0: ~}|*4 + | + ]]) + end + + api.nvim_win_set_config(float, { win = tab2_win }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: + [No Name] }{3: [No Name] }{9: }{10:2}{9:+ No Name] }{5: }{9:X}| + [6:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + olleh| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 (hidden) + hello | + {0:~ }|*4 + ## grid 5 (hidden) + {1:hello }| + {2:~ }|*4 + ## grid 6 + ^ | + {0: ~}|*4 + ]], + }) + else + screen:expect([[ + {9: + [No Name] }{3: [No Name] }{9: }{10:2}{9:+ No Name] }{5: }{9:X}| + ^ | + {0: ~}|*4 + | + ]]) + end + + -- Try converting a split to a float, then moving it to another tabpage in one call. + command('set norightleft | new') + fn.setline(1, 'floaty mcfloatface') + api.nvim_win_set_config(0, { relative = 'editor', win = tab1_win, row = 3, col = 3, width = 15, height = 5 }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: }{10:2}{9:+ No Name] }{3: [No Name] }{9: }{10:2}{9:+ No Name] }{5: }{9:X}| + [6:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + olleh| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 (hidden) + hello | + {0:~ }|*4 + ## grid 5 (hidden) + {1:hello }| + {2:~ }|*4 + ## grid 6 + ^ | + {0:~ }|*4 + ## grid 7 (hidden) + floaty mcfloatface | + {0:~ }| + ]], + }) + else + screen:expect([[ + {9: }{10:2}{9:+ No Name] }{3: [No Name] }{9: }{10:2}{9:+ No Name] }{5: }{9:X}| + ^ | + {0:~ }|*4 + | + ]]) + end + + command('tabfirst') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {3: }{11:2}{3:+ No Name] }{9: [No Name] }{10:2}{9:+ No Name] }{5: }{9:X}| + [2:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 + olle^h| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 (hidden) + hello | + {0:~ }|*4 + ## grid 5 (hidden) + {1:hello }| + {2:~ }|*4 + ## grid 6 (hidden) + | + {0:~ }|*4 + ## grid 7 + {1:floaty mcfloatf}| + {1:ace }| + {2:~ }|*3 + ]], + float_pos = { + [7] = { 1004, 'NW', 1, 3, 3, true, 50, 1, 1, 3 }, + }, + }) + else + screen:expect([[ + {3: }{11:2}{3:+ No Name] }{9: [No Name] }{10:2}{9:+ No Name] }{5: }{9:X}| + {1:floaty mcfloatf} olle^h| + {0: }{1:ace }{0: ~}| + {0: }{2:~ }{0: ~}|*3 + | + ]]) + end + + -- Works when doing the same between two non-current tabpages. + local float2 = api.nvim_open_win(0, false, { split = 'below', win = tab3_win }) + api.nvim_win_set_config(float2, { relative = 'win', win = tab2_win, row = 2, col = 7, width = 4, height = 4, border = 'single' }) + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {3: }{11:2}{3:+ No Name] }{9: [No Name] }{10:3}{9:+ No Name] }{5: }{9:X}| + [2:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 + olle^h| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 (hidden) + hello | + {0:~ }|*4 + ## grid 5 (hidden) + {1:hello }| + {2:~ }|*4 + ## grid 6 (hidden) + | + {0:~ }|*4 + ## grid 7 + {1:floaty mcfloatf}| + {1:ace }| + {2:~ }|*3 + ]], + float_pos = { + [7] = { 1004, 'NW', 1, 3, 3, true, 50, 1, 1, 3 }, + }, + }) + else + screen:expect([[ + {3: }{11:2}{3:+ No Name] }{9: [No Name] }{10:3}{9:+ No Name] }{5: }{9:X}| + {1:floaty mcfloatf} olle^h| + {0: }{1:ace }{0: ~}| + {0: }{2:~ }{0: ~}|*3 + | + ]]) + end + + command('tabnext') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: }{10:2}{9:+ No Name] }{3: [No Name] }{9: }{10:3}{9:+ No Name] }{5: }{9:X}| + [6:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + olleh| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 (hidden) + hello | + {0:~ }|*4 + ## grid 5 (hidden) + {1:hello }| + {2:~ }|*4 + ## grid 6 + ^ | + {0:~ }|*4 + ## grid 7 (hidden) + {1:floaty mcfloatf}| + {1:ace }| + {2:~ }|*3 + ]], + }) + else + screen:expect([[ + {9: }{10:2}{9:+ No Name] }{3: [No Name] }{9: }{10:3}{9:+ No Name] }{5: }{9:X}| + ^ | + {0:~ }|*4 + | + ]]) + end + + command('tabnext') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: }{10:2}{9:+ No Name] [No Name] }{3: }{11:3}{3:+ No Name] }{5: }{9:X}| + [4:----------------------------------------]|*5 + [3:----------------------------------------]| + ## grid 2 (hidden) + olleh| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 + ^hello | + {0:~ }|*4 + ## grid 5 + {1:hello }| + {2:~ }|*4 + ## grid 6 (hidden) + | + {0:~ }|*4 + ## grid 7 (hidden) + {1:floaty mcfloatf}| + {1:ace }| + {2:~ }|*3 + ## grid 8 + {5:┌────┐}| + {5:│}{1:lleh}{5:│}| + {5:│}{1: o}{5:│}| + {5:│}{2: ~}{5:│}|*2 + {5:└────┘}| + ]], + float_pos = { + [5] = { 1002, 'NW', 4, 0, 0, true, 50, 1, 1, 0 }, + [8] = { 1005, 'NW', 4, 2, 7, true, 50, 2, 0, 7 }, + }, + }) + else + screen:expect([[ + {9: }{10:2}{9:+ No }{5:┌────┐}{9: [No Name] }{3: }{11:3}{3:+ No Name] }{5: }{9:X}| + {1:^hello }{5:│}{1:lleh}{5:│} | + {2:~ }{5:│}{1: o}{5:│}{0: }| + {2:~ }{5:│}{2: ~}{5:│}{0: }|*2 + {2:~ }{5:└────┘}{0: }| + | + ]]) + end + + -- Used relative=win on two floats relative to this window. + -- Split it to the right to ensure both follow. + command('topleft vsplit') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + {9: }{10:2}{9:+ No Name] [No Name] }{3: }{11:4}{3:+ No Name] }{5: }{9:X}| + [9:--------------------]{5:│}[4:-------------------]|*4 + {4:[No Name] [+] }{5:[No Name] [+] }| + [3:----------------------------------------]| + ## grid 2 (hidden) + olleh| + {0: ~}|*4 + ## grid 3 + | + ## grid 4 + hello | + {0:~ }|*3 + ## grid 5 + {1:hello }| + {2:~ }|*4 + ## grid 6 (hidden) + | + {0:~ }|*4 + ## grid 7 (hidden) + {1:floaty mcfloatf}| + {1:ace }| + {2:~ }|*3 + ## grid 8 + {5:┌────┐}| + {5:│}{1:lleh}{5:│}| + {5:│}{1: o}{5:│}| + {5:│}{2: ~}{5:│}|*2 + {5:└────┘}| + ## grid 9 + ^hello | + {0:~ }|*3 + ]], + float_pos = { + [5] = { 1002, 'NW', 4, 0, 0, true, 50, 1, 1, 21 }, + [8] = { 1005, 'NW', 4, 2, 7, true, 50, 2, 0, 28 }, + }, + }) + else + screen:expect([[ + {9: }{10:2}{9:+ No Name] [No Name] }{3: }{11:4}{3:+ }{5:┌────┐}{3:e] }{5: }{9:X}| + ^hello {5:│}{1:hello }{5:│}{1:lleh}{5:│} | + {0:~ }{5:│}{2:~ }{5:│}{1: o}{5:│}{0: }| + {0:~ }{5:│}{2:~ }{5:│}{2: ~}{5:│}{0: }|*2 + {4:[No Name] [+] }{2:~ }{5:└────┘ }| + | + ]]) + end + end) end describe('with ext_multigrid and actual mouse grid', function() diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index 4159563fcf..223088b3e8 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -2346,8 +2346,7 @@ describe('ui/ext_messages', function() screen:expect([[ | {1:~ }{4:^ }{1: }| - {1:~ }|*21 - {2:[No Name] }| + {1:~ }|*22 ]]) end) diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index d9501b8400..d60992974a 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -1795,9 +1795,14 @@ local function fmt_ext_state(name, state) elseif name == 'float_pos' then local str = '{\n' for k, v in pairs(state) do - str = str .. ' [' .. k .. '] = {' .. v[1] - for i = 2, #v do - str = str .. ', ' .. inspect(v[i]) + str = str .. ' [' .. k .. '] = {' + if v.external then + str = str .. ' external = true ' + else + str = str .. v[1] + for i = 2, #v do + str = str .. ', ' .. inspect(v[i]) + end end str = str .. '};\n' end