commit fafc329bbd1e15f9ab595568e8cd8b10295113dd
parent 9c89212de1eaaa62a654dd941b346fa51f634fdb
Author: glepnir <glephunter@gmail.com>
Date: Fri, 10 Oct 2025 22:14:50 +0800
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.
Diffstat:
17 files changed, 649 insertions(+), 133 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -3,6 +3,7 @@
#include <stdbool.h>
#include <stddef.h> // 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -6720,6 +6720,21 @@ local options = {
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,
desc = [=[
diff --git 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
@@ -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
@@ -2,6 +2,7 @@
#include <stdbool.h>
+#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
@@ -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
@@ -2151,7 +2151,7 @@ describe('builtin popupmenu', function()
feed('<C-E><ESC>')
end)
- it('avoid modified original info text #test', function()
+ it('avoid modified original info text', function()
command('call Append_multipe()')
feed('S<C-x><C-o><C-P><C-P>')
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<C-x><C-o>')
+ 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) .. '<C-x><C-o>')
+ 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('<ESC>Stwo' .. ('<CR>'):rep(6) .. 'tw<C-N>')
+ 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(':<TAB>')
+ 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<C-x><C-o>')
+ 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()