Skip to content

Commit 45babeb

Browse files
authored
Improved neovim floating windows, and misc. other improvements (#921)
1 parent 5947510 commit 45babeb

File tree

4 files changed

+180
-67
lines changed

4 files changed

+180
-67
lines changed

autoload/lsp/ui/vim/documentation.vim

Lines changed: 104 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ let s:use_vim_popup = has('patch-8.1.1517') && !has('nvim')
22
let s:use_nvim_float = exists('*nvim_open_win') && has('nvim')
33

44
let s:last_popup_id = -1
5+
let s:last_timer_id = v:false
56

6-
function! s:complete_done() abort
7+
function! s:complete_changed() abort
78
if !g:lsp_documentation_float | return | endif
89
" Use a timer to avoid textlock (see :h textlock).
9-
let l:event = deepcopy(v:event)
10-
call timer_start(0, {-> s:show_documentation(l:event)})
10+
let l:event = copy(v:event)
11+
if s:last_timer_id
12+
call timer_stop(s:last_timer_id)
13+
let s:last_timer_id = v:false
14+
endif
15+
let s:last_timer_id = timer_start(g:lsp_documentation_debounce, {-> s:show_documentation(l:event)})
1116
endfunction
1217

1318
function! s:show_documentation(event) abort
@@ -17,54 +22,118 @@ function! s:show_documentation(event) abort
1722
return
1823
endif
1924

20-
let l:right = wincol() < winwidth(0) / 2
21-
22-
" TODO: Neovim
23-
if l:right
24-
let l:line = a:event['row'] + 1
25-
let l:col = a:event['col'] + a:event['width'] + 1 + (a:event['scrollbar'] ? 1 : 0)
26-
else
27-
let l:line = a:event['row'] + 1
28-
let l:col = a:event['col'] - 1
29-
endif
3025

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

37-
let l:current_win_id = win_getid()
3832

39-
if s:use_vim_popup
40-
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]})
41-
elseif s:use_nvim_float
42-
let l:height = float2nr(winheight(0) - l:line + 1)
43-
let l:width = float2nr(l:right ? winwidth(0) - l:col + 1 : l:col)
44-
if l:width <= 0
45-
let l:width = 1
33+
" Neovim
34+
if s:use_nvim_float
35+
let l:event = a:event
36+
let l:event.row = float2nr(l:event.row)
37+
let l:event.col = float2nr(l:event.col)
38+
39+
let l:buffer = nvim_create_buf(v:false, v:true)
40+
let l:curpos = win_screenpos(nvim_get_current_win())[0] + winline() - 1
41+
let g:lsp_documentation_float_docked = get(g:, 'lsp_documentation_float_docked', 0)
42+
43+
if g:lsp_documentation_float_docked
44+
let g:lsp_documentation_float_docked_maxheight = get(g:, ':lsp_documentation_float_docked_maxheight', &previewheight)
45+
let l:dock_downwards = max([screenrow(), l:curpos]) < (&lines / 2)
46+
let l:height = min([len(l:data), g:lsp_documentation_float_docked_maxheight])
47+
let l:width = &columns
48+
let l:col = 0
49+
if l:dock_downwards
50+
let l:anchor = 'SW'
51+
let l:row = &lines - &cmdheight - 1
52+
let l:height = min([l:height, &lines - &cmdheight - l:event.row - l:event.height])
53+
else " dock upwards
54+
let l:anchor = 'NW'
55+
let l:row = 0
56+
let l:height = min([l:height, l:event.row - 1])
57+
endif
58+
59+
else " not docked
60+
let l:row = l:event['row']
61+
let l:height = max([&lines - &cmdheight - l:row, &previewheight])
62+
63+
let l:right_area = &columns - l:event.col - l:event.width + 1 " 1 for the padding of popup
64+
let l:left_area = l:event.col - 1
65+
let l:right = l:right_area > l:left_area
66+
if l:right
67+
let l:anchor = 'NW'
68+
let l:width = l:right_area - 1
69+
let l:col = l:event.col + l:event.width + (l:event.scrollbar ? 1 : 0)
70+
else
71+
let l:anchor = 'NE'
72+
let l:width = l:left_area
73+
let l:col = l:event.col - 1 " 1 due to padding of completion popup
74+
endif
75+
endif
76+
77+
call setbufvar(l:buffer, 'lsp_syntax_highlights', l:syntax_lines)
78+
call setbufvar(l:buffer, 'lsp_do_conceal', 1)
79+
80+
" add padding on both sides of lines containing text
81+
for l:index in range(len(l:lines))
82+
if len(l:lines[l:index]) > 0
83+
let l:lines[l:index] = ' ' . l:lines[l:index] . ' '
84+
endif
85+
endfor
86+
87+
call nvim_buf_set_lines(l:buffer, 0, -1, v:false, l:lines)
88+
call nvim_buf_set_option(l:buffer, 'readonly', v:true)
89+
call nvim_buf_set_option(l:buffer, 'modifiable', v:false)
90+
call nvim_buf_set_option(l:buffer, 'filetype', l:ft.'.lsp-hover')
91+
92+
if !g:lsp_documentation_float_docked
93+
let l:bufferlines = nvim_buf_line_count(l:buffer)
94+
let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)'))
95+
if g:lsp_preview_max_width > 0
96+
let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth])
97+
endif
98+
let l:width = min([float2nr(l:width), l:maxwidth])
99+
let l:height = min([float2nr(l:height), l:bufferlines])
46100
endif
47-
if l:height <= 0
48-
let l:height = 1
101+
if g:lsp_preview_max_height > 0
102+
let l:maxheight = g:lsp_preview_max_height
103+
let l:height = min([l:height, l:maxheight])
49104
endif
50-
let s:last_popup_id = lsp#ui#vim#output#floatingpreview([])
51-
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})
105+
106+
" Height and width must be atleast 1, otherwise error
107+
let l:height = (l:height < 1 ? 1 : l:height)
108+
let l:width = (l:width < 1 ? 1 : l:width)
109+
110+
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'})
111+
return
52112
endif
53113

