Merge pull request #27330 from seandewar/win_set_config-fixes

fix(api): various window-related function fixes

This is a big one!
This commit is contained in:
Sean Dewar
2024-03-09 22:32:20 +00:00
committed by GitHub
24 changed files with 1777 additions and 389 deletions

View File

@@ -3271,9 +3271,9 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()*
• footer_pos: Footer position. Must be set with `footer` • footer_pos: Footer position. Must be set with `footer`
option. Value can be one of "left", "center", or "right". option. Value can be one of "left", "center", or "right".
Default is `"left"`. Default is `"left"`.
• noautocmd: If true then no buffer-related autocommand • noautocmd: If true then autocommands triggered from
events such as |BufEnter|, |BufLeave| or |BufWinEnter| may setting the `buffer` to display are blocked (e.g:
fire from calling this function. |BufEnter|, |BufLeave|, |BufWinEnter|).
• fixed: If true when anchor is NW or SW, the float window • fixed: If true when anchor is NW or SW, the float window
would be kept fixed even if the window would be truncated. would be kept fixed even if the window would be truncated.
• hide: If true the floating window will be hidden. • hide: If true the floating window will be hidden.
@@ -3297,11 +3297,11 @@ nvim_win_get_config({window}) *nvim_win_get_config()*
Map defining the window configuration, see |nvim_open_win()| Map defining the window configuration, see |nvim_open_win()|
nvim_win_set_config({window}, {config}) *nvim_win_set_config()* nvim_win_set_config({window}, {config}) *nvim_win_set_config()*
Configures window layout. Currently only for floating and external windows Configures window layout. Cannot be used to move the last window in a
(including changing a split window to those layouts). tabpage to a different one.
When reconfiguring a floating window, absent option keys will not be When reconfiguring a window, absent option keys will not be changed.
changed. `row`/`col` and `relative` must be reconfigured together. `row`/`col` and `relative` must be reconfigured together.
Parameters: ~ Parameters: ~
• {window} Window handle, or 0 for current window • {window} Window handle, or 0 for current window

View File

@@ -8901,10 +8901,10 @@ win_screenpos({nr}) *win_screenpos()*
tabpage. tabpage.
win_splitmove({nr}, {target} [, {options}]) *win_splitmove()* win_splitmove({nr}, {target} [, {options}]) *win_splitmove()*
Move the window {nr} to a new split of the window {target}. Temporarily switch to window {target}, then move window {nr}
This is similar to moving to {target}, creating a new window to a new split adjacent to {target}.
using |:split| but having the same contents as window {nr}, and Unlike commands such as |:split|, no new windows are created
then closing {nr}. (the |window-ID| of window {nr} is unchanged after the move).
Both {nr} and {target} can be window numbers or |window-ID|s. Both {nr} and {target} can be window numbers or |window-ID|s.
Both must be in the current tab page. Both must be in the current tab page.

View File

@@ -502,35 +502,33 @@ horizontally split windows. CTRL-W H does it the other way around.
*CTRL-W_K* *CTRL-W_K*
CTRL-W K Move the current window to be at the very top, using the full CTRL-W K Move the current window to be at the very top, using the full
width of the screen. This works like closing the current width of the screen. This works like `:topleft split`, except
window and then creating another one with ":topleft split", it is applied to the current window and no new window is
except that the current window contents is used for the new created.
window.
*CTRL-W_J* *CTRL-W_J*
CTRL-W J Move the current window to be at the very bottom, using the CTRL-W J Move the current window to be at the very bottom, using the
full width of the screen. This works like closing the current full width of the screen. This works like `:botright split`,
window and then creating another one with ":botright split", except it is applied to the current window and no new window
except that the current window contents is used for the new is created.
window.
*CTRL-W_H* *CTRL-W_H*
CTRL-W H Move the current window to be at the far left, using the CTRL-W H Move the current window to be at the far left, using the
full height of the screen. This works like closing the full height of the screen. This works like
current window and then creating another one with `:vert topleft split`, except it is applied to the current
`:vert topleft split`, except that the current window contents window and no new window is created.
is used for the new window.
*CTRL-W_L* *CTRL-W_L*
CTRL-W L Move the current window to be at the far right, using the full CTRL-W L Move the current window to be at the far right, using the full
height of the screen. This works like closing the height of the screen. This works like `:vert botright split`,
current window and then creating another one with except it is applied to the current window and no new window
`:vert botright split`, except that the current window is created.
contents is used for the new window.
*CTRL-W_T* *CTRL-W_T*
CTRL-W T Move the current window to a new tab page. This fails if CTRL-W T Move the current window to a new tab page. This fails if
there is only one window in the current tab page. there is only one window in the current tab page.
This works like `:tab split`, except the previous window is
closed.
When a count is specified the new tab page will be opened When a count is specified the new tab page will be opened
before the tab page with this index. Otherwise it comes after before the tab page with this index. Otherwise it comes after
the current tab page. the current tab page.

View File

@@ -1718,9 +1718,9 @@ function vim.api.nvim_open_term(buffer, opts) end
--- • footer_pos: Footer position. Must be set with `footer` --- • footer_pos: Footer position. Must be set with `footer`
--- option. Value can be one of "left", "center", or "right". --- option. Value can be one of "left", "center", or "right".
--- Default is `"left"`. --- Default is `"left"`.
--- • noautocmd: If true then no buffer-related autocommand --- • noautocmd: If true then autocommands triggered from
--- events such as `BufEnter`, `BufLeave` or `BufWinEnter` may --- setting the `buffer` to display are blocked (e.g:
--- fire from calling this function. --- `BufEnter`, `BufLeave`, `BufWinEnter`).
--- • fixed: If true when anchor is NW or SW, the float window --- • fixed: If true when anchor is NW or SW, the float window
--- would be kept fixed even if the window would be truncated. --- would be kept fixed even if the window would be truncated.
--- • hide: If true the floating window will be hidden. --- • hide: If true the floating window will be hidden.
@@ -2223,11 +2223,11 @@ function vim.api.nvim_win_remove_ns(window, ns_id) end
--- @param buffer integer Buffer handle --- @param buffer integer Buffer handle
function vim.api.nvim_win_set_buf(window, buffer) end function vim.api.nvim_win_set_buf(window, buffer) end
--- Configures window layout. Currently only for floating and external windows --- Configures window layout. Cannot be used to move the last window in a
--- (including changing a split window to those layouts). --- tabpage to a different one.
--- ---
--- When reconfiguring a floating window, absent option keys will not be --- When reconfiguring a window, absent option keys will not be changed.
--- changed. `row`/`col` and `relative` must be reconfigured together. --- `row`/`col` and `relative` must be reconfigured together.
--- ---
--- @param window integer Window handle, or 0 for current window --- @param window integer Window handle, or 0 for current window
--- @param config vim.api.keyset.win_config Map defining the window configuration, see `nvim_open_win()` --- @param config vim.api.keyset.win_config Map defining the window configuration, see `nvim_open_win()`

View File

@@ -10598,10 +10598,10 @@ function vim.fn.win_move_statusline(nr, offset) end
--- @return any --- @return any
function vim.fn.win_screenpos(nr) end function vim.fn.win_screenpos(nr) end
--- Move the window {nr} to a new split of the window {target}. --- Temporarily switch to window {target}, then move window {nr}
--- This is similar to moving to {target}, creating a new window --- to a new split adjacent to {target}.
--- using |:split| but having the same contents as window {nr}, and --- Unlike commands such as |:split|, no new windows are created
--- then closing {nr}. --- (the |window-ID| of window {nr} is unchanged after the move).
--- ---
--- Both {nr} and {target} can be window numbers or |window-ID|s. --- Both {nr} and {target} can be window numbers or |window-ID|s.
--- Both must be in the current tab page. --- Both must be in the current tab page.

View File

