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

96dbab257a

Co-authored-by: Hirohito Higashi <h.east.727@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zeertzjq
2026-06-08 20:53:10 +08:00
committed by GitHub
parent 3f37b230af
commit 4ab670399b
7 changed files with 310 additions and 5 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)
{

View File

@@ -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:

View File

@@ -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 <q-args>
set wildmenu wildoptions=pum completeopt=menu,popup mouse=a
]])
feed(':DictCmd <Tab>')
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('<Esc>')
end)
-- oldtest: Test_cmdline_complete_findfunc_dict()
it("'findfunc' can return extra info for cmdline completion", function()
screen:try_resize(55, 12)

View File

@@ -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 <q-args>
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 <F6> <Cmd>call ScrollInfo(repeat("\<ScrollWheelDown>", 3))<CR>
cnoremap <F7> <Cmd>call ScrollInfo(repeat("\<ScrollWheelUp>", 2))<CR>
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 \<Tab>")
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, "\<F6>")
call VerifyScreenDump(buf, 'Test_wildmenu_pum_info_mouse_scroll_2', {})
" Scrolling back up scrolls the info popup up again.
call term_sendkeys(buf, "\<F7>")
call VerifyScreenDump(buf, 'Test_wildmenu_pum_info_mouse_scroll_3', {})
call term_sendkeys(buf, "\<Esc>")
call StopVimInTerminal(buf)
endfunc
func Test_cmdline_complete_findfunc_dict()
CheckScreendump