diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index 13f2d89a2d..5946f26e76 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. Select the next match, as if CTRL-N was used, but don't insert it. +CTRL-SHIFT- + 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- + Scroll the info popup down one line. +CTRL-SHIFT- + Scroll the info popup up one page. +CTRL-SHIFT- + Scroll the info popup down one page. +CTRL-SHIFT-P Like CTRL-SHIFT-, scroll the info popup up one line. +CTRL-SHIFT-N Like CTRL-SHIFT-, 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|. or Stop completion without changing the match and insert the typed character. diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 0df50ff606..244a7510fb 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 2496d1b695..b0c95e50fb 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 2b839d7f5c..3eba12dab5 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- scrolls the info popup up a line, + // CTRL-SHIFT- 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-/ 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 ea20fa96b8..d553fee46b 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-/ 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-/ scroll the info popup a line, + // CTRL-SHIFT-/ 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 47ba62ad6c..d02b9009cb 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 79ff1b09cc..989fbbae24 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 14e4a6fd29..acdf37d30f 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 8fbaa502b8..ad48c54ad5 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'), "\n") + return [{'word': 'apple', 'info': info}, {'word': 'banana', 'info': info}] + endfunc + command -nargs=1 -complete=customlist,DictComp DictCmd echo + 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 mapping runs without closing the wildmenu, so it can report the + " info popup state while completion is active. + cnoremap call InfoState() + 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 \") + call TermWait(buf, 50) + call term_sendkeys(buf, "\") + 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, "\[1;6B") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_equal([2, 1], s:ReadCmdlineInfo())}) + call term_sendkeys(buf, "\[1;6A") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_equal([1, 1], s:ReadCmdlineInfo())}) + + " Ctrl-Shift-N then Ctrl-Shift-P scroll like the arrows. + call term_sendkeys(buf, "\[27;6;110~") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_equal([2, 1], s:ReadCmdlineInfo())}) + call term_sendkeys(buf, "\[27;6;112~") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_equal([1, 1], s:ReadCmdlineInfo())}) + + " Ctrl-Shift-PageDown scrolls down by a page (more than one line). + call term_sendkeys(buf, "\[6;6~") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_true(s:ReadCmdlineInfo()[0] > 2)}) + + call term_sendkeys(buf, "\") + 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 2850ce08b7..a10a921054 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'), "\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 mapping runs without closing the completion menu, so it can + " report the info popup state while completion is active. + inoremap call InfoState() + 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\\") + call TermWait(buf, 50) + call term_sendkeys(buf, "\") + 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, "\[1;6B") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_equal([2, 1, 0], s:ReadInfoState())}) + call term_sendkeys(buf, "\[1;6A") + call term_sendkeys(buf, "\") + 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, "\[27;6;110~") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_equal([2, 1, 0], s:ReadInfoState())}) + call term_sendkeys(buf, "\[27;6;112~") + call term_sendkeys(buf, "\") + 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, "\[6;6~") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_true(s:ReadInfoState()[0] > 2)}) + + " Plain Ctrl-N still moves the selection to the next item. + call term_sendkeys(buf, "\") + call term_sendkeys(buf, "\") + call WaitForAssert({-> assert_equal(1, s:ReadInfoState()[2])}) + + call term_sendkeys(buf, "\") + 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 df1311759e..6f4499c39a 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, /**/