commit 07d0da64ed51202f0dbd92d43c89aa5f246b175e
parent 4129fa5bac3f42b9107714e1fae37e5ee216f9b0
Author: glepnir <glephunter@gmail.com>
Date: Fri, 24 Oct 2025 06:44:02 +0800
feat(ui): overlay scrollbar on 'pumborder' #36273
Problem: When pumborder is set, the scrollbar still occupies
a column on the screen, wasting a 1 column of space.
Solution: Render the scrollbar on the right/left (rl mode) side
of the border when pumborder is set.
Diffstat:
3 files changed, 97 insertions(+), 54 deletions(-)
diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c
@@ -175,7 +175,6 @@ static const char *highlight_init_both[] = {
"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",
@@ -382,6 +381,7 @@ static const char *highlight_init_light[] = {
"OkMsg guifg=NvimDarkGreen ctermfg=2",
"Pmenu guibg=NvimLightGrey3 cterm=reverse",
"PmenuThumb guibg=NvimLightGrey4",
+ "PmenuShadow guibg=NvimLightGrey4 ctermbg=0 blend=100",
"Question guifg=NvimDarkCyan ctermfg=6",
"QuickFixLine guifg=NvimDarkCyan ctermfg=6",
"RedrawDebugClear guibg=NvimLightYellow ctermfg=15 ctermbg=3",
@@ -467,6 +467,7 @@ static const char *highlight_init_dark[] = {
"OkMsg guifg=NvimLightGreen ctermfg=10",
"Pmenu guibg=NvimDarkGrey3 cterm=reverse",
"PmenuThumb guibg=NvimDarkGrey4",
+ "PmenuShadow guibg=NvimDarkGrey4 ctermbg=0 blend=100",
"Question guifg=NvimLightCyan ctermfg=14",
"QuickFixLine guifg=NvimLightCyan ctermfg=14",
"RedrawDebugClear guibg=NvimDarkYellow ctermfg=0 ctermbg=11",
diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c
@@ -599,18 +599,14 @@ void pum_redraw(void)
extra_space = true;
}
}
- if (pum_scrollbar > 0) {
- grid_width++;
- if (pum_rl) {
- col_off++;
- }
- }
-
WinConfig fconfig = WIN_CONFIG_INIT;
int border_width = pum_border_width();
+ int border_attr = 0;
+ schar_T border_char = 0;
+ schar_T fill_char = schar_from_ascii(' ');
+ bool has_border = border_width > 0;
// setup popup menu border if 'pumborder' option is set
if (border_width > 0) {
- fconfig.border = true;
Error err = ERROR_INIT;
if (!parse_winborder(&fconfig, p_pumborder, &err)) {
if (ERROR_SET(&err)) {
@@ -641,6 +637,17 @@ void pum_redraw(void)
fconfig.border_attr[i] = attr;
}
api_clear_error(&err);
+ if (pum_scrollbar) {
+ border_char = schar_from_str(fconfig.border_chars[3]);
+ border_attr = fconfig.border_attr[3];
+ }
+ }
+
+ if (pum_scrollbar > 0 && !fconfig.border) {
+ grid_width++;
+ if (pum_rl) {
+ col_off++;
+ }
}
grid_assign_handle(&pum_grid);
@@ -889,13 +896,10 @@ void pum_redraw(void)
}
if (pum_scrollbar > 0) {
- if (pum_rl) {
- grid_line_puts(col_off - pum_width, " ", 1,
- i >= thumb_pos && i < thumb_pos + thumb_height ? attr_thumb : attr_scroll);
- } else {
- grid_line_puts(col_off + pum_width, " ", 1,
- i >= thumb_pos && i < thumb_pos + thumb_height ? attr_thumb : attr_scroll);
- }
+ bool thumb = i >= thumb_pos && i < thumb_pos + thumb_height;
+ int scrollbar_col = col_off + (pum_rl ? -pum_width : pum_width);
+ grid_line_put_schar(scrollbar_col, (has_border && !thumb) ? border_char : fill_char,
+ thumb ? attr_thumb : (has_border ? border_attr : attr_scroll));
}
grid_line_flush();
row++;
@@ -954,7 +958,10 @@ static void pum_preview_set_text(buf_T *buf, char *info, linenr_T *lnum, int *ma
static void pum_adjust_info_position(win_T *wp, int width)
{
int border_width = pum_border_width();
- int col = pum_col + pum_width + pum_scrollbar + 1 + border_width;
+ int col = pum_col + pum_width + 1 + border_width;
+ if (border_width < 0) {
+ col += pum_scrollbar;
+ }
// TODO(glepnir): support config align border by using completepopup
// align menu
int right_extra = Columns - col;
diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua
@@ -1064,6 +1064,7 @@ describe('builtin popupmenu', function()
[113] = { background = Screen.colors.Yellow, foreground = Screen.colors.Black },
[114] = { background = Screen.colors.Grey0, blend = 100 },
[115] = { background = Screen.colors.Grey0, blend = 80 },
+ [116] = { blend = 100, background = Screen.colors.NvimLightGrey4 },
-- popup non-selected item
n = { background = Screen.colors.Plum1 },
-- popup scrollbar knob
@@ -8738,11 +8739,12 @@ describe('builtin popupmenu', function()
before_each(function()
screen:try_resize(30, 11)
exec([[
+ let g:list = [#{word: "one", info: "1info"}, #{word: "two", info: "2info"}, #{word: "three", info: "3info"}]
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"}]
+ return g:list
endfunc
hi link PmenuBorder FloatBorder
set omnifunc=Omni_test
@@ -8992,9 +8994,9 @@ describe('builtin popupmenu', function()
end
end)
- it('pum border on cmdline', function()
+ it("'pumborder' on cmdline and scrollbar rendering", function()
command('set wildoptions=pum')
- feed(':<TAB>')
+ feed(':t<TAB>')
if multigrid then
screen:expect({
grid = [[
@@ -9005,18 +9007,18 @@ describe('builtin popupmenu', function()
|
{1:~ }|*9
## grid 3
- :!^ |
+ :t^ |
## grid 4
- ╭─────────────────╮|
- │{12: ! }{c: }│|
- │{n: # }{12: }│|
- │{n: & }{12: }│|
- │{n: < }{12: }│|
- │{n: = }{12: }│|
- │{n: > }{12: }│|
- │{n: @ }{12: }│|
- │{n: Next }{12: }│|
- ╰─────────────────╯|
+ ╭────────────────╮|
+ │{12: t }{c: }|
+ │{n: tNext }│|
+ │{n: tab }│|
+ │{n: tabNext }│|
+ │{n: tabclose }│|
+ │{n: tabdo }│|
+ │{n: tabedit }│|
+ │{n: tabfind }│|
+ ╰────────────────╯|
]],
win_pos = {
[2] = {
@@ -9053,17 +9055,50 @@ describe('builtin popupmenu', function()
})
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: }|
- :!^ |
+ ╭────────────────╮ |
+ │{12: t }{c: }{1: }|
+ │{n: tNext }│{1: }|
+ │{n: tab }│{1: }|
+ │{n: tabNext }│{1: }|
+ │{n: tabclose }│{1: }|
+ │{n: tabdo }│{1: }|
+ │{n: tabedit }│{1: }|
+ │{n: tabfind }│{1: }|
+ ╰────────────────╯{1: }|
+ :t^ |
+ ]])
+ end
+
+ feed(('<C-N>'):rep(20))
+ if not multigrid then
+ screen:expect([[
+ ╭────────────────╮ |
+ │{n: tabs }│{1: }|
+ │{n: tag }│{1: }|
+ │{n: tags }│{1: }|
+ │{n: tcd }{c: }{1: }|
+ │{12: tchdir }│{1: }|
+ │{n: tcl }│{1: }|
+ │{n: tcldo }│{1: }|
+ │{n: tclfile }│{1: }|
+ ╰────────────────╯{1: }|
+ :tchdir^ |
+ ]])
+ end
+ feed(('<C-P>'):rep(20))
+ if not multigrid then
+ screen:expect([[
+ ╭────────────────╮ |
+ │{12: t }{c: }{1: }|
+ │{n: tNext }│{1: }|
+ │{n: tab }│{1: }|
+ │{n: tabNext }│{1: }|
+ │{n: tabclose }│{1: }|
+ │{n: tabdo }│{1: }|
+ │{n: tabedit }│{1: }|
+ │{n: tabfind }│{1: }|
+ ╰────────────────╯{1: }|
+ :t^ |
]])
end
end)
@@ -9084,9 +9119,9 @@ describe('builtin popupmenu', function()
## grid 3
{5:-- }{6:match 1 of 3} |
## grid 4
- ╭────────────────╮|
- │{12:one }{c: }│|
- ╰────────────────╯|
+ ╭───────────────╮|
+ │{12:one }{c: }|
+ ╰───────────────╯|
]],
win_pos = {
[2] = {
@@ -9124,9 +9159,9 @@ describe('builtin popupmenu', function()
else
screen:expect([[
one^ |
- ╭────────────────╮{1: }|
- │{12:one }{c: }│{1: }|
- ╰────────────────╯{1: }|
+ ╭───────────────╮{1: }|
+ │{12:one }{c: }{1: }|
+ ╰───────────────╯{1: }|
{1:~ }|
{3:[No Name] [+] }|
{5:-- }{6:match 1 of 3} |
@@ -9167,9 +9202,9 @@ describe('builtin popupmenu', function()
{n:1info}|
## grid 5
{12:one }{114: }|
- {n:two }{115: }|
- {n:three }{115: }|
- {114: }{115: }|
+ {n:two }{116: }|
+ {n:three }{116: }|
+ {114: }{116: }|
]],
win_pos = {
[2] = {
@@ -9225,9 +9260,9 @@ describe('builtin popupmenu', function()
screen:expect([[
one^ |
{12:one }{114: }{n:1info}{1: }|
- {n:two }{115: }{1: }|
- {n:three }{115: }{1: }|
- {114: }{115: }{1: }|
+ {n:two }{116: }{1: }|
+ {n:three }{116: }{1: }|
+ {114: }{116: }{1: }|
{1:~ }|*5
{5:-- }{6:match 1 of 3} |
]])