How to scroll the vim completion popup window
After popup windows is added in vim 8.2, we can display documentation provided by ycm in a popup window. Different from display documentation in a preview window, it won't change the layout. But there is one disadvantage, I can't scroll the popup window by keyboard. It is not vim-style enough.
Enable the Completion Popup Window
Add the following line in the vimrc file.
set completeopt+=popup
Scroll the Popup Window
dedowsdi provide a solution about how to scroll a popup window here. There is another problem now. Not like coc, the popup window triggered by ycm may be far away from the cursor. If I follow this solution, I must let vim to scan over the whole screen to find the popup window. It is too slow.
After looking into VIM REFERENCE MANUAL and doing some tests, I thought I can use popup_findinfo
to get the id of the completion popup window. But, I'm not sure it is the correct way. So I have the following code:
nnoremap <C-e> :call ScrollPopup(1)<CR>
nnoremap <C-y> :call ScrollPopup(0)<CR>
function ScrollPopup(down)
let winid = popup_findinfo()
if winid == 0
return 0
endif
" The popup window has been hidden in the normal mode, we should make it show again.
call popup_show(winid)
let pp = popup_getpos(winid)
call popup_setoptions( winid,
\ {'firstline' : pp.firstline + ( a:down ? 1 : -1 ) } )
return 1
endfunction
Then, I can read and scroll the completion popup window in normal mode. It isn't good enough still: 1. I must switch to normal mode which will close the completion popup menu. If the selection isn't the one I want, I must delete it and trigger completion again. 2. After scrolling to the bottom, the height of the popup window will reduce to one, if I keep pressing CTRL-e.
Scroll the Popup Window without Closing the Popup Menu
Firstly, I should change nnoremap
to inoremap
. So the key-binding will work in insert mode.
inoremap <C-e> :call ScrollPopup(1)<CR>
inoremap <C-y> :call ScrollPopup(0)<CR>
I haven't spend much time with vimscript even if I have used vim for years. Instead of calling ScrollPopup
, it just inserts ":call ScrollPopup(1)\n". Finally, I find out how map
works.
map {lhs} {rhs}
It just maps the left hand side keystrokes to the right hand side keystrokes. If I want to run something, I should add <expr>
.
map <expr> {lhs} {rhs}
The {rhs} is evaluated to obtain the right hand side keystrokes.
inoremap <expr> <C-e> ScrollPopup(3) ? '' : '<C-e>'
inoremap <expr> <C-y> ScrollPopup(-3) ? '' : '<C-y>'
If scroll happened, it maps CTRL-e/CTRL-y to non-operation. Otherwise, it doesn't change the keystrokes.
Stop after Reaching the bottom
ScrollPopup
should stop changing firstline
after the bottom of the buffer has showed. But, how does it know that? With popup_getpos
, we have following properties:
col screen column of the popup, one-based
line screen line of the popup, one-based
width width of the whole popup in screen cells
height height of the whole popup in screen cells
core_col screen column of the text box
core_line screen line of the text box
core_width width of the text box in screen cells
core_height height of the text box in screen cells
firstline line of the buffer at top (1 unless scrolled) (not the value of the "firstline" property)
lastline line of the buffer at the bottom (updated when the popup is redrawn)
scrollbar non-zero if a scrollbar is displayed
visible one if the popup is displayed, zero if hidden
At a glance, a formula has come up: lastline
- firstline
< height
. But it doesn't work. lastline
is not the lastline of the text. It is the lastline showed in the popup window. After some search, I found I can have the lastline by win_execute(winid, "echo line('$')")
. Executing a command in the popup window to get the lastline of the buffer.
It still doesn't work as I thought. Sometimes, it won't scroll down. Occasionally, I show the line number in the popup window. firstline
and lastline
is the line of the text. height
is the line of the window. With wrap on, one line of text will be many lines in the window. Finally, I have:
function! ScrollPopup(down)
let winid = popup_findinfo()
if winid == 0
return 0
endif
" if the popup window is hidden, bypass the keystrokes
let pp = popup_getpos(winid)
if pp.visible != 1
return 0
endif
let firstline = pp.firstline + a:down
let buf_lastline = str2nr(trim(win_execute(winid, "echo line('$')")))
if firstline < 1
let firstline = 1
elseif pp.lastline + a:down > buf_lastline
let firstline = firstline - a:down + buf_lastline - pp.lastline
endif
" The appear of scrollbar will change the layout of the content which will cause inconsistent height.
call popup_setoptions( winid,
\ {'scrollbar': 0, 'firstline' : firstline } )
return 1
endfunction
inoremap <expr> <C-e> ScrollPopup(3) ? '' : '<C-e>'
inoremap <expr> <C-y> ScrollPopup(-3) ? '' : '<C-y>'