From 4ab670399bd9f670fd6a640c0d7aef69276ca62e Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Mon, 8 Jun 2026 20:53:10 +0800 Subject: [PATCH] vim-patch:9.2.0596: cmdline completion popup cannot be scrolled with the mouse (#40142) Problem: In command-line completion with a popup menu ('wildoptions' contains "pum"), the info popup shown next to the menu could not be scrolled, unlike the Insert mode completion info popup which scrolls with the mouse wheel. Solution: When the mouse pointer is on top of the info popup, scroll it with the mouse wheel in command-line mode as well, without closing the completion popup menu. closes: vim/vim#20146 closes: vim/vim#20418 https://github.com/vim/vim/commit/96dbab257a881ee4a552e1acf62e7d4168dc444a Co-authored-by: Hirohito Higashi Co-authored-by: Claude Opus 4.8 (1M context) --- runtime/doc/options.txt | 5 +- runtime/lua/vim/_meta/options.gen.lua | 5 +- src/nvim/ex_getln.c | 16 ++- src/nvim/mouse.c | 53 ++++++++ src/nvim/options.lua | 5 +- test/functional/ui/popupmenu_spec.lua | 185 ++++++++++++++++++++++++++ test/old/testdir/test_cmdline.vim | 46 +++++++ 7 files changed, 310 insertions(+), 5 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 1e3b9ed804..999f069301 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -7512,7 +7512,10 @@ A jump table for the options with a short description can be found at |Q_op|. is not supported for file and directory names and instead wildcard expansion is used. pum Display the completion matches using the popup menu in - the same style as the |ins-completion-menu|. + the same style as the |ins-completion-menu|. When an + info popup is shown next to the menu, it can be + scrolled by moving the mouse pointer on top of it and + using the scroll wheel. tagfile When using CTRL-D to list matching tags, the kind of tag and the file of the tag is listed. Only one match is displayed per line. Often used tag kinds are: diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua index 60ee5a71ae..57c98a4ee4 100644 --- a/runtime/lua/vim/_meta/options.gen.lua +++ b/runtime/lua/vim/_meta/options.gen.lua @@ -8175,7 +8175,10 @@ vim.go.wim = vim.go.wildmode --- is not supported for file and directory names and --- instead wildcard expansion is used. --- pum Display the completion matches using the popup menu in ---- the same style as the `ins-completion-menu`. +--- the same style as the `ins-completion-menu`. When an +--- info popup is shown next to the menu, it can be +--- scrolled by moving the mouse pointer on top of it and +--- using the scroll wheel. --- tagfile When using CTRL-D to list matching tags, the kind of --- tag and the file of the tag is listed. Only one match --- is displayed per line. Often used tag kinds are: diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index a9e7631223..4590b86f18 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1451,7 +1451,9 @@ static int command_line_execute(VimState *state, int key) && s->c != Ctrl_L); end_wildmenu = end_wildmenu && (!cmdline_pum_active() || (s->c != K_PAGEDOWN && s->c != K_PAGEUP - && s->c != K_KPAGEDOWN && s->c != K_KPAGEUP)); + && s->c != K_KPAGEDOWN && s->c != K_KPAGEUP + && s->c != K_MOUSEDOWN && s->c != K_MOUSEUP + && s->c != K_MOUSELEFT && s->c != K_MOUSERIGHT)); // free expanded names when finished walking through matches if (end_wildmenu) { @@ -2210,11 +2212,21 @@ static int command_line_handle_key(CommandLineState *s) command_line_left_right_mouse(s); return command_line_not_changed(s); - // Mouse scroll wheel: ignored here + // Mouse scroll wheel: scroll the completion info popup when the mouse + // is on top of it, otherwise ignored here. case K_MOUSEDOWN: case K_MOUSEUP: case K_MOUSELEFT: case K_MOUSERIGHT: + if (cmdline_pum_active()) { + cmdline_mousescroll(s->c == K_MOUSEDOWN + ? MSCR_DOWN + : (s->c == K_MOUSEUP + ? MSCR_UP + : s->c == K_MOUSELEFT ? MSCR_LEFT : MSCR_RIGHT)); + } + return command_line_not_changed(s); + // Alternate buttons ignored here case K_X1MOUSE: case K_X1DRAG: diff --git a/src/nvim/mouse.c b/src/nvim/mouse.c index 596dd2d4af..76723b119e 100644 --- a/src/nvim/mouse.c +++ b/src/nvim/mouse.c @@ -8,6 +8,7 @@ #include "nvim/buffer.h" #include "nvim/buffer_defs.h" #include "nvim/charset.h" +#include "nvim/cmdexpand.h" #include "nvim/cursor.h" #include "nvim/decoration.h" #include "nvim/drawscreen.h" @@ -1135,6 +1136,58 @@ void ins_mousescroll(int dir) } } +/// Command-line mode implementation for scrolling in direction "dir", which is +/// one of the MSCR_ values. Scrolls the completion info popup when the mouse +/// pointer is on top of it. +/// Returns true when the info popup was scrolled. +bool cmdline_mousescroll(int dir) +{ + cmdarg_T cap; + oparg_T oa; + CLEAR_FIELD(cap); + clear_oparg(&oa); + cap.oap = &oa; + cap.arg = dir; + + switch (dir) { + case MSCR_UP: + cap.cmdchar = K_MOUSEUP; break; + case MSCR_DOWN: + cap.cmdchar = K_MOUSEDOWN; break; + case MSCR_LEFT: + cap.cmdchar = K_MOUSELEFT; break; + case MSCR_RIGHT: + cap.cmdchar = K_MOUSERIGHT; break; + } + + if (mouse_row < 0 || mouse_col < 0) { + return false; + } + + int grid = mouse_grid; + int row = mouse_row; + int col = mouse_col; + + // Only scroll when the mouse is on top of the info popup. + win_T *wp = mouse_find_win_inner(&grid, &row, &col); + if (wp == NULL || !wp->w_float_is_info) { + return false; + } + + win_T *old_curwin = curwin; + + curwin = wp; + curbuf = wp->w_buffer; + // Call the common mouse scroll function shared with other modes. + do_mousescroll(&cap); + curwin = old_curwin; + curbuf = curwin->w_buffer; + + // Cmdline mode doesn't normally call update_screen(), so call it here. + update_screen(); + return true; +} + /// Return true if "c" is a mouse key. bool is_mouse_key(int c) { diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 295af7a334..6e73e7e6e3 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -10497,7 +10497,10 @@ local options = { is not supported for file and directory names and instead wildcard expansion is used. pum Display the completion matches using the popup menu in - the same style as the |ins-completion-menu|. + the same style as the |ins-completion-menu|. When an + info popup is shown next to the menu, it can be + scrolled by moving the mouse pointer on top of it and + using the scroll wheel. tagfile When using CTRL-D to list matching tags, the kind of tag and the file of the tag is listed. Only one match is displayed per line. Often used tag kinds are: diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index 5b091af274..99fcbdb1b0 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -5690,6 +5690,191 @@ describe('builtin popupmenu', function() end end) + -- oldtest: Test_wildmenu_pum_info_mouse_scroll() + it('scrolling cmdline pum info popup', function() + screen:try_resize(55, 12) + exec([[ + func DictComp(A, L, P) + let info = join(map(range(1, 30), '"info line " .. v:val'), "\n") + return [ + \ {'word': 'apple', 'kind': 'f', 'menu': 'fruit', 'info': info}, + \ {'word': 'banana', 'kind': 'f', 'menu': 'fruit', 'info': info}, + \ ] + endfunc + command -nargs=1 -complete=customlist,DictComp DictCmd echo + set wildmenu wildoptions=pum completeopt=menu,popup mouse=a + ]]) + + feed(':DictCmd ') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:-------------------------------------------------------]|*11 + [3:-------------------------------------------------------]| + ## grid 2 + | + {1:~ }|*10 + ## grid 3 + :DictCmd apple^ | + ## grid 4 + {n:info line 1 }| + {n:info line 2 }| + {n:info line 3 }| + {n:info line 4 }| + {n:info line 5 }| + {n:info line 6 }| + {n:info line 7 }| + {n:info line 8 }| + {n:info line 9 }| + {n:info line 10}| + {n:info line 11}| + {n:info line 12}| + ## grid 5 + {12: apple f fruit }| + {n: banana f fruit }| + ]], + float_pos = { + [5] = { -1, 'SW', 1, 11, 8, false, 250, 3, 9, 8 }, + [4] = { 1001, 'NW', 1, 9, 24, true, 50, 1, 0, 24 }, + }, + }) + else + screen:expect([[ + {n:info line 1 } | + {1:~ }{n:info line 2 }{1: }| + {1:~ }{n:info line 3 }{1: }| + {1:~ }{n:info line 4 }{1: }| + {1:~ }{n:info line 5 }{1: }| + {1:~ }{n:info line 6 }{1: }| + {1:~ }{n:info line 7 }{1: }| + {1:~ }{n:info line 8 }{1: }| + {1:~ }{n:info line 9 }{1: }| + {1:~ }{12: apple f fruit }{n:info line 10}{1: }| + {1:~ }{n: banana f fruit info line 11}{1: }| + :DictCmd apple^ | + ]]) + end + + if send_mouse_grid then + api.nvim_input_mouse('wheel', 'down', '', 4, 0, 0) + api.nvim_input_mouse('wheel', 'down', '', 4, 0, 0) + api.nvim_input_mouse('wheel', 'down', '', 4, 0, 0) + else + api.nvim_input_mouse('wheel', 'down', '', 0, 0, 24) + api.nvim_input_mouse('wheel', 'down', '', 0, 0, 24) + api.nvim_input_mouse('wheel', 'down', '', 0, 0, 24) + end + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:-------------------------------------------------------]|*11 + [3:-------------------------------------------------------]| + ## grid 2 + | + {1:~ }|*10 + ## grid 3 + :DictCmd apple^ | + ## grid 4 + {n:info line 10}| + {n:info line 11}| + {n:info line 12}| + {n:info line 13}| + {n:info line 14}| + {n:info line 15}| + {n:info line 16}| + {n:info line 17}| + {n:info line 18}| + {n:info line 19}| + {n:info line 20}| + {n:info line 21}| + ## grid 5 + {12: apple f fruit }| + {n: banana f fruit }| + ]], + float_pos = { + [5] = { -1, 'SW', 1, 11, 8, false, 250, 3, 9, 8 }, + [4] = { 1001, 'NW', 1, 9, 24, true, 50, 1, 0, 24 }, + }, + }) + else + screen:expect([[ + {n:info line 10} | + {1:~ }{n:info line 11}{1: }| + {1:~ }{n:info line 12}{1: }| + {1:~ }{n:info line 13}{1: }| + {1:~ }{n:info line 14}{1: }| + {1:~ }{n:info line 15}{1: }| + {1:~ }{n:info line 16}{1: }| + {1:~ }{n:info line 17}{1: }| + {1:~ }{n:info line 18}{1: }| + {1:~ }{12: apple f fruit }{n:info line 19}{1: }| + {1:~ }{n: banana f fruit info line 20}{1: }| + :DictCmd apple^ | + ]]) + end + + if send_mouse_grid then + api.nvim_input_mouse('wheel', 'up', '', 4, 0, 0) + api.nvim_input_mouse('wheel', 'up', '', 4, 0, 0) + else + api.nvim_input_mouse('wheel', 'up', '', 0, 0, 24) + api.nvim_input_mouse('wheel', 'up', '', 0, 0, 24) + end + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:-------------------------------------------------------]|*11 + [3:-------------------------------------------------------]| + ## grid 2 + | + {1:~ }|*10 + ## grid 3 + :DictCmd apple^ | + ## grid 4 + {n:info line 4 }| + {n:info line 5 }| + {n:info line 6 }| + {n:info line 7 }| + {n:info line 8 }| + {n:info line 9 }| + {n:info line 10}| + {n:info line 11}| + {n:info line 12}| + {n:info line 13}| + {n:info line 14}| + {n:info line 15}| + ## grid 5 + {12: apple f fruit }| + {n: banana f fruit }| + ]], + float_pos = { + [5] = { -1, 'SW', 1, 11, 8, false, 250, 3, 9, 8 }, + [4] = { 1001, 'NW', 1, 9, 24, true, 50, 1, 0, 24 }, + }, + }) + else + screen:expect([[ + {n:info line 4 } | + {1:~ }{n:info line 5 }{1: }| + {1:~ }{n:info line 6 }{1: }| + {1:~ }{n:info line 7 }{1: }| + {1:~ }{n:info line 8 }{1: }| + {1:~ }{n:info line 9 }{1: }| + {1:~ }{n:info line 10}{1: }| + {1:~ }{n:info line 11}{1: }| + {1:~ }{n:info line 12}{1: }| + {1:~ }{12: apple f fruit }{n:info line 13}{1: }| + {1:~ }{n: banana f fruit info line 14}{1: }| + :DictCmd apple^ | + ]]) + end + + feed('') + end) + -- oldtest: Test_cmdline_complete_findfunc_dict() it("'findfunc' can return extra info for cmdline completion", function() screen:try_resize(55, 12) diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index 160f77bbda..1ebb13145d 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -4709,6 +4709,52 @@ func Test_customlist_dict_completion_info_popup() call StopVimInTerminal(buf) endfunc +" Test that the mouse scroll wheel scrolls the info popup of the command line +" completion popup menu when the mouse pointer is on top of it. +func Test_wildmenu_pum_info_mouse_scroll() + CheckScreendump + CheckFeature quickfix + + let lines =<< trim END + func DictComp(A, L, P) + let info = join(map(range(1, 30), '"info line " .. v:val'), "\n") + return [ + \ {'word': 'apple', 'kind': 'f', 'menu': 'fruit', 'info': info}, + \ {'word': 'banana', 'kind': 'f', 'menu': 'fruit', 'info': info}, + \ ] + endfunc + command -nargs=1 -complete=customlist,DictComp DictCmd echo + set wildmenu wildoptions=pum completeopt=menu,popup mouse=a + + " Put the mouse on top of the info popup and turn the scroll wheel. + func ScrollInfo(keys) + let pos = popup_getpos(popup_findinfo()) + call test_setmouse(pos.line + 1, pos.col + 1) + call feedkeys(a:keys, 'nt') + endfunc + cnoremap call ScrollInfo(repeat("\", 3)) + cnoremap call ScrollInfo(repeat("\", 2)) + END + call writefile(lines, 'XtestWildmenuMouseScroll', 'D') + let buf = RunVimInTerminal('-S XtestWildmenuMouseScroll', #{rows: 12}) + + " The info popup is shown next to the completion popup menu. + call term_sendkeys(buf, ":DictCmd \") + call VerifyScreenDump(buf, 'Test_wildmenu_pum_info_mouse_scroll_1', {}) + + " Scrolling down with the wheel scrolls the info popup without closing the + " completion popup menu. + call term_sendkeys(buf, "\") + call VerifyScreenDump(buf, 'Test_wildmenu_pum_info_mouse_scroll_2', {}) + + " Scrolling back up scrolls the info popup up again. + call term_sendkeys(buf, "\") + call VerifyScreenDump(buf, 'Test_wildmenu_pum_info_mouse_scroll_3', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + func Test_cmdline_complete_findfunc_dict() CheckScreendump