@@ -146,7 +146,11 @@ void nvim_tabpage_set_win(Tabpage tabpage, Window win, Error *err)
} }
if (tp == curtab) { if (tp == curtab) {
win_enter(wp, true); try_start();
win_goto(wp);
if (!try_end(err) && curwin != wp) {
api_set_error(err, kErrorTypeException, "Failed to switch to window %d", win);
}
} else if (tp->tp_curwin != wp) { } else if (tp->tp_curwin != wp) {
tp->tp_prevwin = tp->tp_curwin; tp->tp_prevwin = tp->tp_curwin;
tp->tp_curwin = wp; tp->tp_curwin = wp;

View File

@@ -12,6 +12,7 @@
#include "nvim/ascii_defs.h" #include "nvim/ascii_defs.h"
#include "nvim/autocmd.h" #include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h" #include "nvim/autocmd_defs.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h" #include "nvim/buffer_defs.h"
#include "nvim/decoration.h" #include "nvim/decoration.h"
#include "nvim/decoration_defs.h" #include "nvim/decoration_defs.h"
@@ -198,9 +199,9 @@
/// - footer_pos: Footer position. Must be set with `footer` option. /// - footer_pos: Footer position. Must be set with `footer` option.
/// Value can be one of "left", "center", or "right". /// Value can be one of "left", "center", or "right".
/// Default is `"left"`. /// Default is `"left"`.
/// - noautocmd: If true then no buffer-related autocommand events such as /// - noautocmd: If true then autocommands triggered from setting the
/// |BufEnter|, |BufLeave| or |BufWinEnter| may fire from /// `buffer` to display are blocked (e.g: |BufEnter|, |BufLeave|,
/// calling this function. /// |BufWinEnter|).
/// - fixed: If true when anchor is NW or SW, the float window /// - fixed: If true when anchor is NW or SW, the float window
/// would be kept fixed even if the window would be truncated. /// would be kept fixed even if the window would be truncated.
/// - hide: If true the floating window will be hidden. /// - hide: If true the floating window will be hidden.
@@ -245,6 +246,10 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Err
} }
} }
if (!check_split_disallowed_err(parent ? parent : curwin, err)) {
return 0; // error already set
}
if (HAS_KEY_X(config, vertical) && !HAS_KEY_X(config, split)) { if (HAS_KEY_X(config, vertical) && !HAS_KEY_X(config, split)) {
if (config->vertical) { if (config->vertical) {
fconfig.split = p_spr ? kWinSplitRight : kWinSplitLeft; fconfig.split = p_spr ? kWinSplitRight : kWinSplitLeft;
@@ -254,18 +259,20 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Err
} }
int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER; int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER;
if (parent == NULL) { TRY_WRAP(err, {
wp = win_split_ins(0, flags, NULL, 0); if (parent == NULL || parent == curwin) {
} else { wp = win_split_ins(0, flags, NULL, 0, NULL);
tp = win_find_tabpage(parent); } else {
switchwin_T switchwin; tp = win_find_tabpage(parent);
// `parent` is valid in `tp`, so switch_win should not fail. switchwin_T switchwin;
const int result = switch_win(&switchwin, parent, tp, true); // `parent` is valid in `tp`, so switch_win should not fail.
(void)result; const int result = switch_win(&switchwin, parent, tp, true);
assert(result == OK); assert(result == OK);
wp = win_split_ins(0, flags, NULL, 0); (void)result;
restore_win(&switchwin, true); wp = win_split_ins(0, flags, NULL, 0, NULL);
} restore_win(&switchwin, true);
}
});
if (wp) { if (wp) {
wp->w_config = fconfig; wp->w_config = fconfig;
} }
@@ -273,21 +280,49 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Err
wp = win_new_float(NULL, false, fconfig, err); wp = win_new_float(NULL, false, fconfig, err);
} }
if (!wp) { if (!wp) {
api_set_error(err, kErrorTypeException, "Failed to create window"); if (!ERROR_SET(err)) {
api_set_error(err, kErrorTypeException, "Failed to create window");
}
return 0; return 0;
} }
// Autocommands may close `wp` or move it to another tabpage, so update and check `tp` after each
// event. In each case, `wp` should already be valid in `tp`, so switch_win should not fail.
// Also, autocommands may free the `buf` to switch to, so store a bufref to check.
bufref_T bufref;
set_bufref(&bufref, buf);
switchwin_T switchwin; switchwin_T switchwin;
if (switch_win_noblock(&switchwin, wp, tp, true) == OK) { {
apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf); const int result = switch_win_noblock(&switchwin, wp, tp, true);
assert(result == OK);
(void)result;
if (apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf)) {
tp = win_find_tabpage(wp);
}
restore_win_noblock(&switchwin, true);
} }
restore_win_noblock(&switchwin, true); if (tp && enter) {
if (enter) {
goto_tabpage_win(tp, wp); goto_tabpage_win(tp, wp);
tp = win_find_tabpage(wp);
} }
if (win_valid_any_tab(wp) && buf != wp->w_buffer) { if (tp && bufref_valid(&bufref) && buf != wp->w_buffer) {
win_set_buf(wp, buf, !enter || fconfig.noautocmd, err); // win_set_buf temporarily makes `wp` the curwin to set the buffer.
// If not entering `wp`, block Enter and Leave events. (cringe)
const bool au_no_enter_leave = curwin != wp && !fconfig.noautocmd;
if (au_no_enter_leave) {
autocmd_no_enter++;
autocmd_no_leave++;
}
win_set_buf(wp, buf, fconfig.noautocmd, err);
if (!fconfig.noautocmd) {
tp = win_find_tabpage(wp);
}
if (au_no_enter_leave) {
autocmd_no_enter--;
autocmd_no_leave--;
}
} }
if (!win_valid_any_tab(wp)) { if (!tp) {
api_set_error(err, kErrorTypeException, "Window was closed immediately"); api_set_error(err, kErrorTypeException, "Window was closed immediately");
return 0; return 0;
} }
@@ -330,11 +365,11 @@ static int win_split_flags(WinSplit split, bool toplevel)
return flags; return flags;
} }
/// Configures window layout. Currently only for floating and external windows /// Configures window layout. Cannot be used to move the last window in a
/// (including changing a split window to those layouts). /// tabpage to a different one.
/// ///
/// When reconfiguring a floating window, absent option keys will not be /// When reconfiguring a window, absent option keys will not be changed.
/// changed. `row`/`col` and `relative` must be reconfigured together. /// `row`/`col` and `relative` must be reconfigured together.
/// ///
/// @see |nvim_open_win()| /// @see |nvim_open_win()|
/// ///
@@ -413,17 +448,59 @@ void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err)
return; return;
} }
if (was_split) { if (!check_split_disallowed_err(win, err)) {
win_T *new_curwin = NULL; return; // error already set
}
// Can't move the cmdwin or its old curwin to a different tabpage.
if ((win == cmdwin_win || win == cmdwin_old_curwin) && parent != NULL
&& win_find_tabpage(parent) != win_tp) {
api_set_error(err, kErrorTypeException, "%s", e_cmdwin);
return;
}
bool to_split_ok = false;
// 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.
const bool curwin_moving_tp
= win == curwin && parent != NULL && win_tp != win_find_tabpage(parent);
if (curwin_moving_tp) {
if (was_split) {
int dir;
win_goto(winframe_find_altwin(win, &dir, NULL, NULL));
} else {
win_goto(win_float_find_altwin(win, NULL));
}
// Autocommands may have been a real nuisance and messed things up...
if (curwin == win) {
api_set_error(err, kErrorTypeException, "Failed to switch away from window %d",
win->handle);
return;
}
win_tp = win_find_tabpage(win);
if (!win_tp || !win_valid_any_tab(parent)) {
api_set_error(err, kErrorTypeException, "Windows to split were closed");
goto restore_curwin;
}
if (was_split == win->w_floating || parent->w_floating) {
api_set_error(err, kErrorTypeException, "Floating state of windows to split changed");
goto restore_curwin;
}
}
int dir = 0;
frame_T *unflat_altfr = NULL;
win_T *altwin = NULL;
if (was_split) {
// If the window is the last in the tabpage or `fconfig.win` is // If the window is the last in the tabpage or `fconfig.win` is
// a handle to itself, we can't split it. // a handle to itself, we can't split it.
if (win->w_frame->fr_parent == NULL) { if (win->w_frame->fr_parent == NULL) {
// FIXME(willothy): if the window is the last in the tabpage but there is another tabpage // FIXME(willothy): if the window is the last in the tabpage but there is another tabpage
// and the target window is in that other tabpage, should we move the window to that // and the target window is in that other tabpage, should we move the window to that
// tabpage and close the previous one, or just error? // tabpage and close the previous one, or just error?
api_set_error(err, kErrorTypeValidation, "Cannot move last window"); api_set_error(err, kErrorTypeException, "Cannot move last window");
return; goto restore_curwin;
} else if (parent != NULL && parent->handle == win->handle) { } else if (parent != NULL && parent->handle == win->handle) {
int n_frames = 0; int n_frames = 0;
for (frame_T *fr = win->w_frame->fr_parent->fr_child; fr != NULL; fr = fr->fr_next) { for (frame_T *fr = win->w_frame->fr_parent->fr_child; fr != NULL; fr = fr->fr_next) {
@@ -459,83 +536,82 @@ void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err)
} }
// If the frame doesn't have a parent, the old frame // If the frame doesn't have a parent, the old frame
// was the root frame and we need to create a top-level split. // was the root frame and we need to create a top-level split.
int dir; altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr);
new_curwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp);
} else if (n_frames == 2) { } else if (n_frames == 2) {
// There are two windows in the frame, we can just rotate it. // There are two windows in the frame, we can just rotate it.
int dir; altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr);
neighbor = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp); neighbor = altwin;
new_curwin = neighbor;
} else { } else {
// There is only one window in the frame, we can't split it. // There is only one window in the frame, we can't split it.
api_set_error(err, kErrorTypeValidation, "Cannot split window into itself"); api_set_error(err, kErrorTypeException, "Cannot split window into itself");
return; goto restore_curwin;
} }
// Set the parent to whatever the correct // Set the parent to whatever the correct neighbor window was determined to be.
// neighbor window was determined to be.
parent = neighbor; parent = neighbor;
} else { } else {
int dir; altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr);
new_curwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp);
}
// move to neighboring window if we're moving the current window to a new tabpage
if (curwin == win && parent != NULL && new_curwin != NULL
&& win_tp != win_find_tabpage(parent)) {
win_enter(new_curwin, true);
}
win_remove(win, win_tp == curtab ? NULL : win_tp);
} else {
win_remove(win, win_tp == curtab ? NULL : win_tp);
ui_comp_remove_grid(&win->w_grid_alloc);
if (win->w_config.external) {
for (tabpage_T *tp = first_tabpage; tp != NULL; tp = tp->tp_next) {
if (tp == curtab) {
continue;
}
if (tp->tp_curwin == win) {
tp->tp_curwin = tp->tp_firstwin;
}
}
}
win->w_pos_changed = true;
}
int flags = win_split_flags(fconfig.split, parent == NULL);
if (parent == NULL) {
if (!win_split_ins(0, flags, win, 0)) {
// TODO(willothy): What should this error message say?
api_set_error(err, kErrorTypeException, "Failed to split window");
return;
} }
} else { } else {
win_execute_T args; altwin = win_float_find_altwin(win, win_tp == curtab ? NULL : win_tp);
tabpage_T *tp = win_find_tabpage(parent);
if (!win_execute_before(&args, parent, tp)) {
// TODO(willothy): how should we handle this / what should the message be?
api_set_error(err, kErrorTypeException, "Failed to switch to tabpage %d", tp->handle);
win_execute_after(&args);
return;
}
// This should return the same ptr to `win`, but we check for
// NULL to detect errors.
win_T *res = win_split_ins(0, flags, win, 0);
win_execute_after(&args);
if (!res) {
// TODO(willothy): What should this error message say?
api_set_error(err, kErrorTypeException, "Failed to split window");
return;
}
} }
win_remove(win, win_tp == curtab ? NULL : win_tp);
if (win_tp == curtab) {
last_status(false); // may need to remove last status line
win_comp_pos(); // recompute window positions
}
int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER;
tabpage_T *const parent_tp = parent ? win_find_tabpage(parent) : curtab;
TRY_WRAP(err, {
const bool need_switch = parent != NULL && parent != curwin;
switchwin_T switchwin;
if (need_switch) {
// `parent` is valid in its tabpage, so switch_win should not fail.
const int result = switch_win(&switchwin, parent, parent_tp, true);
(void)result;
assert(result == OK);
}
to_split_ok = win_split_ins(0, flags, win, 0, unflat_altfr) != NULL;
if (!to_split_ok) {
// Restore `win` to the window list now, so it's valid for restore_win (if used).
win_append(win->w_prev, win, win_tp == curtab ? NULL : win_tp);
}
if (need_switch) {
restore_win(&switchwin, true);
}
});
if (!to_split_ok) {
if (was_split) {
// win_split_ins doesn't change sizes or layout if it fails to insert an existing window, so
// just undo winframe_remove.
winframe_restore(win, dir, unflat_altfr);
}
if (!ERROR_SET(err)) {
api_set_error(err, kErrorTypeException, "Failed to move window %d into split", win->handle);
}
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;
}
// If `win` moved tabpages and was the curwin of its old one, select a new curwin for it.
if (win_tp != parent_tp && win_tp->tp_curwin == win) {
win_tp->tp_curwin = altwin;
}
if (HAS_KEY_X(config, width)) { if (HAS_KEY_X(config, width)) {
win_setwidth_win(fconfig.width, win); win_setwidth_win(fconfig.width, win);
} }
if (HAS_KEY_X(config, height)) { if (HAS_KEY_X(config, height)) {
win_setheight_win(fconfig.height, win); win_setheight_win(fconfig.height, win);
} }
redraw_later(win, UPD_NOT_VALID);
return;
} else { } else {
win_config_float(win, fconfig); win_config_float(win, fconfig);
win->w_pos_changed = true; win->w_pos_changed = true;
@@ -1071,11 +1147,15 @@ static bool parse_float_config(Dict(win_config) *config, WinConfig *fconfig, boo
fconfig->window = config->win; fconfig->window = config->win;
} }
} }
} else if (has_relative) { } else if (HAS_KEY_X(config, win)) {
if (HAS_KEY_X(config, win)) { if (has_relative) {
api_set_error(err, kErrorTypeValidation, api_set_error(err, kErrorTypeValidation,
"'win' key is only valid with relative='win' and relative=''"); "'win' key is only valid with relative='win' and relative=''");
return false; return false;
} else if (!is_split) {
api_set_error(err, kErrorTypeValidation,
"non-float with 'win' requires at least 'split' or 'vertical'");
return false;
} }
} }

View File

@@ -1333,7 +1333,7 @@ void aucmd_prepbuf(aco_save_T *aco, buf_T *buf)
block_autocmds(); // We don't want BufEnter/WinEnter autocommands. block_autocmds(); // We don't want BufEnter/WinEnter autocommands.
if (need_append) { if (need_append) {
win_append(lastwin, auc_win); win_append(lastwin, auc_win, NULL);
pmap_put(int)(&window_handles, auc_win->handle, auc_win); pmap_put(int)(&window_handles, auc_win->handle, auc_win);
win_config_float(auc_win, auc_win->w_config); win_config_float(auc_win, auc_win->w_config);
} }

View File

