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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Hirohito Higashi
2026-06-09 19:24:25 +00:00
committed by Christian Brabandt
parent 60ebdf7e34
commit e2cf855bbe
11 changed files with 269 additions and 8 deletions
+17 -1
View 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.
+3 -2
View 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:
+3 -1
View 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 ~
---------
+35
View 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
+36 -3
View 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))
+4 -1
View 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));
+34
View 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)
{
+1
View 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);
+62
View 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
+72
View 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
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
609,
/**/
608,
/**/