diff --git a/autoload/lsp/ui/vim/documentation.vim b/autoload/lsp/ui/vim/documentation.vim index 1bb944806..42fb9f960 100644 --- a/autoload/lsp/ui/vim/documentation.vim +++ b/autoload/lsp/ui/vim/documentation.vim @@ -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 @@ -17,16 +22,6 @@ 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') @@ -34,37 +29,111 @@ function! s:show_documentation(event) abort 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 @@ -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 diff --git a/autoload/lsp/ui/vim/output.vim b/autoload/lsp/ui/vim/output.vim index f1e45cd09..9ae5c7bdb 100644 --- a/autoload/lsp/ui/vim/output.vim +++ b/autoload/lsp/ui/vim/output.vim @@ -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, @@ -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]) @@ -121,7 +115,7 @@ 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) @@ -129,8 +123,11 @@ function! lsp#ui#vim#output#floatingpreview(data) abort 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 :pclose + call nvim_buf_set_keymap(l:buf, 'n', '', ':pclose', {'silent': v:true}) elseif s:use_vim_popup let l:options = { \ 'moved': 'any', @@ -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 @@ -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] @@ -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() @@ -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 @@ -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 User lsp_float_opened + doautocmd User lsp_float_opened endif if !g:lsp_preview_keep_focus diff --git a/doc/vim-lsp.txt b/doc/vim-lsp.txt index f94bdd450..6bff4dc6a 100644 --- a/doc/vim-lsp.txt +++ b/doc/vim-lsp.txt @@ -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| @@ -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` @@ -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` diff --git a/plugin/lsp.vim b/plugin/lsp.vim index b80e865a7..71a7a7d31 100644 --- a/plugin/lsp.vim +++ b/plugin/lsp.vim @@ -21,7 +21,10 @@ let g:lsp_signs_information = get(g:, 'lsp_signs_information', {}) let g:lsp_signs_hint = get(g:, 'lsp_signs_hint', {}) let g:lsp_signs_priority = get(g:, 'lsp_signs_priority', 10) let g:lsp_signs_priority_map = get(g:, 'lsp_signs_priority_map', {}) +let g:lsp_documentation_debounce = get(g:, 'lsp_documentation_debounce', 80) let g:lsp_documentation_float = get(g:, 'lsp_documentation_float', 1) +let g:lsp_documentation_float_docked = get(g:, 'lsp_documentation_float_docked', 0) +let g:lsp_documentation_float_docked_maxheight = get(g:, ':lsp_documentation_float_docked_maxheight', &previewheight) let g:lsp_diagnostics_enabled = get(g:, 'lsp_diagnostics_enabled', 1) let g:lsp_diagnostics_echo_cursor = get(g:, 'lsp_diagnostics_echo_cursor', 0) let g:lsp_diagnostics_echo_delay = get(g:, 'lsp_diagnostics_echo_delay', 500)