@@ -117,7 +117,6 @@
# include "buffer.c.generated.h" # include "buffer.c.generated.h"
#endif #endif
static const char *e_auabort = N_("E855: Autocommands caused command to abort");
static const char e_attempt_to_delete_buffer_that_is_in_use_str[] static const char e_attempt_to_delete_buffer_that_is_in_use_str[]
= N_("E937: Attempt to delete a buffer that is in use: %s"); = N_("E937: Attempt to delete a buffer that is in use: %s");
@@ -569,7 +568,7 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i
} }
buf->b_locked--; buf->b_locked--;
buf->b_locked_split--; buf->b_locked_split--;
if (abort_if_last && last_nonfloat(win)) { if (abort_if_last && one_window(win)) {
// Autocommands made this the only window. // Autocommands made this the only window.
emsg(_(e_auabort)); emsg(_(e_auabort));
return false; return false;
@@ -588,7 +587,7 @@ bool close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last, bool i
} }
buf->b_locked--; buf->b_locked--;
buf->b_locked_split--; buf->b_locked_split--;
if (abort_if_last && last_nonfloat(win)) { if (abort_if_last && one_window(win)) {
// Autocommands made this the only window. // Autocommands made this the only window.
emsg(_(e_auabort)); emsg(_(e_auabort));
return false; return false;

View File

@@ -1513,7 +1513,7 @@ static void win_update(win_T *wp)
// Make sure skipcol is valid, it depends on various options and the window // Make sure skipcol is valid, it depends on various options and the window
// width. // width.
if (wp->w_skipcol > 0) { if (wp->w_skipcol > 0 && wp->w_width_inner > win_col_off(wp)) {
int w = 0; int w = 0;
int width1 = wp->w_width_inner - win_col_off(wp); int width1 = wp->w_width_inner - win_col_off(wp);
int width2 = width1 + win_col_off2(wp); int width2 = width1 + win_col_off2(wp);

View File

@@ -12699,10 +12699,10 @@ M.funcs = {
args = { 2, 3 }, args = { 2, 3 },
base = 1, base = 1,
desc = [=[ desc = [=[
Move the window {nr} to a new split of the window {target}. Temporarily switch to window {target}, then move window {nr}
This is similar to moving to {target}, creating a new window to a new split adjacent to {target}.
using |:split| but having the same contents as window {nr}, and Unlike commands such as |:split|, no new windows are created
then closing {nr}. (the |window-ID| of window {nr} is unchanged after the move).
Both {nr} and {target} can be window numbers or |window-ID|s. Both {nr} and {target} can be window numbers or |window-ID|s.
Both must be in the current tab page. Both must be in the current tab page.

View File

@@ -14,6 +14,7 @@
#include "nvim/eval/typval.h" #include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h" #include "nvim/eval/typval_defs.h"
#include "nvim/eval/window.h" #include "nvim/eval/window.h"
#include "nvim/ex_getln.h"
#include "nvim/garray.h" #include "nvim/garray.h"
#include "nvim/garray_defs.h" #include "nvim/garray_defs.h"
#include "nvim/gettext_defs.h" #include "nvim/gettext_defs.h"
@@ -583,9 +584,13 @@ void f_win_getid(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
void f_win_gotoid(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) void f_win_gotoid(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{ {
int id = (int)tv_get_number(&argvars[0]); int id = (int)tv_get_number(&argvars[0]);
if (curwin->handle == id) {
// Nothing to do.
rettv->vval.v_number = 1;
return;
}
if (cmdwin_type != 0) { if (text_or_buf_locked()) {
emsg(_(e_cmdwin));
return; return;
} }
FOR_ALL_TAB_WINDOWS(tp, wp) { FOR_ALL_TAB_WINDOWS(tp, wp) {
@@ -659,55 +664,19 @@ void f_win_screenpos(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
tv_list_append_number(rettv->vval.v_list, wp == NULL ? 0 : wp->w_wincol + 1); tv_list_append_number(rettv->vval.v_list, wp == NULL ? 0 : wp->w_wincol + 1);
} }
/// Move the window wp into a new split of targetwin in a given direction
static void win_move_into_split(win_T *wp, win_T *targetwin, int size, int flags)
{
int height = wp->w_height;
win_T *oldwin = curwin;
if (wp == targetwin || is_aucmd_win(wp)) {
return;
}
// Jump to the target window
if (curwin != targetwin) {
win_goto(targetwin);
}
// Remove the old window and frame from the tree of frames
int dir;
winframe_remove(wp, &dir, NULL);
win_remove(wp, NULL);
last_status(false); // may need to remove last status line
win_comp_pos(); // recompute window positions
// Split a window on the desired side and put the old window there
win_split_ins(size, flags, wp, dir);
// If splitting horizontally, try to preserve height
if (size == 0 && !(flags & WSP_VERT)) {
win_setheight_win(height, wp);
if (p_ea) {
win_equal(wp, true, 'v');
}
}
if (oldwin != curwin) {
win_goto(oldwin);
}
}
/// "win_splitmove()" function /// "win_splitmove()" function
void f_win_splitmove(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) void f_win_splitmove(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{ {
win_T *wp = find_win_by_nr_or_id(&argvars[0]); win_T *wp = find_win_by_nr_or_id(&argvars[0]);
win_T *targetwin = find_win_by_nr_or_id(&argvars[1]); win_T *targetwin = find_win_by_nr_or_id(&argvars[1]);
win_T *oldwin = curwin;
rettv->vval.v_number = -1;
if (wp == NULL || targetwin == NULL || wp == targetwin if (wp == NULL || targetwin == NULL || wp == targetwin
|| !win_valid(wp) || !win_valid(targetwin) || !win_valid(wp) || !win_valid(targetwin)
|| win_float_valid(wp) || win_float_valid(targetwin)) { || targetwin->w_floating) {
emsg(_(e_invalwindow)); emsg(_(e_invalwindow));
rettv->vval.v_number = -1;
return; return;
} }
@@ -732,7 +701,27 @@ void f_win_splitmove(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
size = (int)tv_dict_get_number(d, "size"); size = (int)tv_dict_get_number(d, "size");
} }
win_move_into_split(wp, targetwin, size, flags); // Check if we can split the target before we bother switching windows.
if (is_aucmd_win(wp) || text_or_buf_locked() || check_split_disallowed(targetwin) == FAIL) {
return;
}
if (curwin != targetwin) {
win_goto(targetwin);
}
// Autocommands may have sent us elsewhere or closed "wp" or "oldwin".
if (curwin == targetwin && win_valid(wp)) {
if (win_splitmove(wp, size, flags) == OK) {
rettv->vval.v_number = 0;
}
} else {
emsg(_(e_auabort));
}
if (oldwin != curwin && win_valid(oldwin)) {
win_goto(oldwin);
}
} }
/// "win_gettype(nr)" function /// "win_gettype(nr)" function
@@ -969,9 +958,11 @@ int switch_win_noblock(switchwin_T *switchwin, win_T *win, tabpage_T *tp, bool n
if (no_display) { if (no_display) {
curtab->tp_firstwin = firstwin; curtab->tp_firstwin = firstwin;
curtab->tp_lastwin = lastwin; curtab->tp_lastwin = lastwin;
curtab->tp_topframe = topframe;
curtab = tp; curtab = tp;
firstwin = curtab->tp_firstwin; firstwin = curtab->tp_firstwin;
lastwin = curtab->tp_lastwin; lastwin = curtab->tp_lastwin;
topframe = curtab->tp_topframe;
} else { } else {
goto_tabpage_tp(tp, false, false); goto_tabpage_tp(tp, false, false);
} }
@@ -1000,9 +991,11 @@ void restore_win_noblock(switchwin_T *switchwin, bool no_display)
if (no_display) { if (no_display) {
curtab->tp_firstwin = firstwin; curtab->tp_firstwin = firstwin;
curtab->tp_lastwin = lastwin; curtab->tp_lastwin = lastwin;
curtab->tp_topframe = topframe;
curtab = switchwin->sw_curtab; curtab = switchwin->sw_curtab;
firstwin = curtab->tp_firstwin; firstwin = curtab->tp_firstwin;
lastwin = curtab->tp_lastwin; lastwin = curtab->tp_lastwin;
topframe = curtab->tp_topframe;
} else { } else {
goto_tabpage_tp(switchwin->sw_curtab, false, false); goto_tabpage_tp(switchwin->sw_curtab, false, false);
} }

View File

@@ -939,6 +939,7 @@ EXTERN const char e_using_float_as_string[] INIT(= N_("E806: Using a Float as a
EXTERN const char e_cannot_edit_other_buf[] INIT(= N_("E788: Not allowed to edit another buffer now")); EXTERN const char e_cannot_edit_other_buf[] INIT(= N_("E788: Not allowed to edit another buffer now"));
EXTERN const char e_using_number_as_bool_nr[] INIT(= N_("E1023: Using a Number as a Bool: %d")); EXTERN const char e_using_number_as_bool_nr[] INIT(= N_("E1023: Using a Number as a Bool: %d"));
EXTERN const char e_not_callable_type_str[] INIT(= N_("E1085: Not a callable type: %s")); EXTERN const char e_not_callable_type_str[] INIT(= N_("E1085: Not a callable type: %s"));
EXTERN const char e_auabort[] INIT(= N_("E855: Autocommands caused command to abort"));
EXTERN const char e_api_error[] INIT(= N_("E5555: API call: %s")); EXTERN const char e_api_error[] INIT(= N_("E5555: API call: %s"));

View File

@@ -1643,7 +1643,7 @@ static void invalidate(TUIData *tui, int top, int bot, int left, int right)
static void ensure_space_buf_size(TUIData *tui, size_t len) static void ensure_space_buf_size(TUIData *tui, size_t len)
{ {
if (len > tui->space_buf_len) { if (len > tui->space_buf_len) {
tui->space_buf = xrealloc(tui->space_buf, len * sizeof *tui->space_buf); tui->space_buf = xrealloc(tui->space_buf, len);
memset(tui->space_buf + tui->space_buf_len, ' ', len - tui->space_buf_len); memset(tui->space_buf + tui->space_buf_len, ' ', len - tui->space_buf_len);
tui->space_buf_len = len; tui->space_buf_len = len;
} }

View File

@@ -455,9 +455,14 @@ newwindow:
case 'H': case 'H':
case 'L': case 'L':
CHECK_CMDWIN; CHECK_CMDWIN;
win_totop(Prenum, if (one_window(curwin)) {
((nchar == 'H' || nchar == 'L') ? WSP_VERT : 0) beep_flush();
| ((nchar == 'H' || nchar == 'K') ? WSP_TOP : WSP_BOT)); } else {
const int dir = ((nchar == 'H' || nchar == 'L') ? WSP_VERT : 0)
| ((nchar == 'H' || nchar == 'K') ? WSP_TOP : WSP_BOT);
win_splitmove(curwin, Prenum, dir);
}
break; break;
// make all windows the same width and/or height // make all windows the same width and/or height
@@ -718,6 +723,7 @@ void win_set_buf(win_T *win, buf_T *buf, bool noautocmd, Error *err)
kErrorTypeException, kErrorTypeException,
"Failed to switch to window %d", "Failed to switch to window %d",
win->handle); win->handle);
goto cleanup;
} }
try_start(); try_start();
@@ -729,10 +735,11 @@ void win_set_buf(win_T *win, buf_T *buf, bool noautocmd, Error *err)
buf->handle); buf->handle);
} }
// If window is not current, state logic will not validate its cursor. // If window is not current, state logic will not validate its cursor. So do it now.
// So do it now. // Still needed if do_buffer returns FAIL (e.g: autocmds abort script after buffer was set).
validate_cursor(); validate_cursor();
cleanup:
restore_win_noblock(&switchwin, true); restore_win_noblock(&switchwin, true);
if (noautocmd) { if (noautocmd) {
unblock_autocmds(); unblock_autocmds();
@@ -903,19 +910,35 @@ void ui_ext_win_viewport(win_T *wp)
} }
} }
/// If "split_disallowed" is set give an error and return FAIL. /// If "split_disallowed" is set or "wp"s buffer is closing, give an error and return FAIL.
/// Otherwise return OK. /// Otherwise return OK.
static int check_split_disallowed(void) int check_split_disallowed(const win_T *wp)
FUNC_ATTR_NONNULL_ALL
{
Error err = ERROR_INIT;
const bool ok = check_split_disallowed_err(wp, &err);
if (ERROR_SET(&err)) {
emsg(_(err.msg));
api_clear_error(&err);
}
return ok ? OK : FAIL;
}
/// Like `check_split_disallowed`, but set `err` to the (untranslated) error message on failure and
/// return false. Otherwise return true.
/// @see check_split_disallowed
bool check_split_disallowed_err(const win_T *wp, Error *err)
FUNC_ATTR_NONNULL_ALL
{ {
if (split_disallowed > 0) { if (split_disallowed > 0) {
emsg(_("E242: Can't split a window while closing another")); api_set_error(err, kErrorTypeException, "E242: Can't split a window while closing another");
return FAIL; return false;
} }
if (curwin->w_buffer->b_locked_split) { if (wp->w_buffer->b_locked_split) {
emsg(_(e_cannot_split_window_when_closing_buffer)); api_set_error(err, kErrorTypeException, "%s", e_cannot_split_window_when_closing_buffer);
return FAIL; return false;
} }
return OK; return true;
} }
// split the current window, implements CTRL-W s and :split // split the current window, implements CTRL-W s and :split
@@ -934,7 +957,7 @@ static int check_split_disallowed(void)
// return FAIL for failure, OK otherwise // return FAIL for failure, OK otherwise
int win_split(int size, int flags) int win_split(int size, int flags)
{ {
if (check_split_disallowed() == FAIL) { if (check_split_disallowed(curwin) == FAIL) {
return FAIL; return FAIL;
} }
@@ -958,14 +981,19 @@ int win_split(int size, int flags)
clear_snapshot(curtab, SNAP_HELP_IDX); clear_snapshot(curtab, SNAP_HELP_IDX);
} }
return win_split_ins(size, flags, NULL, 0) == NULL ? FAIL : OK; return win_split_ins(size, flags, NULL, 0, NULL) == NULL ? FAIL : OK;
} }
/// When "new_wp" is NULL: split the current window in two. /// When "new_wp" is NULL: split the current window in two.
/// When "new_wp" is not NULL: insert this window at the far /// When "new_wp" is not NULL: insert this window at the far
/// top/left/right/bottom. /// top/left/right/bottom.
/// When "to_flatten" is not NULL: flatten this frame before reorganising frames;
/// remains unflattened on failure.
///
/// On failure, if "new_wp" was not NULL, no changes will have been made to the
/// window layout or sizes.
/// @return NULL for failure, or pointer to new window /// @return NULL for failure, or pointer to new window
win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir) win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir, frame_T *to_flatten)
{ {
win_T *wp = new_wp; win_T *wp = new_wp;
@@ -986,13 +1014,12 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir)
int need_status = 0; int need_status = 0;
int new_size = size; int new_size = size;
bool new_in_layout = (new_wp == NULL || new_wp->w_floating);
bool vertical = flags & WSP_VERT; bool vertical = flags & WSP_VERT;
bool toplevel = flags & (WSP_TOP | WSP_BOT); bool toplevel = flags & (WSP_TOP | WSP_BOT);
// add a status line when p_ls == 1 and splitting the first window // add a status line when p_ls == 1 and splitting the first window
if (one_nonfloat() && p_ls == 1 && oldwin->w_status_height == 0) { if (one_window(firstwin) && p_ls == 1 && oldwin->w_status_height == 0) {
if (oldwin->w_height <= p_wmh && new_in_layout) { if (oldwin->w_height <= p_wmh) {
emsg(_(e_noroom)); emsg(_(e_noroom));
return NULL; return NULL;
} }
@@ -1041,7 +1068,7 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir)
available = oldwin->w_frame->fr_width; available = oldwin->w_frame->fr_width;
needed += minwidth; needed += minwidth;
} }
if (available < needed && new_in_layout) { if (available < needed) {
emsg(_(e_noroom)); emsg(_(e_noroom));
return NULL; return NULL;
} }
@@ -1121,7 +1148,7 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir)
available = oldwin->w_frame->fr_height; available = oldwin->w_frame->fr_height;
needed += minheight; needed += minheight;
} }
if (available < needed && new_in_layout) { if (available < needed) {
emsg(_(e_noroom)); emsg(_(e_noroom));
return NULL; return NULL;
} }
@@ -1191,13 +1218,13 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir)
if (new_wp == NULL) { if (new_wp == NULL) {
wp = win_alloc(oldwin, false); wp = win_alloc(oldwin, false);
} else { } else {
win_append(oldwin, wp); win_append(oldwin, wp, NULL);
} }
} else { } else {
if (new_wp == NULL) { if (new_wp == NULL) {
wp = win_alloc(oldwin->w_prev, false); wp = win_alloc(oldwin->w_prev, false);
} else { } else {
win_append(oldwin->w_prev, wp); win_append(oldwin->w_prev, wp, NULL);
} }
} }
@@ -1211,13 +1238,37 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir)
// make the contents of the new window the same as the current one // make the contents of the new window the same as the current one
win_init(wp, curwin, flags); win_init(wp, curwin, flags);
} else if (wp->w_floating) { } else if (wp->w_floating) {
new_frame(wp); ui_comp_remove_grid(&wp->w_grid_alloc);
if (ui_has(kUIMultigrid)) {
wp->w_pos_changed = true;
} else {
// No longer a float, a non-multigrid UI shouldn't draw it as such
ui_call_win_hide(wp->w_grid_alloc.handle);
win_free_grid(wp, true);
}
// External windows are independent of tabpages, and may have been the curwin of others.
if (wp->w_config.external) {
FOR_ALL_TABS(tp) {
if (tp != curtab && tp->tp_curwin == wp) {
tp->tp_curwin = tp->tp_firstwin;
}
}
}
wp->w_floating = false; wp->w_floating = false;
new_frame(wp);
// non-floating window doesn't store float config or have a border. // non-floating window doesn't store float config or have a border.
wp->w_config = WIN_CONFIG_INIT; wp->w_config = WIN_CONFIG_INIT;
CLEAR_FIELD(wp->w_border_adj); CLEAR_FIELD(wp->w_border_adj);
} }
// Going to reorganize frames now, make sure they're flat.
if (to_flatten != NULL) {
frame_flatten(to_flatten);
}
bool before; bool before;
frame_T *curfrp; frame_T *curfrp;
@@ -1453,7 +1504,7 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir)
if (!(flags & WSP_NOENTER)) { if (!(flags & WSP_NOENTER)) {
// make the new window the current window // make the new window the current window
win_enter_ext(wp, WEE_TRIGGER_NEW_AUTOCMDS | WEE_TRIGGER_ENTER_AUTOCMDS win_enter_ext(wp, (new_wp == NULL ? WEE_TRIGGER_NEW_AUTOCMDS : 0) | WEE_TRIGGER_ENTER_AUTOCMDS
| WEE_TRIGGER_LEAVE_AUTOCMDS); | WEE_TRIGGER_LEAVE_AUTOCMDS);
} }
if (vertical) { if (vertical) {
@@ -1690,7 +1741,7 @@ static void win_exchange(int Prenum)
return; return;
} }
if (firstwin == curwin && lastwin_nofloating() == curwin) { if (one_window(curwin)) {
// just one window // just one window
beep_flush(); beep_flush();
return; return;
@@ -1732,13 +1783,13 @@ static void win_exchange(int Prenum)
if (wp->w_prev != curwin) { if (wp->w_prev != curwin) {
win_remove(curwin, NULL); win_remove(curwin, NULL);
frame_remove(curwin->w_frame); frame_remove(curwin->w_frame);
win_append(wp->w_prev, curwin); win_append(wp->w_prev, curwin, NULL);
frame_insert(frp, curwin->w_frame); frame_insert(frp, curwin->w_frame);
} }
if (wp != wp2) { if (wp != wp2) {
win_remove(wp, NULL); win_remove(wp, NULL);
frame_remove(wp->w_frame); frame_remove(wp->w_frame);
win_append(wp2, wp); win_append(wp2, wp, NULL);
if (frp2 == NULL) { if (frp2 == NULL) {
frame_insert(wp->w_frame->fr_parent->fr_child, wp->w_frame); frame_insert(wp->w_frame->fr_parent->fr_child, wp->w_frame);
} else { } else {
@@ -1782,7 +1833,7 @@ static void win_rotate(bool upwards, int count)
return; return;
} }
if (count <= 0 || (firstwin == curwin && lastwin_nofloating() == curwin)) { if (count <= 0 || one_window(curwin)) {
// nothing to do // nothing to do
beep_flush(); beep_flush();
return; return;
@@ -1812,7 +1863,7 @@ static void win_rotate(bool upwards, int count)
// find last frame and append removed window/frame after it // find last frame and append removed window/frame after it
for (; frp->fr_next != NULL; frp = frp->fr_next) {} for (; frp->fr_next != NULL; frp = frp->fr_next) {}
win_append(frp->fr_win, wp1); win_append(frp->fr_win, wp1, NULL);
frame_append(frp, wp1->w_frame); frame_append(frp, wp1->w_frame);
wp2 = frp->fr_win; // previously last window wp2 = frp->fr_win; // previously last window
@@ -1827,7 +1878,7 @@ static void win_rotate(bool upwards, int count)
assert(frp->fr_parent->fr_child); assert(frp->fr_parent->fr_child);
// append the removed window/frame before the first in the list // append the removed window/frame before the first in the list
win_append(frp->fr_parent->fr_child->fr_win->w_prev, wp1); win_append(frp->fr_parent->fr_child->fr_win->w_prev, wp1, NULL);
frame_insert(frp->fr_parent->fr_child, frp); frame_insert(frp->fr_parent->fr_child, frp);
} }
@@ -1856,48 +1907,58 @@ static void win_rotate(bool upwards, int count)
redraw_all_later(UPD_NOT_VALID); redraw_all_later(UPD_NOT_VALID);
} }
// Move the current window to the very top/bottom/left/right of the screen. /// Move "wp" into a new split in a given direction, possibly relative to the
static void win_totop(int size, int flags) /// current window.
/// "wp" must be valid in the current tabpage.
/// Returns FAIL for failure, OK otherwise.
int win_splitmove(win_T *wp, int size, int flags)
{ {
int dir = 0; int dir = 0;
int height = curwin->w_height; int height = wp->w_height;
if (firstwin == curwin && lastwin_nofloating() == curwin) { if (firstwin == wp && lastwin_nofloating() == wp) {
beep_flush(); return OK; // nothing to do
return;
} }
if (is_aucmd_win(curwin)) { if (is_aucmd_win(wp) || check_split_disallowed(wp) == FAIL) {
return; return FAIL;
}
if (check_split_disallowed() == FAIL) {
return;
} }
if (curwin->w_floating) { frame_T *unflat_altfr = NULL;
ui_comp_remove_grid(&curwin->w_grid_alloc); if (wp->w_floating) {
if (ui_has(kUIMultigrid)) { win_remove(wp, NULL);
curwin->w_pos_changed = true;
} else {
// No longer a float, a non-multigrid UI shouldn't draw it as such
ui_call_win_hide(curwin->w_grid_alloc.handle);
win_free_grid(curwin, true);
}
} else { } else {
// Remove the window and frame from the tree of frames. // Remove the window and frame from the tree of frames. Don't flatten any
winframe_remove(curwin, &dir, NULL); // frames yet so we can restore things if win_split_ins fails.
winframe_remove(wp, &dir, NULL, &unflat_altfr);
win_remove(wp, NULL);
last_status(false); // may need to remove last status line
win_comp_pos(); // recompute window positions
} }
win_remove(curwin, NULL);
last_status(false); // may need to remove last status line
win_comp_pos(); // recompute window positions
// Split a window on the desired side and put the window there. // Split a window on the desired side and put "wp" there.
win_split_ins(size, flags, curwin, dir); if (win_split_ins(size, flags, wp, dir, unflat_altfr) == NULL) {
if (!(flags & WSP_VERT)) { win_append(wp->w_prev, wp, NULL);
win_setheight(height); if (!wp->w_floating) {
// win_split_ins doesn't change sizes or layout if it fails to insert an
// existing window, so just undo winframe_remove.
winframe_restore(wp, dir, unflat_altfr);
win_comp_pos(); // recompute window positions
}
return FAIL;
}
// If splitting horizontally, try to preserve height.
// Note that win_split_ins autocommands may have immediately closed "wp", or made it floating!
if (size == 0 && !(flags & WSP_VERT) && win_valid(wp) && !wp->w_floating) {
win_setheight_win(height, wp);
if (p_ea) { if (p_ea) {
win_equal(curwin, true, 'v'); // Equalize windows. Note that win_split_ins autocommands may have
// made a window other than "wp" current.
win_equal(curwin, curwin == wp, 'v');
} }
} }
return OK;
} }
// Move window "win1" to below/right of "win2" and make "win1" the current // Move window "win1" to below/right of "win2" and make "win1" the current
@@ -1955,7 +2016,7 @@ void win_move_after(win_T *win1, win_T *win2)
} }
win_remove(win1, NULL); win_remove(win1, NULL);
frame_remove(win1->w_frame); frame_remove(win1->w_frame);
win_append(win2, win1); win_append(win2, win1, NULL);
frame_append(win2->w_frame, win1->w_frame); frame_append(win2->w_frame, win1->w_frame);
win_comp_pos(); // recompute w_winrow for all windows win_comp_pos(); // recompute w_winrow for all windows
@@ -2434,37 +2495,13 @@ bool last_window(win_T *win) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
} }
/// Check if "win" is the only non-floating window in the current tabpage. /// Check if "win" is the only non-floating window in the current tabpage.
///
/// This should be used in place of ONE_WINDOW when necessary,
/// with "firstwin" or the affected window as argument depending on the situation.
bool one_window(win_T *win) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT bool one_window(win_T *win) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{ {
if (win->w_floating) { assert(!firstwin->w_floating);
return false; return firstwin == win && (win->w_next == NULL || win->w_next->w_floating);
}
bool seen_one = false;
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (!wp->w_floating) {
if (seen_one) {
return false;
}
seen_one = true;
}
}
return true;
}
/// Like ONE_WINDOW but only considers non-floating windows
bool one_nonfloat(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
return firstwin->w_next == NULL || firstwin->w_next->w_floating;
}
/// if wp is the last non-floating window
///
/// always false for a floating window
bool last_nonfloat(win_T *wp) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
return wp != NULL && firstwin == wp && !(wp->w_next && !wp->w_floating);
} }
/// Check if floating windows in the current tab can be closed. /// Check if floating windows in the current tab can be closed.
@@ -2759,13 +2796,10 @@ int win_close(win_T *win, bool free_buf, bool force)
ui_comp_remove_grid(&win->w_grid_alloc); ui_comp_remove_grid(&win->w_grid_alloc);
assert(first_tabpage != NULL); // suppress clang "Dereference of NULL pointer" assert(first_tabpage != NULL); // suppress clang "Dereference of NULL pointer"
if (win->w_config.external) { if (win->w_config.external) {
for (tabpage_T *tp = first_tabpage; tp != NULL; tp = tp->tp_next) { FOR_ALL_TABS(tp) {
if (tp == curtab) { if (tp != curtab && tp->tp_curwin == win) {
continue;
}
if (tp->tp_curwin == win) {
// NB: an autocmd can still abort the closing of this window, // NB: an autocmd can still abort the closing of this window,
// bur carring out this change anyway shouldn't be a catastrophe. // but carrying out this change anyway shouldn't be a catastrophe.
tp->tp_curwin = tp->tp_firstwin; tp->tp_curwin = tp->tp_firstwin;
} }
} }
@@ -3006,24 +3040,11 @@ static win_T *win_free_mem(win_T *win, int *dirp, tabpage_T *tp)
if (!win->w_floating) { if (!win->w_floating) {
// Remove the window and its frame from the tree of frames. // Remove the window and its frame from the tree of frames.
frame_T *frp = win->w_frame; frame_T *frp = win->w_frame;
wp = winframe_remove(win, dirp, tp); wp = winframe_remove(win, dirp, tp, NULL);
xfree(frp); xfree(frp);
} else { } else {
*dirp = 'h'; // Dummy value. *dirp = 'h'; // Dummy value.
if (tp == NULL) { wp = win_float_find_altwin(win, tp);
if (win_valid(prevwin) && prevwin != win) {
wp = prevwin;
} else {
wp = firstwin;
}
} else {
assert(tp != curtab);
if (tabpage_win_valid(tp, tp->tp_prevwin) && tp->tp_prevwin != win) {
wp = tp->tp_prevwin;
} else {
wp = tp->tp_firstwin;
}
}
} }
win_free(win, tp); win_free(win, tp);
@@ -3087,9 +3108,60 @@ void win_free_all(void)
/// ///
/// @param dirp set to 'v' or 'h' for direction if 'ea' /// @param dirp set to 'v' or 'h' for direction if 'ea'
/// @param tp tab page "win" is in, NULL for current /// @param tp tab page "win" is in, NULL for current
/// @param unflat_altfr if not NULL, set to pointer of frame that got
/// the space, and it is not flattened
/// ///
/// @return a pointer to the window that got the freed up space. /// @return a pointer to the window that got the freed up space.
win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp) win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp, frame_T **unflat_altfr)
FUNC_ATTR_NONNULL_ARG(1, 2)
{
frame_T *altfr;
win_T *wp = winframe_find_altwin(win, dirp, tp, &altfr);
if (wp == NULL) {
return NULL;
}
frame_T *frp_close = win->w_frame;
// Remove this frame from the list of frames.
frame_remove(frp_close);
if (*dirp == 'v') {
frame_new_height(altfr, altfr->fr_height + frp_close->fr_height,
altfr == frp_close->fr_next, false);
} else {
assert(*dirp == 'h');
frame_new_width(altfr, altfr->fr_width + frp_close->fr_width,
altfr == frp_close->fr_next, false);
}
// If rows/columns go to a window below/right its positions need to be
// updated. Can only be done after the sizes have been updated.
if (altfr == frp_close->fr_next) {
int row = win->w_winrow;
int col = win->w_wincol;
frame_comp_pos(altfr, &row, &col);
}
if (unflat_altfr == NULL) {
frame_flatten(altfr);
} else {
*unflat_altfr = altfr;
}
return wp;
}
/// Find the window that will get the freed space from a call to `winframe_remove`.
/// Makes no changes to the window layout.
///
/// @param dirp set to 'v' or 'h' for the direction where "altfr" will be resized
/// to fill the space
/// @param tp tab page "win" is in, NULL for current
/// @param altfr if not NULL, set to pointer of frame that will get the space
///
/// @return a pointer to the window that will get the freed up space.
win_T *winframe_find_altwin(win_T *win, int *dirp, tabpage_T *tp, frame_T **altfr)
FUNC_ATTR_NONNULL_ARG(1, 2) FUNC_ATTR_NONNULL_ARG(1, 2)
{ {
assert(tp == NULL || tp != curtab); assert(tp == NULL || tp != curtab);
@@ -3101,13 +3173,10 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp)
frame_T *frp_close = win->w_frame; frame_T *frp_close = win->w_frame;
// Remove the window from its frame. // Find the window and frame that gets the space.
frame_T *frp2 = win_altframe(win, tp); frame_T *frp2 = win_altframe(win, tp);
win_T *wp = frame2win(frp2); win_T *wp = frame2win(frp2);
// Remove this frame from the list of frames.
frame_remove(frp_close);
if (frp_close->fr_parent->fr_layout == FR_COL) { if (frp_close->fr_parent->fr_layout == FR_COL) {
// When 'winfixheight' is set, try to find another frame in the column // When 'winfixheight' is set, try to find another frame in the column
// (as close to the closed frame as possible) to distribute the height // (as close to the closed frame as possible) to distribute the height
@@ -3134,8 +3203,6 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp)
} }
} }
} }
frame_new_height(frp2, frp2->fr_height + frp_close->fr_height,
frp2 == frp_close->fr_next, false);
*dirp = 'v'; *dirp = 'v';
} else { } else {
// When 'winfixwidth' is set, try to find another frame in the column // When 'winfixwidth' is set, try to find another frame in the column
@@ -3163,70 +3230,124 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp)
} }
} }
} }
frame_new_width(frp2, frp2->fr_width + frp_close->fr_width,
frp2 == frp_close->fr_next, false);
*dirp = 'h'; *dirp = 'h';
} }
// If rows/columns go to a window below/right its positions need to be assert(wp != win && frp2 != frp_close);
// updated. Can only be done after the sizes have been updated. if (altfr != NULL) {
if (frp2 == frp_close->fr_next) { *altfr = frp2;
int row = win->w_winrow;
int col = win->w_wincol;
frame_comp_pos(frp2, &row, &col);
} }
if (frp2->fr_next == NULL && frp2->fr_prev == NULL) { return wp;
// There is no other frame in this list, move its info to the parent }
// and remove it.
frp2->fr_parent->fr_layout = frp2->fr_layout; /// Flatten "frp" into its parent frame if it's the only child, also merging its
frp2->fr_parent->fr_child = frp2->fr_child; /// list with the grandparent if they share the same layout.
frame_T *frp; /// Frees "frp" if flattened; also "frp->fr_parent" if it has the same layout.
FOR_ALL_FRAMES(frp, frp2->fr_child) { /// "frp" must be valid in the current tabpage.
frp->fr_parent = frp2->fr_parent; static void frame_flatten(frame_T *frp)
FUNC_ATTR_NONNULL_ALL
{
if (frp->fr_next != NULL || frp->fr_prev != NULL) {
return;
}
// There is no other frame in this list, move its info to the parent
// and remove it.
frp->fr_parent->fr_layout = frp->fr_layout;
frp->fr_parent->fr_child = frp->fr_child;
frame_T *frp2;
FOR_ALL_FRAMES(frp2, frp->fr_child) {
frp2->fr_parent = frp->fr_parent;
}
frp->fr_parent->fr_win = frp->fr_win;
if (frp->fr_win != NULL) {
frp->fr_win->w_frame = frp->fr_parent;
}
frp2 = frp->fr_parent;
if (topframe->fr_child == frp) {
topframe->fr_child = frp2;
}
xfree(frp);
frp = frp2->fr_parent;
if (frp != NULL && frp->fr_layout == frp2->fr_layout) {
// The frame above the parent has the same layout, have to merge
// the frames into this list.
if (frp->fr_child == frp2) {
frp->fr_child = frp2->fr_child;
} }
frp2->fr_parent->fr_win = frp2->fr_win; assert(frp2->fr_child);
if (frp2->fr_win != NULL) { frp2->fr_child->fr_prev = frp2->fr_prev;
frp2->fr_win->w_frame = frp2->fr_parent; if (frp2->fr_prev != NULL) {
frp2->fr_prev->fr_next = frp2->fr_child;
}
for (frame_T *frp3 = frp2->fr_child;; frp3 = frp3->fr_next) {
frp3->fr_parent = frp;
if (frp3->fr_next == NULL) {
frp3->fr_next = frp2->fr_next;
if (frp2->fr_next != NULL) {
frp2->fr_next->fr_prev = frp3;
}
break;
}
} }
frp = frp2->fr_parent;
if (topframe->fr_child == frp2) { if (topframe->fr_child == frp2) {
topframe->fr_child = frp; topframe->fr_child = frp;
} }
xfree(frp2); xfree(frp2);
}
}
frp2 = frp->fr_parent; /// Undo changes from a prior call to winframe_remove, also restoring lost
if (frp2 != NULL && frp2->fr_layout == frp->fr_layout) { /// vertical separators and statuslines, and changed window positions for
// The frame above the parent has the same layout, have to merge /// windows within "unflat_altfr".
// the frames into this list. /// Caller must ensure no other changes were made to the layout or window sizes!
if (frp2->fr_child == frp) { void winframe_restore(win_T *wp, int dir, frame_T *unflat_altfr)
frp2->fr_child = frp->fr_child; FUNC_ATTR_NONNULL_ALL
} {
assert(frp->fr_child); frame_T *frp = wp->w_frame;
frp->fr_child->fr_prev = frp->fr_prev;
if (frp->fr_prev != NULL) { // Put "wp"'s frame back where it was.
frp->fr_prev->fr_next = frp->fr_child; if (frp->fr_prev != NULL) {
} frame_append(frp->fr_prev, frp);
frame_T *frp3; } else {
for (frp3 = frp->fr_child;; frp3 = frp3->fr_next) { frame_insert(frp->fr_next, frp);
frp3->fr_parent = frp2; }
if (frp3->fr_next == NULL) {
frp3->fr_next = frp->fr_next; // Vertical separators to the left may have been lost. Restore them.
if (frp->fr_next != NULL) { if (wp->w_vsep_width == 0 && frp->fr_parent->fr_layout == FR_ROW && frp->fr_prev != NULL) {
frp->fr_next->fr_prev = frp3; frame_add_vsep(frp->fr_prev);
} }
break;
} // Statuslines or horizontal separators above may have been lost. Restore them.
} if (frp->fr_parent->fr_layout == FR_COL && frp->fr_prev != NULL) {
if (topframe->fr_child == frp) { if (global_stl_height() == 0 && wp->w_status_height == 0) {
topframe->fr_child = frp2; frame_add_statusline(frp->fr_prev);
} } else if (wp->w_hsep_height == 0) {
xfree(frp); frame_add_hsep(frp->fr_prev);
} }
} }
return wp; int row = wp->w_winrow;
int col = wp->w_wincol;
// Restore the lost room that was redistributed to the altframe.
if (dir == 'v') {
frame_new_height(unflat_altfr, unflat_altfr->fr_height - frp->fr_height,
unflat_altfr == frp->fr_next, false);
row += frp->fr_height;
} else if (dir == 'h') {
frame_new_width(unflat_altfr, unflat_altfr->fr_width - frp->fr_width,
unflat_altfr == frp->fr_next, false);
col += frp->fr_width;
}
// If rows/columns went to a window below/right, its positions need to be
// restored. Can only be done after the sizes have been updated.
if (unflat_altfr == frp->fr_next) {
frame_comp_pos(unflat_altfr, &row, &col);
}
} }
/// If 'splitbelow' or 'splitright' is set, the space goes above or to the left /// If 'splitbelow' or 'splitright' is set, the space goes above or to the left
@@ -3792,7 +3913,7 @@ void close_others(int message, int forceit)
return; return;
} }
if (one_nonfloat() && !lastwin->w_floating) { if (one_window(firstwin) && !lastwin->w_floating) {
if (message if (message
&& !autocmd_busy) { && !autocmd_busy) {
msg(_(m_onlyone), 0); msg(_(m_onlyone), 0);
@@ -4331,7 +4452,7 @@ static void tabpage_check_windows(tabpage_T *old_curtab)
if (wp->w_floating) { if (wp->w_floating) {
if (wp->w_config.external) { if (wp->w_config.external) {
win_remove(wp, old_curtab); win_remove(wp, old_curtab);
win_append(lastwin_nofloating(), wp); win_append(lastwin_nofloating(), wp, NULL);
} else { } else {
ui_comp_remove_grid(&wp->w_grid_alloc); ui_comp_remove_grid(&wp->w_grid_alloc);
} }
@@ -4971,7 +5092,7 @@ win_T *win_alloc(win_T *after, bool hidden)
block_autocmds(); block_autocmds();
// link the window in the window list // link the window in the window list
if (!hidden) { if (!hidden) {
win_append(after, new_wp); win_append(after, new_wp, NULL);
} }
new_wp->w_wincol = 0; new_wp->w_wincol = 0;
@@ -5141,21 +5262,29 @@ void win_free_grid(win_T *wp, bool reinit)
} }
} }
// Append window "wp" in the window list after window "after". /// Append window "wp" in the window list after window "after".
void win_append(win_T *after, win_T *wp) ///
/// @param tp tab page "win" (and "after", if not NULL) is in, NULL for current
void win_append(win_T *after, win_T *wp, tabpage_T *tp)
FUNC_ATTR_NONNULL_ARG(2)
{ {
assert(tp == NULL || tp != curtab);
win_T **first = tp == NULL ? &firstwin : &tp->tp_firstwin;
win_T **last = tp == NULL ? &lastwin : &tp->tp_lastwin;
// after NULL is in front of the first // after NULL is in front of the first
win_T *before = after == NULL ? firstwin : after->w_next; win_T *before = after == NULL ? *first : after->w_next;
wp->w_next = before; wp->w_next = before;
wp->w_prev = after; wp->w_prev = after;
if (after == NULL) { if (after == NULL) {
firstwin = wp; *first = wp;
} else { } else {
after->w_next = wp; after->w_next = wp;
} }
if (before == NULL) { if (before == NULL) {
lastwin = wp; *last = wp;
} else { } else {
before->w_prev = wp; before->w_prev = wp;
} }
@@ -7058,7 +7187,7 @@ int global_stl_height(void)
/// @param morewin pretend there are two or more windows if true. /// @param morewin pretend there are two or more windows if true.
int last_stl_height(bool morewin) int last_stl_height(bool morewin)
{ {
return (p_ls > 1 || (p_ls == 1 && (morewin || !one_nonfloat()))) ? STATUS_HEIGHT : 0; return (p_ls > 1 || (p_ls == 1 && (morewin || !one_window(firstwin)))) ? STATUS_HEIGHT : 0;
} }
/// Return the minimal number of rows that is needed on the screen to display /// Return the minimal number of rows that is needed on the screen to display

View File

@@ -55,13 +55,26 @@ win_T *win_new_float(win_T *wp, bool last, WinConfig fconfig, Error *err)
api_set_error(err, kErrorTypeException, api_set_error(err, kErrorTypeException,
"Cannot change window from different tabpage into float"); "Cannot change window from different tabpage into float");
return NULL; 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) {
if (wp2 != wp && wp2 != cmdwin_win) {
other_nonfloat = true;
break;
}
}
if (!other_nonfloat) {
api_set_error(err, kErrorTypeException, "%s", e_cmdwin);
return NULL;
}
} }
int dir; int dir;
winframe_remove(wp, &dir, NULL); winframe_remove(wp, &dir, NULL, NULL);
XFREE_CLEAR(wp->w_frame); XFREE_CLEAR(wp->w_frame);
win_comp_pos(); // recompute window positions win_comp_pos(); // recompute window positions
win_remove(wp, NULL); win_remove(wp, NULL);
win_append(lastwin_nofloating(), wp); win_append(lastwin_nofloating(), wp, NULL);
} }
wp->w_floating = true; wp->w_floating = true;
wp->w_status_height = 0; wp->w_status_height = 0;
@@ -306,3 +319,21 @@ win_T *win_float_find_preview(void)
} }
return NULL; return NULL;
} }
/// Select an alternative window to `win` (assumed floating) in tabpage `tp`.
///
/// Useful for finding a window to switch to if `win` is the current window, but is then closed or
/// moved to a different tabpage.
///
/// @param tp `win`'s original tabpage, or NULL for current.
win_T *win_float_find_altwin(const win_T *win, const tabpage_T *tp)
FUNC_ATTR_NONNULL_ARG(1)
{
if (tp == NULL) {
return (win_valid(prevwin) && prevwin != win) ? prevwin : firstwin;
}
assert(tp != curtab);
return (tabpage_win_valid(tp, tp->tp_prevwin) && tp->tp_prevwin != win) ? tp->tp_prevwin
: tp->tp_firstwin;
}

