neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

win_config.c (48531B)


      1 #include <assert.h>
      2 #include <stdbool.h>
      3 #include <string.h>
      4 
      5 #include "klib/kvec.h"
      6 #include "nvim/api/extmark.h"
      7 #include "nvim/api/keysets_defs.h"
      8 #include "nvim/api/private/defs.h"
      9 #include "nvim/api/private/dispatch.h"
     10 #include "nvim/api/private/helpers.h"
     11 #include "nvim/api/win_config.h"
     12 #include "nvim/ascii_defs.h"
     13 #include "nvim/autocmd.h"
     14 #include "nvim/autocmd_defs.h"
     15 #include "nvim/buffer.h"
     16 #include "nvim/buffer_defs.h"
     17 #include "nvim/decoration_defs.h"
     18 #include "nvim/drawscreen.h"
     19 #include "nvim/errors.h"
     20 #include "nvim/eval/window.h"
     21 #include "nvim/globals.h"
     22 #include "nvim/highlight_group.h"
     23 #include "nvim/macros_defs.h"
     24 #include "nvim/mbyte.h"
     25 #include "nvim/memory.h"
     26 #include "nvim/memory_defs.h"
     27 #include "nvim/option.h"
     28 #include "nvim/option_vars.h"
     29 #include "nvim/pos_defs.h"
     30 #include "nvim/strings.h"
     31 #include "nvim/syntax.h"
     32 #include "nvim/types_defs.h"
     33 #include "nvim/ui.h"
     34 #include "nvim/ui_defs.h"
     35 #include "nvim/vim_defs.h"
     36 #include "nvim/window.h"
     37 #include "nvim/winfloat.h"
     38 
     39 #include "api/win_config.c.generated.h"
     40 
     41 /// Opens a new split window, or a floating window if `relative` is specified,
     42 /// or an external window (managed by the UI) if `external` is specified.
     43 ///
     44 /// Floats are windows that are drawn above the split layout, at some anchor
     45 /// position in some other window. Floats can be drawn internally or by external
     46 /// GUI with the |ui-multigrid| extension. External windows are only supported
     47 /// with multigrid GUIs, and are displayed as separate top-level windows.
     48 ///
     49 /// For a general overview of floats, see |api-floatwin|.
     50 ///
     51 /// The `width` and `height` of the new window must be specified when opening
     52 /// a floating window, but are optional for normal windows.
     53 ///
     54 /// If `relative` and `external` are omitted, a normal "split" window is created.
     55 /// The `win` property determines which window will be split. If no `win` is
     56 /// provided or `win == 0`, a window will be created adjacent to the current window.
     57 /// If -1 is provided, a top-level split will be created. `vertical` and `split` are
     58 /// only valid for normal windows, and are used to control split direction. For `vertical`,
     59 /// the exact direction is determined by 'splitright' and 'splitbelow'.
     60 /// Split windows cannot have `bufpos`, `row`, `col`, `border`, `title`, `footer` properties.
     61 ///
     62 /// With relative=editor (row=0,col=0) refers to the top-left corner of the
     63 /// screen-grid and (row=Lines-1,col=Columns-1) refers to the bottom-right
     64 /// corner. Fractional values are allowed, but the builtin implementation
     65 /// (used by non-multigrid UIs) will always round down to nearest integer.
     66 ///
     67 /// Out-of-bounds values, and configurations that make the float not fit inside
     68 /// the main editor, are allowed. The builtin implementation truncates values
     69 /// so floats are fully within the main screen grid. External GUIs
     70 /// could let floats hover outside of the main window like a tooltip, but
     71 /// this should not be used to specify arbitrary WM screen positions.
     72 ///
     73 /// Examples:
     74 ///
     75 /// ```lua
     76 /// -- Window-relative float with 'statusline' enabled:
     77 /// local w1 = vim.api.nvim_open_win(0, false,
     78 ///   {relative='win', row=3, col=3, width=40, height=4})
     79 /// vim.wo[w1].statusline = vim.o.statusline
     80 ///
     81 /// -- Buffer-relative float (travels as buffer is scrolled):
     82 /// vim.api.nvim_open_win(0, false,
     83 ///   {relative='win', width=40, height=4, bufpos={100,10}})
     84 ///
     85 /// -- Vertical split left of the current window:
     86 /// vim.api.nvim_open_win(0, false, { split = 'left', win = 0, })
     87 /// ```
     88 ///
     89 /// @param buffer Buffer to display, or 0 for current buffer
     90 /// @param enter  Enter the window (make it the current window)
     91 /// @param config Map defining the window configuration. Keys:
     92 ///   - anchor: Decides which corner of the float to place at (row,col):
     93 ///      - "NW" northwest (default)
     94 ///      - "NE" northeast
     95 ///      - "SW" southwest
     96 ///      - "SE" southeast
     97 ///   - border: (`string|string[]`) (defaults to 'winborder' option) Window border. The string form
     98 ///     accepts the same values as the 'winborder' option. The array form must have a length of
     99 ///     eight or any divisor of eight, specifying the chars that form the border in a clockwise
    100 ///     fashion starting from the top-left corner. For example, the double-box style can be
    101 ///     specified as:
    102 ///     ```
    103 ///     [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
    104 ///     ```
    105 ///     If fewer than eight chars are given, they will be repeated. An ASCII border could be
    106 ///     specified as:
    107 ///     ```
    108 ///     [ "/", "-", \"\\\\\", "|" ],
    109 ///     ```
    110 ///     Or one char for all sides:
    111 ///     ```
    112 ///     [ "x" ].
    113 ///     ```
    114 ///     Empty string can be used to hide a specific border. This example will show only vertical
    115 ///     borders, not horizontal:
    116 ///     ```
    117 ///     [ "", "", "", ">", "", "", "", "<" ]
    118 ///     ```
    119 ///     By default, |hl-FloatBorder| highlight is used, which links to |hl-WinSeparator| when not
    120 ///     defined. Each border side can specify an optional highlight:
    121 ///     ```
    122 ///     [ ["+", "MyCorner"], ["x", "MyBorder"] ].
    123 ///     ```
    124 ///   - bufpos: Places float relative to buffer text (only when
    125 ///       relative="win"). Takes a tuple of zero-indexed `[line, column]`.
    126 ///       `row` and `col` if given are applied relative to this
    127 ///       position, else they default to:
    128 ///       - `row=1` and `col=0` if `anchor` is "NW" or "NE"
    129 ///       - `row=0` and `col=0` if `anchor` is "SW" or "SE"
    130 ///         (thus like a tooltip near the buffer text).
    131 ///   - col: Column position in units of screen cell width, may be fractional.
    132 ///   - external: GUI should display the window as an external
    133 ///       top-level window. Currently accepts no other positioning
    134 ///       configuration together with this.
    135 ///   - fixed: If true when anchor is NW or SW, the float window
    136 ///            would be kept fixed even if the window would be truncated.
    137 ///   - focusable: Enable focus by user actions (wincmds, mouse events).
    138 ///       Defaults to true. Non-focusable windows can be entered by
    139 ///       |nvim_set_current_win()|, or, when the `mouse` field is set to true,
    140 ///       by mouse events. See |focusable|.
    141 ///   - footer: (optional) Footer in window border, string or list.
    142 ///       List should consist of `[text, highlight]` tuples.
    143 ///       If string, or a tuple lacks a highlight, the default highlight group is `FloatFooter`.
    144 ///   - footer_pos: Footer position. Must be set with `footer` option.
    145 ///       Value can be one of "left", "center", or "right".
    146 ///       Default is `"left"`.
    147 ///   - height: Window height (in character cells). Minimum of 1.
    148 ///   - hide: If true the floating window will be hidden and the cursor will be invisible when
    149 ///           focused on it.
    150 ///   - mouse: Specify how this window interacts with mouse events.
    151 ///       Defaults to `focusable` value.
    152 ///       - If false, mouse events pass through this window.
    153 ///       - If true, mouse events interact with this window normally.
    154 ///   - noautocmd: Block all autocommands for the duration of the call. Cannot be changed by
    155 ///     |nvim_win_set_config()|.
    156 ///   - relative: Sets the window layout to "floating", placed at (row,col)
    157 ///                 coordinates relative to:
    158 ///      - "cursor"     Cursor position in current window.
    159 ///      - "editor"     The global editor grid.
    160 ///      - "laststatus" 'laststatus' if present, or last row.
    161 ///      - "mouse"      Mouse position.
    162 ///      - "tabline"    Tabline if present, or first row.
    163 ///      - "win"        Window given by the `win` field, or current window.
    164 ///   - row: Row position in units of "screen cell height", may be fractional.
    165 ///   - split: Split direction: "left", "right", "above", "below".
    166 ///   - style: (optional) Configure the appearance of the window. Currently
    167 ///       only supports one value:
    168 ///       - "minimal"  Nvim will display the window with many UI options
    169 ///                    disabled. This is useful when displaying a temporary
    170 ///                    float where the text should not be edited. Disables
    171 ///                    'number', 'relativenumber', 'cursorline', 'cursorcolumn',
    172 ///                    'foldcolumn', 'spell' and 'list' options. 'signcolumn'
    173 ///                    is changed to `auto` and 'colorcolumn' is cleared.
    174 ///                    'statuscolumn' is changed to empty. The end-of-buffer
    175 ///                     region is hidden by setting `eob` flag of
    176 ///                    'fillchars' to a space char, and clearing the
    177 ///                    |hl-EndOfBuffer| region in 'winhighlight'.
    178 ///   - title: (optional) Title in window border, string or list.
    179 ///       List should consist of `[text, highlight]` tuples.
    180 ///       If string, or a tuple lacks a highlight, the default highlight group is `FloatTitle`.
    181 ///   - title_pos: Title position. Must be set with `title` option.
    182 ///       Value can be one of "left", "center", or "right".
    183 ///       Default is `"left"`.
    184 ///   - vertical: Split vertically |:vertical|.
    185 ///   - width: Window width (in character cells). Minimum of 1.
    186 ///   - win: |window-ID| window to split, or relative window when creating a float (relative="win").
    187 ///       When splitting, negative value works like |:topleft|, |:botright|.
    188 ///   - zindex: Stacking order. floats with higher `zindex` go on top on
    189 ///               floats with lower indices. Must be larger than zero. The
    190 ///               following screen elements have hard-coded z-indices:
    191 ///       - 100: insert completion popupmenu
    192 ///       - 200: message scrollback
    193 ///       - 250: cmdline completion popupmenu (when wildoptions+=pum)
    194 ///     The default value for floats are 50.  In general, values below 100 are
    195 ///     recommended, unless there is a good reason to overshadow builtin
    196 ///     elements.
    197 ///   - _cmdline_offset: (EXPERIMENTAL) When provided, anchor the |cmdline-completion|
    198 ///     popupmenu to this window, with an offset in screen cell width.
    199 ///
    200 /// @param[out] err Error details, if any
    201 ///
    202 /// @return |window-ID|, or 0 on error
    203 Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Error *err)
    204  FUNC_API_SINCE(6) FUNC_API_TEXTLOCK_ALLOW_CMDWIN
    205 {
    206 #define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key)
    207  buf_T *buf = find_buffer_by_handle(buffer, err);
    208  if (!buf) {
    209    return 0;
    210  }
    211  if ((cmdwin_type != 0 && enter) || buf == cmdwin_buf) {
    212    api_set_error(err, kErrorTypeException, "%s", e_cmdwin);
    213    return 0;
    214  }
    215 
    216  WinConfig fconfig = WIN_CONFIG_INIT;
    217  if (!parse_win_config(NULL, config, &fconfig, false, err)) {
    218    return 0;
    219  }
    220 
    221  bool is_split = HAS_KEY_X(config, split) || HAS_KEY_X(config, vertical);
    222  Window rv = 0;
    223  if (fconfig.noautocmd) {
    224    block_autocmds();
    225  }
    226 
    227  win_T *wp = NULL;
    228  tabpage_T *tp = curtab;
    229  assert(curwin != NULL);
    230  win_T *parent = config->win == 0 ? curwin : NULL;
    231  if (config->win > 0) {
    232    parent = find_window_by_handle(fconfig.window, err);
    233    if (!parent) {
    234      // find_window_by_handle has already set the error
    235      goto cleanup;
    236    } else if (is_split && parent->w_floating) {
    237      api_set_error(err, kErrorTypeException, "Cannot split a floating window");
    238      goto cleanup;
    239    }
    240    tp = win_find_tabpage(parent);
    241  }
    242  if (is_split) {
    243    if (!check_split_disallowed_err(parent ? parent : curwin, err)) {
    244      goto cleanup;  // error already set
    245    }
    246 
    247    if (HAS_KEY_X(config, vertical) && !HAS_KEY_X(config, split)) {
    248      if (config->vertical) {
    249        fconfig.split = p_spr ? kWinSplitRight : kWinSplitLeft;
    250      } else {
    251        fconfig.split = p_sb ? kWinSplitBelow : kWinSplitAbove;
    252      }
    253    }
    254    int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER;
    255    int size = (flags & WSP_VERT) ? fconfig.width : fconfig.height;
    256 
    257    TRY_WRAP(err, {
    258      if (parent == NULL || parent == curwin) {
    259        wp = win_split_ins(size, flags, NULL, 0, NULL);
    260      } else {
    261        switchwin_T switchwin;
    262        // `parent` is valid in `tp`, so switch_win should not fail.
    263        const int result = switch_win(&switchwin, parent, tp, true);
    264        assert(result == OK);
    265        (void)result;
    266        wp = win_split_ins(size, flags, NULL, 0, NULL);
    267        restore_win(&switchwin, true);
    268      }
    269    });
    270    if (wp) {
    271      wp->w_config = fconfig;
    272      if (size > 0) {
    273        // Without room for the requested size, window sizes may have been equalized instead.
    274        // If the size differs from what was requested, try to set it again now.
    275        if ((flags & WSP_VERT) && wp->w_width != size) {
    276          win_setwidth_win(size, wp);
    277        } else if (!(flags & WSP_VERT) && wp->w_height != size) {
    278          win_setheight_win(size, wp);
    279        }
    280      }
    281    }
    282  } else {
    283    // Unlike check_split_disallowed_err, ignore `split_disallowed`, as opening a float shouldn't
    284    // mess with the frame structure. Still check `b_locked_split` to avoid opening more windows
    285    // into a closing buffer, though.
    286    if (curwin->w_buffer->b_locked_split) {  // Can't instead check `buf` in case win_set_buf fails!
    287      api_set_error(err, kErrorTypeException, "E1159: Cannot open a float when closing the buffer");
    288      goto cleanup;
    289    }
    290    wp = win_new_float(NULL, false, fconfig, err);
    291  }
    292  if (!wp) {
    293    if (!ERROR_SET(err)) {
    294      api_set_error(err, kErrorTypeException, "Failed to create window");
    295    }
    296    goto cleanup;
    297  }
    298 
    299  if (fconfig._cmdline_offset < INT_MAX) {
    300    cmdline_win = wp;
    301  }
    302 
    303  // Autocommands may close `wp` or move it to another tabpage, so update and check `tp` after each
    304  // event. In each case, `wp` should already be valid in `tp`, so switch_win should not fail.
    305  // Also, autocommands may free the `buf` to switch to, so store a bufref to check.
    306  bufref_T bufref;
    307  set_bufref(&bufref, buf);
    308  if (!fconfig.noautocmd) {
    309    switchwin_T switchwin;
    310    const int result = switch_win_noblock(&switchwin, wp, tp, true);
    311    assert(result == OK);
    312    (void)result;
    313    if (apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf)) {
    314      tp = win_find_tabpage(wp);
    315    }
    316    restore_win_noblock(&switchwin, true);
    317  }
    318  if (tp && enter) {
    319    goto_tabpage_win(tp, wp);
    320    tp = win_find_tabpage(wp);
    321  }
    322  if (tp && bufref_valid(&bufref) && buf != wp->w_buffer) {
    323    // win_set_buf temporarily makes `wp` the curwin to set the buffer.
    324    // If not entering `wp`, block Enter and Leave events. (cringe)
    325    const bool au_no_enter_leave = curwin != wp && !fconfig.noautocmd;
    326    if (au_no_enter_leave) {
    327      autocmd_no_enter++;
    328      autocmd_no_leave++;
    329    }
    330    win_set_buf(wp, buf, err);
    331    if (!fconfig.noautocmd) {
    332      tp = win_find_tabpage(wp);
    333    }
    334    if (au_no_enter_leave) {
    335      autocmd_no_enter--;
    336      autocmd_no_leave--;
    337    }
    338  }
    339  if (!tp) {
    340    api_clear_error(err);  // may have been set by win_set_buf
    341    api_set_error(err, kErrorTypeException, "Window was closed immediately");
    342    goto cleanup;
    343  }
    344 
    345  if (fconfig.style == kWinStyleMinimal) {
    346    win_set_minimal_style(wp);
    347    didset_window_options(wp, true);
    348  }
    349  rv = wp->handle;
    350 
    351 cleanup:
    352  if (fconfig.noautocmd) {
    353    unblock_autocmds();
    354  }
    355  return rv;
    356 #undef HAS_KEY_X
    357 }
    358 
    359 static WinSplit win_split_dir(win_T *win)
    360 {
    361  if (win->w_frame == NULL || win->w_frame->fr_parent == NULL) {
    362    return kWinSplitLeft;
    363  }
    364 
    365  char layout = win->w_frame->fr_parent->fr_layout;
    366  if (layout == FR_COL) {
    367    return win->w_frame->fr_next ? kWinSplitAbove : kWinSplitBelow;
    368  } else {
    369    return win->w_frame->fr_next ? kWinSplitLeft : kWinSplitRight;
    370  }
    371 }
    372 
    373 static int win_split_flags(WinSplit split, bool toplevel)
    374 {
    375  int flags = 0;
    376  if (split == kWinSplitAbove || split == kWinSplitBelow) {
    377    flags |= WSP_HOR;
    378  } else {
    379    flags |= WSP_VERT;
    380  }
    381  if (split == kWinSplitAbove || split == kWinSplitLeft) {
    382    flags |= toplevel ? WSP_TOP : WSP_ABOVE;
    383  } else {
    384    flags |= toplevel ? WSP_BOT : WSP_BELOW;
    385  }
    386  return flags;
    387 }
    388 
    389 /// Reconfigures the layout of a window.
    390 ///
    391 /// - Absent (`nil`) keys will not be changed.
    392 /// - `row` / `col` / `relative` must be reconfigured together.
    393 /// - Cannot be used to move the last window in a tabpage to a different one.
    394 ///
    395 /// Example: to convert a floating window to a "normal" split window, specify the `win` field:
    396 ///
    397 /// ```lua
    398 /// vim.api.nvim_win_set_config(0, { split = 'above', win = vim.fn.win_getid(1), })
    399 /// ```
    400 ///
    401 /// @see |nvim_open_win()|
    402 ///
    403 /// @param      window  |window-ID|, or 0 for current window
    404 /// @param      config  Map defining the window configuration, see [nvim_open_win()]
    405 /// @param[out] err     Error details, if any
    406 void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err)
    407  FUNC_API_SINCE(6)
    408 {
    409 #define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key)
    410  win_T *win = find_window_by_handle(window, err);
    411  if (!win) {
    412    return;
    413  }
    414 
    415  tabpage_T *win_tp = win_find_tabpage(win);
    416  bool was_split = !win->w_floating;
    417  bool has_split = HAS_KEY_X(config, split);
    418  bool has_vertical = HAS_KEY_X(config, vertical);
    419  // reuse old values, if not overridden
    420  WinConfig fconfig = win->w_config;
    421 
    422  bool to_split = config->relative.size == 0
    423                  && !(HAS_KEY_X(config, external) ? config->external : fconfig.external)
    424                  && (has_split || has_vertical || was_split);
    425 
    426  if (!parse_win_config(win, config, &fconfig, !was_split || to_split, err)) {
    427    return;
    428  }
    429 
    430  if (was_split && !to_split) {
    431    if (!win_new_float(win, false, fconfig, err)) {
    432      return;
    433    }
    434    redraw_later(win, UPD_NOT_VALID);
    435  } else if (to_split) {
    436    win_T *parent = NULL;
    437    tabpage_T *parent_tp = NULL;
    438    if (config->win == 0) {
    439      parent = curwin;
    440      parent_tp = curtab;
    441    } else if (config->win > 0) {
    442      parent = find_window_by_handle(fconfig.window, err);
    443      if (!parent) {
    444        return;
    445      }
    446      parent_tp = win_find_tabpage(parent);
    447    }
    448    if (parent) {
    449      if (parent->w_floating) {
    450        api_set_error(err, kErrorTypeException, "Cannot split a floating window");
    451        return;
    452      }
    453      if (is_aucmd_win(win) && win_tp != parent_tp) {
    454        api_set_error(err, kErrorTypeException, "Cannot move autocmd window to another tabpage");
    455        return;
    456      }
    457      // Can't move the cmdwin or its old curwin to a different tabpage.
    458      if ((win == cmdwin_win || win == cmdwin_old_curwin) && win_tp != parent_tp) {
    459        api_set_error(err, kErrorTypeException, "%s", e_cmdwin);
    460        return;
    461      }
    462    }
    463 
    464    WinSplit old_split = win_split_dir(win);
    465    if (has_vertical && !has_split) {
    466      if (config->vertical) {
    467        fconfig.split = (old_split == kWinSplitRight || p_spr) ? kWinSplitRight : kWinSplitLeft;
    468      } else {
    469        fconfig.split = (old_split == kWinSplitBelow || p_sb) ? kWinSplitBelow : kWinSplitAbove;
    470      }
    471    }
    472    merge_win_config(&win->w_config, fconfig);
    473 
    474    // If there's no "vertical" or "split" set, or if "split" is unchanged,
    475    // then we can just change the size of the window.
    476    if ((!has_vertical && !has_split)
    477        || (was_split && !HAS_KEY_X(config, win) && old_split == fconfig.split)) {
    478      if (HAS_KEY_X(config, width)) {
    479        win_setwidth_win(fconfig.width, win);
    480      }
    481      if (HAS_KEY_X(config, height)) {
    482        win_setheight_win(fconfig.height, win);
    483      }
    484      redraw_later(win, UPD_NOT_VALID);
    485      return;
    486    }
    487 
    488    if (!check_split_disallowed_err(win, err)) {
    489      return;  // error already set
    490    }
    491 
    492    bool to_split_ok = false;
    493    // If we are moving curwin to another tabpage, switch windows *before* we remove it from the
    494    // window list or remove its frame (if non-floating), so it's valid for autocommands.
    495    const bool curwin_moving_tp = win == curwin && parent && win_tp != parent_tp;
    496    if (curwin_moving_tp) {
    497      if (was_split) {
    498        int dir;
    499        win_T *altwin = winframe_find_altwin(win, &dir, NULL, NULL);
    500        // Autocommands may still make this the last non-float after this check.
    501        // That case will be caught later when trying to move the window.
    502        if (!altwin) {
    503          api_set_error(err, kErrorTypeException, "Cannot move last non-floating window");
    504          return;
    505        }
    506        win_goto(altwin);
    507      } else {
    508        win_goto(win_float_find_altwin(win, NULL));
    509      }
    510 
    511      // Autocommands may have been a real nuisance and messed things up...
    512      if (curwin == win) {
    513        api_set_error(err, kErrorTypeException, "Failed to switch away from window %d",
    514                      win->handle);
    515        return;
    516      }
    517      win_tp = win_find_tabpage(win);
    518      if (!win_tp || !win_valid_any_tab(parent)) {
    519        api_set_error(err, kErrorTypeException, "Windows to split were closed");
    520        goto restore_curwin;
    521      }
    522      if (was_split == win->w_floating || parent->w_floating) {
    523        api_set_error(err, kErrorTypeException, "Floating state of windows to split changed");
    524        goto restore_curwin;
    525      }
    526    }
    527 
    528    int dir = 0;
    529    frame_T *unflat_altfr = NULL;
    530    win_T *altwin = NULL;
    531 
    532    if (was_split) {
    533      // If the window is the last in the tabpage or `fconfig.win` is
    534      // a handle to itself, we can't split it.
    535      if (win->w_frame->fr_parent == NULL) {
    536        // FIXME(willothy): if the window is the last in the tabpage but there is another tabpage
    537        // and the target window is in that other tabpage, should we move the window to that
    538        // tabpage and close the previous one, or just error?
    539        api_set_error(err, kErrorTypeException, "Cannot move last non-floating window");
    540        goto restore_curwin;
    541      } else if (parent != NULL && parent->handle == win->handle) {
    542        int n_frames = 0;
    543        for (frame_T *fr = win->w_frame->fr_parent->fr_child; fr != NULL; fr = fr->fr_next) {
    544          n_frames++;
    545        }
    546 
    547        win_T *neighbor = NULL;
    548 
    549        if (n_frames > 2) {
    550          // There are three or more windows in the frame, we need to split a neighboring window.
    551          frame_T *frame = win->w_frame->fr_parent;
    552 
    553          if (frame->fr_parent) {
    554            //   ┌──────────────┐
    555            //   │      A       │
    556            //   ├────┬────┬────┤
    557            //   │ B  │ C  │ D  │
    558            //   └────┴────┴────┘
    559            //          ||
    560            //          \/
    561            // ┌───────────────────┐
    562            // │         A         │
    563            // ├─────────┬─────────┤
    564            // │         │    C    │
    565            // │    B    ├─────────┤
    566            // │         │    D    │
    567            // └─────────┴─────────┘
    568            if (fconfig.split == kWinSplitAbove || fconfig.split == kWinSplitLeft) {
    569              neighbor = win->w_next;
    570            } else {
    571              neighbor = win->w_prev;
    572            }
    573          }
    574          // If the frame doesn't have a parent, the old frame
    575          // was the root frame and we need to create a top-level split.
    576          altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr);
    577        } else if (n_frames == 2) {
    578          // There are two windows in the frame, we can just rotate it.
    579          altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr);
    580          neighbor = altwin;
    581        } else {
    582          // There is only one window in the frame, we can't split it.
    583          api_set_error(err, kErrorTypeException, "Cannot split window into itself");
    584          goto restore_curwin;
    585        }
    586        // Set the parent to whatever the correct neighbor window was determined to be.
    587        parent = neighbor;
    588      } else {
    589        altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr);
    590      }
    591    } else {
    592      altwin = win_float_find_altwin(win, win_tp == curtab ? NULL : win_tp);
    593    }
    594 
    595    win_remove(win, win_tp == curtab ? NULL : win_tp);
    596    if (win_tp == curtab) {
    597      last_status(false);  // may need to remove last status line
    598      win_comp_pos();  // recompute window positions
    599    }
    600 
    601    int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER;
    602    parent_tp = parent ? win_find_tabpage(parent) : curtab;
    603 
    604    TRY_WRAP(err, {
    605      const bool need_switch = parent != NULL && parent != curwin;
    606      switchwin_T switchwin;
    607      if (need_switch) {
    608        // `parent` is valid in its tabpage, so switch_win should not fail.
    609        const int result = switch_win(&switchwin, parent, parent_tp, true);
    610        (void)result;
    611        assert(result == OK);
    612      }
    613      to_split_ok = win_split_ins(0, flags, win, 0, unflat_altfr) != NULL;
    614      if (!to_split_ok) {
    615        // Restore `win` to the window list now, so it's valid for restore_win (if used).
    616        win_append(win->w_prev, win, win_tp == curtab ? NULL : win_tp);
    617      }
    618      if (need_switch) {
    619        restore_win(&switchwin, true);
    620      }
    621    });
    622    if (!to_split_ok) {
    623      if (was_split) {
    624        // win_split_ins doesn't change sizes or layout if it fails to insert an existing window, so
    625        // just undo winframe_remove.
    626        winframe_restore(win, dir, unflat_altfr);
    627      }
    628      if (!ERROR_SET(err)) {
    629        api_set_error(err, kErrorTypeException, "Failed to move window %d into split", win->handle);
    630      }
    631 
    632 restore_curwin:
    633      // If `win` was the original curwin, and autocommands didn't move it outside of curtab, be a
    634      // good citizen and try to return to it.
    635      if (curwin_moving_tp && win_valid(win)) {
    636        win_goto(win);
    637      }
    638      return;
    639    }
    640 
    641    // If `win` moved tabpages and was the curwin of its old one, select a new curwin for it.
    642    if (win_tp != parent_tp && win_tp->tp_curwin == win) {
    643      win_tp->tp_curwin = altwin;
    644    }
    645 
    646    if (HAS_KEY_X(config, width)) {
    647      win_setwidth_win(fconfig.width, win);
    648    }
    649    if (HAS_KEY_X(config, height)) {
    650      win_setheight_win(fconfig.height, win);
    651    }
    652  } else {
    653    win_config_float(win, fconfig);
    654  }
    655  if (HAS_KEY_X(config, style)) {
    656    if (fconfig.style == kWinStyleMinimal) {
    657      win_set_minimal_style(win);
    658      didset_window_options(win, true);
    659    }
    660  }
    661  if (fconfig._cmdline_offset < INT_MAX) {
    662    cmdline_win = win;
    663  } else if (win == cmdline_win && fconfig._cmdline_offset == INT_MAX) {
    664    cmdline_win = NULL;
    665  }
    666 #undef HAS_KEY_X
    667 }
    668 
    669 #define PUT_KEY_X(d, key, value) PUT_KEY(d, win_config, key, value)
    670 static void config_put_bordertext(Dict(win_config) *config, WinConfig *fconfig,
    671                                  BorderTextType bordertext_type, Arena *arena)
    672 {
    673  VirtText vt;
    674  AlignTextPos align;
    675  switch (bordertext_type) {
    676  case kBorderTextTitle:
    677    vt = fconfig->title_chunks;
    678    align = fconfig->title_pos;
    679    break;
    680  case kBorderTextFooter:
    681    vt = fconfig->footer_chunks;
    682    align = fconfig->footer_pos;
    683    break;
    684  }
    685 
    686  Array bordertext = virt_text_to_array(vt, true, arena);
    687 
    688  char *pos;
    689  switch (align) {
    690  case kAlignLeft:
    691    pos = "left";
    692    break;
    693  case kAlignCenter:
    694    pos = "center";
    695    break;
    696  case kAlignRight:
    697    pos = "right";
    698    break;
    699  }
    700 
    701  switch (bordertext_type) {
    702  case kBorderTextTitle:
    703    PUT_KEY_X(*config, title, ARRAY_OBJ(bordertext));
    704    PUT_KEY_X(*config, title_pos, cstr_as_string(pos));
    705    break;
    706  case kBorderTextFooter:
    707    PUT_KEY_X(*config, footer, ARRAY_OBJ(bordertext));
    708    PUT_KEY_X(*config, footer_pos, cstr_as_string(pos));
    709  }
    710 }
    711 
    712 /// Gets window configuration in the form of a dict which can be passed as the `config` parameter of
    713 /// |nvim_open_win()|.
    714 ///
    715 /// For non-floating windows, `relative` is empty.
    716 ///
    717 /// @param      window |window-ID|, or 0 for current window
    718 /// @param[out] err Error details, if any
    719 /// @return     Map defining the window configuration, see |nvim_open_win()|
    720 Dict(win_config) nvim_win_get_config(Window window, Arena *arena, Error *err)
    721  FUNC_API_SINCE(6)
    722 {
    723  /// Keep in sync with FloatRelative in buffer_defs.h
    724  static const char *const float_relative_str[] = {
    725    "editor", "win", "cursor", "mouse", "tabline", "laststatus"
    726  };
    727 
    728  /// Keep in sync with WinSplit in buffer_defs.h
    729  static const char *const win_split_str[] = { "left", "right", "above", "below" };
    730 
    731  Dict(win_config) rv = KEYDICT_INIT;
    732 
    733  win_T *wp = find_window_by_handle(window, err);
    734  if (!wp) {
    735    return rv;
    736  }
    737 
    738  WinConfig *config = &wp->w_config;
    739 
    740  PUT_KEY_X(rv, focusable, config->focusable);
    741  PUT_KEY_X(rv, external, config->external);
    742  PUT_KEY_X(rv, hide, config->hide);
    743  PUT_KEY_X(rv, mouse, config->mouse);
    744 
    745  if (wp->w_floating) {
    746    PUT_KEY_X(rv, width, config->width);
    747    PUT_KEY_X(rv, height, config->height);
    748    if (!config->external) {
    749      if (config->relative == kFloatRelativeWindow) {
    750        PUT_KEY_X(rv, win, config->window);
    751        if (config->bufpos.lnum >= 0) {
    752          Array pos = arena_array(arena, 2);
    753          ADD_C(pos, INTEGER_OBJ(config->bufpos.lnum));
    754          ADD_C(pos, INTEGER_OBJ(config->bufpos.col));
    755          PUT_KEY_X(rv, bufpos, pos);
    756        }
    757      }
    758      PUT_KEY_X(rv, anchor, cstr_as_string(float_anchor_str[config->anchor]));
    759      PUT_KEY_X(rv, row, config->row);
    760      PUT_KEY_X(rv, col, config->col);
    761      PUT_KEY_X(rv, zindex, config->zindex);
    762    }
    763    if (config->border) {
    764      Array border = arena_array(arena, 8);
    765      for (size_t i = 0; i < 8; i++) {
    766        String s = cstrn_as_string(config->border_chars[i], MAX_SCHAR_SIZE);
    767 
    768        int hi_id = config->border_hl_ids[i];
    769        char *hi_name = syn_id2name(hi_id);
    770        if (hi_name[0]) {
    771          Array tuple = arena_array(arena, 2);
    772          ADD_C(tuple, STRING_OBJ(s));
    773          ADD_C(tuple, CSTR_AS_OBJ(hi_name));
    774          ADD_C(border, ARRAY_OBJ(tuple));
    775        } else {
    776          ADD_C(border, STRING_OBJ(s));
    777        }
    778      }
    779      PUT_KEY_X(rv, border, ARRAY_OBJ(border));
    780      if (config->title) {
    781        config_put_bordertext(&rv, config, kBorderTextTitle, arena);
    782      }
    783      if (config->footer) {
    784        config_put_bordertext(&rv, config, kBorderTextFooter, arena);
    785      }
    786    } else {
    787      PUT_KEY_X(rv, border, STRING_OBJ(cstr_as_string("none")));
    788    }
    789  } else if (!config->external) {
    790    PUT_KEY_X(rv, width, wp->w_width);
    791    PUT_KEY_X(rv, height, wp->w_height);
    792    WinSplit split = win_split_dir(wp);
    793    PUT_KEY_X(rv, split, cstr_as_string(win_split_str[split]));
    794  }
    795 
    796  const char *rel = (wp->w_floating && !config->external
    797                     ? float_relative_str[config->relative] : "");
    798  PUT_KEY_X(rv, relative, cstr_as_string(rel));
    799  if (config->_cmdline_offset < INT_MAX) {
    800    PUT_KEY_X(rv, _cmdline_offset, config->_cmdline_offset);
    801  }
    802 
    803  return rv;
    804 }
    805 
    806 static bool parse_float_anchor(String anchor, FloatAnchor *out)
    807 {
    808  if (anchor.size == 0) {
    809    *out = (FloatAnchor)0;
    810  }
    811  char *str = anchor.data;
    812  if (striequal(str, "NW")) {
    813    *out = 0;  //  NW is the default
    814  } else if (striequal(str, "NE")) {
    815    *out = kFloatAnchorEast;
    816  } else if (striequal(str, "SW")) {
    817    *out = kFloatAnchorSouth;
    818  } else if (striequal(str, "SE")) {
    819    *out = kFloatAnchorSouth | kFloatAnchorEast;
    820  } else {
    821    return false;
    822  }
    823  return true;
    824 }
    825 
    826 static bool parse_float_relative(String relative, FloatRelative *out)
    827 {
    828  char *str = relative.data;
    829  if (striequal(str, "editor")) {
    830    *out = kFloatRelativeEditor;
    831  } else if (striequal(str, "win")) {
    832    *out = kFloatRelativeWindow;
    833  } else if (striequal(str, "cursor")) {
    834    *out = kFloatRelativeCursor;
    835  } else if (striequal(str, "mouse")) {
    836    *out = kFloatRelativeMouse;
    837  } else if (striequal(str, "tabline")) {
    838    *out = kFloatRelativeTabline;
    839  } else if (striequal(str, "laststatus")) {
    840    *out = kFloatRelativeLaststatus;
    841  } else {
    842    return false;
    843  }
    844  return true;
    845 }
    846 
    847 static bool parse_config_split(String split, WinSplit *out)
    848 {
    849  char *str = split.data;
    850  if (striequal(str, "left")) {
    851    *out = kWinSplitLeft;
    852  } else if (striequal(str, "right")) {
    853    *out = kWinSplitRight;
    854  } else if (striequal(str, "above")) {
    855    *out = kWinSplitAbove;
    856  } else if (striequal(str, "below")) {
    857    *out = kWinSplitBelow;
    858  } else {
    859    return false;
    860  }
    861  return true;
    862 }
    863 
    864 static bool parse_float_bufpos(Array bufpos, lpos_T *out)
    865 {
    866  if (bufpos.size != 2 || bufpos.items[0].type != kObjectTypeInteger
    867      || bufpos.items[1].type != kObjectTypeInteger) {
    868    return false;
    869  }
    870  out->lnum = (linenr_T)bufpos.items[0].data.integer;
    871  out->col = (colnr_T)bufpos.items[1].data.integer;
    872  return true;
    873 }
    874 
    875 static void parse_bordertext(Object bordertext, BorderTextType bordertext_type, WinConfig *fconfig,
    876                             Error *err)
    877 {
    878  if (bordertext.type != kObjectTypeString && bordertext.type != kObjectTypeArray) {
    879    api_set_error(err, kErrorTypeValidation, "title/footer must be string or array");
    880    return;
    881  }
    882 
    883  if (bordertext.type == kObjectTypeArray && bordertext.data.array.size == 0) {
    884    api_set_error(err, kErrorTypeValidation, "title/footer cannot be an empty array");
    885    return;
    886  }
    887 
    888  bool *is_present;
    889  VirtText *chunks;
    890  int *width;
    891  switch (bordertext_type) {
    892  case kBorderTextTitle:
    893    is_present = &fconfig->title;
    894    chunks = &fconfig->title_chunks;
    895    width = &fconfig->title_width;
    896    break;
    897  case kBorderTextFooter:
    898    is_present = &fconfig->footer;
    899    chunks = &fconfig->footer_chunks;
    900    width = &fconfig->footer_width;
    901    break;
    902  }
    903 
    904  if (bordertext.type == kObjectTypeString) {
    905    if (bordertext.data.string.size == 0) {
    906      *is_present = false;
    907      return;
    908    }
    909    kv_init(*chunks);
    910    kv_push(*chunks, ((VirtTextChunk){ .text = xstrdup(bordertext.data.string.data),
    911                                       .hl_id = -1 }));
    912    *width = (int)mb_string2cells(bordertext.data.string.data);
    913    *is_present = true;
    914    return;
    915  }
    916 
    917  *width = 0;
    918  *chunks = parse_virt_text(bordertext.data.array, err, width);
    919 
    920  *is_present = true;
    921 }
    922 
    923 static bool parse_bordertext_pos(win_T *wp, String bordertext_pos, BorderTextType bordertext_type,
    924                                 WinConfig *fconfig, Error *err)
    925 {
    926  AlignTextPos *align;
    927  switch (bordertext_type) {
    928  case kBorderTextTitle:
    929    align = &fconfig->title_pos;
    930    break;
    931  case kBorderTextFooter:
    932    align = &fconfig->footer_pos;
    933    break;
    934  }
    935 
    936  if (bordertext_pos.size == 0) {
    937    if (!wp) {
    938      *align = kAlignLeft;
    939    }
    940    return true;
    941  }
    942 
    943  char *pos = bordertext_pos.data;
    944 
    945  if (strequal(pos, "left")) {
    946    *align = kAlignLeft;
    947  } else if (strequal(pos, "center")) {
    948    *align = kAlignCenter;
    949  } else if (strequal(pos, "right")) {
    950    *align = kAlignRight;
    951  } else {
    952    switch (bordertext_type) {
    953    case kBorderTextTitle:
    954      api_set_error(err, kErrorTypeValidation, "invalid title_pos value");
    955      break;
    956    case kBorderTextFooter:
    957      api_set_error(err, kErrorTypeValidation, "invalid footer_pos value");
    958      break;
    959    }
    960    return false;
    961  }
    962  return true;
    963 }
    964 
    965 void parse_border_style(Object style, WinConfig *fconfig, Error *err)
    966 {
    967  struct {
    968    const char *name;
    969    char chars[8][MAX_SCHAR_SIZE];
    970    bool shadow_color;
    971  } defaults[] = {
    972    { opt_winborder_values[1], { "╔", "═", "╗", "║", "╝", "═", "╚", "║" }, false },
    973    { opt_winborder_values[2], { "┌", "─", "┐", "│", "┘", "─", "└", "│" }, false },
    974    { opt_winborder_values[3], { "", "", " ", " ", " ", " ", " ", "" }, true },
    975    { opt_winborder_values[4], { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, false },
    976    { opt_winborder_values[5], { " ", " ", " ", " ", " ", " ", " ", " " }, false },
    977    { opt_winborder_values[6], { "┏", "━", "┓", "┃", "┛", "━", "┗", "┃" }, false },
    978    { NULL, { { NUL } }, false },
    979  };
    980 
    981  char(*chars)[MAX_SCHAR_SIZE] = fconfig->border_chars;
    982  int *hl_ids = fconfig->border_hl_ids;
    983 
    984  fconfig->border = true;
    985 
    986  if (style.type == kObjectTypeArray) {
    987    Array arr = style.data.array;
    988    size_t size = arr.size;
    989    if (!size || size > 8 || (size & (size - 1))) {
    990      api_set_error(err, kErrorTypeValidation, "invalid number of border chars");
    991      return;
    992    }
    993    for (size_t i = 0; i < size; i++) {
    994      Object iytem = arr.items[i];
    995      String string;
    996      int hl_id = 0;
    997      if (iytem.type == kObjectTypeArray) {
    998        Array iarr = iytem.data.array;
    999        if (!iarr.size || iarr.size > 2) {
   1000          api_set_error(err, kErrorTypeValidation, "invalid border char");
   1001          return;
   1002        }
   1003        if (iarr.items[0].type != kObjectTypeString) {
   1004          api_set_error(err, kErrorTypeValidation, "invalid border char");
   1005          return;
   1006        }
   1007        string = iarr.items[0].data.string;
   1008        if (iarr.size == 2) {
   1009          hl_id = object_to_hl_id(iarr.items[1], "border char highlight", err);
   1010          if (ERROR_SET(err)) {
   1011            return;
   1012          }
   1013        }
   1014      } else if (iytem.type == kObjectTypeString) {
   1015        string = iytem.data.string;
   1016      } else {
   1017        api_set_error(err, kErrorTypeValidation, "invalid border char");
   1018        return;
   1019      }
   1020      if (string.size && mb_string2cells_len(string.data, string.size) > 1) {
   1021        api_set_error(err, kErrorTypeValidation, "border chars must be one cell");
   1022        return;
   1023      }
   1024      size_t len = MIN(string.size, sizeof(*chars) - 1);
   1025      if (len) {
   1026        memcpy(chars[i], string.data, len);
   1027      }
   1028      chars[i][len] = NUL;
   1029      hl_ids[i] = hl_id;
   1030    }
   1031    while (size < 8) {
   1032      memcpy(chars + size, chars, sizeof(*chars) * size);
   1033      memcpy(hl_ids + size, hl_ids, sizeof(*hl_ids) * size);
   1034      size <<= 1;
   1035    }
   1036    if ((chars[7][0] && chars[1][0] && !chars[0][0])
   1037        || (chars[1][0] && chars[3][0] && !chars[2][0])
   1038        || (chars[3][0] && chars[5][0] && !chars[4][0])
   1039        || (chars[5][0] && chars[7][0] && !chars[6][0])) {
   1040      api_set_error(err, kErrorTypeValidation, "corner between used edges must be specified");
   1041    }
   1042  } else if (style.type == kObjectTypeString) {
   1043    String str = style.data.string;
   1044    if (str.size == 0 || strequal(str.data, "none")) {
   1045      fconfig->border = false;
   1046      // border text does not work with border equal none
   1047      fconfig->title = false;
   1048      fconfig->footer = false;
   1049      return;
   1050    }
   1051    for (size_t i = 0; defaults[i].name; i++) {
   1052      if (strequal(str.data, defaults[i].name)) {
   1053        memcpy(chars, defaults[i].chars, sizeof(defaults[i].chars));
   1054        memset(hl_ids, 0, 8 * sizeof(*hl_ids));
   1055        if (defaults[i].shadow_color) {
   1056          int hl_blend = SYN_GROUP_STATIC("FloatShadow");
   1057          int hl_through = SYN_GROUP_STATIC("FloatShadowThrough");
   1058          hl_ids[2] = hl_through;
   1059          hl_ids[3] = hl_blend;
   1060          hl_ids[4] = hl_blend;
   1061          hl_ids[5] = hl_blend;
   1062          hl_ids[6] = hl_through;
   1063        }
   1064        return;
   1065      }
   1066    }
   1067    api_set_error(err, kErrorTypeValidation, "invalid border style \"%s\"", str.data);
   1068  }
   1069 }
   1070 
   1071 static void generate_api_error(win_T *wp, const char *attribute, Error *err)
   1072 {
   1073  if (wp != NULL && wp->w_floating) {
   1074    api_set_error(err, kErrorTypeValidation,
   1075                  "Missing 'relative' field when reconfiguring floating window %d",
   1076                  wp->handle);
   1077  } else {
   1078    api_set_error(err, kErrorTypeValidation, "non-float cannot have '%s'", attribute);
   1079  }
   1080 }
   1081 
   1082 /// Parses a border style name or custom (comma-separated) style.
   1083 bool parse_winborder(WinConfig *fconfig, char *border_opt, Error *err)
   1084 {
   1085  if (!fconfig) {
   1086    return false;
   1087  }
   1088  Object style = OBJECT_INIT;
   1089 
   1090  if (strchr(border_opt, ',')) {
   1091    Array border_chars = ARRAY_DICT_INIT;
   1092    char *p = border_opt;
   1093    char part[MAX_SCHAR_SIZE] = { 0 };
   1094    int count = 0;
   1095 
   1096    while (*p != NUL) {
   1097      if (count >= 8) {
   1098        api_free_array(border_chars);
   1099        return false;
   1100      }
   1101 
   1102      size_t part_len = copy_option_part(&p, part, sizeof(part), ",");
   1103      if (part_len == 0 || part[0] == NUL) {
   1104        api_free_array(border_chars);
   1105        return false;
   1106      }
   1107 
   1108      String str = cstr_to_string(part);
   1109      ADD(border_chars, STRING_OBJ(str));
   1110      count++;
   1111    }
   1112 
   1113    if (count != 8) {
   1114      api_free_array(border_chars);
   1115      return false;
   1116    }
   1117 
   1118    style = ARRAY_OBJ(border_chars);
   1119  } else {
   1120    style = CSTR_TO_OBJ(border_opt);
   1121  }
   1122 
   1123  parse_border_style(style, fconfig, err);
   1124  api_free_object(style);
   1125  return !ERROR_SET(err);
   1126 }
   1127 
   1128 static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fconfig, bool reconf,
   1129                             Error *err)
   1130 {
   1131 #define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key)
   1132  bool has_relative = false, relative_is_win = false, is_split = false;
   1133  if (config->relative.size > 0) {
   1134    if (!parse_float_relative(config->relative, &fconfig->relative)) {
   1135      api_set_error(err, kErrorTypeValidation, "Invalid value of 'relative' key");
   1136      goto fail;
   1137    }
   1138 
   1139    if (config->relative.size > 0 && !(HAS_KEY_X(config, row) && HAS_KEY_X(config, col))
   1140        && !HAS_KEY_X(config, bufpos)) {
   1141      api_set_error(err, kErrorTypeValidation, "'relative' requires 'row'/'col' or 'bufpos'");
   1142      goto fail;
   1143    }
   1144 
   1145    has_relative = true;
   1146    fconfig->external = false;
   1147    if (fconfig->relative == kFloatRelativeWindow) {
   1148      relative_is_win = true;
   1149      fconfig->bufpos.lnum = -1;
   1150    }
   1151  } else if (!config->external) {
   1152    if (HAS_KEY_X(config, vertical) || HAS_KEY_X(config, split)) {
   1153      is_split = true;
   1154    } else if (wp == NULL) {  // new win
   1155      api_set_error(err, kErrorTypeValidation,
   1156                    "Must specify 'relative' or 'external' when creating a float");
   1157      goto fail;
   1158    }
   1159  }
   1160 
   1161  if (HAS_KEY_X(config, vertical)) {
   1162    if (!is_split) {
   1163      api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'vertical'");
   1164      goto fail;
   1165    }
   1166  }
   1167 
   1168  if (HAS_KEY_X(config, split)) {
   1169    if (!is_split) {
   1170      api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'split'");
   1171      goto fail;
   1172    }
   1173    if (!parse_config_split(config->split, &fconfig->split)) {
   1174      api_set_error(err, kErrorTypeValidation, "Invalid value of 'split' key");
   1175      goto fail;
   1176    }
   1177  }
   1178 
   1179  if (HAS_KEY_X(config, anchor)) {
   1180    if (!parse_float_anchor(config->anchor, &fconfig->anchor)) {
   1181      api_set_error(err, kErrorTypeValidation, "Invalid value of 'anchor' key");
   1182      goto fail;
   1183    }
   1184  }
   1185 
   1186  if (HAS_KEY_X(config, row)) {
   1187    if (!has_relative || is_split) {
   1188      generate_api_error(wp, "row", err);
   1189      goto fail;
   1190    }
   1191    fconfig->row = config->row;
   1192  }
   1193 
   1194  if (HAS_KEY_X(config, col)) {
   1195    if (!has_relative || is_split) {
   1196      generate_api_error(wp, "col", err);
   1197      goto fail;
   1198    }
   1199    fconfig->col = config->col;
   1200  }
   1201 
   1202  if (HAS_KEY_X(config, bufpos)) {
   1203    if (!has_relative || is_split) {
   1204      generate_api_error(wp, "bufpos", err);
   1205      goto fail;
   1206    } else {
   1207      if (!parse_float_bufpos(config->bufpos, &fconfig->bufpos)) {
   1208        api_set_error(err, kErrorTypeValidation, "Invalid value of 'bufpos' key");
   1209        goto fail;
   1210      }
   1211 
   1212      if (!HAS_KEY_X(config, row)) {
   1213        fconfig->row = (fconfig->anchor & kFloatAnchorSouth) ? 0 : 1;
   1214      }
   1215      if (!HAS_KEY_X(config, col)) {
   1216        fconfig->col = 0;
   1217      }
   1218    }
   1219  }
   1220 
   1221  if (HAS_KEY_X(config, width)) {
   1222    if (config->width > 0) {
   1223      fconfig->width = (int)config->width;
   1224    } else {
   1225      api_set_error(err, kErrorTypeValidation, "'width' key must be a positive Integer");
   1226      goto fail;
   1227    }
   1228  } else if (!reconf && !is_split) {
   1229    api_set_error(err, kErrorTypeValidation, "Must specify 'width'");
   1230    goto fail;
   1231  }
   1232 
   1233  if (HAS_KEY_X(config, height)) {
   1234    if (config->height > 0) {
   1235      fconfig->height = (int)config->height;
   1236    } else {
   1237      api_set_error(err, kErrorTypeValidation, "'height' key must be a positive Integer");
   1238      goto fail;
   1239    }
   1240  } else if (!reconf && !is_split) {
   1241    api_set_error(err, kErrorTypeValidation, "Must specify 'height'");
   1242    goto fail;
   1243  }
   1244 
   1245  if (relative_is_win || is_split) {
   1246    if (reconf && relative_is_win) {
   1247      win_T *target_win = find_window_by_handle(config->win, err);
   1248      if (!target_win) {
   1249        goto fail;
   1250      }
   1251 
   1252      if (target_win == wp) {
   1253        api_set_error(err, kErrorTypeException, "floating window cannot be relative to itself");
   1254        goto fail;
   1255      }
   1256    }
   1257    fconfig->window = curwin->handle;
   1258    if (HAS_KEY_X(config, win)) {
   1259      if (config->win > 0) {
   1260        fconfig->window = config->win;
   1261      }
   1262    }
   1263  } else if (HAS_KEY_X(config, win)) {
   1264    if (has_relative) {
   1265      api_set_error(err, kErrorTypeValidation,
   1266                    "'win' key is only valid with relative='win' and relative=''");
   1267      goto fail;
   1268    } else if (!is_split) {
   1269      api_set_error(err, kErrorTypeValidation,
   1270                    "non-float with 'win' requires at least 'split' or 'vertical'");
   1271      goto fail;
   1272    }
   1273  }
   1274 
   1275  if (HAS_KEY_X(config, external)) {
   1276    fconfig->external = config->external;
   1277    if (has_relative && fconfig->external) {
   1278      api_set_error(err, kErrorTypeValidation,
   1279                    "Only one of 'relative' and 'external' must be used");
   1280      goto fail;
   1281    }
   1282    if (fconfig->external && !ui_has(kUIMultigrid)) {
   1283      api_set_error(err, kErrorTypeValidation, "UI doesn't support external windows");
   1284      goto fail;
   1285    }
   1286  }
   1287 
   1288  if (HAS_KEY_X(config, focusable)) {
   1289    fconfig->focusable = config->focusable;
   1290    fconfig->mouse = config->focusable;
   1291  }
   1292 
   1293  if (HAS_KEY_X(config, mouse)) {
   1294    fconfig->mouse = config->mouse;
   1295  }
   1296 
   1297  if (HAS_KEY_X(config, zindex)) {
   1298    if (is_split) {
   1299      api_set_error(err, kErrorTypeValidation, "non-float cannot have 'zindex'");
   1300      goto fail;
   1301    }
   1302    if (config->zindex > 0) {
   1303      fconfig->zindex = (int)config->zindex;
   1304    } else {
   1305      api_set_error(err, kErrorTypeValidation, "'zindex' key must be a positive Integer");
   1306      goto fail;
   1307    }
   1308  }
   1309 
   1310  if (HAS_KEY_X(config, title)) {
   1311    if (is_split) {
   1312      api_set_error(err, kErrorTypeValidation, "non-float cannot have 'title'");
   1313      goto fail;
   1314    }
   1315 
   1316    parse_bordertext(config->title, kBorderTextTitle, fconfig, err);
   1317    if (ERROR_SET(err)) {
   1318      goto fail;
   1319    }
   1320 
   1321    // handles unset 'title_pos' same as empty string
   1322    if (!parse_bordertext_pos(wp, config->title_pos, kBorderTextTitle, fconfig, err)) {
   1323      goto fail;
   1324    }
   1325  } else {
   1326    if (HAS_KEY_X(config, title_pos)) {
   1327      api_set_error(err, kErrorTypeException, "title_pos requires title to be set");
   1328      goto fail;
   1329    }
   1330  }
   1331 
   1332  if (HAS_KEY_X(config, footer)) {
   1333    if (is_split) {
   1334      api_set_error(err, kErrorTypeValidation, "non-float cannot have 'footer'");
   1335      goto fail;
   1336    }
   1337 
   1338    parse_bordertext(config->footer, kBorderTextFooter, fconfig, err);
   1339    if (ERROR_SET(err)) {
   1340      goto fail;
   1341    }
   1342 
   1343    // handles unset 'footer_pos' same as empty string
   1344    if (!parse_bordertext_pos(wp, config->footer_pos, kBorderTextFooter, fconfig, err)) {
   1345      goto fail;
   1346    }
   1347  } else {
   1348    if (HAS_KEY_X(config, footer_pos)) {
   1349      api_set_error(err, kErrorTypeException, "footer_pos requires footer to be set");
   1350      goto fail;
   1351    }
   1352  }
   1353 
   1354  Object border_style = OBJECT_INIT;
   1355  if (HAS_KEY_X(config, border)) {
   1356    if (is_split) {
   1357      api_set_error(err, kErrorTypeValidation, "non-float cannot have 'border'");
   1358      goto fail;
   1359    }
   1360    border_style = config->border;
   1361    if (border_style.type != kObjectTypeNil) {
   1362      parse_border_style(border_style, fconfig, err);
   1363      if (ERROR_SET(err)) {
   1364        goto fail;
   1365      }
   1366    }
   1367  } else if (*p_winborder != NUL && (wp == NULL || !wp->w_floating)
   1368             && !parse_winborder(fconfig, p_winborder, err)) {
   1369    goto fail;
   1370  }
   1371 
   1372  if (HAS_KEY_X(config, style)) {
   1373    if (config->style.data[0] == NUL) {
   1374      fconfig->style = kWinStyleUnused;
   1375    } else if (striequal(config->style.data, "minimal")) {
   1376      fconfig->style = kWinStyleMinimal;
   1377    } else {
   1378      api_set_error(err, kErrorTypeValidation, "Invalid value of 'style' key");
   1379      goto fail;
   1380    }
   1381  }
   1382 
   1383  if (HAS_KEY_X(config, noautocmd)) {
   1384    if (wp && config->noautocmd != fconfig->noautocmd) {
   1385      api_set_error(err, kErrorTypeValidation,
   1386                    "'noautocmd' cannot be changed with existing windows");
   1387      goto fail;
   1388    }
   1389    fconfig->noautocmd = config->noautocmd;
   1390  }
   1391 
   1392  if (HAS_KEY_X(config, fixed)) {
   1393    fconfig->fixed = config->fixed;
   1394  }
   1395 
   1396  if (HAS_KEY_X(config, hide)) {
   1397    fconfig->hide = config->hide;
   1398  }
   1399 
   1400  if (HAS_KEY_X(config, _cmdline_offset)) {
   1401    fconfig->_cmdline_offset = (int)config->_cmdline_offset;
   1402  }
   1403 
   1404  return true;
   1405 
   1406 fail:
   1407  merge_win_config(fconfig, wp != NULL ? wp->w_config : WIN_CONFIG_INIT);
   1408  return false;
   1409 #undef HAS_KEY_X
   1410 }