Skip to content
Merged
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
137 changes: 104 additions & 33 deletions autoload/lsp/ui/vim/documentation.vim
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ let s:use_vim_popup = has('patch-8.1.1517') && !has('nvim')
let s:use_nvim_float = exists('*nvim_open_win') && has('nvim')

let s:last_popup_id = -1
let s:last_timer_id = v:false

function! s:complete_done() abort
function! s:complete_changed() abort
if !g:lsp_documentation_float | return | endif
" Use a timer to avoid textlock (see :h textlock).
let l:event = deepcopy(v:event)
call timer_start(0, {-> s:show_documentation(l:event)})
let l:event = copy(v:event)
if s:last_timer_id
call timer_stop(s:last_timer_id)
let s:last_timer_id = v:false
endif
let s:last_timer_id = timer_start(g:lsp_documentation_debounce, {-> s:show_documentation(l:event)})
endfunction

function! s:show_documentation(event) abort
Expand All @@ -17,54 +22,118 @@ function! s:show_documentation(event) abort
return
endif

let l:right = wincol() < winwidth(0) / 2

" TODO: Neovim
if l:right
let l:line = a:event['row'] + 1
let l:col = a:event['col'] + a:event['width'] + 1 + (a:event['scrollbar'] ? 1 : 0)
else
let l:line = a:event['row'] + 1
let l:col = a:event['col'] - 1
endif

" TODO: Support markdown
let l:data = split(a:event['completed_item']['info'], '\n')
let l:lines = []
let l:syntax_lines = []
let l:ft = lsp#ui#vim#output#append(l:data, l:lines, l:syntax_lines)

let l:current_win_id = win_getid()

if s:use_vim_popup
let s:last_popup_id = popup_create('(no documentation available)', {'line': l:line, 'col': l:col, 'pos': l:right ? 'topleft' : 'topright', 'padding': [0, 1, 0, 1]})
elseif s:use_nvim_float
let l:height = float2nr(winheight(0) - l:line + 1)
let l:width = float2nr(l:right ? winwidth(0) - l:col + 1 : l:col)
if l:width <= 0
let l:width = 1
" Neovim
if s:use_nvim_float
let l:event = a:event
let l:event.row = float2nr(l:event.row)
let l:event.col = float2nr(l:event.col)

let l:buffer = nvim_create_buf(v:false, v:true)
let l:curpos = win_screenpos(nvim_get_current_win())[0] + winline() - 1
let g:lsp_documentation_float_docked = get(g:, 'lsp_documentation_float_docked', 0)

if g:lsp_documentation_float_docked
let g:lsp_documentation_float_docked_maxheight = get(g:, ':lsp_documentation_float_docked_maxheight', &previewheight)
let l:dock_downwards = max([screenrow(), l:curpos]) < (&lines / 2)
let l:height = min([len(l:data), g:lsp_documentation_float_docked_maxheight])
let l:width = &columns
let l:col = 0
if l:dock_downwards
let l:anchor = 'SW'
let l:row = &lines - &cmdheight - 1
let l:height = min([l:height, &lines - &cmdheight - l:event.row - l:event.height])
else " dock upwards
let l:anchor = 'NW'
let l:row = 0
let l:height = min([l:height, l:event.row - 1])
endif

else " not docked
let l:row = l:event['row']
let l:height = max([&lines - &cmdheight - l:row, &previewheight])

let l:right_area = &columns - l:event.col - l:event.width + 1 " 1 for the padding of popup
let l:left_area = l:event.col - 1
let l:right = l:right_area > l:left_area
if l:right
let l:anchor = 'NW'
let l:width = l:right_area - 1
let l:col = l:event.col + l:event.width + (l:event.scrollbar ? 1 : 0)
else
let l:anchor = 'NE'
let l:width = l:left_area
let l:col = l:event.col - 1 " 1 due to padding of completion popup
endif
endif

call setbufvar(l:buffer, 'lsp_syntax_highlights', l:syntax_lines)
call setbufvar(l:buffer, 'lsp_do_conceal', 1)

" add padding on both sides of lines containing text
for l:index in range(len(l:lines))
if len(l:lines[l:index]) > 0
let l:lines[l:index] = ' ' . l:lines[l:index] . ' '
endif
endfor