114+
" Vim
115+
let l:current_win_id = win_getid()
116+
117+
let l:right = wincol() < winwidth(0) / 2
118+
if l:right
119+
let l:line = a:event['row'] + 1
120+
let l:col = a:event['col'] + a:event['width'] + 1 + (a:event['scrollbar'] ? 1 : 0)
121+
else
122+
let l:line = a:event['row'] + 1
123+
let l:col = a:event['col'] - 1
124+
endif
125+
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]})
54126
call setbufvar(winbufnr(s:last_popup_id), 'lsp_syntax_highlights', l:syntax_lines)
55127
call setbufvar(winbufnr(s:last_popup_id), 'lsp_do_conceal', 1)
56128
call lsp#ui#vim#output#setcontent(s:last_popup_id, l:lines, l:ft)
57-
let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info()
58-
59129
call win_gotoid(l:current_win_id)
60-
61-
if s:use_nvim_float
62-
call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth)
63-
call nvim_win_set_config(s:last_popup_id, {'relative': 'win', 'row': l:line - 1, 'col': l:col - 1})
64-
endif
65130
endfunction
66131

67132
function! s:close_popup() abort
133+
if s:last_timer_id
134+
call timer_stop(s:last_timer_id)
135+
let s:last_timer_id = v:false
136+
endif
68137
if s:last_popup_id >= 0
69138
if s:use_vim_popup | call popup_close(s:last_popup_id) | endif
70139
if s:use_nvim_float && nvim_win_is_valid(s:last_popup_id) | call nvim_win_close(s:last_popup_id, 1) | endif
@@ -77,8 +146,10 @@ function! lsp#ui#vim#documentation#setup() abort
77146
augroup lsp_documentation_popup
78147
autocmd!
79148
if exists('##CompleteChanged')
80-
autocmd CompleteChanged * call s:complete_done()
149+
autocmd CompleteChanged * call s:complete_changed()
81150
endif
82151
autocmd CompleteDone * call s:close_popup()
83152
augroup end
84153
endfunction
154+
155+
" vim: et ts=4

