aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2026-03-14 17:44:06 -0400
committerGitHub <noreply@github.com>2026-03-14 17:44:06 -0400
commit8c2b6d6062cbfe01acd8cfc396dab56bac09253e (patch)
tree12bf4e87e36da984a3d3168046a330aac671a860
parent0a9d6dc9aae61374ed6ec91c68e05b27e19cb108 (diff)
parent3115e3d0d11e0e02ba9040ac1a7bb032afa762bb (diff)
downloadrneovim-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.txt8
-rw-r--r--runtime/doc/news.txt1
-rw-r--r--runtime/lua/vim/_meta/api.lua5
-rw-r--r--src/nvim/api/win_config.c264
-rw-r--r--src/nvim/arglist.c2
-rw-r--r--src/nvim/buffer.c2
-rw-r--r--src/nvim/ex_getln.c2
-rw-r--r--src/nvim/statusline.c2
-rw-r--r--src/nvim/window.c35
-rw-r--r--src/nvim/winfloat.c40
-rw-r--r--test/functional/api/window_spec.lua312
-rw-r--r--test/functional/ui/float_spec.lua514
-rw-r--r--test/functional/ui/messages_spec.lua3
-rw-r--r--test/functional/ui/screen.lua11
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