call nvim_buf_set_lines(l:buffer, 0, -1, v:false, l:lines)
call nvim_buf_set_option(l:buffer, 'readonly', v:true)
call nvim_buf_set_option(l:buffer, 'modifiable', v:false)
call nvim_buf_set_option(l:buffer, 'filetype', l:ft.'.lsp-hover')

if !g:lsp_documentation_float_docked
let l:bufferlines = nvim_buf_line_count(l:buffer)
let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)'))
if g:lsp_preview_max_width > 0
let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth])
endif
let l:width = min([float2nr(l:width), l:maxwidth])
let l:height = min([float2nr(l:height), l:bufferlines])
endif
if l:height <= 0
let l:height = 1
if g:lsp_preview_max_height > 0
let l:maxheight = g:lsp_preview_max_height
let l:height = min([l:height, l:maxheight])
endif
let s:last_popup_id = lsp#ui#vim#output#floatingpreview([])
call nvim_win_set_config(s:last_popup_id, {'relative': 'win', 'anchor': l:right ? 'NW' : 'NE', 'row': l:line - 1, 'col': l:col - 1, 'height': l:height, 'width': l:width})

" Height and width must be atleast 1, otherwise error
let l:height = (l:height < 1 ? 1 : l:height)
let l:width = (l:width < 1 ? 1 : l:width)

let s:last_popup_id = nvim_open_win(l:buffer, v:false, {'relative': 'editor', 'anchor': l:anchor, 'row': l:row, 'col': l:col, 'height': l:height, 'width': l:width, 'style': 'minimal'})
return
endif

" Vim
let l:current_win_id = win_getid()

let l:right = wincol() < winwidth(0) / 2
if l:right
let l:line = a:event['row'] + 1
let l:col = a:event['col'] + a:event['width'] + 1 + (a:event['scrollbar'] ? 1 : 0)
else
let l:line = a:event['row'] + 1
let l:col = a:event['col'] - 1
endif
let s:last_popup_id = popup_create('(no documentation available)', {'line': l:line, 'col': l:col, 'pos': l:right ? 'topleft' : 'topright', 'padding': [0, 1, 0, 1]})
call setbufvar(winbufnr(s:last_popup_id), 'lsp_syntax_highlights', l:syntax_lines)
call setbufvar(winbufnr(s:last_popup_id), 'lsp_do_conceal', 1)
call lsp#ui#vim#output#setcontent(s:last_popup_id, l:lines, l:ft)
let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info()

call win_gotoid(l:current_win_id)

if s:use_nvim_float
call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth)
call nvim_win_set_config(s:last_popup_id, {'relative': 'win', 'row': l:line - 1, 'col': l:col - 1})
endif
endfunction

function! s:close_popup() abort
if s:last_timer_id
call timer_stop(s:last_timer_id)
let s:last_timer_id = v:false
endif
if s:last_popup_id >= 0
if s:use_vim_popup | call popup_close(s:last_popup_id) | endif
if s:use_nvim_float && nvim_win_is_valid(s:last_popup_id) | call nvim_win_close(s:last_popup_id, 1) | endif
Expand All @@ -77,8 +146,10 @@ function! lsp#ui#vim#documentation#setup() abort
augroup lsp_documentation_popup
autocmd!
if exists('##CompleteChanged')
autocmd CompleteChanged * call s:complete_done()
autocmd CompleteChanged * call s:complete_changed()
endif
autocmd CompleteDone * call s:close_popup()
augroup end
endfunction

" vim: et ts=4
72 changes: 38 additions & 34 deletions autoload/lsp/ui/vim/output.vim
Original file line number Diff line number Diff line change
Expand Up @@ -75,29 +75,24 @@ endfunction
function! s:get_float_positioning(height, width) abort
let l:height = a:height
let l:width = a:width
" For a start show it below/above the cursor
" TODO: add option to configure it 'docked' at the bottom/top/right
let l:y = winline()
if l:y + l:height >= winheight(0)
" Float does not fit
if l:y > l:height
" Fits above
let l:y = winline() - l:height - 1
elseif l:y - 2 > winheight(0) - l:y
" Take space above cursor
let l:y = 1
let l:height = winline()-2
else
" Take space below cursor
let l:height = winheight(0) -l:y
endif
endif
let l:col = col('.')