autoload/lsp/ui/vim/output.vim

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -75,29 +75,24 @@ endfunction
7575
function! s:get_float_positioning(height, width) abort
7676
let l:height = a:height
7777
let l:width = a:width
78-
" For a start show it below/above the cursor
7978
" TODO: add option to configure it 'docked' at the bottom/top/right
80-
let l:y = winline()
81-
if l:y + l:height >= winheight(0)
82-
" Float does not fit
83-
if l:y > l:height
84-
" Fits above
85-
let l:y = winline() - l:height - 1
86-
elseif l:y - 2 > winheight(0) - l:y
87-
" Take space above cursor
88-
let l:y = 1
89-
let l:height = winline()-2
90-
else
91-
" Take space below cursor
92-
let l:height = winheight(0) -l:y
93-
endif
94-
endif
95-
let l:col = col('.')
79+
80+
" NOTE: screencol() and screenrow() start from (1,1)
81+
" but the popup window co-ordinates start from (0,0)
82+
" Very convenient!
83+
" For a simple single-line 'tooltip', the following
84+
" two lines are enough to determine the position
85+
86+
let l:col = screencol()
87+
let l:row = screenrow()
88+
89+
let l:height = min([l:height, max([&lines - &cmdheight - l:row, &previewheight])])
90+
9691
let l:style = 'minimal'
9792
" Positioning is not window but screen relative
9893
let l:opts = {
99-
\ 'relative': 'win',
100-
\ 'row': l:y,
94+
\ 'relative': 'editor',
95+
\ 'row': l:row,
10196
\ 'col': l:col,
10297
\ 'width': l:width,
10398
\ 'height': l:height,
@@ -109,7 +104,6 @@ endfunction
109104
function! lsp#ui#vim#output#floatingpreview(data) abort
110105
if s:use_nvim_float
111106
let l:buf = nvim_create_buf(v:false, v:true)
112-
call setbufvar(l:buf, '&signcolumn', 'no')
113107

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

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

124-
let s:winid = nvim_open_win(l:buf, v:true, l:opts)
118+
let s:winid = nvim_open_win(l:buf, v:false, l:opts)
125119
call nvim_win_set_option(s:winid, 'winhl', 'Normal:Pmenu,NormalNC:Pmenu')
126120
call nvim_win_set_option(s:winid, 'foldenable', v:false)
127121
call nvim_win_set_option(s:winid, 'wrap', v:true)
128122
call nvim_win_set_option(s:winid, 'statusline', '')
129123
call nvim_win_set_option(s:winid, 'number', v:false)
130124
call nvim_win_set_option(s:winid, 'relativenumber', v:false)
131125
call nvim_win_set_option(s:winid, 'cursorline', v:false)
126+
call nvim_win_set_option(s:winid, 'cursorcolumn', v:false)
127+
call nvim_win_set_option(s:winid, 'colorcolumn', '')
128+
call nvim_win_set_option(s:winid, 'signcolumn', 'no')
132129
" Enable closing the preview with esc, but map only in the scratch buffer
133-
nmap <buffer><silent> <esc> :pclose<cr>
130+
call nvim_buf_set_keymap(l:buf, 'n', '<esc>', ':pclose<cr>', {'silent': v:true})
134131
elseif s:use_vim_popup
135132
let l:options = {
136133
\ 'moved': 'any',
@@ -156,11 +153,15 @@ function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort
156153
" vim popup
157154
call setbufline(winbufnr(a:winid), 1, a:lines)
158155
call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover')
156+
159157
else
160158
" nvim floating or preview
161-
call setline(1, a:lines)
162-
setlocal readonly nomodifiable
163-
silent! let &l:filetype = a:ft . '.lsp-hover'
159+
160+
call nvim_buf_set_lines(winbufnr(a:winid), 0, -1, v:false, a:lines)
161+
call nvim_buf_set_option(winbufnr(a:winid), 'readonly', v:true)
162+
call nvim_buf_set_option(winbufnr(a:winid), 'modifiable', v:false)
163+
call nvim_buf_set_option(winbufnr(a:winid), 'filetype', a:ft.'.lsp-hover')
164+
call nvim_win_set_cursor(a:winid, [1, 0])
164165
endif
165166
endfunction
166167

@@ -281,21 +282,26 @@ function! s:align_preview(options) abort
281282
endif
282283
endfunction
283284

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

291293
" Determine, for each line, how many "virtual" lines it spans, and add
292294
" these together for all lines in the buffer
293-
for l:line in getline(1, '$')
295+
for l:line in getbufline(l:buffer, 1, '$')
294296
let l:num_lines = str2nr(string(ceil(strdisplaywidth(l:line) * 1.0 / g:lsp_preview_max_width)))
295297
let l:bufferlines += max([l:num_lines, 1])
296298
endfor
297299
else
298-
let l:bufferlines = line('$')
300+
if s:use_vim_popup
301+
let l:bufferlines = getbufinfo(l:buffer)[0].linecount
302+
elseif s:use_nvim_float
303+
let l:bufferlines = nvim_buf_line_count(winbufnr(a:winid))
304+
endif
299305
endif
300306

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

328332
let l:current_window_id = win_getid()
329333

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

356-
let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info()
360+
let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(s:winid)
357361

358362
if s:use_preview
359363
" Set statusline
@@ -379,7 +383,7 @@ function! lsp#ui#vim#output#preview(server, data, options) abort
379383
" Vim popups
380384
call s:set_cursor(l:current_window_id, a:options)
381385
endif
382-
doautocmd <nomodeline> User lsp_float_opened
386+
doautocmd <nomodeline> User lsp_float_opened
383387
endif
384388

385389
if !g:lsp_preview_keep_focus

doc/vim-lsp.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ CONTENTS *vim-lsp-contents*
2020
g:lsp_preview_doubletap |g:lsp_preview_doubletap|
2121
g:lsp_insert_text_enabled |g:lsp_insert_text_enabled|
2222
g:lsp_text_edit_enabled |g:lsp_text_edit_enabled|
23+
g:lsp_documentation_debounce |g:lsp_documentation_debounce|
2324
g:lsp_documentation_float |g:lsp_documentation_float|
25+
g:lsp_documentation_float_docked |g:lsp_documentation_float_docked|
26+
g:lsp_documentation_float_docked_maxheight
27+
|g:lsp_documentation_float_docked_maxheight|
2428
g:lsp_diagnostics_echo_cursor |g:lsp_diagnostics_echo_cursor|
2529
g:lsp_diagnostics_echo_delay |g:lsp_diagnostics_echo_delay|
2630
g:lsp_diagnostics_float_cursor |g:lsp_diagnostics_float_cursor|
@@ -367,6 +371,17 @@ g:lsp_text_edit_enabled *g:lsp_text_edit_enabled*
367371
let g:lsp_text_edit_enabled = 1
368372
let g:lsp_text_edit_enabled = 0
369373
374+
g:lsp_documentation_debounce *g:lsp_documentation_debounce*
375+
Type: |Number|
376+
Default: `80`
377+
378+
Time in milliseconds to delay the completion documentation popup. Might
379+
help with performance. Set this to `0` to disable debouncing.
380+
381+
Example: >
382+
let g:lsp_documentation_debounce = 120
383+
let g:lsp_documentation_debounce = 0
384+
370385
g:lsp_documentation_float *g:lsp_documentation_float*
371386
Type: |Number|
372387
Default: `1`
@@ -377,6 +392,26 @@ g:lsp_documentation_float *g:lsp_documentation_float*
377392
let g:lsp_documentation_float = 1
378393
let g:lsp_documentation_float = 0
379394
395+
g:lsp_documentation_float_docked *g:lsp_documentation_float_docked*
396+
Type: |Number|
397+
Default: `0`
398+
399+
Dock the floating documentation window for complete items if enabled.
400+
401+
Example: >
402+
let g:lsp_documentation_float_docked = 1
403+
let g:lsp_documentation_float_docked = 0
404+
405+
g:lsp_documentation_float_docked_maxheight *g:lsp_documentation_float_docked_maxheight*
406+
Type: |Number|
407+
Default: `&previewheight`
408+
409+
The maximum height of the docked documentation window if enabled.
410+
411+
Example: >
412+
let g:lsp_documentation_float_docked_maxheight = 1
413+
let g:lsp_documentation_float_docked_maxheight = 0
414+
380415
g:lsp_diagnostics_echo_cursor *g:lsp_diagnostics_echo_cursor*
381416
Type: |Number|
382417
Default: `0`

0 commit comments

Comments
 (0)