feat(api): make nvim_open_win support non-floating windows (#25550)

Adds support to `nvim_open_win` and `nvim_win_set_config` for creating
and manipulating split (non-floating) windows.
This commit is contained in:
Will Hopkins 2024-01-31 19:43:35 -08:00 committed by GitHub
parent 8fa67fdae5
commit 6bba4beced
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1129 additions and 141 deletions

View File

@ -3093,18 +3093,28 @@ nvim_win_text_height({window}, {*opts}) *nvim_win_text_height()*
Win_Config Functions *api-win_config*
nvim_open_win({buffer}, {enter}, {*config}) *nvim_open_win()*
Open a new window.
Opens a new split window, or a floating window if `relative` is specified,
or an external window (managed by the UI) if `external` is specified.
Currently this is used to open floating and external windows. Floats are
windows that are drawn above the split layout, at some anchor position in
some other window. Floats can be drawn internally or by external GUI with
the |ui-multigrid| extension. External windows are only supported with
multigrid GUIs, and are displayed as separate top-level windows.
Floats are windows that are drawn above the split layout, at some anchor
position in some other window. Floats can be drawn internally or by
external GUI with the |ui-multigrid| extension. External windows are only
supported with multigrid GUIs, and are displayed as separate top-level
windows.
For a general overview of floats, see |api-floatwin|.
Exactly one of `external` and `relative` must be specified. The `width`
and `height` of the new window must be specified.
The `width` and `height` of the new window must be specified when opening
a floating window, but are optional for normal windows.
If `relative` and `external` are omitted, a normal "split" window is
created. The `win` property determines which window will be split. If no
`win` is provided or `win == 0`, a window will be created adjacent to the
current window. If -1 is provided, a top-level split will be created.
`vertical` and `split` are only valid for normal windows, and are used to
control split direction. For `vertical`, the exact direction is determined
by |'splitright'| and |'splitbelow'|. Split windows cannot have
`bufpos`/`row`/`col`/`border`/`title`/`footer` properties.
With relative=editor (row=0,col=0) refers to the top-left corner of the
screen-grid and (row=Lines-1,col=Columns-1) refers to the bottom-right
@ -3127,6 +3137,13 @@ nvim_open_win({buffer}, {enter}, {*config}) *nvim_open_win()*
{relative='win', width=12, height=3, bufpos={100,10}})
<
Example (Lua): vertical split left of the current window >lua
vim.api.nvim_open_win(0, false, {
split = 'left',
win = 0
})
<
Attributes: ~
not allowed when |textlock| is active
@ -3142,7 +3159,8 @@ nvim_open_win({buffer}, {enter}, {*config}) *nvim_open_win()*
• "cursor" Cursor position in current window.
• "mouse" Mouse position
• win: |window-ID| for relative="win".
• win: |window-ID| window to split, or relative window when
creating a float (relative="win").
• anchor: Decides which corner of the float to place at
(row,col):
• "NW" northwest (default)
@ -3239,6 +3257,8 @@ nvim_open_win({buffer}, {enter}, {*config}) *nvim_open_win()*
• fixed: If true when anchor is NW or SW, the float window
would be kept fixed even if the window would be truncated.
• hide: If true the floating window will be hidden.
• vertical: Split vertically |:vertical|.
• split: Split direction: "left", "right", "above", "below".
Return: ~
Window handle, or 0 on error

View File

@ -406,6 +406,9 @@ The following changes to existing APIs or features add new behavior.
• |:checkhealth| buffer can now be opened in a split window using modifiers like
|:vertical|, |:horizontal| and |:botright|.
• |nvim_open_win()| and |nvim_win_set_config()| now support opening normal (split)
windows, and moving floating windows into split windows.
==============================================================================
REMOVED FEATURES *news-removed*

View File

