From 6bba4becedaea5a330c0c9d9427fb495e8092dac Mon Sep 17 00:00:00 2001 From: Will Hopkins Date: Wed, 31 Jan 2024 19:43:35 -0800 Subject: [PATCH] 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. --- runtime/doc/api.txt | 38 +- runtime/doc/news.txt | 3 + runtime/lua/vim/_meta/api.lua | 39 +- runtime/lua/vim/_meta/api_keysets.lua | 2 + src/nvim/api/keysets_defs.h | 2 + src/nvim/api/win_config.c | 454 +++++++++++++++++--- src/nvim/buffer_defs.h | 10 + src/nvim/window.c | 67 +-- src/nvim/window.h | 19 +- test/functional/api/window_spec.lua | 579 ++++++++++++++++++++++++++ test/functional/ui/float_spec.lua | 57 ++- 11 files changed, 1129 insertions(+), 141 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 1831b78e9e..89d2860ad2 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -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 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 3051b81189..36b42e28e3 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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* diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index bedc218626..77a29cb4c1 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -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 diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index c9ef4e9b4f..1b6c6811a2 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -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 diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index 85896c1fa7..2f1b38d1e5 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -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; diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 6df86683c1..5089367114 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -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); + + 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; } @@ -529,7 +833,7 @@ static bool parse_bordertext_pos(String bordertext_pos, BorderTextType bordertex return true; } -static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) +static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) { struct { const char *name; @@ -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; diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 24f97dd92c..ea014c3918 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -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, \ diff --git a/src/nvim/window.c b/src/nvim/window.c index 66169bcb74..d07ae22c31 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -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) } } - // 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 (!(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 (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". diff --git a/src/nvim/window.h b/src/nvim/window.h index 3c88965324..d20b799e20 100644 --- a/src/nvim/window.h +++ b/src/nvim/window.h @@ -21,15 +21,16 @@ enum { /// arguments for win_split() enum { - WSP_ROOM = 0x01, ///< require enough room - WSP_VERT = 0x02, ///< split/equalize vertically - WSP_HOR = 0x04, ///< equalize horizontally - WSP_TOP = 0x08, ///< window at top-left of shell - WSP_BOT = 0x10, ///< window at bottom-right of shell - WSP_HELP = 0x20, ///< creating the help window - 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_ROOM = 0x01, ///< require enough room + WSP_VERT = 0x02, ///< split/equalize vertically + WSP_HOR = 0x04, ///< equalize horizontally + WSP_TOP = 0x08, ///< window at top-left of shell + WSP_BOT = 0x10, ///< window at bottom-right of shell + WSP_HELP = 0x20, ///< creating the help window + 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 { diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index aadf59eb5a..097a546ef2 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -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() diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index b05e7aceda..397c1f6132 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -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('WinEnter', { + callback = function() + enter_triggered = true + end + }) vim.api.nvim_create_autocmd('WinNew', { - callback = function(opt) - if opt.buf == buf then - triggerd = true - end + 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()