From a10efb37a2f54653da3fb7087e4bc5ac2f1b9f8c Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 13 Sep 2019 17:18:12 +0200 Subject: [PATCH 1/9] pull in PRs needed to test this --- autoload/lsp.vim | 5 ++++ autoload/lsp/omni.vim | 6 +++- autoload/lsp/ui/vim/output.vim | 54 +++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/autoload/lsp.vim b/autoload/lsp.vim index 46ac50f04..16655c772 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -386,6 +386,9 @@ function! lsp#default_get_supported_capabilities(server_info) abort \ }, \ 'textDocument': { \ 'completion': { + \ 'completionItem': { + \ 'documentationFormat': ['plaintext'] + \ }, \ 'completionItemKind': { \ 'valueSet': lsp#omni#get_completion_item_kinds() \ } @@ -696,6 +699,8 @@ function! s:handle_initialize(server_name, data) abort call lsp#ui#vim#signature_help#setup() endif + call lsp#ui#vim#documentation#setup() + doautocmd User lsp_server_init endfunction diff --git a/autoload/lsp/omni.vim b/autoload/lsp/omni.vim index 26aa79911..e85fe5875 100644 --- a/autoload/lsp/omni.vim +++ b/autoload/lsp/omni.vim @@ -248,8 +248,12 @@ function! lsp#omni#default_get_vim_completion_item(item, ...) abort endif if has_key(a:item, 'documentation') - if type(a:item['documentation']) == type('') + if type(a:item['documentation']) == type('') " field is string let l:completion['info'] .= a:item['documentation'] + elseif type(a:item['documentation']) == type({}) && + \ has_key(a:item['documentation'], 'value') + " field is MarkupContent (hopefully 'plaintext') + let l:completion['info'] .= a:item['documentation']['value'] endif endif diff --git a/autoload/lsp/ui/vim/output.vim b/autoload/lsp/ui/vim/output.vim index 1ca200019..15b255035 100644 --- a/autoload/lsp/ui/vim/output.vim +++ b/autoload/lsp/ui/vim/output.vim @@ -143,10 +143,10 @@ function! lsp#ui#vim#output#floatingpreview(data) abort return s:winid endfunction -function! s:setcontent(lines, ft) abort +function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort if s:use_vim_popup " vim popup - call setbufline(winbufnr(s:winid), 1, a:lines) + call setbufline(winbufnr(a:winid), 1, a:lines) let l:lightline_toggle = v:false if exists('#lightline') && !has('nvim') " Lightline does not work in popups but does not recognize it yet. @@ -154,7 +154,7 @@ function! s:setcontent(lines, ft) abort let l:lightline_toggle = v:true call lightline#disable() endif - call win_execute(s:winid, 'setlocal filetype=' . a:ft . '.lsp-hover') + call win_execute(a:winid, 'setlocal filetype=' . a:ft . '.lsp-hover') if l:lightline_toggle call lightline#enable() endif @@ -167,7 +167,7 @@ function! s:setcontent(lines, ft) abort endif endfunction -function! s:adjust_float_placement(bufferlines, maxwidth) abort +function! lsp#ui#vim#output#adjust_float_placement(bufferlines, maxwidth) abort if s:use_nvim_float let l:win_config = {} let l:height = min([winheight(s:winid), a:bufferlines]) @@ -284,6 +284,26 @@ function! s:align_preview(options) abort endif endfunction +function! lsp#ui#vim#output#get_size_info() abort + " Get size information while still having the buffer active + let l:maxwidth = max(map(getline(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, '$') + 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('$') + endif + + return [l:bufferlines, l:maxwidth] +endfunction + function! lsp#ui#vim#output#preview(server, data, options) abort if s:winid && type(s:preview_data) == type(a:data) \ && s:preview_data == a:data @@ -303,7 +323,7 @@ function! lsp#ui#vim#output#preview(server, data, options) abort let s:preview_data = a:data let l:lines = [] let l:syntax_lines = [] - let l:ft = s:append(a:data, l:lines, l:syntax_lines) + let l:ft = lsp#ui#vim#output#append(a:data, l:lines, l:syntax_lines) if has_key(a:options, 'filetype') let l:ft = a:options['filetype'] @@ -318,23 +338,9 @@ function! lsp#ui#vim#output#preview(server, data, options) abort call setbufvar(winbufnr(s:winid), 'lsp_syntax_highlights', l:syntax_lines) call setbufvar(winbufnr(s:winid), 'lsp_do_conceal', l:do_conceal) - call s:setcontent(l:lines, l:ft) - - " Get size information while still having the buffer active - let l:maxwidth = max(map(getline(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]) + call lsp#ui#vim#output#setcontent(s:winid, l:lines, l:ft) - " 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, '$') - 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('$') - endif + let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info() if s:use_preview " Set statusline @@ -353,7 +359,7 @@ function! lsp#ui#vim#output#preview(server, data, options) abort if s:winid && (s:use_vim_popup || s:use_nvim_float) if s:use_nvim_float " Neovim floats - call s:adjust_float_placement(l:bufferlines, l:maxwidth) + call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth) call s:set_cursor(l:current_window_id, a:options) call s:add_float_closing_hooks() elseif s:use_vim_popup @@ -371,10 +377,10 @@ function! lsp#ui#vim#output#preview(server, data, options) abort return '' endfunction -function! s:append(data, lines, syntax_lines) abort +function! lsp#ui#vim#output#append(data, lines, syntax_lines) abort if type(a:data) == type([]) for l:entry in a:data - call s:append(entry, a:lines, a:syntax_lines) + call lsp#ui#vim#output#append(entry, a:lines, a:syntax_lines) endfor return 'markdown' From 0f18974f5fc3c40627d2cd291c2333c8e002bc90 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 13 Sep 2019 17:48:40 +0200 Subject: [PATCH 2/9] refactor to avoid entering neovim float --- autoload/lsp/ui/vim/output.vim | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/autoload/lsp/ui/vim/output.vim b/autoload/lsp/ui/vim/output.vim index 15b255035..f0cfe735c 100644 --- a/autoload/lsp/ui/vim/output.vim +++ b/autoload/lsp/ui/vim/output.vim @@ -112,15 +112,8 @@ function! lsp#ui#vim#output#floatingpreview(data) abort endif let l:opts = s:get_float_positioning(l:height, l:width) - - let s:winid = nvim_open_win(buf, v:true, 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) + let l:opts['style'] = 'minimal' + let s:winid = nvim_open_win(buf, v:false, l:opts) " Enable closing the preview with esc, but map only in the scratch buffer nmap :pclose elseif s:use_vim_popup @@ -144,9 +137,9 @@ function! lsp#ui#vim#output#floatingpreview(data) abort endfunction function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort - if s:use_vim_popup - " vim popup - call setbufline(winbufnr(a:winid), 1, a:lines) + if s:use_vim_popup || s:use_nvim_float + let l:buf = winbufnr(a:winid) + call setbufline(l:buf, 1, a:lines) let l:lightline_toggle = v:false if exists('#lightline') && !has('nvim') " Lightline does not work in popups but does not recognize it yet. @@ -154,12 +147,23 @@ function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort let l:lightline_toggle = v:true call lightline#disable() endif - call win_execute(a:winid, 'setlocal filetype=' . a:ft . '.lsp-hover') + + let l:filetype = a:ft . '.lsp-hover' + if s:use_vim_popup + call win_execute(a:winid, 'setlocal filetype=' . l:filetype) + elseif s:use_nvim_float + let l:height = len(a:lines) + let l:width = max(map(a:lines, 'strdisplaywidth(v:val)')) + let l:opts = s:get_float_positioning(l:height, l:width) + call nvim_win_set_config(a:winid, l:opts) + call nvim_buf_set_option(l:buf, 'filetype', l:filetype) + endif + if l:lightline_toggle call lightline#enable() endif else - " nvim floating or preview + " preview call setline(1, a:lines) setlocal readonly nomodifiable From 45a3b4e6214a9d1475ec5c413e6b54caac633052 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 13 Sep 2019 18:24:49 +0200 Subject: [PATCH 3/9] add missing documentation.vim (prev PR) --- autoload/lsp/ui/vim/documentation.vim | 75 +++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 autoload/lsp/ui/vim/documentation.vim diff --git a/autoload/lsp/ui/vim/documentation.vim b/autoload/lsp/ui/vim/documentation.vim new file mode 100644 index 000000000..1bef56409 --- /dev/null +++ b/autoload/lsp/ui/vim/documentation.vim @@ -0,0 +1,75 @@ +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 + +function! s:complete_done() abort + " Use a timer to avoid textlock (see :h textlock). + let l:event = deepcopy(v:event) + call timer_start(0, {-> s:show_documentation(l:event)}) +endfunction + +function! s:show_documentation(event) abort + call s:close_popup() + + if !has_key(a:event['completed_item'], 'info') || empty(a:event['completed_item']['info']) + 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 = winheight(0) - l:line + 1 + let l:width = l:right ? winwidth(0) - l:col + 1 : l:col + 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}) + endif + + 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_popup_id >= 0 + if s:use_vim_popup | call popup_close(s:last_popup_id) | endif + if s:use_nvim_float | call nvim_win_close(s:last_popup_id, 1) | endif + + let s:last_popup_id = -1 + endif +endfunction + +function! lsp#ui#vim#documentation#setup() abort + augroup lsp_documentation_popup + autocmd! + autocmd CompleteChanged * call s:complete_done() + autocmd CompleteDone * call s:close_popup() + augroup end +endfunction From c2c5144183a81c675f00d4dde22c3e2034ab0b4c Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 13 Sep 2019 19:10:16 +0200 Subject: [PATCH 4/9] refactor get_size_info --- autoload/lsp/ui/vim/documentation.vim | 2 +- autoload/lsp/ui/vim/output.vim | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/autoload/lsp/ui/vim/documentation.vim b/autoload/lsp/ui/vim/documentation.vim index 1bef56409..0525b9dde 100644 --- a/autoload/lsp/ui/vim/documentation.vim +++ b/autoload/lsp/ui/vim/documentation.vim @@ -47,7 +47,7 @@ function! s:show_documentation(event) abort 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() + let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(getline(1, '$')) call win_gotoid(l:current_win_id) diff --git a/autoload/lsp/ui/vim/output.vim b/autoload/lsp/ui/vim/output.vim index f0cfe735c..e1db7cb91 100644 --- a/autoload/lsp/ui/vim/output.vim +++ b/autoload/lsp/ui/vim/output.vim @@ -288,16 +288,16 @@ 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(lines) abort " Get size information while still having the buffer active - let l:maxwidth = max(map(getline(1, '$'), 'strdisplaywidth(v:val)')) + let l:maxwidth = max(map(a:lines, '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 a:lines 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 @@ -344,7 +344,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(getlines(1, '$')) if s:use_preview " Set statusline From cc4d0b75ecef8be75810233dc654f8906e7224f7 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 13 Sep 2019 19:25:52 +0200 Subject: [PATCH 5/9] typo --- autoload/lsp/ui/vim/output.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/lsp/ui/vim/output.vim b/autoload/lsp/ui/vim/output.vim index e1db7cb91..2b107cca0 100644 --- a/autoload/lsp/ui/vim/output.vim +++ b/autoload/lsp/ui/vim/output.vim @@ -344,7 +344,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(getlines(1, '$')) + let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(getline(1, '$')) if s:use_preview " Set statusline From 22c85edc226f344a2f1e87c5e58cf8a207e5b52b Mon Sep 17 00:00:00 2001 From: Thomas Faingnaert Date: Fri, 13 Sep 2019 23:30:16 +0200 Subject: [PATCH 6/9] Initial version float-api --- doc/float-api.txt | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 doc/float-api.txt diff --git a/doc/float-api.txt b/doc/float-api.txt new file mode 100644 index 000000000..681138146 --- /dev/null +++ b/doc/float-api.txt @@ -0,0 +1,123 @@ +============================================================================== +LSP-SPECIFIC API *lsp-specific-api* + +parse_lsp_response({data}) + + Parses a LSP response (be it a String, MarkedString, Markdown segment, + MarkupContent, ...; from hereon just {data}) to: + + - A |list| of |String|s that represent the "stripped" content. E.g. + markdown `*bold*` strings will be converted to just `bold`, code + blocks will have the markers removed, etc. It's the "plain-text, no + colour" version of the {data}. + + - A |list| of |Dicts| that add colour to the plaintext. Each entry + essentially says: highlight this character range using this Vim + syntax group. Can be used for displaying stripped `*bold*` in a bold + font, or to apply syntax highlighting to a region. + + Example of an entry: + { + 'range': { + 'start': { + 'line': 5, + 'character': 10 + }, + 'end': { + 'line': 5, + 'character': 15 + } + }, + 'group': 'markdownBold' + } + + +============================================================================== +TOP-LEVEL API *top-level-api* + +All the following functions take the same initial arguments: + + - {lines}: A |list| of |String|s that represents the plain-text content of + the popup. + - {syn-ranges}: A |list| of |dict|s, each representing a syntax highlight + to be applied to the popup. See parse_lsp_response for more info. + - {filetype}: The |'filetype'| of the popup buffer. This determines the + syntax highlighting of the entire buffer, which ftplugin is used, and so + on. + + +show_cursor_tooltip({lines}, {syn-ranges}, {filetype}, {options}) + + Shows a tooltip at the current cursor position, either above or below, + depending on where there is enough space. + + The preview's content is set depending on {lines}, {syn-ranges} and + {filetype}, see above. + + Returns the window ID of the created popup. + + {options} is a |dict| that can contain the following keys: + TODO + + +show_pum_tooltip({lines}, {syn-ranges}, {filetype}, {options}) + + Shows a tooltip associated with the currently selected item in the popup + menu. It can be either to the left/right of the item, depending on where + there is enough space. Settings from |'completepopup'| is taken into + account. + + The preview's content is set depending on {lines}, {syn-ranges} and + {filetype}, see above. + + Returns the window ID of the created popup. + + {options} is a |dict| that can contain the following keys: + TODO + + +update_pum_tooltip({winid}, {lines}, {syn-ranges}, {filetype}, {options}) + + Changes the content of the tooltip associated with the currently selected + item in the popup menu. {winid} must be the value returned by + `show_pum_tooltip`. + + For the meaning of {options}, see `show_pum_tooltip`. + +============================================================================== +INTERMEDIATE-LEVEL API *intermediate-level-api* + +These functions will be backend specific (Vim popup/Nvim float). + +create_tooltip() + + Creates a hidden floating window, with undefined position, and an empty + buffer associated with it. + + Returns the created window ID. + + +set_tooltip_contents({winid}, {lines}) + + Updates the contents of a given floating window, and unhides it. Also + retriggers size calculation. + + +set_tooltip_position({winid}, {options}) + + Sets the position of the given floating window, be it cursor-relative, + pum-relative, etc. Also retriggers size calculation. + + TODO: Which {options}? + + +close_tooltip({winid}) + + Closes the tooltip with the ID {winid}. + +============================================================================== +LOW-LEVEL API *low-level-api* + +To be determined. + +vim:tw=78:ts=4:ft=help:et From 198755de4669c3da62fe4141140a93935fcd5da2 Mon Sep 17 00:00:00 2001 From: Thomas Faingnaert Date: Fri, 13 Sep 2019 23:35:33 +0200 Subject: [PATCH 7/9] Add possible options --- doc/float-api.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/float-api.txt b/doc/float-api.txt index 681138146..7a020b8fe 100644 --- a/doc/float-api.txt +++ b/doc/float-api.txt @@ -57,7 +57,8 @@ show_cursor_tooltip({lines}, {syn-ranges}, {filetype}, {options}) Returns the window ID of the created popup. {options} is a |dict| that can contain the following keys: - TODO + TODO: possibly add some options or maxwidth, close_on_cursor_move, + firstline, cursor_pos, cursor_alignment show_pum_tooltip({lines}, {syn-ranges}, {filetype}, {options}) From 9603f3ad62b9fa54a559b6c89e0febdb0bbb2d84 Mon Sep 17 00:00:00 2001 From: Thomas Faingnaert Date: Fri, 13 Sep 2019 23:36:58 +0200 Subject: [PATCH 8/9] Add comment options pum --- doc/float-api.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/float-api.txt b/doc/float-api.txt index 7a020b8fe..e7e9719e6 100644 --- a/doc/float-api.txt +++ b/doc/float-api.txt @@ -74,7 +74,8 @@ show_pum_tooltip({lines}, {syn-ranges}, {filetype}, {options}) Returns the window ID of the created popup. {options} is a |dict| that can contain the following keys: - TODO + TODO: might not make sense to have options here, because everything is in + 'completepopup' already? update_pum_tooltip({winid}, {lines}, {syn-ranges}, {filetype}, {options}) From 1a2c128e7a94fd5bcba88bb453cca2e7e93ca755 Mon Sep 17 00:00:00 2001 From: Thomas Faingnaert Date: Fri, 13 Sep 2019 23:53:55 +0200 Subject: [PATCH 9/9] Document empty syn-ranges --- doc/float-api.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/float-api.txt b/doc/float-api.txt index e7e9719e6..d2b5afe80 100644 --- a/doc/float-api.txt +++ b/doc/float-api.txt @@ -41,6 +41,8 @@ All the following functions take the same initial arguments: the popup. - {syn-ranges}: A |list| of |dict|s, each representing a syntax highlight to be applied to the popup. See parse_lsp_response for more info. + You can pass an empty |list| `[]` if you don't want any additional + syntax highlighting (of course, the syntax of {filetype} still applies). - {filetype}: The |'filetype'| of the popup buffer. This determines the syntax highlighting of the entire buffer, which ftplugin is used, and so on.