Repository: autozimu/LanguageClient-neovim (language: rust)

Issue:
Add support for calling LSP completionItem/resolve implementation when completion items are selected
(I originally posted this over in [the deoplete repo](https://github.com/Shougo/deoplete.nvim) ([\#1004](https://github.com/Shougo/deoplete.nvim/issues/1004)) because I thought that might be the right place for it, but @Shougo seems to think this is best handled inside LanguageClient-neovim, so I'm creating this ticket to see if that's correct.)

> Looking at the LSP spec, I see [this](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) (emphasis added):
>
>>  If computing full completion items is expensive, servers can additionally provide a handler for the completion item resolve request (‘completionItem/resolve’). This request is sent when a completion item is selected in the user interface. **A typical use case is for example: the ‘textDocument/completion’ request doesn’t fill in the documentation property** for returned completion items since it is expensive to compute. **When the item is selected in the user interface then a ‘completionItem/resolve’ request is sent** with the selected completion item as a parameter. The returned completion item should have the documentation property filled in.
>
> To this effect, LanguageClient-neovim [implemented](https://github.com/autozimu/LanguageClient-neovim/pull/514) a `LanguageClient#completionItem_resolve()` function that can be called to get additional information about a completion. The obvious use-case here is to show info about the selection completing in a preview window (or floating window).
>
> As far as I can tell, LanguageClient-neovim sets its `LanguageClient#complete()` function as the `'omnifunc'`, and deoplete calls that to get completions. But when you move through completions, deoplete doesn't call `LanguageClient#completionItem_resolve()` and so doesn't get the extra info that an LSP server might provide. One such example server would be [typescript-language-server](https://github.com/theia-ide/typescript-language-server), which doesn't provide extended information for `textDocument/completion` requests, but does implement `completionItem/resolve`.
>
> So this is really a feature request. Could we have an option to make deoplete call an LSP `completionItem/resolve` function when completion items are selected? This means that `'completopt=preview'` would be more useful, because it would have information to show about selected completions in more cases. coc.nvim [implements this](https://github.com/neoclide/coc.nvim/blob/3e0eec9c4f08003304ba4d50bb0866dbaf7cc707/src/provider/index.ts#L40-L52), in case you wanted to check out what they're doing.

Looking into `:h CompleteChanged`, I see that support for it was added to Neovim [here](https://github.com/neovim/neovim/pull/9616) by @chemzqm (the [Coc](https://github.com/neoclide/coc.nvim/issues/441) maintainer).

It's still not clear to me exactly where the responsibility for this functionality should lie (that's why I opened the issue in deoplete; but it may be here), but the end goal is to have something like what you can see in the GIF in the Coc README:

https://user-images.githubusercontent.com/251450/55285193-400a9000-53b9-11e9-8cff-ffe4983c5947.gif

You can see how the completion menu appears, and then, asynchronously, a floating preview appears to the side with additional information about the selected completion:

<img width="1262" alt="Screen_Shot_2019-08-24_at_21_10_47_png" src="https://user-images.githubusercontent.com/7074/63641761-06158f80-c6b4-11e9-9bf4-2236179a7c6c.png">

Thoughts?


Discussion / hints:
Has anyone started working on this already?
@wincent just out of curiosity but did https://github.com/wincent/wincent/commit/8888453d9b96ca36fd441936c9209b546690184b work? :eyes: 
Yeah it works fine, although I haven't found a way to get it to show previews like this yet.
@wincent nice! how is the previews shown? 🤔 in the preview window? :eyes: 
@take I made [a short screencast on it](https://youtu.be/3jl8a5Wo2l0) a few weeks ago. Since then I tweaked a few things (mostly color related) to make it look nicer, but you can get a sense of the basics behavior from that.

Fix patch:
diff --git a/autoload/LanguageClient.vim b/autoload/LanguageClient.vim
index f208d1137..772b0fed0 100644
--- a/autoload/LanguageClient.vim
+++ b/autoload/LanguageClient.vim
@@ -11,6 +11,9 @@ let s:TYPE = {
 let s:FLOAT_WINDOW_AVAILABLE = exists('*nvim_open_win')
 let s:POPUP_WINDOW_AVAILABLE = exists('*popup_atcursor')
 
+" timers to control throttling
+let s:timers = {}
+
 function! s:AddPrefix(message) abort
     return '[LC] ' . a:message
 endfunction
@@ -426,7 +429,9 @@ endfunction
 "   - Floating window on Neovim (0.4.0 or later)
 "   - popup window on vim (8.2 or later)
 "   - Preview window on Neovim (0.3.0 or earlier) or Vim
-function! s:OpenHoverPreview(bufname, lines, filetype) abort
+"
+" Receives two optional arguments which are the X and Y position 
+function! s:OpenHoverPreview(bufname, lines, filetype, ...) abort
     " Use local variable since parameter is not modifiable
     let lines = a:lines
     let bufnr = bufnr('%')
@@ -492,8 +497,15 @@ function! s:OpenHoverPreview(bufname, lines, filetype) abort
             let col = 1
         endif
 
+        let relative = 'cursor'
+        let col = get(a:000, 0, col)
+        let row = get(a:000, 1, row)
+        if get(a:000, 0, v:null) isnot v:null && get(a:000, 1, v:null) isnot v:null
+          let relative = 'win'
+        endif
+
         let s:float_win_id = nvim_open_win(bufnr, v:true, {
-        \   'relative': 'cursor',
+        \   'relative': relative,
         \   'anchor': vert . hor,
         \   'row': row,
         \   'col': col,
@@ -507,7 +519,12 @@ function! s:OpenHoverPreview(bufname, lines, filetype) abort
         let float_win_highlight = s:GetVar('LanguageClient_floatingHoverHighlight', 'Normal:CursorLine')
         execute printf('setlocal winhl=%s', float_win_highlight)
     elseif display_approach ==# 'popup_win'
-        let pop_win_id = popup_atcursor(a:lines, {})
+        let l:padding = [1, 1, 1, 1]
+        if get(a:000, 0, v:null) isnot v:null && get(a:000, 1, v:null) isnot v:null
+          let pop_win_id = popup_create(a:lines, { 'line': get(a:000, 1) + 1, 'col': get(a:000, 0) + 1, 'padding': l:padding })
+        else
+          let pop_win_id = popup_atcursor(a:lines, { 'padding': l:padding })
+        endif
         call setbufvar(winbufnr(pop_win_id), '&filetype', a:filetype)
     elseif display_approach ==# 'preview'
         execute 'silent! noswapfile pedit!' a:bufname
@@ -525,7 +542,7 @@ function! s:OpenHoverPreview(bufname, lines, filetype) abort
 
         call setline(1, lines)
         " trigger refresh on plasticboy/vim-markdown
-        normal! i
+        doautocmd InsertLeave
         setlocal nomodified nomodifiable
 
         wincmd p
@@ -1053,7 +1070,7 @@ function! LanguageClient#completionItem_resolve(completion_item, ...) abort
                 \ 'completionItem': a:completion_item,
                 \ 'handle': s:IsFalse(l:Callback)
                 \ }
-    call extend(l:params, get(a:000, 0, {}))
+    call extend(l:params, get(a:000, 0, {})) " extend with pumpos params
     return LanguageClient#Call('completionItem/resolve', l:params, l:Callback)
 endfunction
 
@@ -1281,8 +1298,12 @@ function! LanguageClient#handleCursorMoved() abort
 endfunction
 
 function! LanguageClient#handleCompleteDone() abort
+    " close any hovers that may have been opened for example for completion
+    " item documentation.
+    call s:ClosePopups()
+
     let user_data = get(v:completed_item, 'user_data', '')
-    if user_data ==# ''
+    if len(user_data) ==# 0
         return
     endif
 
@@ -1643,4 +1664,90 @@ function! LanguageClient#debugInfo(...) abort
     return LanguageClient#Call('languageClient/debugInfo', l:params, l:Callback)
 endfunction
 
+function! s:ClosePopups(...) abort
+  if s:ShouldUseFloatWindow()
+    call s:CloseFloatingHover()
+  elseif exists('*popup_clear') && s:GetVar('LanguageClient_usePopupHover', v:true)
+    call popup_clear()
+  else
+    :pclose
+  endif
+endfunction
+
+" receives the v:event from the CompleteChanged autocmd
+function! LanguageClient#handleCompleteChanged(event) abort
+  " this timer is just to stop textlock from locking our changes
+  call timer_start(0, funcref('s:ClosePopups'))
+
+  if has_key(s:timers, 'LanguageClient#handleCompleteChanged')
+    call timer_stop(s:timers['LanguageClient#handleCompleteChanged'])
+  endif
+
+  function! Debounced(event) abort
+    let l:user_data = get(v:completed_item, 'user_data', '')
+    if len(l:user_data) ==# 0
+      return
+    endif
+
+    if type(l:user_data) ==# v:t_string
+      let l:user_data = json_decode(l:user_data)
+    endif
+
+    let l:completed_item = {}
+
+    " LCN completion items
+    if has_key(l:user_data, 'lspitem')
+      let l:completed_item = l:user_data['lspitem']
+    endif
+
+    " NCM2 completion items
+    if has_key(l:user_data, 'ncm2_lspitem')
+      let l:completed_item = l:user_data['ncm2_lspitem']
+    endif
+
+    if l:completed_item ==# {}
+      return
+    endif
+
+    if has_key(l:completed_item, 'documentation')
+      call s:ShowCompletionItemDocumentation(l:completed_item['documentation'], a:event)
+    else
+      call LanguageClient#completionItem_resolve(l:completed_item, { 'pumpos': a:event })
+    endif
+  endfunction
+
+  let s:timers['LanguageClient#handleCompleteChanged'] = timer_start(100, { -> Debounced(a:event) })
+endfunction
+
+function! s:ShowCompletionItemDocumentation(doc, completion_event) abort
+  let l:kind = 'text'
+
+  " some servers send a dictionary with kind and value whereas others just
+  " send the value
+  if type(a:doc) is s:TYPE.dict
+    let l:lines = split(a:doc['value'], "\n")
+    if has_key(a:doc, 'kind')
+      let l:kind = a:doc['kind']
+    endif
+  else
+    let l:lines = split(a:doc, "\n")
+  endif
+
+  if len(l:lines) ==# 0
+    return
+  endif
+
+  for l:line in l:lines
+    let l:line = ' ' . l:line . ' '
+  endfor
+
+  let l:pos = a:completion_event
+  if exists('*pum_getpos')
+    " favor pum_getpos output if available
+    let l:pos = pum_getpos()
+  endif
+  let l:x_pos = l:pos['width'] + l:pos['col'] + 1
+  call s:OpenHoverPreview('CompletionItemDocumentation', l:lines, l:kind, l:x_pos, l:pos['row'])
+endfunction
+
 let g:LanguageClient_loaded = s:Launch()
diff --git a/doc/LanguageClient.txt b/doc/LanguageClient.txt
index 7947afde1..66f6e39f9 100644
--- a/doc/LanguageClient.txt
+++ b/doc/LanguageClient.txt
@@ -671,6 +671,13 @@ handles its own count independently.
 
 Default: 5
 
+2.45 g:LanguageClient_showCompletionDocs  *g:LanguageClient_showCompletionDocs*
+
+Show completion item documentation in a floating window right next to pmenu.
+
+Default: 1
+Valid options: 1 | 0
+
 ==============================================================================
 3. Commands                                           *LanguageClientCommands*
 
diff --git a/plugin/LanguageClient.vim b/plugin/LanguageClient.vim
index daa446274..93a667fb9 100644
--- a/plugin/LanguageClient.vim
+++ b/plugin/LanguageClient.vim
@@ -150,6 +150,10 @@ function! LanguageClient_textDocument_switchSourceHeader(...)
     return call('LanguageClient#textDocument_switchSourceHeader', a:000)
 endfunction
 
+function! LanguageClient_showCompletionItemDocumentation(...)
+    return call('LanguageClient#showCompletionItemDocumentation', a:000)
+endfunction
+
 command! -nargs=* LanguageClientStart :call LanguageClient#startServer(<f-args>)
 command! LanguageClientStop call LanguageClient#shutdown()
 
@@ -184,12 +188,14 @@ function! s:ConfigureAutocmds()
     endif
     autocmd CursorMoved <buffer> call LanguageClient#handleCursorMoved()
     autocmd VimLeavePre <buffer> call LanguageClient#handleVimLeavePre()
-
     autocmd CompleteDone <buffer> call LanguageClient#handleCompleteDone()
     if get(g:, 'LanguageClient_signatureHelpOnCompleteDone', 0)
         autocmd CompleteDone <buffer>
                     \ call LanguageClient#textDocument_signatureHelp({}, 's:HandleOutputNothing')
     endif
+    if exists('##CompleteChanged') && get(g:, 'LanguageClient_showCompletionDocs', 1)
+      autocmd CompleteChanged <buffer> call LanguageClient#handleCompleteChanged(deepcopy(v:event))
+    endif
 
     nnoremap <Plug>(lcn-menu)               :call LanguageClient_contextMenu()<CR>
     nnoremap <Plug>(lcn-hover)              :call LanguageClient_textDocument_hover()<CR>
diff --git a/src/language_server_protocol.rs b/src/language_server_protocol.rs
index bb5d4f63a..25edf17c0 100644
--- a/src/language_server_protocol.rs
+++ b/src/language_server_protocol.rs
@@ -29,7 +29,7 @@ use lsp_types::{
     DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams,
     DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentChangeOperation, DocumentChanges,
     DocumentFormattingParams, DocumentHighlight, DocumentHighlightKind,
-    DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse,
+    DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse, Documentation,
     ExecuteCommandParams, FormattingOptions, GenericCapability, GotoCapability,
     GotoDefinitionResponse, Hover, HoverCapability, InitializeParams, InitializeResult,
     InitializedParams, Location, LogMessageParams, MarkupKind, MessageType, NumberOrString,
@@ -1763,11 +1763,25 @@ impl LanguageClient {
 
     #[tracing::instrument(level = "info", skip(self))]
     pub fn completion_item_resolve(&self, params: &Value) -> Result<Value> {
-        self.text_document_did_change(params)?;
         let filename = self.vim()?.get_filename(params)?;
         let language_id = self.vim()?.get_language_id(&filename, params)?;
+        let has_capability = self.get(|state| match state.capabilities.get(&language_id) {
+            None => false,
+            Some(result) => result
+                .capabilities
+                .completion_provider
+                .as_ref()
+                .map(|cp| cp.resolve_provider.unwrap_or_default())
+                .unwrap_or_default(),
+        })?;
+        if !has_capability {
+            return Ok(Value::Null);
+        }
+
         let completion_item: CompletionItem = try_get("completionItem", params)?
             .ok_or_else(|| anyhow!("completionItem not found in request!"))?;
+        let pumpos: Value =
+            try_get("pumpos", params)?.ok_or_else(|| anyhow!("pumpos not found in request!"))?;
 
         let result = self.get_client(&Some(language_id))?.call(
             lsp_types::request::ResolveCompletionItem::METHOD,
@@ -1778,10 +1792,16 @@ impl LanguageClient {
             return Ok(result);
         }
 
-        // TODO: proper integration.
-        let msg = format!("completionItem/resolve result not handled: {:?}", result);
-        warn!("{}", msg);
-        self.vim()?.echowarn(&msg)?;
+        let item = CompletionItem::deserialize(result)?;
+        match item.documentation {
+            None => return Ok(Value::Null),
+            Some(Documentation::String(s)) if s.is_empty() => return Ok(Value::Null),
+            Some(Documentation::MarkupContent(m)) if m.value.is_empty() => return Ok(Value::Null),
+            _ => self.vim()?.rpcclient.notify(
+                "s:ShowCompletionItemDocumentation",
+                json!([item.documentation, pumpos]),
+            )?,
+        }
 
         Ok(Value::Null)
     }


Test patch: