From fafc329bbd1e15f9ab595568e8cd8b10295113dd Mon Sep 17 00:00:00 2001 From: glepnir Date: Fri, 10 Oct 2025 22:14:50 +0800 Subject: [PATCH] feat(ui): 'pumborder' (popup menu border) #25541 Problem: Popup menu cannot have a border. Solution: Support 'pumborder' option. Generalize `win_redr_border` to `grid_redr_border`, which redraws border for window grid and pum grid. --- runtime/doc/options.txt | 6 + runtime/lua/vim/_meta/options.lua | 7 + src/nvim/api/win_config.c | 8 +- src/nvim/drawscreen.c | 110 +------ src/nvim/grid.c | 106 +++++++ src/nvim/grid.h | 1 + src/nvim/highlight.c | 14 +- src/nvim/highlight.h | 1 + src/nvim/highlight_defs.h | 1 + src/nvim/highlight_group.c | 3 + src/nvim/option_vars.h | 1 + src/nvim/options.lua | 15 + src/nvim/optionstr.c | 27 +- src/nvim/popupmenu.c | 74 ++++- src/nvim/popupmenu.h | 1 + test/functional/ui/cursor_spec.lua | 4 +- test/functional/ui/popupmenu_spec.lua | 403 +++++++++++++++++++++++++- 17 files changed, 649 insertions(+), 133 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 3adc9efb32..96b507fe6a 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -4908,6 +4908,12 @@ A jump table for the options with a short description can be found at |Q_op|. < UI-dependent. Works best with RGB colors. 'termguicolors' + *'pumborder'* +'pumborder' string (default "") + global + Defines the default border style of popupmenu windows. Same as + 'winborder'. + *'pumheight'* *'ph'* 'pumheight' 'ph' number (default 0) global diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 15b97652ee..059623fec7 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -5129,6 +5129,13 @@ vim.o.pb = vim.o.pumblend vim.go.pumblend = vim.o.pumblend vim.go.pb = vim.go.pumblend +--- Defines the default border style of popupmenu windows. Same as +--- 'winborder'. +--- +--- @type string +vim.o.pumborder = "" +vim.go.pumborder = vim.o.pumborder + --- Maximum number of items to show in the popup menu --- (`ins-completion-menu`). Zero means "use available screen space". --- diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index ec68a55389..81c33c1da0 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -961,7 +961,7 @@ static bool parse_bordertext_pos(win_T *wp, String bordertext_pos, BorderTextTyp return true; } -static void parse_border_style(Object style, WinConfig *fconfig, Error *err) +void parse_border_style(Object style, WinConfig *fconfig, Error *err) { struct { const char *name; @@ -1079,14 +1079,14 @@ static void generate_api_error(win_T *wp, const char *attribute, Error *err) } /// Parses a border style name or custom (comma-separated) style. -bool parse_winborder(WinConfig *fconfig, Error *err) +bool parse_winborder(WinConfig *fconfig, const char *border_opt, Error *err) { if (!fconfig) { return false; } Object style = OBJECT_INIT; - if (strchr(p_winborder, ',')) { + if (strchr(border_opt, ',')) { Array border_chars = ARRAY_DICT_INIT; char *p = p_winborder; char part[MAX_SCHAR_SIZE] = { 0 }; @@ -1364,7 +1364,7 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco } } } else if (*p_winborder != NUL && (wp == NULL || !wp->w_floating) - && !parse_winborder(fconfig, err)) { + && !parse_winborder(fconfig, p_winborder, err)) { goto fail; } diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index fedd1c1331..3c1dd4defe 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -111,6 +111,7 @@ #include "nvim/statusline.h" #include "nvim/strings.h" #include "nvim/syntax.h" +#include "nvim/syntax_defs.h" #include "nvim/terminal.h" #include "nvim/types_defs.h" #include "nvim/ui.h" @@ -657,7 +658,8 @@ int update_screen(void) win_grid_alloc(wp); if (wp->w_redr_border || wp->w_redr_type >= UPD_NOT_VALID) { - win_redr_border(wp); + grid_draw_border(&wp->w_grid_alloc, wp->w_config, wp->w_border_adj, (int)wp->w_p_winbl, + wp->w_ns_hl_attr); } if (wp->w_redr_type != 0) { @@ -743,112 +745,6 @@ void end_search_hl(void) screen_search_hl.rm.regprog = NULL; } -static void win_redr_bordertext(win_T *wp, VirtText vt, int col, BorderTextType bt) -{ - for (size_t i = 0; i < kv_size(vt);) { - int attr = -1; - char *text = next_virt_text_chunk(vt, &i, &attr); - if (text == NULL) { - break; - } - if (attr == -1) { // No highlight specified. - attr = wp->w_ns_hl_attr[bt == kBorderTextTitle ? HLF_BTITLE : HLF_BFOOTER]; - } - attr = hl_apply_winblend(wp, attr); - col += grid_line_puts(col, text, -1, attr); - } -} - -int win_get_bordertext_col(int total_col, int text_width, AlignTextPos align) -{ - switch (align) { - case kAlignLeft: - return 1; - case kAlignCenter: - return MAX((total_col - text_width) / 2 + 1, 1); - case kAlignRight: - return MAX(total_col - text_width + 1, 1); - } - UNREACHABLE; -} - -static void win_redr_border(win_T *wp) -{ - wp->w_redr_border = false; - if (!(wp->w_floating && wp->w_config.border)) { - return; - } - - ScreenGrid *grid = &wp->w_grid_alloc; - - schar_T chars[8]; - for (int i = 0; i < 8; i++) { - chars[i] = schar_from_str(wp->w_config.border_chars[i]); - } - int *attrs = wp->w_config.border_attr; - - int *adj = wp->w_border_adj; - int irow = wp->w_view_height + wp->w_winbar_height; - int icol = wp->w_view_width; - - if (adj[0]) { - screengrid_line_start(grid, 0, 0); - if (adj[3]) { - grid_line_put_schar(0, chars[0], attrs[0]); - } - - for (int i = 0; i < icol; i++) { - grid_line_put_schar(i + adj[3], chars[1], attrs[1]); - } - - if (wp->w_config.title) { - int title_col = win_get_bordertext_col(icol, wp->w_config.title_width, - wp->w_config.title_pos); - win_redr_bordertext(wp, wp->w_config.title_chunks, title_col, kBorderTextTitle); - } - if (adj[1]) { - grid_line_put_schar(icol + adj[3], chars[2], attrs[2]); - } - grid_line_flush(); - } - - for (int i = 0; i < irow; i++) { - if (adj[3]) { - screengrid_line_start(grid, i + adj[0], 0); - grid_line_put_schar(0, chars[7], attrs[7]); - grid_line_flush(); - } - if (adj[1]) { - int ic = (i == 0 && !adj[0] && chars[2]) ? 2 : 3; - screengrid_line_start(grid, i + adj[0], 0); - grid_line_put_schar(icol + adj[3], chars[ic], attrs[ic]); - grid_line_flush(); - } - } - - if (adj[2]) { - screengrid_line_start(grid, irow + adj[0], 0); - if (adj[3]) { - grid_line_put_schar(0, chars[6], attrs[6]); - } - - for (int i = 0; i < icol; i++) { - int ic = (i == 0 && !adj[3] && chars[6]) ? 6 : 5; - grid_line_put_schar(i + adj[3], chars[ic], attrs[ic]); - } - - if (wp->w_config.footer) { - int footer_col = win_get_bordertext_col(icol, wp->w_config.footer_width, - wp->w_config.footer_pos); - win_redr_bordertext(wp, wp->w_config.footer_chunks, footer_col, kBorderTextFooter); - } - if (adj[1]) { - grid_line_put_schar(icol + adj[3], chars[4], attrs[4]); - } - grid_line_flush(); - } -} - /// Set cursor to its position in the current window. void setcursor(void) { diff --git a/src/nvim/grid.c b/src/nvim/grid.c index 2e73373671..12fd694f3d 100644 --- a/src/nvim/grid.c +++ b/src/nvim/grid.c @@ -1084,6 +1084,112 @@ void grid_del_lines(ScreenGrid *grid, int row, int line_count, int end, int col, } } +static void grid_draw_bordertext(VirtText vt, int col, int winbl, const int *hl_attr, + BorderTextType bt) +{ + for (size_t i = 0; i < kv_size(vt);) { + int attr = -1; + char *text = next_virt_text_chunk(vt, &i, &attr); + if (text == NULL) { + break; + } + if (attr == -1) { // No highlight specified. + attr = hl_attr[bt == kBorderTextTitle ? HLF_BTITLE : HLF_BFOOTER]; + } + attr = hl_apply_winblend(winbl, attr); + col += grid_line_puts(col, text, -1, attr); + } +} + +static int get_bordertext_col(int total_col, int text_width, AlignTextPos align) +{ + switch (align) { + case kAlignLeft: + return 1; + case kAlignCenter: + return MAX((total_col - text_width) / 2 + 1, 1); + case kAlignRight: + return MAX(total_col - text_width + 1, 1); + } + UNREACHABLE; +} + +/// draw border on floating window grid +void grid_draw_border(ScreenGrid *grid, WinConfig config, int *adj, int winbl, int *hl_attr) +{ + int *attrs = config.border_attr; + int default_adj[4] = { 1, 1, 1, 1 }; + if (adj == NULL) { + adj = default_adj; + } + schar_T chars[8]; + if (!hl_attr) { + hl_attr = hl_attr_active; + } + + for (int i = 0; i < 8; i++) { + chars[i] = schar_from_str(config.border_chars[i]); + } + + int irow = grid->rows - adj[0] - adj[2]; + int icol = grid->cols - adj[1] - adj[3]; + + if (adj[0]) { + screengrid_line_start(grid, 0, 0); + if (adj[3]) { + grid_line_put_schar(0, chars[0], attrs[0]); + } + + for (int i = 0; i < icol; i++) { + grid_line_put_schar(i + adj[3], chars[1], attrs[1]); + } + + if (config.title) { + int title_col = get_bordertext_col(icol, config.title_width, config.title_pos); + grid_draw_bordertext(config.title_chunks, title_col, winbl, hl_attr, kBorderTextTitle); + } + if (adj[1]) { + grid_line_put_schar(icol + adj[3], chars[2], attrs[2]); + } + grid_line_flush(); + } + + for (int i = 0; i < irow; i++) { + if (adj[3]) { + screengrid_line_start(grid, i + adj[0], 0); + grid_line_put_schar(0, chars[7], attrs[7]); + grid_line_flush(); + } + if (adj[1]) { + int ic = (i == 0 && !adj[0] && chars[2]) ? 2 : 3; + screengrid_line_start(grid, i + adj[0], 0); + grid_line_put_schar(icol + adj[3], chars[ic], attrs[ic]); + grid_line_flush(); + } + } + + if (adj[2]) { + screengrid_line_start(grid, irow + adj[0], 0); + if (adj[3]) { + grid_line_put_schar(0, chars[6], attrs[6]); + } + + for (int i = 0; i < icol; i++) { + int ic = (i == 0 && !adj[3] && chars[6]) ? 6 : 5; + grid_line_put_schar(i + adj[3], chars[ic], attrs[ic]); + } + + if (config.footer) { + int footer_col = get_bordertext_col(icol, config.footer_width, config.footer_pos); + grid_draw_bordertext(config.footer_chunks, footer_col, winbl, hl_attr, kBorderTextFooter); + } + if (adj[1]) { + grid_line_put_schar(icol + adj[3], chars[4], attrs[4]); + } + grid_line_flush(); + } +} + static void linecopy(ScreenGrid *grid, int to, int from, int col, int width) { unsigned off_to = (unsigned)(grid->line_offset[to] + (size_t)col); diff --git a/src/nvim/grid.h b/src/nvim/grid.h index 7b8fb94d2f..473f727d84 100644 --- a/src/nvim/grid.h +++ b/src/nvim/grid.h @@ -3,6 +3,7 @@ #include #include // IWYU pragma: keep +#include "nvim/buffer_defs.h" // IWYU pragma: keep #include "nvim/grid_defs.h" // IWYU pragma: keep #include "nvim/macros_defs.h" #include "nvim/pos_defs.h" diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c index 6c9930d49c..fb1d7f5de0 100644 --- a/src/nvim/highlight.c +++ b/src/nvim/highlight.c @@ -325,16 +325,16 @@ int hl_get_ui_attr(int ns_id, int idx, int final_id, bool optional) /// Apply 'winblend' to highlight attributes. /// -/// @param wp The window to get 'winblend' value from. +/// @param winbl The 'winblend' value. /// @param attr The original attribute code. /// /// @return The attribute code with 'winblend' applied. -int hl_apply_winblend(win_T *wp, int attr) +int hl_apply_winblend(int winbl, int attr) { HlEntry entry = attr_entry(attr); // if blend= attribute is not set, 'winblend' value overrides it. - if (entry.attr.hl_blend == -1 && wp->w_p_winbl > 0) { - entry.attr.hl_blend = (int)wp->w_p_winbl; + if (entry.attr.hl_blend == -1 && winbl > 0) { + entry.attr.hl_blend = winbl; attr = get_attr_entry(entry); } return attr; @@ -379,7 +379,7 @@ void update_window_hl(win_T *wp, bool invalid) } if (wp->w_floating) { - wp->w_hl_attr_normal = hl_apply_winblend(wp, wp->w_hl_attr_normal); + wp->w_hl_attr_normal = hl_apply_winblend((int)wp->w_p_winbl, wp->w_hl_attr_normal); } wp->w_config.shadow = false; @@ -390,7 +390,7 @@ void update_window_hl(win_T *wp, bool invalid) attr = hl_get_ui_attr(ns_id, HLF_BORDER, wp->w_config.border_hl_ids[i], false); } - attr = hl_apply_winblend(wp, attr); + attr = hl_apply_winblend((int)wp->w_p_winbl, attr); if (syn_attr2entry(attr).hl_blend > 0) { wp->w_config.shadow = true; } @@ -411,7 +411,7 @@ void update_window_hl(win_T *wp, bool invalid) } if (wp->w_floating) { - wp->w_hl_attr_normalnc = hl_apply_winblend(wp, wp->w_hl_attr_normalnc); + wp->w_hl_attr_normalnc = hl_apply_winblend((int)wp->w_p_winbl, wp->w_hl_attr_normalnc); } } diff --git a/src/nvim/highlight.h b/src/nvim/highlight.h index 30527a4ea4..ef6edf0054 100644 --- a/src/nvim/highlight.h +++ b/src/nvim/highlight.h @@ -86,6 +86,7 @@ EXTERN const char *hlf_names[] INIT( = { [HLF_TS] = "StatusLineTerm", [HLF_TSNC] = "StatusLineTermNC", [HLF_PRE] = "PreInsert", + [HLF_PBR] = "PmenuBorder", }); EXTERN int highlight_attr[HLF_COUNT]; // Highl. attr for each context. diff --git a/src/nvim/highlight_defs.h b/src/nvim/highlight_defs.h index a4a6a42b02..bb27a24f80 100644 --- a/src/nvim/highlight_defs.h +++ b/src/nvim/highlight_defs.h @@ -110,6 +110,7 @@ typedef enum { HLF_PSX, ///< popup menu selected item "menu" (extra text) HLF_PSB, ///< popup menu scrollbar HLF_PST, ///< popup menu scrollbar thumb + HLF_PBR, ///< popup menu border HLF_TP, ///< tabpage line HLF_TPS, ///< tabpage line selected HLF_TPF, ///< tabpage line filler diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index 2871a059ee..14dfc583bc 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -174,6 +174,9 @@ static const char *highlight_init_both[] = { "default link PmenuKind Pmenu", "default link PmenuKindSel PmenuSel", "default link PmenuSbar Pmenu", + "default link PmenuBorder Pmenu", + "default link PmenuShadow FloatShadow", + "default link PmenuShadowThrough FloatShadowThrough", "default link PreInsert Added", "default link ComplMatchIns NONE", "default link ComplHint NonText", diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index 3198169e85..4a210dc14c 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -308,6 +308,7 @@ EXTERN OptInt p_acl; ///< 'autocompletedelay' #ifdef BACKSLASH_IN_FILENAME EXTERN char *p_csl; ///< 'completeslash' #endif +EXTERN char *p_pumborder; ///< 'pumborder' EXTERN OptInt p_pb; ///< 'pumblend' EXTERN OptInt p_ph; ///< 'pumheight' EXTERN OptInt p_pw; ///< 'pumwidth' diff --git a/src/nvim/options.lua b/src/nvim/options.lua index c3cbdb2339..3fd511e1e9 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -6719,6 +6719,21 @@ local options = { type = 'number', varname = 'p_pb', }, + { + full_name = 'pumborder', + scope = { 'global' }, + cb = 'did_set_pumborder', + defaults = { if_true = '' }, + values = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'bold', 'none' }, + desc = [=[ + Defines the default border style of popupmenu windows. Same as + 'winborder'. + ]=], + short_desc = N_('border of popupmenu'), + type = 'string', + list = 'onecomma', + varname = 'p_pumborder', + }, { abbreviation = 'ph', defaults = 0, diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index c5e047b748..6b072b8ad4 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -13,6 +13,7 @@ #include "nvim/cmdexpand_defs.h" #include "nvim/cursor.h" #include "nvim/cursor_shape.h" +#include "nvim/decoration.h" #include "nvim/diff.h" #include "nvim/digraph.h" #include "nvim/drawscreen.h" @@ -2124,16 +2125,32 @@ const char *did_set_winbar(optset_T *args) return did_set_statustabline_rulerformat(args, false, false); } -/// The 'winborder' option is changed. -const char *did_set_winborder(optset_T *args) +static bool parse_border_opt(const char *border_opt) { WinConfig fconfig = WIN_CONFIG_INIT; Error err = ERROR_INIT; - if (!parse_winborder(&fconfig, &err)) { - api_clear_error(&err); - return e_invarg; + bool result = true; + if (!parse_winborder(&fconfig, border_opt, &err)) { + result = false; } api_clear_error(&err); + return result; +} + +/// The 'winborder' option is changed. +const char *did_set_winborder(optset_T *args) +{ + if (!parse_border_opt(p_winborder)) { + return e_invarg; + } + return NULL; +} + +const char *did_set_pumborder(optset_T *args) +{ + if (!parse_border_opt(p_pumborder)) { + return e_invarg; + } return NULL; } diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c index 285ef3db47..26955dd315 100644 --- a/src/nvim/popupmenu.c +++ b/src/nvim/popupmenu.c @@ -10,12 +10,15 @@ #include "nvim/api/buffer.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/vim.h" +#include "nvim/api/win_config.h" #include "nvim/ascii_defs.h" #include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/buffer_defs.h" #include "nvim/charset.h" #include "nvim/cmdexpand.h" +#include "nvim/decoration.h" #include "nvim/drawscreen.h" #include "nvim/errors.h" #include "nvim/eval/typval.h" @@ -28,8 +31,10 @@ #include "nvim/gettext_defs.h" #include "nvim/globals.h" #include "nvim/grid.h" +#include "nvim/grid_defs.h" #include "nvim/highlight.h" #include "nvim/highlight_defs.h" +#include "nvim/highlight_group.h" #include "nvim/insexpand.h" #include "nvim/keycodes.h" #include "nvim/mbyte.h" @@ -47,6 +52,7 @@ #include "nvim/pos_defs.h" #include "nvim/state_defs.h" #include "nvim/strings.h" +#include "nvim/syntax.h" #include "nvim/types_defs.h" #include "nvim/ui.h" #include "nvim/ui_compositor.h" @@ -116,7 +122,7 @@ static void pum_compute_size(void) /// Calculate vertical placement for popup menu. /// Sets pum_row and pum_height based on available space. static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_win_row, - int above_row, int below_row) + int above_row, int below_row, int pum_border_size) { int context_lines; @@ -128,7 +134,7 @@ static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_ // Put the pum below "pum_win_row" if possible. // If there are few lines decide on where there is more room. - if (pum_win_row + 2 >= below_row - pum_height + if (pum_win_row + 2 + pum_border_size >= below_row - pum_height && pum_win_row - above_row > (below_row - above_row) / 2) { // pum above "pum_win_row" pum_above = true; @@ -152,6 +158,14 @@ static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_ pum_row += pum_height - (int)p_ph; pum_height = (int)p_ph; } + + if (pum_border_size > 0 && pum_border_size + pum_row + pum_height >= pum_win_row) { + if (pum_row < 2) { + pum_height -= pum_border_size; + } else { + pum_row -= pum_border_size; + } + } } else { // pum below "pum_win_row" pum_above = false; @@ -172,6 +186,10 @@ static void pum_compute_vertical_placement(int size, win_T *target_win, int pum_ if (p_ph > 0 && pum_height > p_ph) { pum_height = (int)p_ph; } + + if (pum_row + pum_height + pum_border_size >= cmdline_row) { + pum_height -= pum_border_size; + } } // If there is a preview window above avoid drawing over it. @@ -279,6 +297,8 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i pum_rl = (State & MODE_CMDLINE) == 0 && curwin->w_p_rl; + int pum_border_size = *p_pumborder != NUL ? 2 : 0; + do { // Mark the pum as visible already here, // to avoid that must_redraw is set when 'cursorcolumn' is on. @@ -366,10 +386,11 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i } // Figure out the vertical size and position of the pum. - pum_compute_vertical_placement(size, target_win, pum_win_row, above_row, below_row); + pum_compute_vertical_placement(size, target_win, pum_win_row, above_row, below_row, + pum_border_size); // don't display when we only have room for one line - if (pum_height < 1 || (pum_height == 1 && size > 1)) { + if (pum_border_size == 0 && (pum_height < 1 || (pum_height == 1 && size > 1))) { return; } @@ -389,6 +410,10 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i // Figure out the horizontal size and position of the pum. pum_compute_horizontal_placement(target_win, cursor_col); + if (pum_col + pum_border_size + pum_width > Columns) { + pum_col -= pum_border_size; + } + // Set selected item and redraw. If the window size changed need to redo // the positioning. Limit this to two times, when there is not much // room the window size will keep changing. @@ -573,18 +598,43 @@ void pum_redraw(void) } } + WinConfig fconfig = WIN_CONFIG_INIT; + int pum_border_width = 0; + // setup popup menu border if 'pumborder' option is set + if (*p_pumborder != NUL) { + pum_border_width = 2; + fconfig.border = true; + Error err = ERROR_INIT; + parse_border_style(CSTR_AS_OBJ(p_pumborder), &fconfig, &err); + // shadow style uses different highlights for different positions + if (strcmp(p_pumborder, opt_winborder_values[3]) == 0) { + int blend = SYN_GROUP_STATIC("PmenuShadow"); + int through = SYN_GROUP_STATIC("PmenuShadowThrough"); + int attrs[] = { 0, 0, through, blend, blend, blend, through, 0 }; + memcpy(fconfig.border_attr, attrs, sizeof(attrs)); + } else { + // Non-shadow styles use PumBorder highlight for all border chars + int attr = hl_attr_active[HLF_PBR]; + for (int i = 0; i < 8; i++) { + fconfig.border_attr[i] = attr; + } + } + api_clear_error(&err); + } grid_assign_handle(&pum_grid); pum_left_col = pum_col - col_off; pum_right_col = pum_left_col + grid_width; bool moved = ui_comp_put_grid(&pum_grid, pum_row, pum_left_col, - pum_height, grid_width, false, true); + pum_height + pum_border_width, grid_width + pum_border_width, false, + true); bool invalid_grid = moved || pum_invalid; pum_invalid = false; must_redraw_pum = false; if (!pum_grid.chars || pum_grid.rows != pum_height || pum_grid.cols != grid_width) { - grid_alloc(&pum_grid, pum_height, grid_width, !invalid_grid, false); + grid_alloc(&pum_grid, pum_height + pum_border_width, grid_width + pum_border_width, + !invalid_grid, false); ui_call_grid_resize(pum_grid.handle, pum_grid.cols, pum_grid.rows); } else if (invalid_grid) { grid_invalidate(&pum_grid); @@ -599,6 +649,15 @@ void pum_redraw(void) } int scroll_range = pum_size - pum_height; + + // avoid set border for mouse menu + int mouse_menu = State != MODE_CMDLINE && pum_grid.zindex == kZIndexCmdlinePopupMenu; + if (!mouse_menu && fconfig.border) { + grid_draw_border(&pum_grid, fconfig, NULL, 0, NULL); + row++; + col_off++; + } + // Never display more than we have pum_first = MIN(pum_first, scroll_range); @@ -871,7 +930,8 @@ static void pum_preview_set_text(buf_T *buf, char *info, linenr_T *lnum, int *ma /// adjust floating info preview window position static void pum_adjust_info_position(win_T *wp, int width) { - int col = pum_col + pum_width + pum_scrollbar + 1; + int extra_width = *p_pumborder != NUL ? 2 : 0; + int col = pum_col + pum_width + pum_scrollbar + 1 + extra_width; // TODO(glepnir): support config align border by using completepopup // align menu int right_extra = Columns - col; diff --git a/src/nvim/popupmenu.h b/src/nvim/popupmenu.h index 8bbe4d27e6..698bef99a9 100644 --- a/src/nvim/popupmenu.h +++ b/src/nvim/popupmenu.h @@ -2,6 +2,7 @@ #include +#include "nvim/buffer_defs.h" #include "nvim/eval/typval_defs.h" // IWYU pragma: keep #include "nvim/grid_defs.h" #include "nvim/macros_defs.h" diff --git a/test/functional/ui/cursor_spec.lua b/test/functional/ui/cursor_spec.lua index 07395deab2..1a075534ed 100644 --- a/test/functional/ui/cursor_spec.lua +++ b/test/functional/ui/cursor_spec.lua @@ -258,11 +258,11 @@ describe('ui/cursor', function() end end if m.hl_id then - m.hl_id = 66 + m.hl_id = 67 m.attr = { background = Screen.colors.DarkGray } end if m.id_lm then - m.id_lm = 77 + m.id_lm = 78 m.attr_lm = {} end end diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index 199341b405..c65b3c5118 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -2151,7 +2151,7 @@ describe('builtin popupmenu', function() feed('') end) - it('avoid modified original info text #test', function() + it('avoid modified original info text', function() command('call Append_multipe()') feed('S') if multigrid then @@ -8730,6 +8730,407 @@ describe('builtin popupmenu', function() ]]) end) end + + describe("'pumborder'", function() + before_each(function() + screen:try_resize(30, 11) + exec([[ + funct Omni_test(findstart, base) + if a:findstart + return col(".") - 1 + endif + return [#{word: "one", info: "1info"}, #{word: "two", info: "2info"}, #{word: "three", info: "3info"}] + endfunc + hi link PmenuBorder FloatBorder + set omnifunc=Omni_test + set completeopt-=preview + set pumborder=rounded + ]]) + end) + + it('can set border', function() + feed('Gi') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:------------------------------]|*10 + [3:------------------------------]| + ## grid 2 + one^ | + {1:~ }|*9 + ## grid 3 + {5:-- }{6:match 1 of 3} | + ## grid 4 + {n:1info}| + ## grid 5 + ╭───────────────╮| + │{12:one }│| + │{n:two }│| + │{n:three }│| + ╰───────────────╯| + ]], + win_pos = { + [2] = { + height = 10, + startcol = 0, + startrow = 0, + width = 30, + win = 1000, + }, + }, + float_pos = { + [5] = { -1, 'NW', 2, 1, 0, false, 100, 2, 1, 0 }, + [4] = { 1001, 'NW', 1, 1, 17, false, 50, 1, 1, 17 }, + }, + win_viewport = { + [2] = { + win = 1000, + topline = 0, + botline = 2, + curline = 0, + curcol = 3, + linecount = 1, + sum_scroll_delta = 0, + }, + [4] = { + win = 1001, + topline = 0, + botline = 1, + curline = 0, + curcol = 0, + linecount = 1, + sum_scroll_delta = 0, + }, + }, + win_viewport_margins = { + [2] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1000, + }, + [4] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1001, + }, + }, + }) + else + screen:expect([[ + one^ | + ╭───────────────╮{n:1info}{1: }| + │{12:one }│{1: }| + │{n:two }│{1: }| + │{n:three }│{1: }| + ╰───────────────╯{1: }| + {1:~ }|*4 + {5:-- }{6:match 1 of 3} | + ]]) + end + + -- avoid out of screen + feed(('a'):rep(25) .. '') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:------------------------------]|*10 + [3:------------------------------]| + ## grid 2 + oneaaaaaaaaaaaaaaaaaaaaaaaaaon| + e^ | + {1:~ }|*8 + ## grid 3 + {5:-- }{6:match 1 of 3} | + ## grid 4 + {n:1info}| + ## grid 5 + ╭─────────────────╮| + │{12: one }│| + │{n: two }│| + │{n: three }│| + ╰─────────────────╯| + ]], + win_pos = { + [2] = { + height = 10, + startcol = 0, + startrow = 0, + width = 30, + win = 1000, + }, + }, + float_pos = { + [5] = { -1, 'NW', 2, 2, 11, false, 100, 2, 2, 11 }, + [4] = { 1001, 'NW', 1, 2, 6, false, 50, 1, 2, 6 }, + }, + win_viewport = { + [2] = { + win = 1000, + topline = 0, + botline = 2, + curline = 0, + curcol = 31, + linecount = 1, + sum_scroll_delta = 0, + }, + [4] = { + win = 1001, + topline = 0, + botline = 1, + curline = 0, + curcol = 0, + linecount = 1, + sum_scroll_delta = 0, + }, + }, + win_viewport_margins = { + [2] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1000, + }, + [4] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1001, + }, + }, + }) + else + screen:expect([[ + oneaaaaaaaaaaaaaaaaaaaaaaaaaon| + e^ | + {1:~ }{n:1info}╭─────────────────╮| + {1:~ }│{12: one }│| + {1:~ }│{n: two }│| + {1:~ }│{n: three }│| + {1:~ }╰─────────────────╯| + {1:~ }|*3 + {5:-- }{6:match 1 of 3} | + ]]) + end + end) + + it('adjust to above when the below row + border out of win height', function() + command('set completeopt+=menuone,noselect') + feed('Stwo' .. (''):rep(6) .. 'tw') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:------------------------------]|*10 + [3:------------------------------]| + ## grid 2 + two | + |*5 + tw^ | + {1:~ }|*3 + ## grid 3 + {5:-- }{19:Back at original} | + ## grid 4 + ╭───────────────╮| + │{n:two }│| + ╰───────────────╯| + ]], + win_pos = { + [2] = { + height = 10, + startcol = 0, + startrow = 0, + width = 30, + win = 1000, + }, + }, + float_pos = { + [4] = { -1, 'SW', 2, 4, 0, false, 100, 1, 3, 0 }, + }, + win_viewport = { + [2] = { + win = 1000, + topline = 0, + botline = 8, + curline = 6, + curcol = 2, + linecount = 7, + sum_scroll_delta = 0, + }, + }, + win_viewport_margins = { + [2] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1000, + }, + }, + }) + else + screen:expect([[ + two | + |*2 + ╭───────────────╮ | + │{n:two }│ | + ╰───────────────╯ | + tw^ | + {1:~ }|*3 + {5:-- }{19:Back at original} | + ]]) + end + end) + + it('pum border on cmdline', function() + command('set wildoptions=pum') + feed(':') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:------------------------------]|*10 + [3:------------------------------]| + ## grid 2 + | + {1:~ }|*9 + ## grid 3 + :!^ | + ## grid 4 + ╭─────────────────╮| + │{12: ! }{c: }│| + │{n: # }{12: }│| + │{n: & }{12: }│| + │{n: < }{12: }│| + │{n: = }{12: }│| + │{n: > }{12: }│| + │{n: @ }{12: }│| + │{n: Next }{12: }│| + ╰─────────────────╯| + ]], + win_pos = { + [2] = { + height = 10, + startcol = 0, + startrow = 0, + width = 30, + win = 1000, + }, + }, + float_pos = { + [4] = { -1, 'SW', 1, 8, 0, false, 250, 2, 0, 0 }, + }, + win_viewport = { + [2] = { + win = 1000, + topline = 0, + botline = 2, + curline = 0, + curcol = 0, + linecount = 1, + sum_scroll_delta = 0, + }, + }, + win_viewport_margins = { + [2] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1000, + }, + }, + }) + else + screen:expect([[ + ╭─────────────────╮ | + │{12: ! }{c: }│{1: }| + │{n: # }{12: }│{1: }| + │{n: & }{12: }│{1: }| + │{n: < }{12: }│{1: }| + │{n: = }{12: }│{1: }| + │{n: > }{12: }│{1: }| + │{n: @ }{12: }│{1: }| + │{n: Next }{12: }│{1: }| + ╰─────────────────╯{1: }| + :!^ | + ]]) + end + end) + + it('reduce pum height when height is not enough', function() + command('set lines=7 laststatus=2') + feed('S') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:------------------------------]|*5 + {3:[No Name] [+] }| + [3:------------------------------]| + ## grid 2 + one^ | + {1:~ }|*4 + ## grid 3 + {5:-- }{6:match 1 of 3} | + ## grid 4 + ╭────────────────╮| + │{12:one }{c: }│| + ╰────────────────╯| + ]], + win_pos = { + [2] = { + height = 5, + startcol = 0, + startrow = 0, + width = 30, + win = 1000, + }, + }, + float_pos = { + [4] = { -1, 'NW', 2, 1, 0, false, 100, 1, 1, 0 }, + }, + win_viewport = { + [2] = { + win = 1000, + topline = 0, + botline = 2, + curline = 0, + curcol = 3, + linecount = 1, + sum_scroll_delta = 0, + }, + }, + win_viewport_margins = { + [2] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1000, + }, + }, + }) + else + screen:expect([[ + one^ | + ╭────────────────╮{1: }| + │{12:one }{c: }│{1: }| + ╰────────────────╯{1: }| + {1:~ }| + {3:[No Name] [+] }| + {5:-- }{6:match 1 of 3} | + ]]) + end + end) + end) end describe('with ext_multigrid and actual mouse grid', function()