commit 072f126453e8f23b1151b50a0c06baefa4bc8b92
parent c7fd0c17b12fa209d936d032bf126ee542b4e235
Author: glepnir <glephunter@gmail.com>
Date: Sun, 12 Oct 2025 10:48:27 +0800
fix(ui): 'pumborder' parsing, shadow #36127
Problem:
1. Setting `pumborder=+,+,+,+,+,+,+,+` failed to render the custom
border characters correctly. The issue occurred in `parse_winborder()`
where it incorrectly used `p_winborder` instead of the `border_opt`
parameter when the option value didn't contain commas.
2. In `pum_redraw()`, calling `parse_border_style()` directly with the
option string failed to parse comma-separated border characters.
3. Missing documentation for PmenuShadow and PmenuShadowThrough
highlight groups used by the shadow border style.
4. Coverity reports CID 631420: passing WinConfig (480 bytes) by value
in `grid_draw_border()`.
5. crash when using `shadow` value on pumborder.
Solution:
1. Fix `parse_winborder()` to use `border_opt` parameter consistently,
ensuring the correct option value is parsed regardless of which
option (winborder/pumborder) is being set.
2. Update `pum_redraw()` to call `parse_winborder()` instead of
`parse_border_style()`, properly handling both predefined styles
and custom comma-separated border characters.
3. Add documentation for PmenuShadow (blended shadow areas) and
PmenuShadowThrough (see-through corners) highlight groups.
4. Change `grid_draw_border()` to accept WinConfig by pointer.
5. When the "shadow" style is used, no additional row and column offset
is applied, and the border width is reduced.
Diffstat:
12 files changed, 169 insertions(+), 29 deletions(-)
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -219,6 +219,7 @@ HIGHLIGHTS
• |hl-DiffTextAdd| highlights added text within a changed line.
• |hl-OkMsg| |hl-StderrMsg| |hl-StdoutMsg|
• |hl-SnippetTabstopActive| highlights the currently active tabstop.
+• |hl-PmenuBorder |hl-PmenuShadow| |hl-PmenuShadowThrough| see 'pumborder'.
LSP
@@ -301,6 +302,7 @@ OPTIONS
• 'winborder' "bold" style, custom border style.
• |g:clipboard| accepts a string name to force any builtin clipboard tool.
• 'busy' sets a buffer "busy" status. Indicated in the default statusline.
+• 'pumborder' add a border to the popup menu.
PERFORMANCE
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -4912,7 +4912,8 @@ A jump table for the options with a short description can be found at |Q_op|.
'pumborder' string (default "")
global
Defines the default border style of popupmenu windows. Same as
- 'winborder'.
+ 'winborder'. |hl-PmenuBorder| is used. When style is "shadow", the
+ |hl-PmenuShadow| and |hl-PmenuShadowThrough| are used.
*'pumheight'* *'ph'*
'pumheight' 'ph' number (default 0)
diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt
@@ -5354,6 +5354,11 @@ Normal Normal text.
NormalFloat Normal text in floating windows.
*hl-FloatBorder*
FloatBorder Border of floating windows.
+ *hl-FloatShadow*
+FloatShadow Blended areas when border is shadow.
+ *hl-FLoatShadowThrough*
+FloatShadownThrough
+ shadow corners when border is shadow.
*hl-FloatTitle*
FloatTitle Title of floating windows.
*hl-FloatFooter*
@@ -5382,6 +5387,13 @@ PmenuMatch Popup menu: Matched text in normal item. Combined with
*hl-PmenuMatchSel*
PmenuMatchSel Popup menu: Matched text in selected item. Combined with
|hl-PmenuMatch| and |hl-PmenuSel|.
+ *hl-PmenuBorder*
+PmenuBorder Popup menu: border of popup menu.
+ *hl-PmenuShadow*
+PmenuShadow Popup menu: blended areas when 'pumborder' is shadow.
+ *hl-PmenuShadowThrough*
+PmenuShadownThrough
+ Popup menu: shadow corners when 'pumborder' is shadow.
*hl-ComplMatchIns*
ComplMatchIns Matched text of the currently inserted completion.
*hl-PreInsert*
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -5130,7 +5130,8 @@ vim.go.pumblend = vim.o.pumblend
vim.go.pb = vim.go.pumblend
--- Defines the default border style of popupmenu windows. Same as
---- 'winborder'.
+--- 'winborder'. `hl-PmenuBorder` is used. When style is "shadow", the
+--- `hl-PmenuShadow` and `hl-PmenuShadowThrough` are used.
---
--- @type string
vim.o.pumborder = ""
diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c
@@ -1079,7 +1079,7 @@ 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, const char *border_opt, Error *err)
+bool parse_winborder(WinConfig *fconfig, char *border_opt, Error *err)
{
if (!fconfig) {
return false;
@@ -1088,7 +1088,7 @@ bool parse_winborder(WinConfig *fconfig, const char *border_opt, Error *err)
if (strchr(border_opt, ',')) {
Array border_chars = ARRAY_DICT_INIT;
- char *p = p_winborder;
+ char *p = border_opt;
char part[MAX_SCHAR_SIZE] = { 0 };
int count = 0;
@@ -1116,7 +1116,7 @@ bool parse_winborder(WinConfig *fconfig, const char *border_opt, Error *err)
style = ARRAY_OBJ(border_chars);
} else {
- style = CSTR_TO_OBJ(p_winborder);
+ style = CSTR_TO_OBJ(border_opt);
}
parse_border_style(style, fconfig, err);
diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c
@@ -658,7 +658,7 @@ int update_screen(void)
win_grid_alloc(wp);
if (wp->w_redr_border || wp->w_redr_type >= UPD_NOT_VALID) {
- grid_draw_border(&wp->w_grid_alloc, wp->w_config, wp->w_border_adj, (int)wp->w_p_winbl,
+ grid_draw_border(&wp->w_grid_alloc, &wp->w_config, wp->w_border_adj, (int)wp->w_p_winbl,
wp->w_ns_hl_attr);
}
diff --git a/src/nvim/grid.c b/src/nvim/grid.c
@@ -1115,9 +1115,9 @@ static int get_bordertext_col(int total_col, int text_width, AlignTextPos align)
}
/// draw border on floating window grid
-void grid_draw_border(ScreenGrid *grid, WinConfig config, int *adj, int winbl, int *hl_attr)
+void grid_draw_border(ScreenGrid *grid, WinConfig *config, int *adj, int winbl, int *hl_attr)
{
- int *attrs = config.border_attr;
+ int *attrs = config->border_attr;
int default_adj[4] = { 1, 1, 1, 1 };
if (adj == NULL) {
adj = default_adj;
@@ -1128,7 +1128,7 @@ void grid_draw_border(ScreenGrid *grid, WinConfig config, int *adj, int winbl, i
}
for (int i = 0; i < 8; i++) {
- chars[i] = schar_from_str(config.border_chars[i]);
+ chars[i] = schar_from_str(config->border_chars[i]);
}
int irow = grid->rows - adj[0] - adj[2];
@@ -1144,9 +1144,9 @@ void grid_draw_border(ScreenGrid *grid, WinConfig config, int *adj, int winbl, 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 (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]);
@@ -1179,9 +1179,9 @@ void grid_draw_border(ScreenGrid *grid, WinConfig config, int *adj, int winbl, i
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 (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]);
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
@@ -6727,7 +6727,8 @@ local options = {
values = { '', 'double', 'single', 'shadow', 'rounded', 'solid', 'bold', 'none' },
desc = [=[
Defines the default border style of popupmenu windows. Same as
- 'winborder'.
+ 'winborder'. |hl-PmenuBorder| is used. When style is "shadow", the
+ |hl-PmenuShadow| and |hl-PmenuShadowThrough| are used.
]=],
short_desc = N_('border of popupmenu'),
type = 'string',
diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c
@@ -2125,7 +2125,7 @@ const char *did_set_winbar(optset_T *args)
return did_set_statustabline_rulerformat(args, false, false);
}
-static bool parse_border_opt(const char *border_opt)
+static bool parse_border_opt(char *border_opt)
{
WinConfig fconfig = WIN_CONFIG_INIT;
Error err = ERROR_INIT;
diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c
@@ -298,6 +298,9 @@ 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;
+ if (strequal(p_pumborder, opt_winborder_values[3])) {
+ pum_border_size -= 1;
+ }
do {
// Mark the pum as visible already here,
@@ -605,19 +608,34 @@ void pum_redraw(void)
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) {
+ if (!parse_winborder(&fconfig, p_pumborder, &err)) {
+ if (ERROR_SET(&err)) {
+ emsg(err.msg);
+ }
+ api_clear_error(&err);
+ return;
+ }
+
+ // Shadow style: only adds border on right and bottom edges
+ if (strequal(p_pumborder, opt_winborder_values[3])) {
+ fconfig.shadow = true;
+ pum_border_width = 1;
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
+ fconfig.border_hl_ids[2] = through;
+ fconfig.border_hl_ids[3] = blend;
+ fconfig.border_hl_ids[4] = blend;
+ fconfig.border_hl_ids[5] = blend;
+ fconfig.border_hl_ids[6] = through;
+ }
+
+ // convert border highlight IDs to attributes, use PmenuBorder as default
+ for (int i = 0; i < 8; i++) {
int attr = hl_attr_active[HLF_PBR];
- for (int i = 0; i < 8; i++) {
- fconfig.border_attr[i] = attr;
+ if (fconfig.border_hl_ids[i]) {
+ attr = hl_get_ui_attr(-1, HLF_PBR, fconfig.border_hl_ids[i], false);
}
+ fconfig.border_attr[i] = attr;
}
api_clear_error(&err);
}
@@ -653,9 +671,11 @@ void pum_redraw(void)
// 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++;
+ grid_draw_border(&pum_grid, &fconfig, NULL, 0, NULL);
+ if (!fconfig.shadow) {
+ row++;
+ col_off++;
+ }
}
// Never display more than we have
diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua
@@ -1061,6 +1061,8 @@ describe('builtin popupmenu', function()
[111] = { background = Screen.colors.Plum1, foreground = Screen.colors.DarkBlue },
[112] = { background = Screen.colors.Plum1, foreground = Screen.colors.DarkGreen },
[113] = { background = Screen.colors.Yellow, foreground = Screen.colors.Black },
+ [114] = { background = Screen.colors.Grey0, blend = 100 },
+ [115] = { background = Screen.colors.Grey0, blend = 80 },
-- popup non-selected item
n = { background = Screen.colors.Plum1 },
-- popup scrollbar knob
@@ -9130,6 +9132,106 @@ describe('builtin popupmenu', function()
]])
end
end)
+ it('custom pumborder characters', function()
+ command('set pumborder=+,+,=,+,+,-,+,+')
+ feed('S<C-x><C-o>')
+ if not multigrid then
+ 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
+ end)
+ it('pumborder with shadow', function()
+ command('set pumborder=shadow')
+ feed('S<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 }{114: }|
+ {n:two }{115: }|
+ {n:three }{115: }|
+ {114: }{115: }|
+ ]],
+ 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^ |
+ {12:one }{114: }{1: }{n:1info}{1: }|
+ {n:two }{115: }{1: }|
+ {n:three }{115: }{1: }|
+ {114: }{115: }{1: }|
+ {1:~ }|*5
+ {5:-- }{6:match 1 of 3} |
+ ]])
+ end
+ end)
end)
end
diff --git a/test/old/testdir/gen_opt_test.vim b/test/old/testdir/gen_opt_test.vim
@@ -300,6 +300,7 @@ let test_values = {
\ 'alpha,hex,bin'],
\ ['xxx']],
\ 'patchmode': [['', 'xxx', '.x'], [&backupext, '*']],
+ \ 'pumborder': [['rounded', 'none', 'single', 'solid'], ['xxx', 'none,solid']],
"\ 'previewpopup': [['', 'height:13', 'width:20', 'highlight:That',
"\ " 'align:item', 'align:menu', 'border:on', 'border:off',
"\ " 'width:10,height:234,highlight:Mine'],