Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions autoload/ale/floating_preview.vim
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,15 @@ function! s:VimShow(lines, options) abort
call s:VimCreate(a:options)
endif

call s:ResetManagedPopupSize()

" Execute commands in window context
for l:command in get(a:options, 'commands', [])
call win_execute(w:preview['id'], l:command)
endfor

call popup_settext(w:preview['id'], a:lines)
call s:LockPopupSize(w:preview['id'], a:lines)

if g:ale_close_preview_on_insert
augroup ale_floating_preview_window
Expand Down Expand Up @@ -144,6 +147,178 @@ function! s:NvimCreate(options) abort
let w:preview = {'id': l:winid, 'buffer': l:buffer}
endfunction

function! s:GetPageScrollOffset(winid) abort
let l:position = popup_getpos(a:winid)

if empty(l:position)
return 1
endif

return max([
\ 1,
\ float2nr(ceil(l:position.core_height / 2.0)),
\])
endfunction

function! s:GetPopupContentWidth(lines) abort
if empty(a:lines)
return 1
endif

return max(map(copy(a:lines), 'strdisplaywidth(v:val)'))
endfunction

function! s:GetPopupLineRows(winid) abort
let l:position = popup_getpos(a:winid)
let l:width = get(l:position, 'core_width', 0)
let l:bufnr = winbufnr(a:winid)

if l:width <= 0 || l:bufnr <= 0
return []
endif

let l:lines = getbufline(l:bufnr, 1, '$')

if !get(popup_getoptions(a:winid), 'wrap', 1)
return map(l:lines, '1')
endif

return map(
\ l:lines,
\ 'max([1, float2nr(ceil(strdisplaywidth(v:val) / (l:width * 1.0)))])',
\)
endfunction

function! s:GetPopupMaxFirstLine(winid) abort
let l:position = popup_getpos(a:winid)
let l:line_rows = s:GetPopupLineRows(a:winid)
let l:remaining_rows = 0

if empty(l:position) || empty(l:line_rows)
return 1
endif

for l:index in reverse(range(0, len(l:line_rows) - 1))
let l:remaining_rows += l:line_rows[l:index]

if l:remaining_rows >= l:position.core_height
return l:index + 1
endif
endfor

return 1
endfunction

function! s:LockPopupSize(winid, lines) abort
let l:options = popup_getoptions(a:winid)
let l:position = popup_getpos(a:winid)
let l:managed_size = {}
let l:size_options = {}

if empty(l:options) || empty(l:position)
return
endif

if get(l:options, 'minwidth', 0) is# 0
\&& get(l:options, 'maxwidth', 0) is# 0
let l:size_options.minwidth = s:GetPopupContentWidth(a:lines)
let l:size_options.maxwidth = l:size_options.minwidth
let l:managed_size.width = 1
endif

if get(l:options, 'minheight', 0) is# 0
\&& get(l:options, 'maxheight', 0) is# 0
let l:size_options.minheight = l:position.core_height
let l:size_options.maxheight = l:position.core_height
let l:managed_size.height = 1
endif

if !empty(l:size_options)
let l:size_options.resize = v:false
call popup_setoptions(a:winid, l:size_options)
let w:preview.managed_size = l:managed_size
endif
endfunction

function! s:ResetManagedPopupSize() abort
let l:managed_size = get(w:preview, 'managed_size', {})
let l:size_options = {}

if empty(l:managed_size)
return
endif

if get(l:managed_size, 'width', 0)
let l:size_options.minwidth = 0
let l:size_options.maxwidth = 0
endif

if get(l:managed_size, 'height', 0)
let l:size_options.minheight = 0
let l:size_options.maxheight = 0
endif

if !empty(l:size_options)
let l:size_options.resize = v:true
call popup_setoptions(w:preview['id'], l:size_options)
endif

unlet! w:preview.managed_size
endfunction

function! s:ScrollPopup(winid, count, direction) abort
let l:position = popup_getpos(a:winid)

if empty(l:position)
return
endif

if a:direction is# 'j'
let l:count = min([
\ a:count,
\ s:GetPopupMaxFirstLine(a:winid) - l:position.firstline,
\])
else
let l:count = min([a:count, l:position.firstline - 1])
endif

if l:count <= 0
return
endif

call win_execute(a:winid, 'normal! ' . l:count . a:direction . 'zt')
endfunction

function! ale#floating_preview#PopupFilter(winid, key) abort
if a:key is# "\<C-n>" || a:key is# "\<ScrollWheelDown>"
call s:ScrollPopup(a:winid, 1, 'j')

return 1
elseif a:key is# "\<C-p>" || a:key is# "\<ScrollWheelUp>"
call s:ScrollPopup(a:winid, 1, 'k')

