Commit: patch 9.2.0609: completion info popup cannot be scrolled with the keyboard

2 views
Skip to first unread message

Christian Brabandt

unread,
Jun 9, 2026, 3:45:12 PM (18 hours ago) Jun 9
to vim...@googlegroups.com
patch 9.2.0609: completion info popup cannot be scrolled with the keyboard

Commit: https://github.com/vim/vim/commit/e2cf855bbe751f14b98b5e2b1542fb2bfc5bfe74
Author: Hirohito Higashi <h.eas...@gmail.com>
Date: Tue Jun 9 19:24:25 2026 +0000

patch 9.2.0609: completion info popup cannot be scrolled with the keyboard

Problem: The info popup shown beside the insert-mode and command-line
completion menu can only be scrolled with the mouse wheel, so
the part below the visible area is unreachable when working
from the keyboard.
Solution: While the completion menu is shown, scroll the info popup with
CTRL-SHIFT-Up/Down (one line), CTRL-SHIFT-PageUp/PageDown (one
page) and CTRL-SHIFT-N/CTRL-SHIFT-P (one line). The menu stays
open and the selected item does not change.

related: #20418
fixes: #20441
closes: #20444

Co-Authored-By: Claude Opus 4.8 (1M context) <nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
index 13f2d89a2..5946f26e7 100644
--- a/runtime/doc/insert.txt
+++ b/runtime/doc/insert.txt
@@ -1,4 +1,4 @@
-*insert.txt* For Vim version 9.2. Last change: 2026 Jun 02
+*insert.txt* For Vim version 9.2. Last change: 2026 Jun 09


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -1449,6 +1449,22 @@ CTRL-E End completion, go back to what was there before selecting a
insert it.
<Down> Select the next match, as if CTRL-N was used, but don't
insert it.
+CTRL-SHIFT-<Up>
+ Scroll the info popup up one line, when it is shown, see
+ |complete-popup|.
+ Note: these CTRL-SHIFT keys need the GUI or a terminal that
+ reports key modifiers; the Linux console does not.
+CTRL-SHIFT-<Down>
+ Scroll the info popup down one line.
+CTRL-SHIFT-<PageUp>
+ Scroll the info popup up one page.
+CTRL-SHIFT-<PageDown>
+ Scroll the info popup down one page.
+CTRL-SHIFT-P Like CTRL-SHIFT-<Up>, scroll the info popup up one line.
+CTRL-SHIFT-N Like CTRL-SHIFT-<Down>, scroll the info popup down one line.
+ Note: CTRL-SHIFT-N and CTRL-SHIFT-P additionally need the
+ terminal to report modifiers for letter keys, see
+ |modifyOtherKeys|.
<Space> or <Tab> Stop completion without changing the match and insert the
typed character.

diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 0df50ff60..244a7510f 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1,4 +1,4 @@
-*options.txt* For Vim version 9.2. Last change: 2026 Jun 04
+*options.txt* For Vim version 9.2. Last change: 2026 Jun 09


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -10526,7 +10526,8 @@ A jump table for the options with a short description can be found at |Q_op|.
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.
+ using the scroll wheel, or with the keyboard like in
+ Insert mode completion, see |popupmenu-keys|.
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/doc/version9.txt b/runtime/doc/version9.txt
index 2496d1b69..b0c95e50f 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.2. Last change: 2026 May 31
+*version9.txt* For Vim version 9.2. Last change: 2026 Jun 09


VIM REFERENCE MANUAL by Bram Moolenaar
@@ -52598,6 +52598,8 @@ Popups ~
"align").
- Support "opacity" setting for 'completepopup' option.
- Support for clipping textproperty popups |popup-clipwindow|.
+- Completion popup menu can be scrolled with the mouse or using keys
+ |popupmenu-keys|.

