From 531442ddd8dfafb2e3866cb834fc5e83d11618fe Mon Sep 17 00:00:00 2001 From: glepnir Date: Fri, 20 Mar 2026 01:11:35 +0800 Subject: [PATCH] fix(ui): apply 'pumborder' to mouse menu, fix overflow #36193 Problem: Mouse popup menus (right-click context menus) do not respect the 'pumborder' option and could overflow screen boundaries when borders were enabled near the edge. Solution: - Remove the mouse menu exclusion from border rendering. - Add boundary check to shift menu left when border would exceed screen width, ensuring complete visibility of menu content and borders. --- runtime/doc/options.txt | 3 + runtime/lua/vim/_meta/options.lua | 3 + src/nvim/options.lua | 3 + src/nvim/popupmenu.c | 37 +++---- test/functional/ui/popupmenu_spec.lua | 140 ++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 17 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index db07548b0b..bceecca98e 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -4903,6 +4903,9 @@ A jump table for the options with a short description can be found at |Q_op|. valid values. |hl-PmenuBorder| is used for highlighting the border, and when style is "shadow" the |hl-PmenuShadow| and |hl-PmenuShadowThrough| groups are used. + This option also applies to mouse popup menus when 'mousemodel' is set to + "popup" or "popup_setpos", which will display borders using the same style. + *'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 d63b3821db..3c9534a091 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -5108,6 +5108,9 @@ vim.go.pb = vim.go.pumblend --- valid values. `hl-PmenuBorder` is used for highlighting the border, and when --- style is "shadow" the `hl-PmenuShadow` and `hl-PmenuShadowThrough` groups are used. --- +--- This option also applies to mouse popup menus when 'mousemodel' is set to +--- "popup" or "popup_setpos", which will display borders using the same style. +--- --- @type string vim.o.pumborder = "" vim.go.pumborder = vim.o.pumborder diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 4b4ba69a57..5f16787413 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -6699,6 +6699,9 @@ local options = { Defines the default border style of popupmenu windows. See 'winborder' for valid values. |hl-PmenuBorder| is used for highlighting the border, and when style is "shadow" the |hl-PmenuShadow| and |hl-PmenuShadowThrough| groups are used. + + This option also applies to mouse popup menus when 'mousemodel' is set to + "popup" or "popup_setpos", which will display borders using the same style. ]=], short_desc = N_('border of popupmenu'), type = 'string', diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c index 8091621c02..455c52f71e 100644 --- a/src/nvim/popupmenu.c +++ b/src/nvim/popupmenu.c @@ -679,10 +679,7 @@ 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) { + if (fconfig.border) { grid_draw_border(&pum_grid, &fconfig, NULL, 0, NULL); if (!fconfig.shadow) { row++; @@ -1437,18 +1434,21 @@ static void pum_position_at_mouse(int min_width) pum_anchor_grid = grid; } - if (max_row - row > pum_size || max_row - row > row - min_row) { + // Both width and height are 1 for shadow border, otherwise 2 + int border_width = pum_border_width(); + int border_height = border_width; + if (max_row - row > pum_size + border_height || max_row - row > row - min_row) { // Enough space below the mouse row, // or there is more space below the mouse row than above. pum_above = false; pum_row = row + 1; - if (pum_height > max_row - pum_row) { - pum_height = max_row - pum_row; + if (pum_height + border_height > max_row - pum_row) { + pum_height = max_row - pum_row - border_height; } } else { // Show above the mouse row, reduce height if it does not fit. pum_above = true; - pum_row = row - pum_size; + pum_row = row - pum_size - border_height; if (pum_row < min_row) { pum_height += pum_row - min_row; pum_row = min_row; @@ -1456,25 +1456,25 @@ static void pum_position_at_mouse(int min_width) } if (pum_rl) { - if (col - min_col + 1 >= pum_base_width - || col - min_col + 1 > min_width) { + if (col - min_col + 1 >= pum_base_width + border_width + || col - min_col + 1 > min_width + border_width) { // Enough space to show at mouse column. pum_col = col; } else { // Not enough space, left align with window. - pum_col = min_col + MIN(pum_base_width, min_width) - 1; + pum_col = min_col + MIN(pum_base_width + border_width, min_width + border_width) - 1; } - pum_width = pum_col - min_col + 1; + pum_width = pum_col - min_col + 1 - border_width; } else { - if (max_col - col >= pum_base_width - || max_col - col > min_width) { + if (max_col - col >= pum_base_width + border_width + || max_col - col > min_width + border_width) { // Enough space to show at mouse column. pum_col = col; } else { // Not enough space, right align with window. - pum_col = max_col - MIN(pum_base_width, min_width); + pum_col = max_col - MIN(pum_base_width + border_width, min_width + border_width); } - pum_width = max_col - pum_col; + pum_width = max_col - pum_col - border_width; } pum_width = MIN(pum_width, pum_base_width + 1); @@ -1492,7 +1492,10 @@ static void pum_select_mouse_pos(void) } if (grid == pum_grid.handle) { - pum_selected = row; + // Offset by 1 when border width is 2 (non-shadow border) + int border_offset = pum_border_width() == 2 ? 1 : 0; + int item = row - border_offset; + pum_selected = (item >= 0 && item < pum_height) ? item : -1; return; } diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index 148b1716ff..83eada10cf 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -9398,6 +9398,146 @@ describe('builtin popupmenu', function() end end) end) + it("'pumborder' on mouse-menu displays completely within screen", function() + screen:try_resize(40, 12) + command('set pumborder=rounded') + -- Click near right edge to test boundary handling + if send_mouse_grid then + api.nvim_input_mouse('right', 'press', '', 2, 2, 35) + else + feed('<35,2>') + end + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*11 + [3:----------------------------------------]| + ## grid 2 + ^ | + {1:~ }|*10 + ## grid 3 + | + ## grid 4 + {n:╭─────────────────────╮}| + {n:│ Inspect │}| + {n:│ │}| + {n:│ Paste │}| + {n:│ Select All │}| + {n:│ │}| + {n:│ How-to disable mouse│}| + {n:╰─────────────────────╯}| + ]], + float_pos = { + [4] = { -1, 'NW', 2, 3, 17, false, 250, 2, 3, 17 }, + }, + }) + else + screen:expect([[ + ^ | + {1:~ }|*2 + {1:~ }{n:╭─────────────────────╮}| + {1:~ }{n:│ Inspect │}| + {1:~ }{n:│ │}| + {1:~ }{n:│ Paste │}| + {1:~ }{n:│ Select All │}| + {1:~ }{n:│ │}| + {1:~ }{n:│ How-to disable mouse│}| + {1:~ }{n:╰─────────────────────╯}| + | + ]]) + end + if send_mouse_grid then + api.nvim_input_mouse('move', '', '', 4, 1, 1) + else + feed('<25,4>') + end + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*11 + [3:----------------------------------------]| + ## grid 2 + ^ | + {1:~ }|*10 + ## grid 3 + | + ## grid 4 + {n:╭─────────────────────╮}| + {n:│}{12: Inspect }{n:│}| + {n:│ │}| + {n:│ Paste │}| + {n:│ Select All │}| + {n:│ │}| + {n:│ How-to disable mouse│}| + {n:╰─────────────────────╯}| + ]], + float_pos = { + [4] = { -1, 'NW', 2, 3, 17, false, 250, 2, 3, 17 }, + }, + }) + else + screen:expect([[ + ^ | + {1:~ }|*2 + {1:~ }{n:╭─────────────────────╮}| + {1:~ }{n:│}{12: Inspect }{n:│}| + {1:~ }{n:│ │}| + {1:~ }{n:│ Paste │}| + {1:~ }{n:│ Select All │}| + {1:~ }{n:│ │}| + {1:~ }{n:│ How-to disable mouse│}| + {1:~ }{n:╰─────────────────────╯}| + | + ]]) + end + -- when right-clicking on the bottom menu, it should appear above the mouse_row + if send_mouse_grid then + api.nvim_input_mouse('right', 'press', '', 2, 8, 20) + else + feed('<20,8>') + end + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*11 + [3:----------------------------------------]| + ## grid 2 + ^ | + {1:~ }|*10 + ## grid 3 + | + ## grid 4 + {n:╭─────────────────────╮}| + {n:│ Inspect │}| + {n:│ │}| + {n:│ Paste │}| + {n:│ Select All │}| + {n:│ │}| + {n:│ How-to disable mouse│}| + {n:╰─────────────────────╯}| + ]], + float_pos = { + [4] = { -1, 'SW', 2, 6, 17, false, 250, 2, 0, 17 }, + }, + }) + else + screen:expect([[ + ^ {n:╭─────────────────────╮}| + {1:~ }{n:│ Inspect │}| + {1:~ }{n:│ │}| + {1:~ }{n:│ Paste │}| + {1:~ }{n:│ Select All │}| + {1:~ }{n:│ │}| + {1:~ }{n:│ How-to disable mouse│}| + {1:~ }{n:╰─────────────────────╯}| + {1:~ }|*3 + | + ]]) + end + end) end describe('with ext_multigrid and actual mouse grid', function()