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

Commit: 
https://github.com/vim/vim/commit/e2cf855bbe751f14b98b5e2b1542fb2bfc5bfe74
Author: Hirohito Higashi <[email protected]>
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) <[email protected]>
    Signed-off-by: Hirohito Higashi <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

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,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wX2Nk-006ifB-HA%40256bit.org.

Raspunde prin e-mail lui