Diff mode ~
---------
diff --git a/src/edit.c b/src/edit.c
index 2b839d7f5..3eba12dab 100644
--- a/src/edit.c
+++ b/src/edit.c
@@ -1233,7 +1233,21 @@ doESCkey:
case K_PAGEUP:
case K_KPAGEUP:
if (pum_visible())
+ {
+#ifdef FEAT_PROP_POPUP
+ // CTRL-SHIFT-<Up> scrolls the info popup up a line,
+ // CTRL-SHIFT-<PageUp> a page. Shift is folded into K_S_UP but
+ // stays in mod_mask for PageUp, hence the asymmetric check.
+ if (c == K_S_UP ? (mod_mask & MOD_MASK_CTRL)
+ : ((mod_mask & MOD_MASK_CTRL)
+ && (mod_mask & MOD_MASK_SHIFT)))
+ {
+ popup_scroll_info(-1, c != K_S_UP);
+ break;
+ }
+#endif
goto docomplete;
+ }
ins_pageup();
break;

@@ -1250,7 +1264,19 @@ doESCkey:
case K_PAGEDOWN:
case K_KPAGEDOWN:
if (pum_visible())
+ {
+#ifdef FEAT_PROP_POPUP
+ // CTRL-SHIFT-<Down>/<PageDown> scroll the info popup down.
+ if (c == K_S_DOWN ? (mod_mask & MOD_MASK_CTRL)
+ : ((mod_mask & MOD_MASK_CTRL)
+ && (mod_mask & MOD_MASK_SHIFT)))
+ {
+ popup_scroll_info(1, c != K_S_DOWN);
+ break;
+ }
+#endif
goto docomplete;
+ }
ins_pagedown();
break;

@@ -1365,6 +1391,15 @@ doESCkey:

case Ctrl_P: // Do previous/next pattern completion
case Ctrl_N:
+#ifdef FEAT_PROP_POPUP
+ // CTRL-SHIFT-P/N scroll the info popup one line.
+ if (pum_visible() && (mod_mask & MOD_MASK_SHIFT)
+ && (c == Ctrl_P || c == Ctrl_N))
+ {
+ popup_scroll_info(c == Ctrl_P ? -1 : 1, false);
+ break;
+ }
+#endif
// if 'complete' is empty then plain ^P is no longer special,
// but it is under other ^X modes
if (*curbuf->b_p_cpt == NUL
diff --git a/src/ex_getln.c b/src/ex_getln.c
index ea20fa96b..d553fee46 100644
--- a/src/ex_getln.c
+++ b/src/ex_getln.c
@@ -2064,15 +2064,18 @@ getcmdline_int(
// navigating the wild menu (i.e. the key is not 'wildchar' or
// 'wildcharm' or Ctrl-N or Ctrl-P or Ctrl-A or Ctrl-L).
// If the popup menu is displayed, then PageDown and PageUp keys are
- // also used to navigate the menu, and the mouse scroll wheel keys
- // scroll the info popup.
+ // also used to navigate the menu, the mouse scroll wheel keys scroll
+ // the info popup, and CTRL-SHIFT-<Up>/<Down> scroll it with the
+ // keyboard.
end_wildmenu = (!key_is_wc
&& c != Ctrl_N && c != Ctrl_P && c != Ctrl_A && c != Ctrl_L);
end_wildmenu = end_wildmenu && (!cmdline_pum_active() ||
(c != K_PAGEDOWN && c != K_PAGEUP
&& c != K_KPAGEDOWN && c != K_KPAGEUP
&& c != K_MOUSEDOWN && c != K_MOUSEUP
- && c != K_MOUSELEFT && c != K_MOUSERIGHT));
+ && c != K_MOUSELEFT && c != K_MOUSERIGHT
+ && !((c == K_S_UP || c == K_S_DOWN)
+ && (mod_mask & MOD_MASK_CTRL))));