View File

@@ -1,5 +1,7 @@
local helpers = require('test.functional.helpers')(after_each) local helpers = require('test.functional.helpers')(after_each)
local clear, eq, ok = helpers.clear, helpers.eq, helpers.ok local clear, eq, ok = helpers.clear, helpers.eq, helpers.ok
local exec = helpers.exec
local feed = helpers.feed
local api = helpers.api local api = helpers.api
local fn = helpers.fn local fn = helpers.fn
local request = helpers.request local request = helpers.request
@@ -86,6 +88,30 @@ describe('api/tabpage', function()
pcall_err(api.nvim_tabpage_set_win, tab1, win3) pcall_err(api.nvim_tabpage_set_win, tab1, win3)
) )
end) end)
it('does not switch window when textlocked or in the cmdwin', function()
local target_win = api.nvim_get_current_win()
feed('q:')
local cur_win = api.nvim_get_current_win()
eq(
'Vim:E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
pcall_err(api.nvim_tabpage_set_win, 0, target_win)
)
eq(cur_win, api.nvim_get_current_win())
command('quit!')
exec(([[
new
call setline(1, 'foo')
setlocal debug=throw indentexpr=nvim_tabpage_set_win(0,%d)
]]):format(target_win))
cur_win = api.nvim_get_current_win()
eq(
'Vim(normal):E5555: API call: Vim:E565: Not allowed to change text or change window',
pcall_err(command, 'normal! ==')
)
eq(cur_win, api.nvim_get_current_win())
end)
end) end)
describe('{get,set,del}_var', function() describe('{get,set,del}_var', function()

View File

@@ -1364,6 +1364,308 @@ describe('API/win', function()
}, },
}, layout) }, layout)
end) end)
local function setup_tabbed_autocmd_test()
local info = {}
info.orig_buf = api.nvim_get_current_buf()
info.other_buf = api.nvim_create_buf(true, true)
info.tab1_curwin = api.nvim_get_current_win()
info.tab1 = api.nvim_get_current_tabpage()
command('tab split | split')
info.tab2_curwin = api.nvim_get_current_win()
info.tab2 = api.nvim_get_current_tabpage()
exec([=[
tabfirst
let result = []
autocmd TabEnter * let result += [["TabEnter", nvim_get_current_tabpage()]]
autocmd TabLeave * let result += [["TabLeave", nvim_get_current_tabpage()]]
autocmd WinEnter * let result += [["WinEnter", win_getid()]]
autocmd WinLeave * let result += [["WinLeave", win_getid()]]
autocmd WinNew * let result += [["WinNew", win_getid()]]
autocmd WinClosed * let result += [["WinClosed", str2nr(expand("<afile>"))]]
autocmd BufEnter * let result += [["BufEnter", win_getid(), bufnr()]]
autocmd BufLeave * let result += [["BufLeave", win_getid(), bufnr()]]
autocmd BufWinEnter * let result += [["BufWinEnter", win_getid(), bufnr()]]
autocmd BufWinLeave * let result += [["BufWinLeave", win_getid(), bufnr()]]
]=])
return info
end
it('fires expected autocmds when creating splits without entering', function()
local info = setup_tabbed_autocmd_test()
-- For these, don't want BufWinEnter if visiting the same buffer, like :{s}buffer.
-- Same tabpage, same buffer.
local new_win = api.nvim_open_win(0, false, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
-- Other tabpage, same buffer.
command('let result = []')
new_win = api.nvim_open_win(0, false, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
-- Same tabpage, other buffer.
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, false, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
-- Other tabpage, other buffer.
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, false, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
eq(info.tab1_curwin, api.nvim_get_current_win())
end)
it('fires expected autocmds when creating and entering splits', function()
local info = setup_tabbed_autocmd_test()
-- Same tabpage, same buffer.
local new_win = api.nvim_open_win(0, true, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'WinEnter', new_win },
}, eval('result'))
-- Same tabpage, other buffer.
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, true, { split = 'left', win = info.tab1_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'WinEnter', new_win },
{ 'BufLeave', new_win, info.orig_buf },
{ 'BufEnter', new_win, info.other_buf },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
-- For these, the other tabpage's prevwin and curwin will change like we switched from its old
-- curwin to the new window, so the extra events near TabEnter reflect that.
-- Other tabpage, same buffer.
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(0, true, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'TabLeave', info.tab1 },
{ 'WinEnter', info.tab2_curwin },
{ 'TabEnter', info.tab2 },
{ 'WinLeave', info.tab2_curwin },
{ 'WinEnter', new_win },
}, eval('result'))
-- Other tabpage, other buffer.
api.nvim_set_current_win(info.tab2_curwin)
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, true, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'WinLeave', info.tab1_curwin },
{ 'TabLeave', info.tab1 },
{ 'WinEnter', info.tab2_curwin },
{ 'TabEnter', info.tab2 },
{ 'WinLeave', info.tab2_curwin },
{ 'WinEnter', new_win },
{ 'BufLeave', new_win, info.orig_buf },
{ 'BufEnter', new_win, info.other_buf },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
-- Other tabpage, other buffer; but other tabpage's curwin has a new buffer active.
api.nvim_set_current_win(info.tab2_curwin)
local new_buf = api.nvim_create_buf(true, true)
api.nvim_set_current_buf(new_buf)
api.nvim_set_current_win(info.tab1_curwin)
command('let result = []')
new_win = api.nvim_open_win(info.other_buf, true, { split = 'left', win = info.tab2_curwin })
eq({
{ 'WinNew', new_win },
{ 'BufLeave', info.tab1_curwin, info.orig_buf },
{ 'WinLeave', info.tab1_curwin },
{ 'TabLeave', info.tab1 },
{ 'WinEnter', info.tab2_curwin },
{ 'TabEnter', info.tab2 },
{ 'BufEnter', info.tab2_curwin, new_buf },
{ 'WinLeave', info.tab2_curwin },
{ 'WinEnter', new_win },
{ 'BufLeave', new_win, new_buf },
{ 'BufEnter', new_win, info.other_buf },
{ 'BufWinEnter', new_win, info.other_buf },
}, eval('result'))
end)
it('OK when new window is moved to other tabpage by autocommands', function()
-- Use nvim_win_set_config in the autocommands, as other methods of moving a window to a
-- different tabpage (e.g: wincmd T) actually creates a new window.
local tab0 = api.nvim_get_current_tabpage()
local tab0_win = api.nvim_get_current_win()
command('tabnew')
local new_buf = api.nvim_create_buf(true, true)
local tab1 = api.nvim_get_current_tabpage()
local tab1_parent = api.nvim_get_current_win()
command(
'tabfirst | autocmd WinNew * ++once call nvim_win_set_config(0, #{split: "left", win: '
.. tab1_parent
.. '})'
)
local new_win = api.nvim_open_win(new_buf, true, { split = 'left' })
eq(tab1, api.nvim_get_current_tabpage())
eq(new_win, api.nvim_get_current_win())
eq(new_buf, api.nvim_get_current_buf())
-- nvim_win_set_config called after entering. It doesn't follow a curwin that is moved to a
-- different tabpage, but instead moves to the win filling the space, which is tab0_win.
command(
'tabfirst | autocmd WinEnter * ++once call nvim_win_set_config(0, #{split: "left", win: '
.. tab1_parent
.. '})'
)
new_win = api.nvim_open_win(new_buf, true, { split = 'left' })
eq(tab0, api.nvim_get_current_tabpage())
eq(tab0_win, api.nvim_get_current_win())
eq(tab1, api.nvim_win_get_tabpage(new_win))
eq(new_buf, api.nvim_win_get_buf(new_win))
command(
'tabfirst | autocmd BufEnter * ++once call nvim_win_set_config(0, #{split: "left", win: '
.. tab1_parent
.. '})'
)
new_win = api.nvim_open_win(new_buf, true, { split = 'left' })
eq(tab0, api.nvim_get_current_tabpage())
eq(tab0_win, api.nvim_get_current_win())
eq(tab1, api.nvim_win_get_tabpage(new_win))
eq(new_buf, api.nvim_win_get_buf(new_win))
end)
it('does not fire BufWinEnter if win_set_buf fails', function()
exec([[
set nohidden modified
autocmd WinNew * ++once only!
let fired = v:false
autocmd BufWinEnter * ++once let fired = v:true
]])
eq(
'Failed to set buffer 2',
pcall_err(api.nvim_open_win, api.nvim_create_buf(true, true), false, { split = 'left' })
)
eq(false, eval('fired'))
end)
it('fires Buf* autocommands when `!enter` if window is entered via autocommands', function()
exec([[
autocmd WinNew * ++once only!
let fired = v:false
autocmd BufEnter * ++once let fired = v:true
]])
api.nvim_open_win(api.nvim_create_buf(true, true), false, { split = 'left' })
eq(true, eval('fired'))
end)
it('no heap-use-after-free if target buffer deleted by autocommands', function()
local cur_buf = api.nvim_get_current_buf()
local new_buf = api.nvim_create_buf(true, true)
command('autocmd WinNew * ++once call nvim_buf_delete(' .. new_buf .. ', #{force: 1})')
api.nvim_open_win(new_buf, true, { split = 'left' })
eq(cur_buf, api.nvim_get_current_buf())
end)
it('checks if splitting disallowed', function()
command('split | autocmd WinEnter * ++once call nvim_open_win(0, 0, #{split: "right"})')
matches("E242: Can't split a window while closing another$", pcall_err(command, 'quit'))
command('only | autocmd BufHidden * ++once call nvim_open_win(0, 0, #{split: "left"})')
matches(
'E1159: Cannot split a window when closing the buffer$',
pcall_err(command, 'new | quit')
)
local w = api.nvim_get_current_win()
command(
'only | new | autocmd BufHidden * ++once call nvim_open_win(0, 0, #{split: "left", win: '
.. w
.. '})'
)
matches(
'E1159: Cannot split a window when closing the buffer$',
pcall_err(api.nvim_win_close, w, true)
)
-- OK when using window to different buffer than `win`s.
w = api.nvim_get_current_win()
command(
'only | autocmd BufHidden * ++once call nvim_open_win(0, 0, #{split: "left", win: '
.. w
.. '})'
)
command('new | quit')
end)
it('restores last known cursor position if BufWinEnter did not move it', function()
-- This test mostly exists to ensure BufWinEnter is executed before enter_buffer's epilogue.
local buf = api.nvim_get_current_buf()
insert([[
foo
bar baz .etc
i love autocommand bugs!
supercalifragilisticexpialidocious
marvim is actually a human
llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
]])
api.nvim_win_set_cursor(0, { 5, 2 })
command('set nostartofline | enew')
local new_win = api.nvim_open_win(buf, false, { split = 'left' })
eq({ 5, 2 }, api.nvim_win_get_cursor(new_win))
exec([[
only!
autocmd BufWinEnter * ++once normal! j6l
]])
new_win = api.nvim_open_win(buf, false, { split = 'left' })
eq({ 2, 6 }, api.nvim_win_get_cursor(new_win))
end)
it('does not block all win_set_buf autocommands if !enter and !noautocmd', function()
local new_buf = fn.bufadd('foobarbaz')
exec([[
let triggered = ""
autocmd BufReadCmd * ++once let triggered = bufname()
]])
api.nvim_open_win(new_buf, false, { split = 'left' })
eq('foobarbaz', eval('triggered'))
end)
it('sets error when no room', function()
matches('E36: Not enough room$', pcall_err(command, 'execute "split|"->repeat(&lines)'))
matches(
'E36: Not enough room$',
pcall_err(api.nvim_open_win, 0, true, { split = 'above', win = 0 })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_open_win, 0, true, { split = 'below', win = 0 })
)
end)
end) end)
describe('set_config', function() describe('set_config', function()
@@ -1471,6 +1773,15 @@ describe('API/win', function()
config = api.nvim_win_get_config(win) config = api.nvim_win_get_config(win)
eq('', config.relative) eq('', config.relative)
eq('below', config.split) eq('below', config.split)
eq(
"non-float with 'win' requires at least 'split' or 'vertical'",
pcall_err(api.nvim_win_set_config, 0, { win = 0 })
)
eq(
"non-float with 'win' requires at least 'split' or 'vertical'",
pcall_err(api.nvim_win_set_config, 0, { win = 0, relative = '' })
)
end) end)
it('creates top-level splits', function() it('creates top-level splits', function()
@@ -1663,6 +1974,474 @@ describe('API/win', function()
}, },
}, fn.winlayout()) }, fn.winlayout())
end) end)
it('closing new curwin when moving window to other tabpage works', function()
command('split | tabnew')
local t2_win = api.nvim_get_current_win()
command('tabfirst | autocmd WinEnter * ++once quit')
local t1_move_win = api.nvim_get_current_win()
-- win_set_config fails to switch away from "t1_move_win" because the WinEnter autocmd that
-- closed the window we're switched to returns us to "t1_move_win", as it filled the space.
eq(
'Failed to switch away from window ' .. t1_move_win,
pcall_err(api.nvim_win_set_config, t1_move_win, { win = t2_win, split = 'left' })
)
eq(t1_move_win, api.nvim_get_current_win())
command('split | split | autocmd WinEnter * ++once quit')
t1_move_win = api.nvim_get_current_win()
-- In this case, we closed the window that we got switched to, but doing so didn't switch us
-- back to "t1_move_win", which is fine.
api.nvim_win_set_config(t1_move_win, { win = t2_win, split = 'left' })
neq(t1_move_win, api.nvim_get_current_win())
end)
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()
command('split')
local t2_win3 = api.nvim_get_current_win()
command('tabfirst | autocmd WinEnter * ++once call nvim_win_close(' .. t2_win1 .. ', 1)')
local cur_win = api.nvim_get_current_win()
eq(
'Windows to split were closed',
pcall_err(api.nvim_win_set_config, 0, { win = t2_win1, split = 'left' })
)
eq(cur_win, api.nvim_get_current_win())
command('split | autocmd WinLeave * ++once quit!')
cur_win = api.nvim_get_current_win()
eq(
'Windows to split were closed',
pcall_err(api.nvim_win_set_config, 0, { win = t2_win2, split = 'left' })
)
neq(cur_win, api.nvim_get_current_win())
exec([[
split
autocmd WinLeave * ++once
\ call nvim_win_set_config(0, #{relative:'editor', row:0, col:0, width:5, height:5})
]])
cur_win = api.nvim_get_current_win()
eq(
'Floating state of windows to split changed',
pcall_err(api.nvim_win_set_config, 0, { win = t2_win3, split = 'left' })
)
eq('editor', api.nvim_win_get_config(0).relative)
eq(cur_win, api.nvim_get_current_win())
command('autocmd WinLeave * ++once wincmd J')
cur_win = api.nvim_get_current_win()
eq(
'Floating state of windows to split changed',
pcall_err(api.nvim_win_set_config, 0, { win = t2_win3, split = 'left' })
)
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.
exec(([[
autocmd WinLeave * ++once silent!
\ 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')
)
-- 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))
end)
it('expected autocmds when moving window to other tabpage', function()
local new_curwin = api.nvim_get_current_win()
command('split')
local win = api.nvim_get_current_win()
command('tabnew')
local parent = api.nvim_get_current_win()
exec([[
tabfirst
let result = []
autocmd WinEnter * let result += ["Enter", win_getid()]
autocmd WinLeave * let result += ["Leave", win_getid()]
autocmd WinNew * let result += ["New", win_getid()]
]])
api.nvim_win_set_config(0, { win = parent, split = 'left' })
-- Shouldn't see WinNew, as we're not creating any new windows, just moving existing ones.
eq({ 'Leave', win, 'Enter', new_curwin }, eval('result'))
end)
it('no autocmds when moving window within same 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()]
]])
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'))
end)
it('checks if splitting disallowed', function()
command('split | autocmd WinEnter * ++once call nvim_win_set_config(0, #{split: "right"})')
matches("E242: Can't split a window while closing another$", pcall_err(command, 'quit'))
command('autocmd BufHidden * ++once call nvim_win_set_config(0, #{split: "left"})')
matches(
'E1159: Cannot split a window when closing the buffer$',
pcall_err(command, 'new | quit')
)
-- OK when using window to different buffer.
local w = api.nvim_get_current_win()
command('autocmd BufHidden * ++once call nvim_win_set_config(' .. w .. ', #{split: "left"})')
command('new | quit')
end)
--- Returns a function to get information about the window layout, sizes and positions of a
--- tabpage.
local function define_tp_info_function()
exec_lua([[
function tp_info(tp)
return {
layout = vim.fn.winlayout(vim.api.nvim_tabpage_get_number(tp)),
pos_sizes = vim.tbl_map(
function(w)
local pos = vim.fn.win_screenpos(w)
return {
row = pos[1],
col = pos[2],
width = vim.fn.winwidth(w),
height = vim.fn.winheight(w)
}
end,
vim.api.nvim_tabpage_list_wins(tp)
)
}
end
]])
return function(tp)
return exec_lua('return tp_info(...)', tp)
end
end
it('attempt to move window with no room', function()
-- Fill the 2nd tabpage full of windows until we run out of room.
-- Use &laststatus=0 to ensure restoring missing statuslines doesn't affect things.
command('set laststatus=0 | tabnew')
matches('E36: Not enough room$', pcall_err(command, 'execute "split|"->repeat(&lines)'))
command('vsplit | wincmd | | wincmd p')
local t2 = api.nvim_get_current_tabpage()
local t2_cur_win = api.nvim_get_current_win()
local t2_top_split = fn.win_getid(1)
local t2_bot_split = fn.win_getid(fn.winnr('$'))
local t2_float = api.nvim_open_win(
0,
false,
{ relative = 'editor', row = 0, col = 0, width = 10, height = 10 }
)
local t2_float_config = api.nvim_win_get_config(t2_float)
local tp_info = define_tp_info_function()
local t2_info = tp_info(t2)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_top_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_top_split, split = 'below' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_bot_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_bot_split, split = 'below' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t2_float, { win = t2_top_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t2_float, { win = t2_top_split, split = 'below' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t2_float, { win = t2_bot_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t2_float, { win = t2_bot_split, split = 'below' })
)
eq(t2_cur_win, api.nvim_get_current_win())
eq(t2_info, tp_info(t2))
eq(t2_float_config, api.nvim_win_get_config(t2_float))
-- Try to move windows from the 1st tabpage to the 2nd.
command('tabfirst | split | wincmd _')
local t1 = api.nvim_get_current_tabpage()
local t1_cur_win = api.nvim_get_current_win()
local t1_float = api.nvim_open_win(
0,
false,
{ relative = 'editor', row = 5, col = 3, width = 7, height = 6 }
)
local t1_float_config = api.nvim_win_get_config(t1_float)
local t1_info = tp_info(t1)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_top_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_top_split, split = 'below' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_bot_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, 0, { win = t2_bot_split, split = 'below' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t1_float, { win = t2_top_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t1_float, { win = t2_top_split, split = 'below' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t1_float, { win = t2_bot_split, split = 'above' })
)
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t1_float, { win = t2_bot_split, split = 'below' })
)
eq(t1_cur_win, api.nvim_get_current_win())
eq(t1_info, tp_info(t1))
eq(t1_float_config, api.nvim_win_get_config(t1_float))
end)
it('attempt to move window from other tabpage with no room', function()
-- Fill up the 1st tabpage with horizontal splits, then create a 2nd with only a few. Go back
-- to the 1st and try to move windows from the 2nd (while it's non-current) to it. Check that
-- window positions and sizes in the 2nd are unchanged.
command('set laststatus=0')
matches('E36: Not enough room$', pcall_err(command, 'execute "split|"->repeat(&lines)'))
command('tab split')
local t2 = api.nvim_get_current_tabpage()
local t2_top = api.nvim_get_current_win()
command('belowright split')
local t2_mid_left = api.nvim_get_current_win()
command('belowright vsplit')
local t2_mid_right = api.nvim_get_current_win()
command('split | wincmd J')
local t2_bot = api.nvim_get_current_win()
local tp_info = define_tp_info_function()
local t2_info = tp_info(t2)
eq({
'col',
{
{ 'leaf', t2_top },
{
'row',
{
{ 'leaf', t2_mid_left },
{ 'leaf', t2_mid_right },
},
},
{ 'leaf', t2_bot },
},
}, t2_info.layout)
local function try_move_t2_wins_to_t1()
for _, w in ipairs({ t2_bot, t2_mid_left, t2_mid_right, t2_top }) do
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, w, { win = 0, split = 'below' })
)
eq(t2_info, tp_info(t2))
end
end
command('tabfirst')
try_move_t2_wins_to_t1()
-- Go to the 2nd tabpage to ensure nothing changes after win_comp_pos, last_status, .etc.
-- from enter_tabpage.
command('tabnext')
eq(t2_info, tp_info(t2))
-- Check things are fine with the global statusline too, for good measure.
-- Set it while the 2nd tabpage is current, so last_status runs for it.
command('set laststatus=3')
t2_info = tp_info(t2)
command('tabfirst')
try_move_t2_wins_to_t1()
end)
it('handles cmdwin and textlock restrictions', function()
command('tabnew')
local t2 = api.nvim_get_current_tabpage()
local t2_win = api.nvim_get_current_win()
command('tabfirst')
local t1_move_win = api.nvim_get_current_win()
command('split')
-- Can't move the cmdwin, or its old curwin to a different tabpage.
local old_curwin = api.nvim_get_current_win()
feed('q:')
eq(
'E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
pcall_err(api.nvim_win_set_config, 0, { split = 'left', win = t2_win })
)
eq(
'E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
pcall_err(api.nvim_win_set_config, old_curwin, { split = 'left', win = t2_win })
)
-- But we can move other windows.
api.nvim_win_set_config(t1_move_win, { split = 'left', win = t2_win })
eq(t2, api.nvim_win_get_tabpage(t1_move_win))
command('quit!')
-- Can't configure windows such that the cmdwin would become the only non-float.
command('only!')
feed('q:')
eq(
'E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
pcall_err(
api.nvim_win_set_config,
old_curwin,
{ relative = 'editor', row = 0, col = 0, width = 5, height = 5 }
)
)
-- old_curwin is now no longer the only other non-float, so we can make it floating now.
local t1_new_win = api.nvim_open_win(
api.nvim_create_buf(true, true),
false,
{ split = 'left', win = old_curwin }
)
api.nvim_win_set_config(
old_curwin,
{ relative = 'editor', row = 0, col = 0, width = 5, height = 5 }
)
eq('editor', api.nvim_win_get_config(old_curwin).relative)
-- ...which means we shouldn't be able to also make the new window floating too!
eq(
'E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
pcall_err(
api.nvim_win_set_config,
t1_new_win,
{ relative = 'editor', row = 0, col = 0, width = 5, height = 5 }
)
)
-- Nothing ought to stop us from making the cmdwin itself floating, though...
api.nvim_win_set_config(0, { relative = 'editor', row = 0, col = 0, width = 5, height = 5 })
eq('editor', api.nvim_win_get_config(0).relative)
-- We can't make our new window from before floating too, as it's now the only non-float.
eq(
'Cannot change last window into float',
pcall_err(
api.nvim_win_set_config,
t1_new_win,
{ relative = 'editor', row = 0, col = 0, width = 5, height = 5 }
)
)
command('quit!')
-- Can't switch away from window before moving it to a different tabpage during textlock.
exec(([[
new
call setline(1, 'foo')
setlocal debug=throw indentexpr=nvim_win_set_config(0,#{split:'left',win:%d})
]]):format(t2_win))
local cur_win = api.nvim_get_current_win()
matches(
'E565: Not allowed to change text or change window$',
pcall_err(command, 'normal! ==')
)
eq(cur_win, api.nvim_get_current_win())
end)
it('updates statusline when moving bottom split', function()
local screen = Screen.new(10, 10)
screen:set_default_attr_ids({
[0] = { bold = true, foreground = Screen.colors.Blue }, -- NonText
[1] = { bold = true, reverse = true }, -- StatusLine
})
screen:attach()
exec([[
set laststatus=0
belowright split
call nvim_win_set_config(0, #{split: 'above', win: win_getid(winnr('#'))})
]])
screen:expect([[
^ |
{0:~ }|*3
{1:[No Name] }|
|
{0:~ }|*3
|
]])
end)
it("updates tp_curwin of moved window's original tabpage", function()
local t1 = api.nvim_get_current_tabpage()
command('tab split | split')
local t2 = api.nvim_get_current_tabpage()
local t2_alt_win = api.nvim_get_current_win()
command('vsplit')
local t2_cur_win = api.nvim_get_current_win()
command('tabprevious')
eq(t2_cur_win, api.nvim_tabpage_get_win(t2))
-- tp_curwin is unchanged when moved within the same tabpage.
api.nvim_win_set_config(t2_cur_win, { split = 'left', win = t2_alt_win })
eq(t2_cur_win, api.nvim_tabpage_get_win(t2))
-- Also unchanged if the move failed.
command('let &winwidth = &columns | let &winminwidth = &columns')
matches(
'E36: Not enough room$',
pcall_err(api.nvim_win_set_config, t2_cur_win, { split = 'left', win = 0 })
)
eq(t2_cur_win, api.nvim_tabpage_get_win(t2))
command('set winminwidth& winwidth&')
-- But is changed if successfully moved to a different tabpage.
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))
-- Now do it for a float, which has different altwin logic.
command('tabnext')
t2_cur_win =
api.nvim_open_win(0, true, { relative = 'editor', row = 5, col = 5, width = 5, height = 5 })
eq(t2_alt_win, fn.win_getid(fn.winnr('#')))
command('tabprevious')
eq(t2_cur_win, api.nvim_tabpage_get_win(t2))
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))
end)
end) end)
describe('get_config', function() describe('get_config', function()

View File

@@ -3664,6 +3664,20 @@ describe('lua stdlib', function()
]] ]]
) )
end) end)
it('layout in current tabpage does not affect windows in others', function()
command('tab split')
local t2_move_win = api.nvim_get_current_win()
command('vsplit')
local t2_other_win = api.nvim_get_current_win()
command('tabprevious')
matches('E36: Not enough room$', pcall_err(command, 'execute "split|"->repeat(&lines)'))
command('vsplit')
-- Without vim-patch:8.2.3862, this gives E36, despite just the 1st tabpage being full.
exec_lua('vim.api.nvim_win_call(..., function() vim.cmd.wincmd "J" end)', t2_move_win)
eq({ 'col', { { 'leaf', t2_other_win }, { 'leaf', t2_move_win } } }, fn.winlayout(2))
end)
end) end)
describe('vim.iconv', function() describe('vim.iconv', function()

View File

@@ -550,6 +550,43 @@ describe('float window', function()
eq({ w0 }, api.nvim_list_wins()) eq({ w0 }, api.nvim_list_wins())
end) end)
it('win_splitmove() can move float into a split', function()
command('split')
eq({'col', {{'leaf', 1001}, {'leaf', 1000}}}, fn.winlayout())
local win1 = api.nvim_open_win(0, true, {relative = 'editor', row = 1, col = 1, width = 5, height = 5})
fn.win_splitmove(win1, 1001, {vertical = true})
eq({'col', {{'row', {{'leaf', win1}, {'leaf', 1001}}}, {'leaf', 1000}}}, fn.winlayout())
eq('', api.nvim_win_get_config(win1).relative)
-- Should be unable to create a split relative to a float, though.
local win2 = api.nvim_open_win(0, true, {relative = 'editor', row = 1, col = 1, width = 5, height = 5})
eq('Vim:E957: Invalid window number', pcall_err(fn.win_splitmove, win1, win2, {vertical = true}))
end)
it('tp_curwin updated if external window is moved into split', function()
local screen = Screen.new(20, 7)
screen:attach { ext_multigrid = true }
command('tabnew')
local external_win = api.nvim_open_win(0, true, {external = true, width = 5, height = 5})
eq(external_win, api.nvim_get_current_win())
eq(2, fn.tabpagenr())
command('tabfirst')
api.nvim_set_current_win(external_win)
eq(external_win, api.nvim_get_current_win())
eq(1, fn.tabpagenr())
command('wincmd J')
eq(external_win, api.nvim_get_current_win())
eq(false, api.nvim_win_get_config(external_win).external)
command('tabnext')
eq(2, fn.tabpagenr())
neq(external_win, api.nvim_get_current_win())
screen:detach()
end)
describe('with only one tabpage,', function() describe('with only one tabpage,', function()
local float_opts = {relative = 'editor', row = 1, col = 1, width = 1, height = 1} local float_opts = {relative = 'editor', row = 1, col = 1, width = 1, height = 1}
local old_buf, old_win local old_buf, old_win
@@ -836,6 +873,39 @@ describe('float window', function()
end) end)
end) end)
describe(':close on non-float with floating windows', function()
it('does not quit Nvim if BufWinLeave makes it the only non-float', function()
exec([[
let firstbuf = bufnr()
new
let midwin = win_getid()
new
setlocal bufhidden=wipe
call nvim_win_set_config(midwin,
\ #{relative: 'editor', row: 5, col: 5, width: 5, height: 5})
autocmd BufWinLeave * ++once exe firstbuf .. 'bwipe!'
]])
eq('Vim(close):E855: Autocommands caused command to abort', pcall_err(command, 'close'))
assert_alive()
end)
pending('does not crash if BufWinLeave makes it the only non-float in tabpage', function()
exec([[
tabnew
let firstbuf = bufnr()
new
let midwin = win_getid()
new
setlocal bufhidden=wipe
call nvim_win_set_config(midwin,
\ #{relative: 'editor', row: 5, col: 5, width: 5, height: 5})
autocmd BufWinLeave * ++once exe firstbuf .. 'bwipe!'
]])
eq('Vim(close):E855: Autocommands caused command to abort', pcall_err(command, 'close'))
assert_alive()
end)
end)
local function with_ext_multigrid(multigrid) local function with_ext_multigrid(multigrid)
local screen, attrs local screen, attrs
before_each(function() before_each(function()
@@ -9101,6 +9171,22 @@ describe('float window', function()
]]} ]]}
end end
end) end)
it('attempt to turn into split with no room', function()
eq('Vim(split):E36: Not enough room', pcall_err(command, 'execute "split |"->repeat(&lines)'))
command('vsplit | wincmd | | wincmd p')
api.nvim_open_win(0, true, {relative = "editor", row = 0, col = 0, width = 5, height = 5})
local config = api.nvim_win_get_config(0)
eq('editor', config.relative)
local layout = fn.winlayout()
local restcmd = fn.winrestcmd()
eq('Vim(wincmd):E36: Not enough room', pcall_err(command, 'wincmd K'))
eq('Vim(wincmd):E36: Not enough room', pcall_err(command, 'wincmd J'))
eq(layout, fn.winlayout())
eq(restcmd, fn.winrestcmd())
eq(config, api.nvim_win_get_config(0))
end)
end end
describe('with ext_multigrid', function() describe('with ext_multigrid', function()

View File

@@ -11,6 +11,7 @@ local exec = helpers.exec
local exec_lua = helpers.exec_lua local exec_lua = helpers.exec_lua
local eval = helpers.eval local eval = helpers.eval
local sleep = vim.uv.sleep local sleep = vim.uv.sleep
local pcall_err = helpers.pcall_err
local mousemodels = { 'extend', 'popup', 'popup_setpos' } local mousemodels = { 'extend', 'popup', 'popup_setpos' }
@@ -474,6 +475,25 @@ describe('global statusline', function()
| |
]]) ]])
end) end)
it('horizontal separators unchanged when failing to split-move window', function()
exec([[
botright split
let &winwidth = &columns
let &winminwidth = &columns
]])
eq('Vim(wincmd):E36: Not enough room', pcall_err(command, 'wincmd L'))
command('mode')
screen:expect([[
|
{1:~ }|*5
────────────────────────────────────────────────────────────|
^ |
{1:~ }|*6
{2:[No Name] 0,0-1 All}|
|
]])
end)
end) end)
it('statusline does not crash if it has Arabic characters #19447', function() it('statusline does not crash if it has Arabic characters #19447', function()

View File

@@ -3,6 +3,7 @@
source view_util.vim source view_util.vim
source check.vim source check.vim
source vim9.vim source vim9.vim
source term_util.vim
func NestedEval() func NestedEval()
let nested = execute('echo "nested\nlines"') let nested = execute('echo "nested\nlines"')
@@ -177,6 +178,27 @@ func Test_win_execute_visual_redraw()
bwipe! bwipe!
endfunc endfunc
func Test_win_execute_on_startup()
CheckRunVimInTerminal
let lines =<< trim END
vim9script
[repeat('x', &columns)]->writefile('Xfile1')
silent tabedit Xfile2
var id = win_getid()
silent tabedit Xfile3
autocmd VimEnter * win_execute(id, 'close')
END
call writefile(lines, 'XwinExecute')
let buf = RunVimInTerminal('-p Xfile1 -Nu XwinExecute', {})
" this was crashing on exit with EXITFREE defined
call StopVimInTerminal(buf)
call delete('XwinExecute')
call delete('Xfile1')
endfunc
func Test_execute_cmd_with_null() func Test_execute_cmd_with_null()
call assert_equal("", execute(v:_null_string)) call assert_equal("", execute(v:_null_string))
call assert_equal("", execute(v:_null_list)) call assert_equal("", execute(v:_null_list))

View File

@@ -963,4 +963,42 @@ func Test_smoothscroll_insert_bottom()
call StopVimInTerminal(buf) call StopVimInTerminal(buf)
endfunc endfunc
func Test_smoothscroll_in_zero_width_window()
set cpo+=n number smoothscroll
set winwidth=99999 winminwidth=0
vsplit
call assert_equal(0, winwidth(winnr('#')))
call win_execute(win_getid(winnr('#')), "norm! \<C-Y>")
only!
set winwidth& winminwidth&
set cpo-=n nonumber nosmoothscroll
endfunc
func Test_smoothscroll_textoff_small_winwidth()
set smoothscroll number
call setline(1, 'llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch')
vsplit
let textoff = getwininfo(win_getid())[0].textoff
execute 'vertical resize' textoff + 1
redraw
call assert_equal(0, winsaveview().skipcol)
execute "normal! 0\<C-E>"
redraw
call assert_equal(1, winsaveview().skipcol)
execute 'vertical resize' textoff - 1
" This caused a signed integer overflow.
redraw
call assert_equal(1, winsaveview().skipcol)
execute 'vertical resize' textoff
" This caused an infinite loop.
redraw
call assert_equal(1, winsaveview().skipcol)
%bw!
set smoothscroll& number&
endfunc
" vim: shiftwidth=2 sts=2 expandtab " vim: shiftwidth=2 sts=2 expandtab

View File

@@ -272,6 +272,16 @@ func Test_window_split_no_room()
for s in range(1, hor_split_count) | split | endfor for s in range(1, hor_split_count) | split | endfor
call assert_fails('split', 'E36:') call assert_fails('split', 'E36:')
botright vsplit
wincmd |
let layout = winlayout()
let restcmd = winrestcmd()
call assert_fails('wincmd J', 'E36:')
call assert_fails('wincmd K', 'E36:')
call assert_equal(layout, winlayout())
call assert_equal(restcmd, winrestcmd())
only
" N vertical windows need >= 2*(N - 1) + 1 columns: " N vertical windows need >= 2*(N - 1) + 1 columns:
" - 1 column + 1 separator for each window (except last window) " - 1 column + 1 separator for each window (except last window)
" - 1 column for the last window which does not have separator " - 1 column for the last window which does not have separator
@@ -284,7 +294,39 @@ func Test_window_split_no_room()
for s in range(1, ver_split_count) | vsplit | endfor for s in range(1, ver_split_count) | vsplit | endfor
call assert_fails('vsplit', 'E36:') call assert_fails('vsplit', 'E36:')
split
wincmd |
let layout = winlayout()
let restcmd = winrestcmd()
call assert_fails('wincmd H', 'E36:')
call assert_fails('wincmd L', 'E36:')
call assert_equal(layout, winlayout())
call assert_equal(restcmd, winrestcmd())
" Check that the last statusline isn't lost.
" Set its window's width to 2 for the test.
wincmd j
set laststatus=0 winminwidth=0
vertical resize 2
set winminwidth&
call setwinvar(winnr('k'), '&statusline', '@#')
let last_stl_row = win_screenpos(0)[0] - 1
redraw
call assert_equal('@#|', GetScreenStr(last_stl_row))
call assert_equal('~ |', GetScreenStr(&lines - &cmdheight))
let restcmd = winrestcmd()
call assert_fails('wincmd H', 'E36:')
call assert_fails('wincmd L', 'E36:')
call assert_equal(layout, winlayout())
call assert_equal(restcmd, winrestcmd())
call setwinvar(winnr('k'), '&statusline', '=-')
redraw
call assert_equal('=-|', GetScreenStr(last_stl_row))
call assert_equal('~ |', GetScreenStr(&lines - &cmdheight))
%bw! %bw!
set laststatus&
endfunc endfunc
func Test_window_exchange() func Test_window_exchange()
@@ -1024,6 +1066,19 @@ func Test_win_splitmove()
leftabove split b leftabove split b
leftabove vsplit c leftabove vsplit c
leftabove split d leftabove split d
" win_splitmove doesn't actually create or close any windows, so expect an
" unchanged winid and no WinNew/WinClosed events, like :wincmd H/J/K/L.
let s:triggered = []
augroup WinSplitMove
au!
" Nvim: WinNewPre not ported yet. Also needs full port of v9.1.0117 to pass.
" au WinNewPre * let s:triggered += ['WinNewPre']
au WinNew * let s:triggered += ['WinNew', win_getid()]
au WinClosed * let s:triggered += ['WinClosed', str2nr(expand('<afile>'))]
augroup END
let winid = win_getid()
call assert_equal(0, win_splitmove(winnr(), winnr('l'))) call assert_equal(0, win_splitmove(winnr(), winnr('l')))
call assert_equal(bufname(winbufnr(1)), 'c') call assert_equal(bufname(winbufnr(1)), 'c')
call assert_equal(bufname(winbufnr(2)), 'd') call assert_equal(bufname(winbufnr(2)), 'd')
@@ -1046,6 +1101,11 @@ func Test_win_splitmove()
call assert_equal(bufname(winbufnr(3)), 'a') call assert_equal(bufname(winbufnr(3)), 'a')
call assert_equal(bufname(winbufnr(4)), 'd') call assert_equal(bufname(winbufnr(4)), 'd')
call assert_fails('call win_splitmove(winnr(), winnr("k"), v:_null_dict)', 'E1297:') call assert_fails('call win_splitmove(winnr(), winnr("k"), v:_null_dict)', 'E1297:')
call assert_equal([], s:triggered)
call assert_equal(winid, win_getid())
unlet! s:triggered
au! WinSplitMove
only | bd only | bd
call assert_fails('call win_splitmove(winnr(), 123)', 'E957:') call assert_fails('call win_splitmove(winnr(), 123)', 'E957:')
@@ -1055,18 +1115,53 @@ func Test_win_splitmove()
tabnew tabnew
call assert_fails('call win_splitmove(1, win_getid(1, 1))', 'E957:') call assert_fails('call win_splitmove(1, win_getid(1, 1))', 'E957:')
tabclose tabclose
endfunc
func Test_floatwin_splitmove() split
vsplit augroup WinSplitMove
let win2 = win_getid() au!
let popup_winid = nvim_open_win(0, 0, {'relative': 'win', au WinEnter * ++once call win_gotoid(win_getid(winnr('#')))
\ 'row': 3, 'col': 3, 'width': 12, 'height': 3}) augroup END
call assert_fails('call win_splitmove(popup_winid, win2)', 'E957:') call assert_fails('call win_splitmove(winnr(), winnr("#"))', 'E855:')
call assert_fails('call win_splitmove(win2, popup_winid)', 'E957:')
call nvim_win_close(popup_winid, 1) augroup WinSplitMove
bwipe au!
au WinLeave * ++once quit
augroup END
call assert_fails('call win_splitmove(winnr(), winnr("#"))', 'E855:')
split
split
augroup WinSplitMove
au!
au WinEnter * ++once let s:triggered = v:true
\| call assert_fails('call win_splitmove(winnr("$"), winnr())', 'E242:')
augroup END
quit
call assert_equal(v:true, s:triggered)
unlet! s:triggered
new
augroup WinSplitMove
au!
au BufHidden * ++once let s:triggered = v:true
\| call assert_fails('call win_splitmove(winnr("#"), winnr())', 'E1159:')
augroup END
hide
call assert_equal(v:true, s:triggered)
unlet! s:triggered
split
let close_win = winnr('#')
augroup WinSplitMove
au!
au WinEnter * ++once quit!
augroup END
call win_splitmove(close_win, winnr())
call assert_equal(0, win_id2win(close_win))
au! WinSplitMove
augroup! WinSplitMove
%bw!
endfunc endfunc
" Test for the :only command " Test for the :only command
@@ -2006,24 +2101,97 @@ func Test_new_help_window_on_error()
call assert_equal(expand("<cword>"), "'mod'") call assert_equal(expand("<cword>"), "'mod'")
endfunc endfunc
func Test_smoothscroll_in_zero_width_window() func Test_splitmove_flatten_frame()
let save_lines = &lines split
let save_columns = &columns vsplit
winsize 0 24 wincmd L
set cpo+=n let layout = winlayout()
exe "noremap 0 \<C-W>n\<C-W>L" wincmd K
norm 000000 wincmd L
set number smoothscroll call assert_equal(winlayout(), layout)
exe "norm \<C-Y>"
only! only!
let &lines = save_lines
let &columns = save_columns
set cpo-=n
unmap 0
set nonumber nosmoothscroll
endfunc endfunc
func Test_autocmd_window_force_room()
" Open as many windows as possible
while v:true
try
split
catch /E36:/
break
endtry
endwhile
while v:true
try
vsplit
catch /E36:/
break
endtry
endwhile
wincmd j
vsplit
call assert_fails('wincmd H', 'E36:')
call assert_fails('wincmd J', 'E36:')
call assert_fails('wincmd K', 'E36:')
call assert_fails('wincmd L', 'E36:')
edit unload me
enew
bunload! unload\ me
augroup AucmdWinForceRoom
au!
au BufEnter * ++once let s:triggered = v:true
\| call assert_equal('autocmd', win_gettype())
augroup END
let layout = winlayout()
let restcmd = winrestcmd()
" bufload opening the autocommand window shouldn't give E36.
call bufload('unload me')
call assert_equal(v:true, s:triggered)
call assert_equal(winlayout(), layout)
call assert_equal(winrestcmd(), restcmd)
unlet! s:triggered
au! AucmdWinForceRoom
augroup! AucmdWinForceRoom
%bw!
endfunc
func Test_win_gotoid_splitmove_textlock_cmdwin()
call setline(1, 'foo')
new
let curwin = win_getid()
call setline(1, 'bar')
set debug+=throw indentexpr=win_gotoid(win_getid(winnr('#')))
call assert_fails('normal! ==', 'E565:')
call assert_equal(curwin, win_getid())
" No error if attempting to switch to curwin; nothing happens.
set indentexpr=assert_equal(1,win_gotoid(win_getid()))
normal! ==
call assert_equal(curwin, win_getid())
set indentexpr=win_splitmove(winnr('#'),winnr())
call assert_fails('normal! ==', 'E565:')
call assert_equal(curwin, win_getid())
%bw!
set debug-=throw indentexpr&
call feedkeys('q:'
\ .. ":call assert_fails('call win_splitmove(winnr(''#''), winnr())', 'E11:')\<CR>"
\ .. ":call assert_equal('command', win_gettype())\<CR>"
\ .. ":call assert_equal('', win_gettype(winnr('#')))\<CR>", 'ntx')
call feedkeys('q:'
\ .. ":call assert_fails('call win_gotoid(win_getid(winnr(''#'')))', 'E11:')\<CR>"
"\ No error if attempting to switch to curwin; nothing happens.
\ .. ":call assert_equal(1, win_gotoid(win_getid()))\<CR>"
\ .. ":call assert_equal('command', win_gettype())\<CR>"
\ .. ":call assert_equal('', win_gettype(winnr('#')))\<CR>", 'ntx')
endfunc
" vim: shiftwidth=2 sts=2 expandtab " vim: shiftwidth=2 sts=2 expandtab