@ -1483,15 +1483,24 @@ function vim.api.nvim_notify(msg, log_level, opts) end
--- @return integer
function vim.api.nvim_open_term(buffer, opts) end
--- Open a new window.
--- Currently this is used to open floating and external windows. Floats are
--- windows that are drawn above the split layout, at some anchor position in
--- some other window. Floats can be drawn internally or by external GUI with
--- the `ui-multigrid` extension. External windows are only supported with
--- multigrid GUIs, and are displayed as separate top-level windows.
--- Opens a new split window, or a floating window if `relative` is specified,
--- or an external window (managed by the UI) if `external` is specified.
--- Floats are windows that are drawn above the split layout, at some anchor
--- position in some other window. Floats can be drawn internally or by
--- external GUI with the `ui-multigrid` extension. External windows are only
--- supported with multigrid GUIs, and are displayed as separate top-level
--- windows.
--- For a general overview of floats, see `api-floatwin`.
--- Exactly one of `external` and `relative` must be specified. The `width`
--- and `height` of the new window must be specified.
--- The `width` and `height` of the new window must be specified when opening
--- a floating window, but are optional for normal windows.
--- If `relative` and `external` are omitted, a normal "split" window is
--- created. The `win` property determines which window will be split. If no
--- `win` is provided or `win == 0`, a window will be created adjacent to the
--- current window. If -1 is provided, a top-level split will be created.
--- `vertical` and `split` are only valid for normal windows, and are used to
--- control split direction. For `vertical`, the exact direction is determined
--- by `'splitright'` and `'splitbelow'`. Split windows cannot have
--- `bufpos`/`row`/`col`/`border`/`title`/`footer` properties.
--- With relative=editor (row=0,col=0) refers to the top-left corner of the
--- screen-grid and (row=Lines-1,col=Columns-1) refers to the bottom-right
--- corner. Fractional values are allowed, but the builtin implementation
@ -1515,6 +1524,15 @@ function vim.api.nvim_open_term(buffer, opts) end
--- {relative='win', width=12, height=3, bufpos={100,10}})
--- ```
---
--- Example (Lua): vertical split left of the current window
---
--- ```lua
--- vim.api.nvim_open_win(0, false, {
--- split = 'left',
--- win = 0
--- })
--- ```
---
--- @param buffer integer Buffer to display, or 0 for current buffer
--- @param enter boolean Enter the window (make it the current window)
--- @param config vim.api.keyset.float_config Map defining the window configuration. Keys:
@ -1526,7 +1544,8 @@ function vim.api.nvim_open_term(buffer, opts) end
--- • "cursor" Cursor position in current window.
--- • "mouse" Mouse position
---
--- • win: `window-ID` for relative="win".
--- • win: `window-ID` window to split, or relative window when
--- creating a float (relative="win").
--- • anchor: Decides which corner of the float to place at
--- (row,col):
--- • "NW" northwest (default)
@ -1623,6 +1642,8 @@ function vim.api.nvim_open_term(buffer, opts) end
--- • fixed: If true when anchor is NW or SW, the float window
--- would be kept fixed even if the window would be truncated.
--- • hide: If true the floating window will be hidden.
--- • vertical: Split vertically `:vertical`.
--- • split: Split direction: "left", "right", "above", "below".
--- @return integer
function vim.api.nvim_open_win(buffer, enter, config) end

View File

@ -118,10 +118,12 @@ error('Cannot require a meta file')
--- @field height? integer
--- @field anchor? string
--- @field relative? string
--- @field split? string
--- @field win? integer
--- @field bufpos? any[]
--- @field external? boolean
--- @field focusable? boolean
--- @field vertical? boolean
--- @field zindex? integer
--- @field border? any
--- @field title? any

View File

@ -115,10 +115,12 @@ typedef struct {
Integer height;
String anchor;
String relative;
String split;
Window win;
Array bufpos;
Boolean external;
Boolean focusable;
Boolean vertical;
Integer zindex;
Object border;
Object title;

View File

@ -7,6 +7,7 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/dispatch.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/tabpage.h"
#include "nvim/api/win_config.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
@ -15,6 +16,8 @@
#include "nvim/decoration.h"
#include "nvim/decoration_defs.h"
#include "nvim/drawscreen.h"
#include "nvim/eval/window.h"
#include "nvim/extmark_defs.h"
#include "nvim/globals.h"
#include "nvim/grid_defs.h"
#include "nvim/highlight_group.h"
@ -22,12 +25,15 @@
#include "nvim/mbyte.h"
#include "nvim/memory.h"
#include "nvim/option.h"
#include "nvim/option_vars.h"
#include "nvim/pos_defs.h"
#include "nvim/strings.h"
#include "nvim/syntax.h"
#include "nvim/types_defs.h"
#include "nvim/ui.h"
#include "nvim/ui_compositor.h"
#include "nvim/ui_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
#include "nvim/winfloat.h"
@ -35,9 +41,9 @@
# include "api/win_config.c.generated.h"
#endif
/// Open a new window.
/// Opens a new split window, or a floating window if `relative` is specified,
/// or an external window (managed by the UI) if `external` is specified.
///
/// Currently this is used to open floating and external windows.
/// Floats are windows that are drawn above the split layout, at some anchor
/// position in some other window. Floats can be drawn internally or by external
/// GUI with the |ui-multigrid| extension. External windows are only supported
@ -45,8 +51,17 @@
///
/// For a general overview of floats, see |api-floatwin|.
///
/// Exactly one of `external` and `relative` must be specified. The `width` and
/// `height` of the new window must be specified.
/// The `width` and `height` of the new window must be specified when opening
/// a floating window, but are optional for normal windows.
///
/// If `relative` and `external` are omitted, a normal "split" window is created.
/// The `win` property determines which window will be split. If no `win` is
/// provided or `win == 0`, a window will be created adjacent to the current window.
/// If -1 is provided, a top-level split will be created. `vertical` and `split` are
/// only valid for normal windows, and are used to control split direction. For `vertical`,
/// the exact direction is determined by |'splitright'| and |'splitbelow'|.
/// Split windows cannot have `bufpos`/`row`/`col`/`border`/`title`/`footer`
/// properties.
///
/// With relative=editor (row=0,col=0) refers to the top-left corner of the
/// screen-grid and (row=Lines-1,col=Columns-1) refers to the bottom-right
@ -73,6 +88,15 @@
/// {relative='win', width=12, height=3, bufpos={100,10}})
/// ```
///
/// Example (Lua): vertical split left of the current window
///
/// ```lua
/// vim.api.nvim_open_win(0, false, {
/// split = 'left',
/// win = 0
/// })
/// ```
///
/// @param buffer Buffer to display, or 0 for current buffer
/// @param enter Enter the window (make it the current window)
/// @param config Map defining the window configuration. Keys:
@ -82,7 +106,8 @@
/// - "win" Window given by the `win` field, or current window.
/// - "cursor" Cursor position in current window.
/// - "mouse" Mouse position
/// - win: |window-ID| for relative="win".
/// - win: |window-ID| window to split, or relative window when creating a
/// float (relative="win").
/// - anchor: Decides which corner of the float to place at (row,col):
/// - "NW" northwest (default)
/// - "NE" northeast
@ -169,13 +194,14 @@
/// - fixed: If true when anchor is NW or SW, the float window
/// would be kept fixed even if the window would be truncated.
/// - hide: If true the floating window will be hidden.
/// - vertical: Split vertically |:vertical|.
/// - split: Split direction: "left", "right", "above", "below".
///
/// @param[out] err Error details, if any
///
/// @return Window handle, or 0 on error
Window nvim_open_win(Buffer buffer, Boolean enter, Dict(float_config) *config, Error *err)
FUNC_API_SINCE(6)
FUNC_API_TEXTLOCK_ALLOW_CMDWIN
FUNC_API_SINCE(6) FUNC_API_TEXTLOCK_ALLOW_CMDWIN
{
buf_T *buf = find_buffer_by_handle(buffer, err);
if (!buf) {
@ -190,22 +216,67 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(float_config) *config, E
if (!parse_float_config(config, &fconfig, false, true, err)) {
return 0;
}
win_T *wp = win_new_float(NULL, false, fconfig, err);
if (!wp) {
bool is_split = HAS_KEY(config, float_config, split) || HAS_KEY(config, float_config, vertical);
win_T *wp = NULL;
tabpage_T *tp = curtab;
if (is_split) {
win_T *parent = NULL;
if (!HAS_KEY(config, float_config, win) || config->win != -1) {
parent = find_window_by_handle(fconfig.window, err);
if (!parent) {
// find_window_by_handle has already set the error
return 0;
} else if (parent->w_floating) {
api_set_error(err, kErrorTypeException, "Cannot split a floating window");
return 0;
}
}
if (HAS_KEY(config, float_config, vertical) && !HAS_KEY(config, float_config, split)) {
if (config->vertical) {
fconfig.split = p_spr ? kWinSplitRight : kWinSplitLeft;
} else {
fconfig.split = p_sb ? kWinSplitBelow : kWinSplitAbove;
}
}
int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER;
if (parent == NULL) {
wp = win_split_ins(0, flags, NULL, 0);
} else {
tp = win_find_tabpage(parent);
switchwin_T switchwin;
// `parent` is valid in `tp`, so switch_win should not fail.
const int result = switch_win(&switchwin, parent, tp, true);
(void)result;
assert(result == OK);
wp = win_split_ins(0, flags, NULL, 0);
restore_win(&switchwin, true);
}
if (wp) {
wp->w_float_config = fconfig;
}
} else {
wp = win_new_float(NULL, false, fconfig, err);
}
if (!wp) {
api_set_error(err, kErrorTypeException, "Failed to create window");
return 0;
}
switchwin_T switchwin;
if (switch_win_noblock(&switchwin, wp, tp, true) == OK) {
apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf);
}
restore_win_noblock(&switchwin, true);
if (enter) {
win_enter(wp, false);
goto_tabpage_win(tp, wp);
}
// autocmds in win_enter or win_set_buf below may close the window
if (win_valid(wp) && buffer > 0) {
Boolean noautocmd = !enter || fconfig.noautocmd;
win_set_buf(wp, buf, noautocmd, err);
if (!fconfig.noautocmd) {
apply_autocmds(EVENT_WINNEW, NULL, NULL, false, buf);
if (win_valid_any_tab(wp) && buf != wp->w_buffer) {
win_set_buf(wp, buf, !enter || fconfig.noautocmd, err);
}
}
if (!win_valid(wp)) {
if (!win_valid_any_tab(wp)) {
api_set_error(err, kErrorTypeException, "Window was closed immediately");
return 0;
}
@ -217,6 +288,36 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(float_config) *config, E
return wp->handle;
}
static WinSplit win_split_dir(win_T *win)
{
if (win->w_frame == NULL || win->w_frame->fr_parent == NULL) {
return kWinSplitLeft;
}
char layout = win->w_frame->fr_parent->fr_layout;
if (layout == FR_COL) {
return win->w_frame->fr_next ? kWinSplitAbove : kWinSplitBelow;
} else {
return win->w_frame->fr_next ? kWinSplitLeft : kWinSplitRight;
}
}
static int win_split_flags(WinSplit split, bool toplevel)
{
int flags = 0;
if (split == kWinSplitAbove || split == kWinSplitBelow) {
flags |= WSP_HOR;
} else {
flags |= WSP_VERT;
}
if (split == kWinSplitAbove || split == kWinSplitLeft) {
flags |= toplevel ? WSP_TOP : WSP_ABOVE;
} else {
flags |= toplevel ? WSP_BOT : WSP_BELOW;
}
return flags;
}
/// Configures window layout. Currently only for floating and external windows
/// (including changing a split window to those layouts).
///
@ -236,18 +337,195 @@ void nvim_win_set_config(Window window, Dict(float_config) *config, Error *err)
if (!win) {
return;
}
bool new_float = !win->w_floating;
tabpage_T *win_tp = win_find_tabpage(win);
bool was_split = !win->w_floating;
bool has_split = HAS_KEY(config, float_config, split);
bool has_vertical = HAS_KEY(config, float_config, vertical);
// reuse old values, if not overridden
FloatConfig fconfig = new_float ? FLOAT_CONFIG_INIT : win->w_float_config;
FloatConfig fconfig = win->w_float_config;
if (!parse_float_config(config, &fconfig, !new_float, false, err)) {
bool to_split = (!HAS_KEY(config, float_config, relative) || striequal(config->relative.data, ""))
&& ((!HAS_KEY(config, float_config, external) && !fconfig.external)
|| !config->external)
&& (has_split || has_vertical || was_split);
if (!parse_float_config(config, &fconfig, !was_split || to_split, false, err)) {
return;
}
if (new_float) {
if (was_split && !to_split) {
if (!win_new_float(win, false, fconfig, err)) {
return;
}
redraw_later(win, UPD_NOT_VALID);
} else if (to_split) {
win_T *parent = NULL;
if (!HAS_KEY(config, float_config, win) || config->win != -1) {
parent = find_window_by_handle(fconfig.window, err);
if (!parent) {
return;
} else if (parent->w_floating) {
api_set_error(err, kErrorTypeException, "Cannot split a floating window");
return;
}
}
WinSplit old_split = win_split_dir(win);
if (has_vertical && !has_split) {
if (config->vertical) {
if (old_split == kWinSplitRight || p_spr) {
fconfig.split = kWinSplitRight;
} else {
fconfig.split = kWinSplitLeft;
}
} else {
if (old_split == kWinSplitBelow || p_sb) {
fconfig.split = kWinSplitBelow;
} else {
fconfig.split = kWinSplitAbove;
}
}
}
win->w_float_config = fconfig;
// If there's no vertical or split set, or if the split is the same as the old split,
// then we can just change the size of the window.
if ((!has_vertical && !has_split)
|| (was_split
&& !HAS_KEY(config, float_config,
win) && ((!has_split && !has_vertical) || old_split == fconfig.split))) {
if (HAS_KEY(config, float_config, width)) {
win_setwidth_win(fconfig.width, win);
}
if (HAS_KEY(config, float_config, height)) {
win_setheight_win(fconfig.height, win);
}
redraw_later(win, UPD_NOT_VALID);
return;
}
if (was_split) {
win_T *new_curwin = NULL;
// If the window is the last in the tabpage or `fconfig.win` is
// a handle to itself, we can't split it.
if (win->w_frame->fr_parent == NULL) {
// 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
// tabpage and close the previous one, or just error?
api_set_error(err, kErrorTypeValidation, "Cannot move last window");
return;
} else if (parent != NULL && parent->handle == win->handle) {
int n_frames = 0;
for (frame_T *fr = win->w_frame->fr_parent->fr_child; fr != NULL; fr = fr->fr_next) {
n_frames++;
}
win_T *neighbor = NULL;
if (n_frames > 2) {
// There are three or more windows in the frame, we need to split a neighboring window.
frame_T *frame = win->w_frame->fr_parent;
if (frame->fr_parent) {
// ┌──────────────┐
// │ A │
// ├────┬────┬────┤
// │ B │ C │ D │
// └────┴────┴────┘
// ||
// \/
// ┌───────────────────┐
// │ A │
// ├─────────┬─────────┤
// │ │ C │
// │ B ├─────────┤
// │ │ D │
// └─────────┴─────────┘
if (fconfig.split == kWinSplitAbove || fconfig.split == kWinSplitLeft) {
neighbor = win->w_next;
} else {
neighbor = win->w_prev;
}
}
// If the frame doesn't have a parent, the old frame
// was the root frame and we need to create a top-level split.
int dir;
new_curwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp);
} else if (n_frames == 2) {
// There are two windows in the frame, we can just rotate it.
int dir;
neighbor = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp);
new_curwin = neighbor;
} else {
// There is only one window in the frame, we can't split it.
api_set_error(err, kErrorTypeValidation, "Cannot split window into itself");
return;
}
// Set the parent to whatever the correct
// neighbor window was determined to be.
parent = neighbor;
} else {
int dir;
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_float_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 {
win_execute_T args;
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;
}
}
if (HAS_KEY(config, float_config, width)) {
win_setwidth_win(fconfig.width, win);
}
if (HAS_KEY(config, float_config, height)) {
win_setheight_win(fconfig.height, win);
}
redraw_later(win, UPD_NOT_VALID);
return;
} else {
win_config_float(win, fconfig);
win->w_pos_changed = true;
@ -317,6 +595,9 @@ Dictionary nvim_win_get_config(Window window, Error *err)
/// Keep in sync with FloatRelative in buffer_defs.h
static const char *const float_relative_str[] = { "editor", "win", "cursor", "mouse" };
/// Keep in sync with WinSplit in buffer_defs.h
static const char *const win_split_str[] = { "left", "right", "above", "below" };
Dictionary rv = ARRAY_DICT_INIT;
win_T *wp = find_window_by_handle(window, err);
@ -373,11 +654,18 @@ Dictionary nvim_win_get_config(Window window, Error *err)
rv = config_put_bordertext(rv, config, kBorderTextFooter);
}
}
} else if (!config->external) {
PUT(rv, "width", INTEGER_OBJ(wp->w_width));
PUT(rv, "height", INTEGER_OBJ(wp->w_height));
WinSplit split = win_split_dir(wp);
PUT(rv, "split", CSTR_TO_OBJ(win_split_str[split]));
}
const char *rel = (wp->w_floating && !config->external
? float_relative_str[config->relative] : "");
PUT(rv, "relative", CSTR_TO_OBJ(rel));
if (wp->w_floating && !config->external) {
PUT(rv, "relative", CSTR_TO_OBJ(float_relative_str[config->relative]));
} else {
PUT(rv, "relative", CSTR_TO_OBJ(""));
}
return rv;
}
@ -419,10 +707,26 @@ static bool parse_float_relative(String relative, FloatRelative *out)
return true;
}
static bool parse_config_split(String split, WinSplit *out)
{
char *str = split.data;
if (striequal(str, "left")) {
*out = kWinSplitLeft;
} else if (striequal(str, "right")) {
*out = kWinSplitRight;
} else if (striequal(str, "above")) {
*out = kWinSplitAbove;
} else if (striequal(str, "below")) {
*out = kWinSplitBelow;
} else {
return false;
}
return true;
}
static bool parse_float_bufpos(Array bufpos, lpos_T *out)
{
if (bufpos.size != 2
|| bufpos.items[0].type != kObjectTypeInteger
if (bufpos.size != 2 || bufpos.items[0].type != kObjectTypeInteger
|| bufpos.items[1].type != kObjectTypeInteger) {
return false;
}
@ -544,7 +848,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err)
{ NULL, { { NUL } }, false },
};
char (*chars)[MAX_SCHAR_SIZE] = fconfig->border_chars;
char(*chars)[MAX_SCHAR_SIZE] = fconfig->border_chars;
int *hl_ids = fconfig->border_hl_ids;
fconfig->border = true;
@ -553,8 +857,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err)
Array arr = style.data.array;
size_t size = arr.size;
if (!size || size > 8 || (size & (size - 1))) {
api_set_error(err, kErrorTypeValidation,
"invalid number of border chars");
api_set_error(err, kErrorTypeValidation, "invalid number of border chars");
return;
}
for (size_t i = 0; i < size; i++) {
@ -584,10 +887,8 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err)
api_set_error(err, kErrorTypeValidation, "invalid border char");
return;
}
if (string.size
&& mb_string2cells_len(string.data, string.size) > 1) {
api_set_error(err, kErrorTypeValidation,
"border chars must be one cell");
if (string.size && mb_string2cells_len(string.data, string.size) > 1) {
api_set_error(err, kErrorTypeValidation, "border chars must be one cell");
return;
}
size_t len = MIN(string.size, sizeof(*chars) - 1);
@ -606,8 +907,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err)
|| (chars[1][0] && chars[3][0] && !chars[2][0])
|| (chars[3][0] && chars[5][0] && !chars[4][0])
|| (chars[5][0] && chars[7][0] && !chars[6][0])) {
api_set_error(err, kErrorTypeValidation,
"corner between used edges must be specified");
api_set_error(err, kErrorTypeValidation, "corner between used edges must be specified");
}
} else if (style.type == kObjectTypeString) {
String str = style.data.string;
@ -634,8 +934,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err)
return;
}
}
api_set_error(err, kErrorTypeValidation,
"invalid border style \"%s\"", str.data);
api_set_error(err, kErrorTypeValidation, "invalid border style \"%s\"", str.data);
}
}
@ -643,17 +942,16 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
bool new_win, Error *err)
{
#define HAS_KEY_X(d, key) HAS_KEY(d, float_config, key)
bool has_relative = false, relative_is_win = false;
// ignore empty string, to match nvim_win_get_config
if (HAS_KEY_X(config, relative) && config->relative.size > 0) {
bool has_relative = false, relative_is_win = false, is_split = false;
if (HAS_KEY_X(config, relative) && !striequal(config->relative.data, "")) {
if (!parse_float_relative(config->relative, &fconfig->relative)) {
api_set_error(err, kErrorTypeValidation, "Invalid value of 'relative' key");
return false;
}
if (!(HAS_KEY_X(config, row) && HAS_KEY_X(config, col)) && !HAS_KEY_X(config, bufpos)) {
api_set_error(err, kErrorTypeValidation,
"'relative' requires 'row'/'col' or 'bufpos'");
if (config->relative.size > 0 && !(HAS_KEY_X(config, row) && HAS_KEY_X(config, col))
&& !HAS_KEY_X(config, bufpos)) {
api_set_error(err, kErrorTypeValidation, "'relative' requires 'row'/'col' or 'bufpos'");
return false;
}
@ -663,6 +961,32 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
relative_is_win = true;
fconfig->bufpos.lnum = -1;
}
} else if (!HAS_KEY_X(config, external) || !config->external) {
if (HAS_KEY_X(config, vertical) || HAS_KEY_X(config, split)) {
is_split = true;
} else if (new_win) {
api_set_error(err, kErrorTypeValidation,
"Must specify 'relative' or 'external' when creating a float");
return false;
}
}
if (HAS_KEY_X(config, vertical)) {
if (!is_split) {
api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'vertical'");
return false;
}
}
if (HAS_KEY_X(config, split)) {
if (!is_split) {
api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'split'");
return false;
}
if (!parse_config_split(config->split, &fconfig->split)) {
api_set_error(err, kErrorTypeValidation, "Invalid value of 'split' key");
return false;
}
}
if (HAS_KEY_X(config, anchor)) {
@ -673,7 +997,7 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
}
if (HAS_KEY_X(config, row)) {
if (!has_relative) {
if (!has_relative || is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'row'");
return false;
}
@ -681,7 +1005,7 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
}
if (HAS_KEY_X(config, col)) {
if (!has_relative) {
if (!has_relative || is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'col'");
return false;
}
@ -689,7 +1013,7 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
}
if (HAS_KEY_X(config, bufpos)) {
if (!has_relative) {
if (!has_relative || is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'bufpos'");
return false;
} else {
@ -714,7 +1038,7 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
api_set_error(err, kErrorTypeValidation, "'width' key must be a positive Integer");
return false;
}
} else if (!reconf) {
} else if (!reconf && !is_split) {
api_set_error(err, kErrorTypeValidation, "Must specify 'width'");
return false;
}
@ -726,21 +1050,22 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
api_set_error(err, kErrorTypeValidation, "'height' key must be a positive Integer");
return false;
}
} else if (!reconf) {
} else if (!reconf && !is_split) {
api_set_error(err, kErrorTypeValidation, "Must specify 'height'");
return false;
}
if (relative_is_win) {
if (relative_is_win || is_split) {
fconfig->window = curwin->handle;
if (HAS_KEY_X(config, win)) {
if (config->win > 0) {
fconfig->window = config->win;
}
}
} else {
} else if (has_relative) {
if (HAS_KEY_X(config, win)) {
api_set_error(err, kErrorTypeValidation, "'win' key is only valid with relative='win'");
api_set_error(err, kErrorTypeValidation,
"'win' key is only valid with relative='win' and relative=''");
return false;
}
}
@ -753,23 +1078,20 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
return false;
}
if (fconfig->external && !ui_has(kUIMultigrid)) {
api_set_error(err, kErrorTypeValidation,
"UI doesn't support external windows");
api_set_error(err, kErrorTypeValidation, "UI doesn't support external windows");
return false;
}
}
if (!reconf && (!has_relative && !fconfig->external)) {
api_set_error(err, kErrorTypeValidation,
"One of 'relative' and 'external' must be used");
return false;
}
if (HAS_KEY_X(config, focusable)) {
fconfig->focusable = config->focusable;
}
if (HAS_KEY_X(config, zindex)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'zindex'");
return false;
}
if (config->zindex > 0) {
fconfig->zindex = (int)config->zindex;
} else {
@ -779,6 +1101,10 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
}
if (HAS_KEY_X(config, title)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'title'");
return false;
}
// title only work with border
if (!HAS_KEY_X(config, border) && !fconfig->border) {
api_set_error(err, kErrorTypeException, "title requires border to be set");
@ -802,6 +1128,10 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
}
if (HAS_KEY_X(config, footer)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'footer'");
return false;
}
// footer only work with border
if (!HAS_KEY_X(config, border) && !fconfig->border) {
api_set_error(err, kErrorTypeException, "footer requires border to be set");
@ -825,6 +1155,10 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig,
}
if (HAS_KEY_X(config, border)) {
if (is_split) {
api_set_error(err, kErrorTypeValidation, "non-float cannot have 'border'");
return false;
}
parse_border_style(config->border, fconfig, err);
if (ERROR_SET(err)) {
return false;

View File

@ -889,6 +889,14 @@ typedef enum {
kFloatRelativeMouse = 3,
} FloatRelative;
/// Keep in sync with win_split_str[] in nvim_win_get_config() (api/win_config.c)
typedef enum {
kWinSplitLeft = 0,
kWinSplitRight = 1,
kWinSplitAbove = 2,
kWinSplitBelow = 3,
} WinSplit;
typedef enum {
kWinStyleUnused = 0,
kWinStyleMinimal, /// Minimal UI: no number column, eob markers, etc
@ -914,6 +922,7 @@ typedef struct {
FloatRelative relative;
bool external;
bool focusable;
WinSplit split;
int zindex;
WinStyle style;
bool border;
@ -939,6 +948,7 @@ typedef struct {
.row = 0, .col = 0, .anchor = 0, \
.relative = 0, .external = false, \
.focusable = true, \
.split = 0, \
.zindex = kZIndexFloatDefault, \
.style = kWinStyleUnused, \
.noautocmd = false, \

View File

@ -713,7 +713,7 @@ void win_set_buf(win_T *win, buf_T *buf, bool noautocmd, Error *err)
}
switchwin_T switchwin;
if (switch_win_noblock(&switchwin, win, tab, false) == FAIL) {
if (switch_win_noblock(&switchwin, win, tab, true) == FAIL) {
api_set_error(err,
kErrorTypeException,
"Failed to switch to window %d",
@ -733,7 +733,7 @@ void win_set_buf(win_T *win, buf_T *buf, bool noautocmd, Error *err)
// So do it now.
validate_cursor();
restore_win_noblock(&switchwin, false);
restore_win_noblock(&switchwin, true);
if (noautocmd) {
unblock_autocmds();
}
@ -928,6 +928,7 @@ static int check_split_disallowed(void)
// WSP_TOP: open window at the top-left of the screen (help window).
// WSP_BOT: open window at the bottom-right of the screen (quickfix window).
// WSP_HELP: creating the help window, keep layout snapshot
// WSP_NOENTER: do not enter the new window or trigger WinNew autocommands
//
// return FAIL for failure, OK otherwise
int win_split(int size, int flags)
@ -956,20 +957,20 @@ int win_split(int size, int flags)
clear_snapshot(curtab, SNAP_HELP_IDX);
}
return win_split_ins(size, flags, NULL, 0);
return win_split_ins(size, flags, NULL, 0) == NULL ? FAIL : OK;
}
/// When "new_wp" is NULL: split the current window in two.
/// When "new_wp" is not NULL: insert this window at the far
/// top/left/right/bottom.
/// @return FAIL for failure, OK otherwise
int win_split_ins(int size, int flags, win_T *new_wp, int dir)
/// @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 *wp = new_wp;
// aucmd_win[] should always remain floating
if (new_wp != NULL && is_aucmd_win(new_wp)) {
return FAIL;
return NULL;
}
win_T *oldwin;
@ -985,22 +986,24 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
int need_status = 0;
int new_size = size;
bool new_in_layout = (new_wp == NULL || new_wp->w_floating);
bool vertical = flags & WSP_VERT;
bool toplevel = flags & (WSP_TOP | WSP_BOT);
// 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 (oldwin->w_height <= p_wmh && new_in_layout) {
emsg(_(e_noroom));
return FAIL;
return NULL;
}
need_status = STATUS_HEIGHT;
}
bool do_equal = false;
int oldwin_height = 0;
const int layout = flags & WSP_VERT ? FR_ROW : FR_COL;
const int layout = vertical ? FR_ROW : FR_COL;
bool did_set_fraction = false;
if (flags & WSP_VERT) {
if (vertical) {
// Check if we are able to split the current window and compute its
// width.
// Current window requires at least 1 space.
@ -1011,7 +1014,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
}
int minwidth;
int available;
if (flags & (WSP_BOT | WSP_TOP)) {
if (toplevel) {
minwidth = frame_minwidth(topframe, NOWIN);
available = topframe->fr_width;
needed += minwidth;
@ -1039,7 +1042,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
}
if (available < needed && new_in_layout) {
emsg(_(e_noroom));
return FAIL;
return NULL;
}
if (new_size == 0) {
new_size = oldwin->w_width / 2;
@ -1092,7 +1095,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
}
int minheight;
int available;
if (flags & (WSP_BOT | WSP_TOP)) {
if (toplevel) {
minheight = frame_minheight(topframe, NOWIN) + need_status;
available = topframe->fr_height;
needed += minheight;
@ -1119,7 +1122,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
}
if (available < needed && new_in_layout) {
emsg(_(e_noroom));
return FAIL;
return NULL;
}
oldwin_height = oldwin->w_height;
if (need_status) {
@ -1182,7 +1185,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
&& ((flags & WSP_BOT)
|| (flags & WSP_BELOW)
|| (!(flags & WSP_ABOVE)
&& ((flags & WSP_VERT) ? p_spr : p_sb)))) {
&& (vertical ? p_spr : p_sb)))) {
// new window below/right of current one
if (new_wp == NULL) {
wp = win_alloc(oldwin, false);
@ -1199,7 +1202,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
if (new_wp == NULL) {
if (wp == NULL) {
return FAIL;
return NULL;
}
new_frame(wp);
@ -1218,9 +1221,9 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
frame_T *curfrp;
// Reorganise the tree of frames to insert the new window.
if (flags & (WSP_TOP | WSP_BOT)) {
if ((topframe->fr_layout == FR_COL && (flags & WSP_VERT) == 0)
|| (topframe->fr_layout == FR_ROW && (flags & WSP_VERT) != 0)) {
if (toplevel) {
if ((topframe->fr_layout == FR_COL && !vertical)
|| (topframe->fr_layout == FR_ROW && vertical)) {
curfrp = topframe->fr_child;
if (flags & WSP_BOT) {
while (curfrp->fr_next != NULL) {
@ -1237,7 +1240,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
before = false;
} else if (flags & WSP_ABOVE) {
before = true;
} else if (flags & WSP_VERT) {
} else if (vertical) {
before = !p_spr;
} else {
before = !p_sb;
@ -1285,14 +1288,14 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
}
wp->w_fraction = oldwin->w_fraction;
if (flags & WSP_VERT) {
if (vertical) {
wp->w_p_scr = curwin->w_p_scr;
if (need_status) {
win_new_height(oldwin, oldwin->w_height - 1);
oldwin->w_status_height = need_status;
}
if (flags & (WSP_TOP | WSP_BOT)) {
if (toplevel) {
// set height and row of new window to full height
wp->w_winrow = tabline_height();
win_new_height(wp, curfrp->fr_height - (p_ls == 1 || p_ls == 2));
@ -1316,7 +1319,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
wp->w_vsep_width = oldwin->w_vsep_width;
oldwin->w_vsep_width = 1;
}
if (flags & (WSP_TOP | WSP_BOT)) {
if (toplevel) {
if (flags & WSP_BOT) {
frame_add_vsep(curfrp);
}
@ -1338,7 +1341,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
} else {
const bool is_stl_global = global_stl_height() > 0;
// width and column of new window is same as current window
if (flags & (WSP_TOP | WSP_BOT)) {
if (toplevel) {
wp->w_wincol = 0;
win_new_width(wp, Columns);
wp->w_vsep_width = 0;
@ -1359,7 +1362,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
wp->w_hsep_height = oldwin->w_hsep_height;
oldwin->w_hsep_height = is_stl_global ? 1 : 0;
}
if (flags & (WSP_TOP | WSP_BOT)) {
if (toplevel) {
int new_fr_height = curfrp->fr_height - new_size;
if (is_stl_global) {
if (flags & WSP_BOT) {
@ -1405,7 +1408,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
frame_fix_height(oldwin);
}
if (flags & (WSP_TOP | WSP_BOT)) {
if (toplevel) {
win_comp_pos();
}
@ -1426,7 +1429,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
// equalize the window sizes.
if (do_equal || dir != 0) {
win_equal(wp, true, (flags & WSP_VERT) ? (dir == 'v' ? 'b' : 'h') : (dir == 'h' ? 'b' : 'v'));
win_equal(wp, true, vertical ? (dir == 'v' ? 'b' : 'h') : (dir == 'h' ? 'b' : 'v'));
} else if (!is_aucmd_win(wp)) {
win_fix_scroll(false);
}
@ -1447,10 +1450,12 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
}
}
if (!(flags & WSP_NOENTER)) {
// make the new window the current window
win_enter_ext(wp, WEE_TRIGGER_NEW_AUTOCMDS | WEE_TRIGGER_ENTER_AUTOCMDS
| WEE_TRIGGER_LEAVE_AUTOCMDS);
if (flags & WSP_VERT) {
}
if (vertical) {
p_wiw = i;
} else {
p_wh = i;
@ -1461,7 +1466,7 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir)
oldwin->w_pos_changed = true;
}
return OK;
return wp;
}
// Initialize window "newp" from window "oldp".

View File

@ -30,6 +30,7 @@ enum {
WSP_BELOW = 0x40, ///< put new window below/right
WSP_ABOVE = 0x80, ///< put new window above/left
WSP_NEWLOC = 0x100, ///< don't copy location list
WSP_NOENTER = 0x200, ///< don't enter the new window
};
enum {

View File

@ -1232,6 +1232,437 @@ describe('API/win', function()
)
eq(wins_before, api.nvim_list_wins())
end)
it('creates a split window', function()
local win = api.nvim_open_win(0, true, {
vertical = false,
})
eq('', api.nvim_win_get_config(win).relative)
end)
it('creates split windows in the correct direction', function()
local initial_win = api.nvim_get_current_win()
local win = api.nvim_open_win(0, true, {
vertical = true,
})
eq('', api.nvim_win_get_config(win).relative)
local layout = fn.winlayout()
eq({
'row',
{
{ 'leaf', win },
{ 'leaf', initial_win },
},
}, layout)
end)
it("respects the 'split' option", function()
local initial_win = api.nvim_get_current_win()
local win = api.nvim_open_win(0, true, {
split = 'below',
})
eq('', api.nvim_win_get_config(win).relative)
local layout = fn.winlayout()
eq({
'col',
{
{ 'leaf', initial_win },
{ 'leaf', win },
},
}, layout)
end)
it(
"doesn't change tp_curwin when splitting window in non-current tab with enter=false",
function()
local tab1 = api.nvim_get_current_tabpage()
local tab1_win = api.nvim_get_current_win()
helpers.command('tabnew')
local tab2 = api.nvim_get_current_tabpage()
local tab2_win = api.nvim_get_current_win()
eq({ tab1_win, tab2_win }, api.nvim_list_wins())
eq({ tab1, tab2 }, api.nvim_list_tabpages())
api.nvim_set_current_tabpage(tab1)
eq(tab1_win, api.nvim_get_current_win())
local tab2_prevwin = fn.tabpagewinnr(tab2, '#')
-- split in tab2 whine in tab2, with enter = false
local tab2_win2 = api.nvim_open_win(api.nvim_create_buf(false, true), false, {
win = tab2_win,
split = 'right',
})
eq(tab1_win, api.nvim_get_current_win()) -- we should still be in the first tp
eq(tab1_win, api.nvim_tabpage_get_win(tab1))
eq(tab2_win, api.nvim_tabpage_get_win(tab2)) -- tab2's tp_curwin should not have changed
eq(tab2_prevwin, fn.tabpagewinnr(tab2, '#')) -- tab2's tp_prevwin should not have changed
eq({ tab1_win, tab2_win, tab2_win2 }, api.nvim_list_wins())
eq({ tab2_win, tab2_win2 }, api.nvim_tabpage_list_wins(tab2))
end
)
it('creates splits in the correct location', function()
local first_win = api.nvim_get_current_win()
-- specifying window 0 should create a split next to the current window
local win = api.nvim_open_win(0, true, {
vertical = false,
})
local layout = fn.winlayout()
eq({
'col',
{
{ 'leaf', win },
{ 'leaf', first_win },
},
}, layout)
-- not specifying a window should create a top-level split
local win2 = api.nvim_open_win(0, true, {
split = 'left',
win = -1,
})
layout = fn.winlayout()
eq({
'row',
{
{ 'leaf', win2 },
{
'col',
{
{ 'leaf', win },
{ 'leaf', first_win },
},
},
},
}, layout)
-- specifying a window should create a split next to that window
local win3 = api.nvim_open_win(0, true, {
win = win,
vertical = false,
})
layout = fn.winlayout()
eq({
'row',
{
{ 'leaf', win2 },
{
'col',
{
{ 'leaf', win3 },
{ 'leaf', win },
{ 'leaf', first_win },
},
},
},
}, layout)
end)
end)
describe('set_config', function()
it('moves a split into a float', function()
local win = api.nvim_open_win(0, true, {
vertical = false,
})
eq('', api.nvim_win_get_config(win).relative)
api.nvim_win_set_config(win, {
relative = 'editor',
row = 5,
col = 5,
width = 5,
height = 5,
})
eq('editor', api.nvim_win_get_config(win).relative)
end)
it('throws error when attempting to move the last window', function()
local err = pcall_err(api.nvim_win_set_config, 0, {
vertical = false,
})
eq('Cannot move last window', err)
end)
it('passing retval of get_config results in no-op', function()
-- simple split layout
local win = api.nvim_open_win(0, true, {
split = 'left',
})
local layout = fn.winlayout()
local config = api.nvim_win_get_config(win)
api.nvim_win_set_config(win, config)
eq(layout, fn.winlayout())
-- nested split layout
local win2 = api.nvim_open_win(0, true, {
vertical = true,
})
local win3 = api.nvim_open_win(0, true, {
win = win2,
vertical = false,
})
layout = fn.winlayout()
config = api.nvim_win_get_config(win2)
api.nvim_win_set_config(win2, config)
eq(layout, fn.winlayout())
config = api.nvim_win_get_config(win3)
api.nvim_win_set_config(win3, config)
eq(layout, fn.winlayout())
end)
it('moves a float into a split', function()
local layout = fn.winlayout()
eq('leaf', layout[1])
local win = api.nvim_open_win(0, true, {
relative = 'editor',
row = 5,
col = 5,
width = 5,
height = 5,
})
api.nvim_win_set_config(win, {
split = 'below',
win = -1,
})
eq('', api.nvim_win_get_config(win).relative)
layout = fn.winlayout()
eq('col', layout[1])
eq(2, #layout[2])
eq(win, layout[2][2][2])
end)
it('respects the "split" option', function()
local layout = fn.winlayout()
eq('leaf', layout[1])
local first_win = layout[2]
local win = api.nvim_open_win(0, true, {
relative = 'editor',
row = 5,
col = 5,
width = 5,
height = 5,
})
api.nvim_win_set_config(win, {
split = 'right',
win = first_win,
})
layout = fn.winlayout()
eq('row', layout[1])
eq(2, #layout[2])
eq(win, layout[2][2][2])
local config = api.nvim_win_get_config(win)
eq('', config.relative)
eq('right', config.split)
api.nvim_win_set_config(win, {
split = 'below',
win = first_win,
})
layout = fn.winlayout()
eq('col', layout[1])
eq(2, #layout[2])
eq(win, layout[2][2][2])
config = api.nvim_win_get_config(win)
eq('', config.relative)
eq('below', config.split)
end)
it('creates top-level splits', function()
local win = api.nvim_open_win(0, true, {
vertical = false,
})
local win2 = api.nvim_open_win(0, true, {
vertical = true,
win = -1,
})
local layout = fn.winlayout()
eq('row', layout[1])
eq(2, #layout[2])
eq(win2, layout[2][1][2])
api.nvim_win_set_config(win, {
split = 'below',
win = -1,
})
layout = fn.winlayout()
eq('col', layout[1])
eq(2, #layout[2])
eq('row', layout[2][1][1])
eq(win, layout[2][2][2])
end)
it('moves splits to other tabpages', function()
local curtab = api.nvim_get_current_tabpage()
local win = api.nvim_open_win(0, false, { split = 'left' })
command('tabnew')
local tabnr = api.nvim_get_current_tabpage()
command('tabprev') -- return to the initial tab
api.nvim_win_set_config(win, {
split = 'right',
win = api.nvim_tabpage_get_win(tabnr),
})
eq(tabnr, api.nvim_win_get_tabpage(win))
-- we are changing the config, the current tabpage should not change
eq(curtab, api.nvim_get_current_tabpage())
command('tabnext') -- switch to the new tabpage so we can get the layout
local layout = fn.winlayout()
eq({
'row',
{
{ 'leaf', api.nvim_tabpage_get_win(tabnr) },
{ 'leaf', win },
},
}, layout)
end)
it('correctly moves curwin when moving curwin to a different tabpage', function()
local curtab = api.nvim_get_current_tabpage()
command('tabnew')
local tab2 = api.nvim_get_current_tabpage()
local tab2_win = api.nvim_get_current_win()
command('tabprev') -- return to the initial tab
local neighbor = api.nvim_get_current_win()
-- create and enter a new split
local win = api.nvim_open_win(0, true, {
vertical = false,
})
eq(curtab, api.nvim_win_get_tabpage(win))
eq({ win, neighbor }, api.nvim_tabpage_list_wins(curtab))
-- move the current win to a different tabpage
api.nvim_win_set_config(win, {
split = 'right',
win = api.nvim_tabpage_get_win(tab2),
})
eq(curtab, api.nvim_get_current_tabpage())
-- win should have moved to tab2
eq(tab2, api.nvim_win_get_tabpage(win))
-- tp_curwin of tab2 should not have changed
eq(tab2_win, api.nvim_tabpage_get_win(tab2))
-- win lists should be correct
eq({ tab2_win, win }, api.nvim_tabpage_list_wins(tab2))
eq({ neighbor }, api.nvim_tabpage_list_wins(curtab))
-- current win should have moved to neighboring win
eq(neighbor, api.nvim_tabpage_get_win(curtab))
end)
it('splits windows in non-current tabpage', function()
local curtab = api.nvim_get_current_tabpage()
command('tabnew')
local tabnr = api.nvim_get_current_tabpage()
command('tabprev') -- return to the initial tab
local win = api.nvim_open_win(0, false, {
vertical = false,
win = api.nvim_tabpage_get_win(tabnr),
})
eq(tabnr, api.nvim_win_get_tabpage(win))
-- since enter = false, the current tabpage should not change
eq(curtab, api.nvim_get_current_tabpage())
end)
it('moves the current split window', function()
local initial_win = api.nvim_get_current_win()
local win = api.nvim_open_win(0, true, {
vertical = true,
})
local win2 = api.nvim_open_win(0, true, {
vertical = true,
})
api.nvim_set_current_win(win)
eq({
'row',
{
{ 'leaf', win2 },
{ 'leaf', win },
{ 'leaf', initial_win },
},
}, fn.winlayout())
api.nvim_win_set_config(0, {
vertical = false,
win = 0,
})
eq(win, api.nvim_get_current_win())
eq({
'col',
{
{ 'leaf', win },
{
'row',
{
{ 'leaf', win2 },
{ 'leaf', initial_win },
},
},
},
}, fn.winlayout())
api.nvim_set_current_win(win2)
local win3 = api.nvim_open_win(0, true, {
vertical = true,
})
eq(win3, api.nvim_get_current_win())
eq({
'col',
{
{ 'leaf', win },
{
'row',
{
{ 'leaf', win3 },
{ 'leaf', win2 },
{ 'leaf', initial_win },
},
},
},
}, fn.winlayout())
api.nvim_win_set_config(0, {
vertical = false,
win = 0,
})
eq(win3, api.nvim_get_current_win())
eq({
'col',
{
{ 'leaf', win },
{
'row',
{
{
'col',
{
{ 'leaf', win3 },
{ 'leaf', win2 },
},
},
{ 'leaf', initial_win },
},
},
},
}, fn.winlayout())
end)
end)
describe('get_config', function()
@ -1292,6 +1723,154 @@ describe('API/win', function()
eq(title, cfg.title)
eq(footer, cfg.footer)
end)
it('includes split for normal windows', function()
local win = api.nvim_open_win(0, true, {
vertical = true,
win = -1,
})
eq('left', api.nvim_win_get_config(win).split)
api.nvim_win_set_config(win, {
vertical = false,
win = -1,
})
eq('above', api.nvim_win_get_config(win).split)
api.nvim_win_set_config(win, {
split = 'below',
win = -1,
})
eq('below', api.nvim_win_get_config(win).split)
end)
it('includes split when splitting with ex commands', function()
local win = api.nvim_get_current_win()
eq('left', api.nvim_win_get_config(win).split)
command('vsplit')
local win2 = api.nvim_get_current_win()
-- initial window now be marked as right split
-- since it was split with a vertical split
-- and 'splitright' is false by default
eq('right', api.nvim_win_get_config(win).split)
eq('left', api.nvim_win_get_config(win2).split)
api.nvim_set_option_value('splitbelow', true, {
scope = 'global',
})
api.nvim_win_close(win, true)
command('split')
local win3 = api.nvim_get_current_win()
eq('below', api.nvim_win_get_config(win3).split)
end)
it("includes the correct 'split' option in complex layouts", function()
local initial_win = api.nvim_get_current_win()
local win = api.nvim_open_win(0, false, {
split = 'right',
win = -1,
})
local win2 = api.nvim_open_win(0, false, {
split = 'below',
win = win,
})
api.nvim_win_set_config(win2, {
width = 50,
})
api.nvim_win_set_config(win, {
split = 'left',
win = -1,
})
local win3 = api.nvim_open_win(0, false, {
split = 'above',
win = -1,
})
local float = api.nvim_open_win(0, false, {
relative = 'editor',
width = 40,
height = 20,
col = 20,
row = 10,
})
api.nvim_win_set_config(float, {
split = 'right',
win = -1,
})
local layout = fn.winlayout()
eq({
'row',
{
{
'col',
{
{ 'leaf', win3 },
{
'row',
{
{ 'leaf', win },
{ 'leaf', initial_win },
{ 'leaf', win2 },
},
},
},
},
{
'leaf',
float,
},
},
}, layout)
eq('above', api.nvim_win_get_config(win3).split)
eq('left', api.nvim_win_get_config(win).split)
eq('left', api.nvim_win_get_config(initial_win).split)
eq('right', api.nvim_win_get_config(win2).split)
eq('right', api.nvim_win_get_config(float).split)
end)
end)
describe('set_config', function()
it('no crash with invalid title', function()
local win = api.nvim_open_win(0, true, {
width = 10,
height = 10,
relative = 'editor',
row = 10,
col = 10,
title = { { 'test' } },
border = 'single',
})
eq(
'title/footer cannot be an empty array',
pcall_err(api.nvim_win_set_config, win, { title = {} })
)
command('redraw!')
assert_alive()
end)
it('no crash with invalid footer', function()
local win = api.nvim_open_win(0, true, {
width = 10,
height = 10,
relative = 'editor',
row = 10,
col = 10,
footer = { { 'test' } },
border = 'single',
})
eq(
'title/footer cannot be an empty array',
pcall_err(api.nvim_win_set_config, win, { footer = {} })
)
command('redraw!')
assert_alive()
end)
end)
describe('set_config', function()

View File

@ -104,14 +104,20 @@ describe('float window', function()
end)
it('open with WinNew autocmd', function()
local res = exec_lua([[
local triggerd = false
local new_triggered_before_enter, new_curwin, win = unpack(exec_lua([[
local enter_triggered = false
local new_triggered_before_enter = false
local new_curwin
local buf = vim.api.nvim_create_buf(true, true)
vim.api.nvim_create_autocmd('WinNew', {
callback = function(opt)
if opt.buf == buf then
triggerd = true
vim.api.nvim_create_autocmd('WinEnter', {
callback = function()
enter_triggered = true
end
})
vim.api.nvim_create_autocmd('WinNew', {
callback = function()
new_triggered_before_enter = not enter_triggered
new_curwin = vim.api.nvim_get_current_win()
end
})
local opts = {
@ -120,10 +126,11 @@ describe('float window', function()
width = 1, height = 1,
noautocmd = false,
}
vim.api.nvim_open_win(buf, true, opts)
return triggerd
]])
eq(true, res)
local win = vim.api.nvim_open_win(buf, true, opts)
return {new_triggered_before_enter, new_curwin, win}
]]))
eq(true, new_triggered_before_enter)
eq(win, new_curwin)
end)
it('opened with correct height', function()
@ -1095,7 +1102,7 @@ describe('float window', function()
local expected = {anchor='NW', col=5, external=false, focusable=true, height=2, relative='editor', row=3, width=20, zindex=60, hide=false}
eq(expected, api.nvim_win_get_config(win))
eq({relative='', external=false, focusable=true, hide=false}, api.nvim_win_get_config(0))
eq({external=false, focusable=true, hide=false, relative='',split="left",width=40,height=6}, api.nvim_win_get_config(0))
if multigrid then
api.nvim_win_set_config(win, {external=true, width=10, height=1})
@ -2878,27 +2885,31 @@ describe('float window', function()
it('API has proper error messages', function()
local buf = api.nvim_create_buf(false,false)
eq("Invalid key: 'bork'",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=2,bork=true}))
eq("'win' key is only valid with relative='win'",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=2,relative='editor',row=0,col=0,win=0}))
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,bork=true}))
eq("'win' key is only valid with relative='win' and relative=''",
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,relative='editor',row=0,col=0,win=0}))
eq("floating windows cannot have 'vertical'",
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,relative='editor',row=0,col=0,vertical=true}))
eq("floating windows cannot have 'split'",
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,relative='editor',row=0,col=0,split="left"}))
eq("Only one of 'relative' and 'external' must be used",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=2,relative='editor',row=0,col=0,external=true}))
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,relative='editor',row=0,col=0,external=true}))
eq("Invalid value of 'relative' key",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=2,relative='shell',row=0,col=0}))
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,relative='shell',row=0,col=0}))
eq("Invalid value of 'anchor' key",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=2,relative='editor',row=0,col=0,anchor='bottom'}))
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,relative='editor',row=0,col=0,anchor='bottom'}))
eq("'relative' requires 'row'/'col' or 'bufpos'",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=2,relative='editor'}))
pcall_err(api.nvim_open_win, buf, false, {width=20,height=2,relative='editor'}))
eq("'width' key must be a positive Integer",
pcall_err(api.nvim_open_win,buf, false, {width=-1,height=2,relative='editor', row=0, col=0}))
pcall_err(api.nvim_open_win, buf, false, {width=-1,height=2,relative='editor', row=0, col=0}))
eq("'height' key must be a positive Integer",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=-1,relative='editor', row=0, col=0}))
pcall_err(api.nvim_open_win, buf, false, {width=20,height=-1,relative='editor', row=0, col=0}))
eq("'height' key must be a positive Integer",
pcall_err(api.nvim_open_win,buf, false, {width=20,height=0,relative='editor', row=0, col=0}))
pcall_err(api.nvim_open_win, buf, false, {width=20,height=0,relative='editor', row=0, col=0}))
eq("Must specify 'width'",
pcall_err(api.nvim_open_win,buf, false, {relative='editor', row=0, col=0}))
pcall_err(api.nvim_open_win, buf, false, {relative='editor', row=0, col=0}))
eq("Must specify 'height'",
pcall_err(api.nvim_open_win,buf, false, {relative='editor', row=0, col=0, width=2}))
pcall_err(api.nvim_open_win, buf, false, {relative='editor', row=0, col=0, width=2}))
end)
it('can be placed relative window or cursor', function()