" NOTE: screencol() and screenrow() start from (1,1)
" but the popup window co-ordinates start from (0,0)
" Very convenient!
" For a simple single-line 'tooltip', the following
" two lines are enough to determine the position

let l:col = screencol()
let l:row = screenrow()

let l:height = min([l:height, max([&lines - &cmdheight - l:row, &previewheight])])

let l:style = 'minimal'
" Positioning is not window but screen relative
let l:opts = {
\ 'relative': 'win',
\ 'row': l:y,
\ 'relative': 'editor',
\ 'row': l:row,
\ 'col': l:col,
\ 'width': l:width,
\ 'height': l:height,
Expand All @@ -109,7 +104,6 @@ endfunction
function! lsp#ui#vim#output#floatingpreview(data) abort
if s:use_nvim_float
let l:buf = nvim_create_buf(v:false, v:true)
call setbufvar(l:buf, '&signcolumn', 'no')

" Try to get as much space around the cursor, but at least 10x10
let l:width = max([s:bufwidth(), 10])
Expand All @@ -121,16 +115,19 @@ function! lsp#ui#vim#output#floatingpreview(data) abort

let l:opts = s:get_float_positioning(l:height, l:width)

let s:winid = nvim_open_win(l:buf, v:true, l:opts)
let s:winid = nvim_open_win(l:buf, v:false, l:opts)
call nvim_win_set_option(s:winid, 'winhl', 'Normal:Pmenu,NormalNC:Pmenu')
call nvim_win_set_option(s:winid, 'foldenable', v:false)
call nvim_win_set_option(s:winid, 'wrap', v:true)
call nvim_win_set_option(s:winid, 'statusline', '')
call nvim_win_set_option(s:winid, 'number', v:false)
call nvim_win_set_option(s:winid, 'relativenumber', v:false)
call nvim_win_set_option(s:winid, 'cursorline', v:false)
call nvim_win_set_option(s:winid, 'cursorcolumn', v:false)
call nvim_win_set_option(s:winid, 'colorcolumn', '')
call nvim_win_set_option(s:winid, 'signcolumn', 'no')
" Enable closing the preview with esc, but map only in the scratch buffer
nmap <buffer><silent> <esc> :pclose<cr>
call nvim_buf_set_keymap(l:buf, 'n', '<esc>', ':pclose<cr>', {'silent': v:true})
elseif s:use_vim_popup
let l:options = {
\ 'moved': 'any',
Expand All @@ -156,11 +153,15 @@ function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort
" vim popup
call setbufline(winbufnr(a:winid), 1, a:lines)
call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')

else
" nvim floating or preview
call setline(1, a:lines)
setlocal readonly nomodifiable
silent! let &l:filetype = a:ft . '.lsp-hover'

call nvim_buf_set_lines(winbufnr(a:winid), 0, -1, v:false, a:lines)
call nvim_buf_set_option(winbufnr(a:winid), 'readonly', v:true)
call nvim_buf_set_option(winbufnr(a:winid), 'modifiable', v:false)
call nvim_buf_set_option(winbufnr(a:winid), 'filetype', a:ft.'.lsp-hover')
call nvim_win_set_cursor(a:winid, [1, 0])
endif
endfunction

Expand Down Expand Up @@ -281,21 +282,26 @@ function! s:align_preview(options) abort
endif
endfunction

function! lsp#ui#vim#output#get_size_info() abort
function! lsp#ui#vim#output#get_size_info(winid) abort
" Get size information while still having the buffer active
let l:maxwidth = max(map(getline(1, '$'), 'strdisplaywidth(v:val)'))
let l:buffer = winbufnr(a:winid)
let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)'))
if g:lsp_preview_max_width > 0
let l:bufferlines = 0
let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth])

