#include #include #include #include "klib/kvec.h" #include "nvim/api/extmark.h" #include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" #include "nvim/api/win_config.h" #include "nvim/ascii_defs.h" #include "nvim/autocmd.h" #include "nvim/autocmd_defs.h" #include "nvim/buffer.h" #include "nvim/buffer_defs.h" #include "nvim/decoration_defs.h" #include "nvim/drawscreen.h" #include "nvim/errors.h" #include "nvim/eval/window.h" #include "nvim/globals.h" #include "nvim/highlight_group.h" #include "nvim/macros_defs.h" #include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/memory_defs.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_defs.h" #include "nvim/vim_defs.h" #include "nvim/window.h" #include "nvim/winfloat.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/win_config.c.generated.h" #endif /// 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|. /// /// 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 /// (used by non-multigrid UIs) will always round down to nearest integer. /// /// Out-of-bounds values, and configurations that make the float not fit inside /// the main editor, are allowed. The builtin implementation truncates values /// so floats are fully within the main screen grid. External GUIs /// could let floats hover outside of the main window like a tooltip, but /// this should not be used to specify arbitrary WM screen positions. /// /// Example (Lua): window-relative float /// /// ```lua /// vim.api.nvim_open_win(0, false, /// {relative='win', row=3, col=3, width=12, height=3}) /// ``` /// /// Example (Lua): buffer-relative float (travels as buffer is scrolled) /// /// ```lua /// vim.api.nvim_open_win(0, false, /// {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: /// - relative: Sets the window layout to "floating", placed at (row,col) /// coordinates relative to: /// - "cursor" Cursor position in current window. /// - "editor" The global editor grid. /// - "laststatus" 'laststatus' if present, or last row. /// - "mouse" Mouse position. /// - "tabline" Tabline if present, or first row. /// - "win" Window given by the `win` field, or current window. /// - 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 /// - "SW" southwest /// - "SE" southeast /// - width: Window width (in character cells). Minimum of 1. /// - height: Window height (in character cells). Minimum of 1. /// - bufpos: Places float relative to buffer text (only when /// relative="win"). Takes a tuple of zero-indexed `[line, column]`. /// `row` and `col` if given are applied relative to this /// position, else they default to: /// - `row=1` and `col=0` if `anchor` is "NW" or "NE" /// - `row=0` and `col=0` if `anchor` is "SW" or "SE" /// (thus like a tooltip near the buffer text). /// - row: Row position in units of "screen cell height", may be fractional. /// - col: Column position in units of "screen cell width", may be /// fractional. /// - focusable: Enable focus by user actions (wincmds, mouse events). /// Defaults to true. Non-focusable windows can be entered by /// |nvim_set_current_win()|, or, when the `mouse` field is set to true, /// by mouse events. See |focusable|. /// - mouse: Specify how this window interacts with mouse events. /// Defaults to `focusable` value. /// - If false, mouse events pass through this window. /// - If true, mouse events interact with this window normally. /// - external: GUI should display the window as an external /// top-level window. Currently accepts no other positioning /// configuration together with this. /// - zindex: Stacking order. floats with higher `zindex` go on top on /// floats with lower indices. Must be larger than zero. The /// following screen elements have hard-coded z-indices: /// - 100: insert completion popupmenu /// - 200: message scrollback /// - 250: cmdline completion popupmenu (when wildoptions+=pum) /// The default value for floats are 50. In general, values below 100 are /// recommended, unless there is a good reason to overshadow builtin /// elements. /// - style: (optional) Configure the appearance of the window. Currently /// only supports one value: /// - "minimal" Nvim will display the window with many UI options /// disabled. This is useful when displaying a temporary /// float where the text should not be edited. Disables /// 'number', 'relativenumber', 'cursorline', 'cursorcolumn', /// 'foldcolumn', 'spell' and 'list' options. 'signcolumn' /// is changed to `auto` and 'colorcolumn' is cleared. /// 'statuscolumn' is changed to empty. The end-of-buffer /// region is hidden by setting `eob` flag of /// 'fillchars' to a space char, and clearing the /// |hl-EndOfBuffer| region in 'winhighlight'. /// - border: Style of (optional) window border. This can either be a string /// or an array. The string values are /// - "none": No border (default). /// - "single": A single line box. /// - "double": A double line box. /// - "rounded": Like "single", but with rounded corners ("╭" etc.). /// - "solid": Adds padding by a single whitespace cell. /// - "shadow": A drop shadow effect by blending with the background. /// - If it is an array, it should have a length of eight or any divisor of /// eight. The array will specify the eight chars building up the border /// in a clockwise fashion starting with the top-left corner. As an /// example, the double box style could be specified as: /// ``` /// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ]. /// ``` /// If the number of chars are less than eight, they will be repeated. Thus /// an ASCII border could be specified as /// ``` /// [ "/", "-", \"\\\\\", "|" ], /// ``` /// or all chars the same as /// ``` /// [ "x" ]. /// ``` /// An empty string can be used to turn off a specific border, for instance, /// ``` /// [ "", "", "", ">", "", "", "", "<" ] /// ``` /// will only make vertical borders but not horizontal ones. /// By default, `FloatBorder` highlight is used, which links to `WinSeparator` /// when not defined. It could also be specified by character: /// ``` /// [ ["+", "MyCorner"], ["x", "MyBorder"] ]. /// ``` /// - title: Title (optional) in window border, string or list. /// List should consist of `[text, highlight]` tuples. /// If string, or a tuple lacks a highlight, the default highlight group is `FloatTitle`. /// - title_pos: Title position. Must be set with `title` option. /// Value can be one of "left", "center", or "right". /// Default is `"left"`. /// - footer: Footer (optional) in window border, string or list. /// List should consist of `[text, highlight]` tuples. /// If string, or a tuple lacks a highlight, the default highlight group is `FloatFooter`. /// - footer_pos: Footer position. Must be set with `footer` option. /// Value can be one of "left", "center", or "right". /// Default is `"left"`. /// - noautocmd: If true then all autocommands are blocked for the duration of /// the call. /// - 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(win_config) *config, Error *err) FUNC_API_SINCE(6) FUNC_API_TEXTLOCK_ALLOW_CMDWIN { #define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) buf_T *buf = find_buffer_by_handle(buffer, err); if (!buf) { return 0; } if ((cmdwin_type != 0 && enter) || buf == cmdwin_buf) { api_set_error(err, kErrorTypeException, "%s", e_cmdwin); return 0; } WinConfig fconfig = WIN_CONFIG_INIT; if (!parse_win_config(NULL, config, &fconfig, false, err)) { return 0; } bool is_split = HAS_KEY_X(config, split) || HAS_KEY_X(config, vertical); Window rv = 0; if (fconfig.noautocmd) { block_autocmds(); } win_T *wp = NULL; tabpage_T *tp = curtab; win_T *parent = NULL; if (config->win != -1) { parent = find_window_by_handle(fconfig.window, err); if (!parent) { // find_window_by_handle has already set the error goto cleanup; } else if (is_split && parent->w_floating) { api_set_error(err, kErrorTypeException, "Cannot split a floating window"); goto cleanup; } tp = win_find_tabpage(parent); } if (is_split) { if (!check_split_disallowed_err(parent ? parent : curwin, err)) { goto cleanup; // error already set } if (HAS_KEY_X(config, vertical) && !HAS_KEY_X(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; TRY_WRAP(err, { int size = (flags & WSP_VERT) ? fconfig.width : fconfig.height; if (parent == NULL || parent == curwin) { wp = win_split_ins(size, flags, NULL, 0, NULL); } else { switchwin_T switchwin; // `parent` is valid in `tp`, so switch_win should not fail. const int result = switch_win(&switchwin, parent, tp, true); assert(result == OK); (void)result; wp = win_split_ins(size, flags, NULL, 0, NULL); restore_win(&switchwin, true); } }); if (wp) { wp->w_config = fconfig; } } else { wp = win_new_float(NULL, false, fconfig, err); } if (!wp) { if (!ERROR_SET(err)) { api_set_error(err, kErrorTypeException, "Failed to create window"); } goto cleanup; } // Autocommands may close `wp` or move it to another tabpage, so update and check `tp` after each // event. In each case, `wp` should already be valid in `tp`, so switch_win should not fail. // Also, autocommands may free the `buf` to switch to, so store a bufref to check. bufref_T bufref; set_bufref(&bufref, buf); if (!fconfig.noautocmd) { switchwin_T switchwin; const int result = switch_win_noblock(&switchwin, wp, tp, true); assert(result == OK); (void)result; if (apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf)) { tp = win_find_tabpage(wp); } restore_win_noblock(&switchwin, true); } if (tp && enter) { goto_tabpage_win(tp, wp); tp = win_find_tabpage(wp); } if (tp && bufref_valid(&bufref) && buf != wp->w_buffer) { // win_set_buf temporarily makes `wp` the curwin to set the buffer. // If not entering `wp`, block Enter and Leave events. (cringe) const bool au_no_enter_leave = curwin != wp && !fconfig.noautocmd; if (au_no_enter_leave) { autocmd_no_enter++; autocmd_no_leave++; } win_set_buf(wp, buf, err); if (!fconfig.noautocmd) { tp = win_find_tabpage(wp); } if (au_no_enter_leave) { autocmd_no_enter--; autocmd_no_leave--; } } if (!tp) { api_set_error(err, kErrorTypeException, "Window was closed immediately"); goto cleanup; } if (fconfig.style == kWinStyleMinimal) { win_set_minimal_style(wp); didset_window_options(wp, true); } rv = wp->handle; cleanup: if (fconfig.noautocmd) { unblock_autocmds(); } return rv; #undef HAS_KEY_X } 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. Cannot be used to move the last window in a /// tabpage to a different one. /// /// When reconfiguring a window, absent option keys will not be changed. /// `row`/`col` and `relative` must be reconfigured together. /// /// @see |nvim_open_win()| /// /// @param window Window handle, or 0 for current window /// @param config Map defining the window configuration, /// see |nvim_open_win()| /// @param[out] err Error details, if any void nvim_win_set_config(Window window, Dict(win_config) *config, Error *err) FUNC_API_SINCE(6) { #define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) win_T *win = find_window_by_handle(window, err); if (!win) { return; } tabpage_T *win_tp = win_find_tabpage(win); bool was_split = !win->w_floating; bool has_split = HAS_KEY_X(config, split); bool has_vertical = HAS_KEY_X(config, vertical); // reuse old values, if not overridden WinConfig fconfig = win->w_config; bool to_split = config->relative.size == 0 && !(HAS_KEY_X(config, external) ? config->external : fconfig.external) && (has_split || has_vertical || was_split); if (!parse_win_config(win, config, &fconfig, !was_split || to_split, err)) { return; } win_T *parent = NULL; if (config->win != -1) { parent = find_window_by_handle(fconfig.window, err); if (!parent) { return; } else if (to_split && parent->w_floating) { api_set_error(err, kErrorTypeException, "Cannot split a floating window"); return; } // Prevent autocmd window from being moved into another tabpage if (is_aucmd_win(win) && win_find_tabpage(win) != win_find_tabpage(parent)) { api_set_error(err, kErrorTypeException, "Cannot move autocmd win to another tabpage"); return; } } if (was_split && !to_split) { if (!win_new_float(win, false, fconfig, err)) { return; } redraw_later(win, UPD_NOT_VALID); } else if (to_split) { 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; } } } merge_win_config(&win->w_config, fconfig); // If there's no "vertical" or "split" set, or if "split" is unchanged, // then we can just change the size of the window. if ((!has_vertical && !has_split) || (was_split && !HAS_KEY_X(config, win) && old_split == fconfig.split)) { if (HAS_KEY_X(config, width)) { win_setwidth_win(fconfig.width, win); } if (HAS_KEY_X(config, height)) { win_setheight_win(fconfig.height, win); } redraw_later(win, UPD_NOT_VALID); return; } if (!check_split_disallowed_err(win, err)) { return; // error already set } // Can't move the cmdwin or its old curwin to a different tabpage. if ((win == cmdwin_win || win == cmdwin_old_curwin) && parent != NULL && win_find_tabpage(parent) != win_tp) { api_set_error(err, kErrorTypeException, "%s", e_cmdwin); return; } bool to_split_ok = false; // If we are moving curwin to another tabpage, switch windows *before* we remove it from the // window list or remove its frame (if non-floating), so it's valid for autocommands. const bool curwin_moving_tp = win == curwin && parent != NULL && win_tp != win_find_tabpage(parent); if (curwin_moving_tp) { if (was_split) { int dir; win_goto(winframe_find_altwin(win, &dir, NULL, NULL)); } else { win_goto(win_float_find_altwin(win, NULL)); } // Autocommands may have been a real nuisance and messed things up... if (curwin == win) { api_set_error(err, kErrorTypeException, "Failed to switch away from window %d", win->handle); return; } win_tp = win_find_tabpage(win); if (!win_tp || !win_valid_any_tab(parent)) { api_set_error(err, kErrorTypeException, "Windows to split were closed"); goto restore_curwin; } if (was_split == win->w_floating || parent->w_floating) { api_set_error(err, kErrorTypeException, "Floating state of windows to split changed"); goto restore_curwin; } } int dir = 0; frame_T *unflat_altfr = NULL; win_T *altwin = NULL; if (was_split) { // If the window is the last in the tabpage or `fconfig.win` is // 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, kErrorTypeException, "Cannot move last window"); goto restore_curwin; } 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. altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr); } else if (n_frames == 2) { // There are two windows in the frame, we can just rotate it. altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr); neighbor = altwin; } else { // There is only one window in the frame, we can't split it. api_set_error(err, kErrorTypeException, "Cannot split window into itself"); goto restore_curwin; } // Set the parent to whatever the correct neighbor window was determined to be. parent = neighbor; } else { altwin = winframe_remove(win, &dir, win_tp == curtab ? NULL : win_tp, &unflat_altfr); } } else { altwin = win_float_find_altwin(win, win_tp == curtab ? NULL : win_tp); } win_remove(win, win_tp == curtab ? NULL : win_tp); if (win_tp == curtab) { last_status(false); // may need to remove last status line win_comp_pos(); // recompute window positions } int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER; tabpage_T *const parent_tp = parent ? win_find_tabpage(parent) : curtab; TRY_WRAP(err, { const bool need_switch = parent != NULL && parent != curwin; switchwin_T switchwin; if (need_switch) { // `parent` is valid in its tabpage, so switch_win should not fail. const int result = switch_win(&switchwin, parent, parent_tp, true); (void)result; assert(result == OK); } to_split_ok = win_split_ins(0, flags, win, 0, unflat_altfr) != NULL; if (!to_split_ok) { // Restore `win` to the window list now, so it's valid for restore_win (if used). win_append(win->w_prev, win, win_tp == curtab ? NULL : win_tp); } if (need_switch) { restore_win(&switchwin, true); } }); if (!to_split_ok) { if (was_split) { // win_split_ins doesn't change sizes or layout if it fails to insert an existing window, so // just undo winframe_remove. winframe_restore(win, dir, unflat_altfr); } if (!ERROR_SET(err)) { api_set_error(err, kErrorTypeException, "Failed to move window %d into split", win->handle); } restore_curwin: // If `win` was the original curwin, and autocommands didn't move it outside of curtab, be a // good citizen and try to return to it. if (curwin_moving_tp && win_valid(win)) { win_goto(win); } return; } // If `win` moved tabpages and was the curwin of its old one, select a new curwin for it. if (win_tp != parent_tp && win_tp->tp_curwin == win) { win_tp->tp_curwin = altwin; } if (HAS_KEY_X(config, width)) { win_setwidth_win(fconfig.width, win); } if (HAS_KEY_X(config, height)) { win_setheight_win(fconfig.height, win); } } else { win_config_float(win, fconfig); win->w_pos_changed = true; } if (HAS_KEY_X(config, style)) { if (fconfig.style == kWinStyleMinimal) { win_set_minimal_style(win); didset_window_options(win, true); } } #undef HAS_KEY_X } #define PUT_KEY_X(d, key, value) PUT_KEY(d, win_config, key, value) static void config_put_bordertext(Dict(win_config) *config, WinConfig *fconfig, BorderTextType bordertext_type, Arena *arena) { VirtText vt; AlignTextPos align; switch (bordertext_type) { case kBorderTextTitle: vt = fconfig->title_chunks; align = fconfig->title_pos; break; case kBorderTextFooter: vt = fconfig->footer_chunks; align = fconfig->footer_pos; break; } Array bordertext = virt_text_to_array(vt, true, arena); char *pos; switch (align) { case kAlignLeft: pos = "left"; break; case kAlignCenter: pos = "center"; break; case kAlignRight: pos = "right"; break; } switch (bordertext_type) { case kBorderTextTitle: PUT_KEY_X(*config, title, ARRAY_OBJ(bordertext)); PUT_KEY_X(*config, title_pos, cstr_as_string(pos)); break; case kBorderTextFooter: PUT_KEY_X(*config, footer, ARRAY_OBJ(bordertext)); PUT_KEY_X(*config, footer_pos, cstr_as_string(pos)); } } /// Gets window configuration. /// /// The returned value may be given to |nvim_open_win()|. /// /// `relative` is empty for normal windows. /// /// @param window Window handle, or 0 for current window /// @param[out] err Error details, if any /// @return Map defining the window configuration, see |nvim_open_win()| Dict(win_config) nvim_win_get_config(Window window, Arena *arena, Error *err) FUNC_API_SINCE(6) { /// Keep in sync with FloatRelative in buffer_defs.h static const char *const float_relative_str[] = { "editor", "win", "cursor", "mouse", "tabline", "laststatus" }; /// Keep in sync with WinSplit in buffer_defs.h static const char *const win_split_str[] = { "left", "right", "above", "below" }; Dict(win_config) rv = KEYDICT_INIT; win_T *wp = find_window_by_handle(window, err); if (!wp) { return rv; } WinConfig *config = &wp->w_config; PUT_KEY_X(rv, focusable, config->focusable); PUT_KEY_X(rv, external, config->external); PUT_KEY_X(rv, hide, config->hide); PUT_KEY_X(rv, mouse, config->mouse); if (wp->w_floating) { PUT_KEY_X(rv, width, config->width); PUT_KEY_X(rv, height, config->height); if (!config->external) { if (config->relative == kFloatRelativeWindow) { PUT_KEY_X(rv, win, config->window); if (config->bufpos.lnum >= 0) { Array pos = arena_array(arena, 2); ADD_C(pos, INTEGER_OBJ(config->bufpos.lnum)); ADD_C(pos, INTEGER_OBJ(config->bufpos.col)); PUT_KEY_X(rv, bufpos, pos); } } PUT_KEY_X(rv, anchor, cstr_as_string(float_anchor_str[config->anchor])); PUT_KEY_X(rv, row, config->row); PUT_KEY_X(rv, col, config->col); PUT_KEY_X(rv, zindex, config->zindex); } if (config->border) { Array border = arena_array(arena, 8); for (size_t i = 0; i < 8; i++) { String s = cstrn_as_string(config->border_chars[i], MAX_SCHAR_SIZE); int hi_id = config->border_hl_ids[i]; char *hi_name = syn_id2name(hi_id); if (hi_name[0]) { Array tuple = arena_array(arena, 2); ADD_C(tuple, STRING_OBJ(s)); ADD_C(tuple, CSTR_AS_OBJ(hi_name)); ADD_C(border, ARRAY_OBJ(tuple)); } else { ADD_C(border, STRING_OBJ(s)); } } PUT_KEY_X(rv, border, ARRAY_OBJ(border)); if (config->title) { config_put_bordertext(&rv, config, kBorderTextTitle, arena); } if (config->footer) { config_put_bordertext(&rv, config, kBorderTextFooter, arena); } } } else if (!config->external) { PUT_KEY_X(rv, width, wp->w_width); PUT_KEY_X(rv, height, wp->w_height); WinSplit split = win_split_dir(wp); PUT_KEY_X(rv, split, cstr_as_string(win_split_str[split])); } const char *rel = (wp->w_floating && !config->external ? float_relative_str[config->relative] : ""); PUT_KEY_X(rv, relative, cstr_as_string(rel)); return rv; } static bool parse_float_anchor(String anchor, FloatAnchor *out) { if (anchor.size == 0) { *out = (FloatAnchor)0; } char *str = anchor.data; if (striequal(str, "NW")) { *out = 0; // NW is the default } else if (striequal(str, "NE")) { *out = kFloatAnchorEast; } else if (striequal(str, "SW")) { *out = kFloatAnchorSouth; } else if (striequal(str, "SE")) { *out = kFloatAnchorSouth | kFloatAnchorEast; } else { return false; } return true; } static bool parse_float_relative(String relative, FloatRelative *out) { char *str = relative.data; if (striequal(str, "editor")) { *out = kFloatRelativeEditor; } else if (striequal(str, "win")) { *out = kFloatRelativeWindow; } else if (striequal(str, "cursor")) { *out = kFloatRelativeCursor; } else if (striequal(str, "mouse")) { *out = kFloatRelativeMouse; } else if (striequal(str, "tabline")) { *out = kFloatRelativeTabline; } else if (striequal(str, "laststatus")) { *out = kFloatRelativeLaststatus; } else { return false; } 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 || bufpos.items[1].type != kObjectTypeInteger) { return false; } out->lnum = (linenr_T)bufpos.items[0].data.integer; out->col = (colnr_T)bufpos.items[1].data.integer; return true; } static void parse_bordertext(Object bordertext, BorderTextType bordertext_type, WinConfig *fconfig, Error *err) { if (bordertext.type != kObjectTypeString && bordertext.type != kObjectTypeArray) { api_set_error(err, kErrorTypeValidation, "title/footer must be string or array"); return; } if (bordertext.type == kObjectTypeArray && bordertext.data.array.size == 0) { api_set_error(err, kErrorTypeValidation, "title/footer cannot be an empty array"); return; } bool *is_present; VirtText *chunks; int *width; switch (bordertext_type) { case kBorderTextTitle: is_present = &fconfig->title; chunks = &fconfig->title_chunks; width = &fconfig->title_width; break; case kBorderTextFooter: is_present = &fconfig->footer; chunks = &fconfig->footer_chunks; width = &fconfig->footer_width; break; } if (bordertext.type == kObjectTypeString) { if (bordertext.data.string.size == 0) { *is_present = false; return; } kv_init(*chunks); kv_push(*chunks, ((VirtTextChunk){ .text = xstrdup(bordertext.data.string.data), .hl_id = -1 })); *width = (int)mb_string2cells(bordertext.data.string.data); *is_present = true; return; } *width = 0; *chunks = parse_virt_text(bordertext.data.array, err, width); *is_present = true; } static bool parse_bordertext_pos(win_T *wp, String bordertext_pos, BorderTextType bordertext_type, WinConfig *fconfig, Error *err) { AlignTextPos *align; switch (bordertext_type) { case kBorderTextTitle: align = &fconfig->title_pos; break; case kBorderTextFooter: align = &fconfig->footer_pos; break; } if (bordertext_pos.size == 0) { if (!wp) { *align = kAlignLeft; } return true; } char *pos = bordertext_pos.data; if (strequal(pos, "left")) { *align = kAlignLeft; } else if (strequal(pos, "center")) { *align = kAlignCenter; } else if (strequal(pos, "right")) { *align = kAlignRight; } else { switch (bordertext_type) { case kBorderTextTitle: api_set_error(err, kErrorTypeValidation, "invalid title_pos value"); break; case kBorderTextFooter: api_set_error(err, kErrorTypeValidation, "invalid footer_pos value"); break; } return false; } return true; } static void parse_border_style(Object style, WinConfig *fconfig, Error *err) { struct { const char *name; char chars[8][MAX_SCHAR_SIZE]; bool shadow_color; } defaults[] = { { "double", { "╔", "═", "╗", "║", "╝", "═", "╚", "║" }, false }, { "single", { "┌", "─", "┐", "│", "┘", "─", "└", "│" }, false }, { "shadow", { "", "", " ", " ", " ", " ", " ", "" }, true }, { "rounded", { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, false }, { "solid", { " ", " ", " ", " ", " ", " ", " ", " " }, false }, { NULL, { { NUL } }, false }, }; char(*chars)[MAX_SCHAR_SIZE] = fconfig->border_chars; int *hl_ids = fconfig->border_hl_ids; fconfig->border = true; if (style.type == kObjectTypeArray) { 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"); return; } for (size_t i = 0; i < size; i++) { Object iytem = arr.items[i]; String string; int hl_id = 0; if (iytem.type == kObjectTypeArray) { Array iarr = iytem.data.array; if (!iarr.size || iarr.size > 2) { api_set_error(err, kErrorTypeValidation, "invalid border char"); return; } if (iarr.items[0].type != kObjectTypeString) { api_set_error(err, kErrorTypeValidation, "invalid border char"); return; } string = iarr.items[0].data.string; if (iarr.size == 2) { hl_id = object_to_hl_id(iarr.items[1], "border char highlight", err); if (ERROR_SET(err)) { return; } } } else if (iytem.type == kObjectTypeString) { string = iytem.data.string; } else { 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"); return; } size_t len = MIN(string.size, sizeof(*chars) - 1); if (len) { memcpy(chars[i], string.data, len); } chars[i][len] = NUL; hl_ids[i] = hl_id; } while (size < 8) { memcpy(chars + size, chars, sizeof(*chars) * size); memcpy(hl_ids + size, hl_ids, sizeof(*hl_ids) * size); size <<= 1; } if ((chars[7][0] && chars[1][0] && !chars[0][0]) || (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"); } } else if (style.type == kObjectTypeString) { String str = style.data.string; if (str.size == 0 || strequal(str.data, "none")) { fconfig->border = false; // border text does not work with border equal none fconfig->title = false; fconfig->footer = false; return; } for (size_t i = 0; defaults[i].name; i++) { if (strequal(str.data, defaults[i].name)) { memcpy(chars, defaults[i].chars, sizeof(defaults[i].chars)); memset(hl_ids, 0, 8 * sizeof(*hl_ids)); if (defaults[i].shadow_color) { int hl_blend = SYN_GROUP_STATIC("FloatShadow"); int hl_through = SYN_GROUP_STATIC("FloatShadowThrough"); hl_ids[2] = hl_through; hl_ids[3] = hl_blend; hl_ids[4] = hl_blend; hl_ids[5] = hl_blend; hl_ids[6] = hl_through; } return; } } api_set_error(err, kErrorTypeValidation, "invalid border style \"%s\"", str.data); } } static void generate_api_error(win_T *wp, const char *attribute, Error *err) { if (wp != NULL && wp->w_floating) { api_set_error(err, kErrorTypeValidation, "Missing 'relative' field when reconfiguring floating window %d", wp->handle); } else { api_set_error(err, kErrorTypeValidation, "non-float cannot have '%s'", attribute); } } static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fconfig, bool reconf, Error *err) { #define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key) bool has_relative = false, relative_is_win = false, is_split = false; if (config->relative.size > 0) { if (!parse_float_relative(config->relative, &fconfig->relative)) { api_set_error(err, kErrorTypeValidation, "Invalid value of 'relative' key"); goto fail; } 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'"); goto fail; } has_relative = true; fconfig->external = false; if (fconfig->relative == kFloatRelativeWindow) { relative_is_win = true; fconfig->bufpos.lnum = -1; } } else if (!config->external) { if (HAS_KEY_X(config, vertical) || HAS_KEY_X(config, split)) { is_split = true; } else if (wp == NULL) { // new win api_set_error(err, kErrorTypeValidation, "Must specify 'relative' or 'external' when creating a float"); goto fail; } } if (HAS_KEY_X(config, vertical)) { if (!is_split) { api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'vertical'"); goto fail; } } if (HAS_KEY_X(config, split)) { if (!is_split) { api_set_error(err, kErrorTypeValidation, "floating windows cannot have 'split'"); goto fail; } if (!parse_config_split(config->split, &fconfig->split)) { api_set_error(err, kErrorTypeValidation, "Invalid value of 'split' key"); goto fail; } } if (HAS_KEY_X(config, anchor)) { if (!parse_float_anchor(config->anchor, &fconfig->anchor)) { api_set_error(err, kErrorTypeValidation, "Invalid value of 'anchor' key"); goto fail; } } if (HAS_KEY_X(config, row)) { if (!has_relative || is_split) { generate_api_error(wp, "row", err); goto fail; } fconfig->row = config->row; } if (HAS_KEY_X(config, col)) { if (!has_relative || is_split) { generate_api_error(wp, "col", err); goto fail; } fconfig->col = config->col; } if (HAS_KEY_X(config, bufpos)) { if (!has_relative || is_split) { generate_api_error(wp, "bufpos", err); goto fail; } else { if (!parse_float_bufpos(config->bufpos, &fconfig->bufpos)) { api_set_error(err, kErrorTypeValidation, "Invalid value of 'bufpos' key"); goto fail; } if (!HAS_KEY_X(config, row)) { fconfig->row = (fconfig->anchor & kFloatAnchorSouth) ? 0 : 1; } if (!HAS_KEY_X(config, col)) { fconfig->col = 0; } } } if (HAS_KEY_X(config, width)) { if (config->width > 0) { fconfig->width = (int)config->width; } else { api_set_error(err, kErrorTypeValidation, "'width' key must be a positive Integer"); goto fail; } } else if (!reconf && !is_split) { api_set_error(err, kErrorTypeValidation, "Must specify 'width'"); goto fail; } if (HAS_KEY_X(config, height)) { if (config->height > 0) { fconfig->height = (int)config->height; } else { api_set_error(err, kErrorTypeValidation, "'height' key must be a positive Integer"); goto fail; } } else if (!reconf && !is_split) { api_set_error(err, kErrorTypeValidation, "Must specify 'height'"); goto fail; } if (relative_is_win || is_split) { if (reconf && relative_is_win) { win_T *target_win = find_window_by_handle(config->win, err); if (!target_win) { goto fail; } if (target_win == wp) { api_set_error(err, kErrorTypeException, "floating window cannot be relative to itself"); goto fail; } } fconfig->window = curwin->handle; if (HAS_KEY_X(config, win)) { if (config->win > 0) { fconfig->window = config->win; } } } else if (HAS_KEY_X(config, win)) { if (has_relative) { api_set_error(err, kErrorTypeValidation, "'win' key is only valid with relative='win' and relative=''"); goto fail; } else if (!is_split) { api_set_error(err, kErrorTypeValidation, "non-float with 'win' requires at least 'split' or 'vertical'"); goto fail; } } if (HAS_KEY_X(config, external)) { fconfig->external = config->external; if (has_relative && fconfig->external) { api_set_error(err, kErrorTypeValidation, "Only one of 'relative' and 'external' must be used"); goto fail; } if (fconfig->external && !ui_has(kUIMultigrid)) { api_set_error(err, kErrorTypeValidation, "UI doesn't support external windows"); goto fail; } } if (HAS_KEY_X(config, focusable)) { fconfig->focusable = config->focusable; fconfig->mouse = config->focusable; } if (HAS_KEY_X(config, mouse)) { fconfig->mouse = config->mouse; } if (HAS_KEY_X(config, zindex)) { if (is_split) { api_set_error(err, kErrorTypeValidation, "non-float cannot have 'zindex'"); goto fail; } if (config->zindex > 0) { fconfig->zindex = (int)config->zindex; } else { api_set_error(err, kErrorTypeValidation, "'zindex' key must be a positive Integer"); goto fail; } } if (HAS_KEY_X(config, title)) { if (is_split) { api_set_error(err, kErrorTypeValidation, "non-float cannot have 'title'"); goto fail; } // title only work with border if (!HAS_KEY_X(config, border) && !fconfig->border) { api_set_error(err, kErrorTypeException, "title requires border to be set"); goto fail; } parse_bordertext(config->title, kBorderTextTitle, fconfig, err); if (ERROR_SET(err)) { goto fail; } // handles unset 'title_pos' same as empty string if (!parse_bordertext_pos(wp, config->title_pos, kBorderTextTitle, fconfig, err)) { goto fail; } } else { if (HAS_KEY_X(config, title_pos)) { api_set_error(err, kErrorTypeException, "title_pos requires title to be set"); goto fail; } } if (HAS_KEY_X(config, footer)) { if (is_split) { api_set_error(err, kErrorTypeValidation, "non-float cannot have 'footer'"); goto fail; } // footer only work with border if (!HAS_KEY_X(config, border) && !fconfig->border) { api_set_error(err, kErrorTypeException, "footer requires border to be set"); goto fail; } parse_bordertext(config->footer, kBorderTextFooter, fconfig, err); if (ERROR_SET(err)) { goto fail; } // handles unset 'footer_pos' same as empty string if (!parse_bordertext_pos(wp, config->footer_pos, kBorderTextFooter, fconfig, err)) { goto fail; } } else { if (HAS_KEY_X(config, footer_pos)) { api_set_error(err, kErrorTypeException, "footer_pos requires footer to be set"); goto fail; } } if (HAS_KEY_X(config, border)) { if (is_split) { api_set_error(err, kErrorTypeValidation, "non-float cannot have 'border'"); goto fail; } parse_border_style(config->border, fconfig, err); if (ERROR_SET(err)) { goto fail; } } if (HAS_KEY_X(config, style)) { if (config->style.data[0] == NUL) { fconfig->style = kWinStyleUnused; } else if (striequal(config->style.data, "minimal")) { fconfig->style = kWinStyleMinimal; } else { api_set_error(err, kErrorTypeValidation, "Invalid value of 'style' key"); goto fail; } } if (HAS_KEY_X(config, noautocmd)) { if (wp) { api_set_error(err, kErrorTypeValidation, "'noautocmd' cannot be used with existing windows"); goto fail; } fconfig->noautocmd = config->noautocmd; } if (HAS_KEY_X(config, fixed)) { fconfig->fixed = config->fixed; } if (HAS_KEY_X(config, hide)) { fconfig->hide = config->hide; } return true; fail: merge_win_config(fconfig, wp != NULL ? wp->w_config : WIN_CONFIG_INIT); return false; #undef HAS_KEY_X }