]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0609: completion info popup cannot be scrolled with the keyboard v9.2.0609
authorHirohito Higashi <h.east.727@gmail.com>
Tue, 9 Jun 2026 19:24:25 +0000 (19:24 +0000)
committerChristian Brabandt <cb@256bit.org>
Tue, 9 Jun 2026 19:31:17 +0000 (19:31 +0000)
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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/doc/insert.txt
runtime/doc/options.txt
runtime/doc/version9.txt
src/edit.c
src/ex_getln.c
src/getchar.c
src/popupwin.c
src/proto/popupwin.pro
src/testdir/test_cmdline.vim
src/testdir/test_popupwin.vim
src/version.c

index 13f2d89a2d906ff6cbf4e2bb7ecdbe04c87d9d5c..5946f26e760c23d298f8d33161d9d0d63b04444a 100644 (file)
@@ -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.
 
index 0df50ff60626d00a29c34d307064e0c31fed305e..244a7510fb864eee00a5f24144cc1bdc84270676 100644 (file)
@@ -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:
index 2496d1b695f58104ead17084b087fb53dafb9268..b0c95e50fb51ac6674c63a9caddfbd1b3bb38f18 100644 (file)
@@ -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 ~
 ---------
index 2b839d7f5c416e4af9edef613b31e0274f5ccab1..3eba12dab54369f85f77163bf5a730f6f3ae3342 100644 (file)
@@ -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
index ea20fa96b8ff9830293477e63b2b20bc24fd0f0e..d553fee46be6c9ea974e8b68d3816bfbebd015ba 100644 (file)
@@ -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))
index 47ba62ad6c83233bd0ae8324b7a00e94ba37385a..d02b9009cb5a2dd9e80af9fe0456be86af7b6a61 100644 (file)
@@ -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));
index 79ff1b09cc8e8faf199cd661861faf03a66107c7..989fbbae24bb64d08e1ec563c426699e52fa75f1 100644 (file)
@@ -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)
 {
index 14e4a6fd29c341a97098303b10df9bcc3fdb4d80..acdf37d30fd420f1bc4c5605f84ffa609cb53029 100644 (file)
@@ -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);
index 8fbaa502b8abd4ca640fb80f47f2c870b7a97069..ad48c54ad5faf56603f8428d1ca3b1af87542dea 100644 (file)
@@ -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'), "\n")
+      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
 
index 2850ce08b731942519fe163e285b3c414d14ebcb..a10a9210548ba3a8fbc66e9f97953dd70d85caac 100644 (file)
@@ -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'), "\n")},
+              \ #{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
index df1311759e4061545bb41b041c47e9cbd4a7fb48..6f4499c39adf53c64ae1654241ca0ce0d748a4d7 100644 (file)
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    609,
 /**/
     608,
 /**/