// free expanded names when finished walking through matches
if (end_wildmenu)
@@ -2518,6 +2521,15 @@ getcmdline_int(

case Ctrl_N: // next match
case Ctrl_P: // previous match
+#ifdef FEAT_PROP_POPUP
+ // CTRL-SHIFT-P/N scroll the info popup one line.
+ if (cmdline_pum_active() && (mod_mask & MOD_MASK_SHIFT))
+ {
+ if (popup_scroll_info(c == Ctrl_P ? -1 : 1, false))
+ cmdline_pum_display();
+ goto cmdline_not_changed;
+ }
+#endif
if (xpc.xp_numfiles > 0)
{
wild_type = (c == Ctrl_P) ? WILD_PREV : WILD_NEXT;
@@ -2534,6 +2546,27 @@ getcmdline_int(
case K_KPAGEUP:
case K_PAGEDOWN:
case K_KPAGEDOWN:
+#ifdef FEAT_PROP_POPUP
+ // CTRL-SHIFT-<Up>/<Down> scroll the info popup a line,
+ // CTRL-SHIFT-<PageUp>/<PageDown> a page. Shift is folded into
+ // K_S_UP/K_S_DOWN but stays in mod_mask for the Page keys.
+ if (cmdline_pum_active()
+ && ((c == K_S_UP || c == K_S_DOWN)
+ ? (mod_mask & MOD_MASK_CTRL)
+ : ((c == K_PAGEUP || c == K_KPAGEUP
+ || c == K_PAGEDOWN || c == K_KPAGEDOWN)
+ && (mod_mask & MOD_MASK_CTRL)
+ && (mod_mask & MOD_MASK_SHIFT))))
+ {
+ int up = c == K_S_UP || c == K_PAGEUP
+ || c == K_KPAGEUP;
+
+ if (popup_scroll_info(up ? -1 : 1,
+ c != K_S_UP && c != K_S_DOWN))
+ cmdline_pum_display();
+ goto cmdline_not_changed;
+ }
+#endif
if (cmdline_pum_active()
&& (c == K_PAGEUP || c == K_PAGEDOWN ||
c == K_KPAGEUP || c == K_KPAGEDOWN))
diff --git a/src/getchar.c b/src/getchar.c
index 47ba62ad6..d02b9009c 100644
--- a/src/getchar.c
+++ b/src/getchar.c
@@ -2728,7 +2728,10 @@ at_ins_compl_key(void)
if (typebuf.tb_len > 3
&& (c == K_SPECIAL || c == CSI) // CSI is used by the GUI
&& p[1] == KS_MODIFIER
- && (p[2] & MOD_MASK_CTRL))
+ && (p[2] & MOD_MASK_CTRL)
+ // CTRL-SHIFT-N/P scroll the info popup, so they must not be folded
+ // to the CTRL-N/CTRL-P completion keys here.
+ && !(p[2] & MOD_MASK_SHIFT))
c = p[3] & 0x1f;
return (ctrl_x_mode_not_default() && vim_is_ctrl_x_key(c))
|| (compl_status_local() && (c == Ctrl_N || c == Ctrl_P));
diff --git a/src/popupwin.c b/src/popupwin.c
index 79ff1b09c..989fbbae2 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -6722,6 +6722,40 @@ popup_find_info_window(void)
}
#endif

+/*
+ * Scroll the completion info popup one line (by_page false) or one page
+ * (by_page true); "dir" negative scrolls up, positive down.
+ * Returns true when an info popup was found.
+ */
+ bool
+popup_scroll_info(int dir, bool by_page)
+{
+#ifdef FEAT_QUICKFIX
+ win_T *wp = popup_find_info_window();
+ int by;
+ linenr_T new_topline;
+
+ if (wp == NULL)
+ return false;
+
+ by = by_page ? (wp->w_height > 2 ? wp->w_height - 1 : 1) : 1;
+ new_topline = wp->w_topline + (dir < 0 ? -by : by);
+ if (new_topline < 1)
+ new_topline = 1;
+ if (new_topline > wp->w_buffer->b_ml.ml_line_count)
+ new_topline = wp->w_buffer->b_ml.ml_line_count;
+ if (new_topline != wp->w_topline)
+ {
+ set_topline(wp, new_topline);
+ popup_set_firstline(wp);
+ redraw_win_later(wp, UPD_NOT_VALID);
+ }
+ return true;
+#else
+ return false;
+#endif
+}
+
void
f_popup_findecho(typval_T *argvars UNUSED, typval_T *rettv)
{
diff --git a/src/proto/popupwin.pro b/src/proto/popupwin.pro
index 14e4a6fd2..acdf37d30 100644
--- a/src/proto/popupwin.pro
+++ b/src/proto/popupwin.pro
@@ -64,6 +64,7 @@ int set_ref_in_popups(int copyID);
int popup_is_popup(win_T *wp);
win_T *popup_find_preview_window(void);
win_T *popup_find_info_window(void);
+bool popup_scroll_info(int dir, bool by_page);
void f_popup_findecho(typval_T *argvars, typval_T *rettv);
void f_popup_findinfo(typval_T *argvars, typval_T *rettv);
void f_popup_findpreview(typval_T *argvars, typval_T *rettv);
diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim
index 8fbaa502b..ad48c54ad 100644
--- a/src/testdir/test_cmdline.vim
+++ b/src/testdir/test_cmdline.vim
@@ -4814,6 +4814,68 @@ func Test_wildmenu_pum_info_mouse_scroll()
call StopVimInTerminal(buf)
endfunc

+func s:ReadCmdlineInfo()
+ let l = filereadable('Xclinfo') ? map(readfile('Xclinfo'), 'str2nr(v:val)') : []
+ return len(l) == 2 ? l : [-1, -1]
+endfunc
+
+func Test_wildmenu_pum_info_scroll_keys()
+ CheckRunVimInTerminal
+ CheckFeature quickfix
+
+ let lines =<< trim END
+ func DictComp(A, L, P)
+ let info = join(map(range(1, 40), '"info line " .. v:val'), "
")
+ return [{'word': 'apple', 'info': info}, {'word': 'banana', 'info': info}]
+ endfunc
+ command -nargs=1 -complete=customlist,DictComp DictCmd echo <q-args>
+ set wildmenu wildoptions=pum completeopt=menu,popup
+ func InfoState()
+ let id = popup_findinfo()
+ call writefile([id ? popup_getpos(id).firstline : -1, wildmenumode()],
+ \ 'Xclinfo')
+ endfunc
+ " A <Cmd> mapping runs without closing the wildmenu, so it can report the
+ " info popup state while completion is active.
+ cnoremap <F4> <Cmd>call InfoState()<CR>
+ END
+ call writefile(lines, 'XtestCmdlineScroll', 'D')
+ let buf = RunVimInTerminal('-S XtestCmdlineScroll', #{rows: 12})
+ call TermWait(buf, 50)
+
+ " Show the completion popup menu with the info popup next to it.
+ call term_sendkeys(buf, ":DictCmd \<Tab>")
+ call TermWait(buf, 50)
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([1, 1], s:ReadCmdlineInfo())})
+
+ " Ctrl-Shift-Down then Ctrl-Shift-Up scroll the info popup by a line without
+ " closing the wildmenu.
+ call term_sendkeys(buf, "\<Esc>[1;6B")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([2, 1], s:ReadCmdlineInfo())})
+ call term_sendkeys(buf, "\<Esc>[1;6A")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([1, 1], s:ReadCmdlineInfo())})
+
+ " Ctrl-Shift-N then Ctrl-Shift-P scroll like the arrows.
+ call term_sendkeys(buf, "\<Esc>[27;6;110~")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([2, 1], s:ReadCmdlineInfo())})
+ call term_sendkeys(buf, "\<Esc>[27;6;112~")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([1, 1], s:ReadCmdlineInfo())})
+
+ " Ctrl-Shift-PageDown scrolls down by a page (more than one line).
+ call term_sendkeys(buf, "\<Esc>[6;6~")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_true(s:ReadCmdlineInfo()[0] > 2)})
+
+ call term_sendkeys(buf, "\<Esc>")
+ call StopVimInTerminal(buf)
+ call delete('Xclinfo')
+endfunc
+
func Test_cmdline_complete_findfunc_dict()
CheckScreendump

diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim
index 2850ce08b..a10a92105 100644
--- a/src/testdir/test_popupwin.vim
+++ b/src/testdir/test_popupwin.vim
@@ -3998,6 +3998,78 @@ func Test_popupmenu_info_border_mouse()
call StopVimInTerminal(buf)
endfunc

+func s:ReadInfoState()
+ let l = filereadable('Xinfofl') ? map(readfile('Xinfofl'), 'str2nr(v:val)') : []
+ return len(l) == 3 ? l : [-1, -1, -1]
+endfunc
+
+func Test_popupmenu_info_scroll_keys()
+ CheckRunVimInTerminal
+ CheckFeature quickfix
+
+ let lines =<< trim END
+ func Omni_test(findstart, base)
+ if a:findstart
+ return col(".")
+ endif
+ return [#{word: "scrollme",
+ \ info: join(map(range(1, 40), '"info line " .. v:val'), "
")},
+ \ #{word: "another", info: "short"}]
+ endfunc
+ set completeopt=menu,menuone,popup
+ set omnifunc=Omni_test
+ func InfoState()
+ let id = popup_findinfo()
+ call writefile([id ? popup_getpos(id).firstline : -1, pumvisible(),
+ \ get(complete_info(['selected']), 'selected', -1)], 'Xinfofl')
+ endfunc
+ " A <Cmd> mapping runs without closing the completion menu, so it can
+ " report the info popup state while completion is active.
+ inoremap <F4> <Cmd>call InfoState()<CR>
+ END
+ call writefile(lines, 'XtestInfoScroll', 'D')
+ let buf = RunVimInTerminal('-S XtestInfoScroll', #{rows: 14})
+ call TermWait(buf, 50)
+
+ " Open insert-mode completion; the info popup is shown, first item selected.
+ call term_sendkeys(buf, "i\<C-X>\<C-O>")
+ call TermWait(buf, 50)
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([1, 1, 0], s:ReadInfoState())})
+
+ " Ctrl-Shift-Down then Ctrl-Shift-Up scroll the info popup by a line; the
+ " menu stays open and the selected item does not change.
+ call term_sendkeys(buf, "\<Esc>[1;6B")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([2, 1, 0], s:ReadInfoState())})
+ call term_sendkeys(buf, "\<Esc>[1;6A")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([1, 1, 0], s:ReadInfoState())})
+
+ " Ctrl-Shift-N then Ctrl-Shift-P scroll like the arrows, again without
+ " moving the selection.
+ call term_sendkeys(buf, "\<Esc>[27;6;110~")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([2, 1, 0], s:ReadInfoState())})
+ call term_sendkeys(buf, "\<Esc>[27;6;112~")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal([1, 1, 0], s:ReadInfoState())})
+
+ " Ctrl-Shift-PageDown scrolls down by a page (more than one line).
+ call term_sendkeys(buf, "\<Esc>[6;6~")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_true(s:ReadInfoState()[0] > 2)})
+
+ " Plain Ctrl-N still moves the selection to the next item.
+ call term_sendkeys(buf, "\<C-N>")
+ call term_sendkeys(buf, "\<F4>")
+ call WaitForAssert({-> assert_equal(1, s:ReadInfoState()[2])})
+
+ call term_sendkeys(buf, "\<Esc>")
+ call StopVimInTerminal(buf)
+ call delete('Xinfofl')
+endfunc
+
func Test_popupmenu_info_align_menu()
CheckScreendump
CheckFeature quickfix
diff --git a/src/version.c b/src/version.c
index df1311759..6f4499c39 100644
--- a/src/version.c
+++ b/src/version.c
@@ -729,6 +729,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 609,
/**/
608,
/**/
Reply all
Reply to author
Forward
0 new messages