" Determine, for each line, how many "virtual" lines it spans, and add
" these together for all lines in the buffer
for l:line in getline(1, '$')
for l:line in getbufline(l:buffer, 1, '$')
let l:num_lines = str2nr(string(ceil(strdisplaywidth(l:line) * 1.0 / g:lsp_preview_max_width)))
let l:bufferlines += max([l:num_lines, 1])
endfor
else
let l:bufferlines = line('$')
if s:use_vim_popup
let l:bufferlines = getbufinfo(l:buffer)[0].linecount
elseif s:use_nvim_float
let l:bufferlines = nvim_buf_line_count(winbufnr(a:winid))
endif
endif

return [l:bufferlines, l:maxwidth]
Expand All @@ -321,9 +327,7 @@ function! lsp#ui#vim#output#preview(server, data, options) abort
return call(g:lsp_preview_doubletap[0], [])
endif
" Close any previously opened preview window
if s:use_preview
pclose
endif
call lsp#ui#vim#output#closepreview()

let l:current_window_id = win_getid()

Expand Down Expand Up @@ -353,7 +357,7 @@ function! lsp#ui#vim#output#preview(server, data, options) abort
call setbufvar(winbufnr(s:winid), 'lsp_do_conceal', l:do_conceal)
call lsp#ui#vim#output#setcontent(s:winid, l:lines, l:ft)

let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info()
let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(s:winid)

if s:use_preview
" Set statusline
Expand All @@ -379,7 +383,7 @@ function! lsp#ui#vim#output#preview(server, data, options) abort
" Vim popups
call s:set_cursor(l:current_window_id, a:options)
endif
doautocmd <nomodeline> User lsp_float_opened
doautocmd <nomodeline> User lsp_float_opened
endif

if !g:lsp_preview_keep_focus
Expand Down
35 changes: 35 additions & 0 deletions doc/vim-lsp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ CONTENTS *vim-lsp-contents*
g:lsp_preview_doubletap |g:lsp_preview_doubletap|
g:lsp_insert_text_enabled |g:lsp_insert_text_enabled|
g:lsp_text_edit_enabled |g:lsp_text_edit_enabled|
g:lsp_documentation_debounce |g:lsp_documentation_debounce|
g:lsp_documentation_float |g:lsp_documentation_float|
g:lsp_documentation_float_docked |g:lsp_documentation_float_docked|
g:lsp_documentation_float_docked_maxheight
|g:lsp_documentation_float_docked_maxheight|
g:lsp_diagnostics_echo_cursor |g:lsp_diagnostics_echo_cursor|
g:lsp_diagnostics_echo_delay |g:lsp_diagnostics_echo_delay|
g:lsp_diagnostics_float_cursor |g:lsp_diagnostics_float_cursor|
Expand Down Expand Up @@ -367,6 +371,17 @@ g:lsp_text_edit_enabled *g:lsp_text_edit_enabled*
let g:lsp_text_edit_enabled = 1
let g:lsp_text_edit_enabled = 0

g:lsp_documentation_debounce *g:lsp_documentation_debounce*
Type: |Number|
Default: `80`

Time in milliseconds to delay the completion documentation popup. Might
help with performance. Set this to `0` to disable debouncing.

Example: >
let g:lsp_documentation_debounce = 120
let g:lsp_documentation_debounce = 0

g:lsp_documentation_float *g:lsp_documentation_float*
Type: |Number|
Default: `1`
Expand All @@ -377,6 +392,26 @@ g:lsp_documentation_float *g:lsp_documentation_float*
let g:lsp_documentation_float = 1
let g:lsp_documentation_float = 0

g:lsp_documentation_float_docked *g:lsp_documentation_float_docked*
Type: |Number|
Default: `0`

Dock the floating documentation window for complete items if enabled.

Example: >
let g:lsp_documentation_float_docked = 1
let g:lsp_documentation_float_docked = 0

g:lsp_documentation_float_docked_maxheight *g:lsp_documentation_float_docked_maxheight*
Type: |Number|
Default: `&previewheight`

The maximum height of the docked documentation window if enabled.

Example: >
let g:lsp_documentation_float_docked_maxheight = 1
let g:lsp_documentation_float_docked_maxheight = 0

g:lsp_diagnostics_echo_cursor *g:lsp_diagnostics_echo_cursor*
Type: |Number|
Default: `0`
Expand Down
Loading