Skip to content

Commit f74b96c

Browse files
authored
Show completion item documentation on selection change (#1043)
1 parent 1f9f5b3 commit f74b96c

File tree

4 files changed

+153
-13
lines changed

4 files changed

+153
-13
lines changed

autoload/LanguageClient.vim

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ let s:TYPE = {
1111
let s:FLOAT_WINDOW_AVAILABLE = exists('*nvim_open_win')
1212
let s:POPUP_WINDOW_AVAILABLE = exists('*popup_atcursor')
1313

14+
" timers to control throttling
15+
let s:timers = {}
16+
1417
function! s:AddPrefix(message) abort
1518
return '[LC] ' . a:message
1619
endfunction
@@ -426,7 +429,9 @@ endfunction
426429
" - Floating window on Neovim (0.4.0 or later)
427430
" - popup window on vim (8.2 or later)
428431
" - Preview window on Neovim (0.3.0 or earlier) or Vim
429-
function! s:OpenHoverPreview(bufname, lines, filetype) abort
432+
"
433+
" Receives two optional arguments which are the X and Y position
434+
function! s:OpenHoverPreview(bufname, lines, filetype, ...) abort
430435
" Use local variable since parameter is not modifiable
431436
let lines = a:lines
432437
let bufnr = bufnr('%')
@@ -492,8 +497,15 @@ function! s:OpenHoverPreview(bufname, lines, filetype) abort
492497
let col = 1
493498
endif
494499

500+
let relative = 'cursor'
501+
let col = get(a:000, 0, col)
502+
let row = get(a:000, 1, row)
503+
if get(a:000, 0, v:null) isnot v:null && get(a:000, 1, v:null) isnot v:null
504+
let relative = 'win'
505+
endif
506+
495507
let s:float_win_id = nvim_open_win(bufnr, v:true, {
496-
\ 'relative': 'cursor',
508+
\ 'relative': relative,
497509
\ 'anchor': vert . hor,
498510
\ 'row': row,
499511
\ 'col': col,
@@ -507,7 +519,12 @@ function! s:OpenHoverPreview(bufname, lines, filetype) abort
507519
let float_win_highlight = s:GetVar('LanguageClient_floatingHoverHighlight', 'Normal:CursorLine')
508520
execute printf('setlocal winhl=%s', float_win_highlight)
509521
elseif display_approach ==# 'popup_win'
510-
let pop_win_id = popup_atcursor(a:lines, {})
522+
let l:padding = [1, 1, 1, 1]
523+
if get(a:000, 0, v:null) isnot v:null && get(a:000, 1, v:null) isnot v:null
524+
let pop_win_id = popup_create(a:lines, { 'line': get(a:000, 1) + 1, 'col': get(a:000, 0) + 1, 'padding': l:padding })
525+
else
526+
let pop_win_id = popup_atcursor(a:lines, { 'padding': l:padding })
527+
endif
511528
call setbufvar(winbufnr(pop_win_id), '&filetype', a:filetype)
512529
elseif display_approach ==# 'preview'
513530
execute 'silent! noswapfile pedit!' a:bufname
@@ -525,7 +542,7 @@ function! s:OpenHoverPreview(bufname, lines, filetype) abort
525542

526543
call setline(1, lines)
527544
" trigger refresh on plasticboy/vim-markdown
528-
normal! i
545+
doautocmd InsertLeave
529546
setlocal nomodified nomodifiable
530547

531548
wincmd p
@@ -1053,7 +1070,7 @@ function! LanguageClient#completionItem_resolve(completion_item, ...) abort
10531070
\ 'completionItem': a:completion_item,
10541071
\ 'handle': s:IsFalse(l:Callback)
10551072
\ }
1056-
call extend(l:params, get(a:000, 0, {}))
1073+
call extend(l:params, get(a:000, 0, {})) " extend with pumpos params
10571074
return LanguageClient#Call('completionItem/resolve', l:params, l:Callback)
10581075
endfunction
10591076

@@ -1281,8 +1298,12 @@ function! LanguageClient#handleCursorMoved() abort
12811298
endfunction
12821299

12831300
function! LanguageClient#handleCompleteDone() abort
1301+
" close any hovers that may have been opened for example for completion
1302+
" item documentation.
1303+
call s:ClosePopups()
1304+
12841305
let user_data = get(v:completed_item, 'user_data', '')
1285-
if user_data ==# ''
1306+
if len(user_data) ==# 0
12861307
return
12871308
endif
12881309

@@ -1643,4 +1664,90 @@ function! LanguageClient#debugInfo(...) abort
16431664
return LanguageClient#Call('languageClient/debugInfo', l:params, l:Callback)
16441665
endfunction
16451666

1667+
function! s:ClosePopups(...) abort
1668+
if s:ShouldUseFloatWindow()
1669+
call s:CloseFloatingHover()
1670+
elseif exists('*popup_clear') && s:GetVar('LanguageClient_usePopupHover', v:true)
1671+
call popup_clear()
1672+
else
1673+
:pclose
1674+
endif
1675+
endfunction
1676+
1677+
" receives the v:event from the CompleteChanged autocmd
1678+
function! LanguageClient#handleCompleteChanged(event) abort
1679+
" this timer is just to stop textlock from locking our changes
1680+
call timer_start(0, funcref('s:ClosePopups'))
1681+
1682+
if has_key(s:timers, 'LanguageClient#handleCompleteChanged')
1683+
call timer_stop(s:timers['LanguageClient#handleCompleteChanged'])
1684+
endif
1685+
1686+
function! Debounced(event) abort
1687+
let l:user_data = get(v:completed_item, 'user_data', '')
1688+
if len(l:user_data) ==# 0
1689+
return
1690+
endif
1691+
1692+
if type(l:user_data) ==# v:t_string
1693+
let l:user_data = json_decode(l:user_data)
1694+
endif
1695+
1696+
let l:completed_item = {}
1697+
1698+
" LCN completion items
1699+
if has_key(l:user_data, 'lspitem')
1700+
let l:completed_item = l:user_data['lspitem']
1701+
endif
1702+
1703+
" NCM2 completion items
1704+
if has_key(l:user_data, 'ncm2_lspitem')
1705+
let l:completed_item = l:user_data['ncm2_lspitem']
1706+
endif
1707+
1708+
if l:completed_item ==# {}
1709+
return
1710+
endif
1711+
1712+
if has_key(l:completed_item, 'documentation')
1713+
call s:ShowCompletionItemDocumentation(l:completed_item['documentation'], a:event)
1714+
else
1715+
call LanguageClient#completionItem_resolve(l:completed_item, { 'pumpos': a:event })
1716+
endif
1717+
endfunction
1718+
1719+
let s:timers['LanguageClient#handleCompleteChanged'] = timer_start(100, { -> Debounced(a:event) })
1720+
endfunction
1721+
1722+
function! s:ShowCompletionItemDocumentation(doc, completion_event) abort
1723+
let l:kind = 'text'
1724+
1725+
" some servers send a dictionary with kind and value whereas others just
1726+
" send the value
1727+
if type(a:doc) is s:TYPE.dict
1728+
let l:lines = split(a:doc['value'], "\n")
1729+
if has_key(a:doc, 'kind')
1730+
let l:kind = a:doc['kind']
1731+
endif
1732+
else
1733+
let l:lines = split(a:doc, "\n")
1734+
endif
1735+
1736+
if len(l:lines) ==# 0
1737+
return
1738+
endif
1739+
1740+
for l:line in l:lines
1741+
let l:line = ' ' . l:line . ' '
1742+
endfor
1743+
1744+
let l:pos = a:completion_event
1745+
if exists('*pum_getpos')
1746+
" favor pum_getpos output if available
1747+
let l:pos = pum_getpos()
1748+
endif
1749+
let l:x_pos = l:pos['width'] + l:pos['col'] + 1
1750+
call s:OpenHoverPreview('CompletionItemDocumentation', l:lines, l:kind, l:x_pos, l:pos['row'])
1751+
endfunction
1752+
16461753
let g:LanguageClient_loaded = s:Launch()

doc/LanguageClient.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,13 @@ handles its own count independently.
671671

672672
Default: 5
673673

674+
2.45 g:LanguageClient_showCompletionDocs *g:LanguageClient_showCompletionDocs*
675+
676+
Show completion item documentation in a floating window right next to pmenu.
677+
678+
Default: 1
679+
Valid options: 1 | 0
680+
674681
==============================================================================
675682
3. Commands *LanguageClientCommands*
676683

plugin/LanguageClient.vim

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ function! LanguageClient_textDocument_switchSourceHeader(...)
150150
return call('LanguageClient#textDocument_switchSourceHeader', a:000)
151151
endfunction
152152

153+
function! LanguageClient_showCompletionItemDocumentation(...)
154+
return call('LanguageClient#showCompletionItemDocumentation', a:000)
155+
endfunction
156+
153157
command! -nargs=* LanguageClientStart :call LanguageClient#startServer(<f-args>)
154158
command! LanguageClientStop call LanguageClient#shutdown()
155159

@@ -184,12 +188,14 @@ function! s:ConfigureAutocmds()
184188
endif
185189
autocmd CursorMoved <buffer> call LanguageClient#handleCursorMoved()
186190
autocmd VimLeavePre <buffer> call LanguageClient#handleVimLeavePre()
187-
188191
autocmd CompleteDone <buffer> call LanguageClient#handleCompleteDone()
189192
if get(g:, 'LanguageClient_signatureHelpOnCompleteDone', 0)
190193
autocmd CompleteDone <buffer>
191194
\ call LanguageClient#textDocument_signatureHelp({}, 's:HandleOutputNothing')
192195
endif
196+
if exists('##CompleteChanged') && get(g:, 'LanguageClient_showCompletionDocs', 1)
197+
autocmd CompleteChanged <buffer> call LanguageClient#handleCompleteChanged(deepcopy(v:event))
198+
endif
193199

194200
nnoremap <Plug>(lcn-menu) :call LanguageClient_contextMenu()<CR>
195201
nnoremap <Plug>(lcn-hover) :call LanguageClient_textDocument_hover()<CR>

src/language_server_protocol.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use lsp_types::{
2929
DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams,
3030
DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentChangeOperation, DocumentChanges,
3131
DocumentFormattingParams, DocumentHighlight, DocumentHighlightKind,
32-
DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse,
32+
DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse, Documentation,
3333
ExecuteCommandParams, FormattingOptions, GenericCapability, GotoCapability,
3434
GotoDefinitionResponse, Hover, HoverCapability, InitializeParams, InitializeResult,
3535
InitializedParams, Location, LogMessageParams, MarkupKind, MessageType, NumberOrString,
@@ -1763,11 +1763,25 @@ impl LanguageClient {
17631763

17641764
#[tracing::instrument(level = "info", skip(self))]
17651765
pub fn completion_item_resolve(&self, params: &Value) -> Result<Value> {
1766-
self.text_document_did_change(params)?;
17671766
let filename = self.vim()?.get_filename(params)?;
17681767
let language_id = self.vim()?.get_language_id(&filename, params)?;
1768+
let has_capability = self.get(|state| match state.capabilities.get(&language_id) {
1769+
None => false,
1770+
Some(result) => result
1771+
.capabilities
1772+
.completion_provider
1773+
.as_ref()
1774+
.map(|cp| cp.resolve_provider.unwrap_or_default())
1775+
.unwrap_or_default(),
1776+
})?;
1777+
if !has_capability {
1778+
return Ok(Value::Null);
1779+
}
1780+
17691781
let completion_item: CompletionItem = try_get("completionItem", params)?
17701782
.ok_or_else(|| anyhow!("completionItem not found in request!"))?;
1783+
let pumpos: Value =
1784+
try_get("pumpos", params)?.ok_or_else(|| anyhow!("pumpos not found in request!"))?;
17711785

17721786
let result = self.get_client(&Some(language_id))?.call(
17731787
lsp_types::request::ResolveCompletionItem::METHOD,
@@ -1778,10 +1792,16 @@ impl LanguageClient {
17781792
return Ok(result);
17791793
}
17801794

1781-
// TODO: proper integration.
1782-
let msg = format!("completionItem/resolve result not handled: {:?}", result);
1783-
warn!("{}", msg);
1784-
self.vim()?.echowarn(&msg)?;
1795+
let item = CompletionItem::deserialize(result)?;
1796+
match item.documentation {
1797+
None => return Ok(Value::Null),
1798+
Some(Documentation::String(s)) if s.is_empty() => return Ok(Value::Null),
1799+
Some(Documentation::MarkupContent(m)) if m.value.is_empty() => return Ok(Value::Null),
1800+
_ => self.vim()?.rpcclient.notify(
1801+
"s:ShowCompletionItemDocumentation",
1802+
json!([item.documentation, pumpos]),
1803+
)?,
1804+
}
17851805

17861806
Ok(Value::Null)
17871807
}

0 commit comments

Comments
 (0)