return 1
elseif a:key is# "\<C-d>"
call s:ScrollPopup(a:winid, s:GetPageScrollOffset(a:winid), 'j')

return 1
elseif a:key is# "\<C-u>"
call s:ScrollPopup(a:winid, s:GetPageScrollOffset(a:winid), 'k')

return 1
elseif a:key is# "\<Esc>"
if exists('w:preview') && get(w:preview, 'id', 0) is# a:winid
call s:VimClose()
else
call popup_close(a:winid)
endif

return 1
endif

return 0
endfunction

function! s:VimCreate(options) abort
" default options
let l:popup_opts = extend({
Expand All @@ -164,6 +339,8 @@ function! s:VimCreate(options) abort
\ get(g:ale_floating_window_border, 4, '+'),
\ get(g:ale_floating_window_border, 5, '+'),
\ ],
\ 'filter': function('ale#floating_preview#PopupFilter'),
\ 'filtermode': 'n',
\ 'moved': 'any',
\ }, s:GetPopupOpts())

Expand Down
52 changes: 52 additions & 0 deletions doc/ale.txt
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,40 @@ or |g:ale_floating_preview| is set to `true` or `1`, the hover information
will show in a floating window. The borders of the floating preview window can
be customized by setting |g:ale_floating_window_border|.

For Vim with |popupwin|, floating preview windows can be scrolled with the
keyboard and dismissed with |<Esc>|. The following keys are supported:

|<C-n>| - Scroll down one line
|<C-p>| - Scroll up one line
|<C-d>| - Scroll down half a page
|<C-u>| - Scroll up half a page
|<Esc>| - Close the popup
Comment on lines +839 to +845
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new key references are formatted as Vim help links (e.g. |<C-n>|, |<Esc>|), but these aren’t standard help tags and will render as broken links. In this help file, key help tags are typically referenced as |CTRL-N|, |CTRL-P|, etc. Consider switching to the existing tag style (or removing |...| markup and just using <C-n>/<Esc> as plain text).

Suggested change
keyboard and dismissed with |<Esc>|. The following keys are supported:
|<C-n>| - Scroll down one line
|<C-p>| - Scroll up one line
|<C-d>| - Scroll down half a page
|<C-u>| - Scroll up half a page
|<Esc>| - Close the popup
keyboard and dismissed with <Esc>. The following keys are supported:
<C-n> - Scroll down one line
<C-p> - Scroll up one line
<C-d> - Scroll down half a page
<C-u> - Scroll up half a page
<Esc> - Close the popup

Copilot uses AI. Check for mistakes.

Mouse scroll wheel also works when |'mouse'| is enabled.

If you do not set popup width or height limits yourself, ALE keeps the popup
at a stable size while it is visible. For width, ALE locks Vim popups to the
maximum display width across the popup contents, so later long lines do not
cause the popup to stay stuck at the initial viewport width.

The filter can be overridden via |g:ale_floating_preview_popup_opts| by
providing a custom `filter` key. For example, to scroll with `j` and `k`
instead: >

function! MyPopupFilter(winid, key) abort
if a:key is# 'j'
return ale#floating_preview#PopupFilter(a:winid, "\<C-n>")
elseif a:key is# 'k'
return ale#floating_preview#PopupFilter(a:winid, "\<C-p>")
elseif a:key is# "\<Esc>" || a:key is# "\<C-d>" || a:key is# "\<C-u>"
return ale#floating_preview#PopupFilter(a:winid, a:key)
endif
return 0
endfunction

let g:ale_floating_preview_popup_opts = {'filter': function('MyPopupFilter')}
<

For Vim 8.1+ terminals, mouse hovering is disabled by default. Enabling
|balloonexpr| commands in terminals can cause scrolling issues in terminals,
so ALE will not attempt to show balloons unless |g:ale_set_balloons| is set to
Expand Down Expand Up @@ -1464,6 +1498,24 @@ g:ale_floating_preview_popup_opts
NOTE: for Vim users see |popup_create-arguments|, for NeoVim users see
|nvim_open_win| for argument details

For Vim floating preview popups, if no `minwidth`/`maxwidth` or
`minheight`/`maxheight` values are supplied, ALE will temporarily keep the
popup at a stable size while it is visible. Width is locked to the maximum
display width across the popup contents, while height is locked to the
current rendered popup height. If you set those size options yourself, your
values will be used as-is.

To set popup size limits explicitly for Vim, put the values in
|g:ale_floating_preview_popup_opts|. For example: >

let g:ale_floating_preview_popup_opts = {
\ 'minwidth': 60,
\ 'maxwidth': 100,
\ 'minheight': 8,
\ 'maxheight': 20,
\}
<

For example, to enhance popups with a title: >

function! CustomOpts() abort
Expand Down
Loading
Loading