From c1099e3c358af8efc920b44ab8284e8960bfd105 Mon Sep 17 00:00:00 2001 From: Billie Cleek Date: Sun, 9 Mar 2025 10:53:10 -0700 Subject: [PATCH] lsp: use FullDocumentation hover response Use the FullDocumentation hover response instead of Structured, because the latter is deprecated and will be removed soon. * Parse FullDocumentation format for use in a hover message, following the same basic format was was previously provided by gopls in its Structured format. * Refactor handlers that relied on the hover response to expect the structured output from the new function that provides it. * Refactor functions that expected documentation links to first rely on a new request, documentLink via go#lsp#DocLink, and then falling back to trusty `go doc` to get what's needed. --- autoload/go/doc.vim | 4 +- autoload/go/lsp.vim | 122 ++++++++++++++++++++++++++++-------- autoload/go/lsp/message.vim | 20 +++++- 3 files changed, 118 insertions(+), 28 deletions(-) diff --git a/autoload/go/doc.vim b/autoload/go/doc.vim index 69542abe54..7ef7388551 100644 --- a/autoload/go/doc.vim +++ b/autoload/go/doc.vim @@ -25,9 +25,9 @@ function! s:docURL(...) abort " will strip any version information from the URL. let [l:out, l:err] = go#lsp#DocLink() if !(l:err || len(l:out) is 0) - let l:url = printf('%s/%s', go#config#DocUrl(), l:out) + let l:url = l:out else - let l:url = '' + let l:url = call('s:docURLFor', a:000) endif else let l:url = call('s:docURLFor', a:000) diff --git a/autoload/go/lsp.vim b/autoload/go/lsp.vim index 15f08cc408..9662dbae34 100644 --- a/autoload/go/lsp.vim +++ b/autoload/go/lsp.vim @@ -1004,21 +1004,22 @@ function! s:hoverHandler(next, diagnostic, msg) abort dict endif try - let l:value = json_decode(a:msg.contents.value) - let l:msg = [] if len(a:diagnostic) > 0 - let l:msg = split(a:diagnostic, "\n") + let l:msg = split(a:diagnostic, '\n') let l:msg = add(l:msg, '') endif - let l:signature = split(l:value.signature, "\n") - let l:msg = extend(l:msg, l:signature) + + let l:value = s:structuredHoverResult(a:msg.contents) + + let l:msg = extend(l:msg, l:value.signature) if go#config#DocBalloon() " use synopsis instead of fullDocumentation to keep the hover window " small. let l:doc = l:value.synopsis if len(l:doc) isnot 0 - let l:msg = extend(l:msg, ['', l:doc]) + let l:msg = add(l:msg, '') + let l:msg = extend(l:msg, l:doc) endif endif @@ -1065,15 +1066,88 @@ function! s:docFromHoverResult(msg) abort dict return ['Undocumented', 0] endif - let l:value = json_decode(a:msg.contents.value) - let l:doc = l:value.fullDocumentation + let l:value = s:structuredHoverResult(a:msg.contents) + let l:doc = join(l:value.fullDocumentation, "\n") if len(l:doc) is 0 let l:doc = 'Undocumented' endif - let l:content = printf("%s\n\n%s", l:value.signature, l:doc) + let l:content = printf("%s\n\n%s", join(l:value.signature, "\n"), l:doc) return [l:content, 0] endfunction +function! s:structuredHoverResult(contents) abort + let l:value = { + \ 'fullDocumentation': [], + \ 'synopsis': '', + \ 'signature': [], + \ 'singleLine': '', + \ 'symbolName': '', + \ 'linkPath': '', + \ 'linkAnchor': '' + \ } + + if !has_key(a:contents, 'value') + return l:value + endif + + let l:contents = a:contents.value + + " The signature is either a multiline identifier (e.g. a struct or an + " interface and will therefore end with a } on a line by itself or it's a + " single line identifier. Check for the former first and fallback to the + " latter. + let l:parts = split(l:contents, '\n}\zs\n') + if len(l:parts) is 1 + let l:parts = split(l:contents, '\n') + else + let l:parts = extend([l:parts[0]], split(l:parts[1], '\n')) + endif + + " The first part will have been the signature. If it was a multiline + " signature, then it was split on '\n}\zs\n' and needs to be split again + " on '\n'. If it was a single line signature, then there's no harm in + " splitting on '\n' again. Either way, a list is returned from split. + let l:value.signature = split(l:parts[0], '\n') + + if len(l:value.signature) is 1 + let l:value.singleLine = s:prepareSingleLineFromSignature(0, l:value.signature[0]) + else + let l:value.singleLine = join(map(copy(l:value.signature), function('s:prepareSingleLineFromSignature')), '') + endif + if l:value.singleLine[-1:] is ';' + let l:value.singleLine = l:value.singleLine[0:-2] + endif + + let l:fullDocumentation = l:parts[1:] + let l:value.fullDocumentation = l:fullDocumentation + + let l:idx= 0 + for l:line in l:fullDocumentation + if l:line is '' + break + end + let l:idx += 1 + endfor + + let l:value.synopsis = l:fullDocumentation[0:(l:idx)] + + return l:value +endfunction + +function s:prepareSingleLineFromSignature(key, val) abort + let l:line = a:val + " remove comments + let l:line = substitute(l:line, '\s\+//.*','','g') + " end field lines that aren't the beginning of a struct with a semicolon + if l:line !~ '^\t\+{$' + let l:line = substitute(l:line, '$',';','') + endif + " replace leading tabs with a single space on field lines + let l:line = substitute(l:line,'^\t\+\ze[{}]',' ','g') + + return l:line +endfunction + function! go#lsp#DocLink() abort let l:fname = expand('%:p') let [l:line, l:col] = go#lsp#lsp#Position() @@ -1081,35 +1155,33 @@ function! go#lsp#DocLink() abort call go#lsp#DidChange(l:fname) let l:lsp = s:lspfactory.get() - let l:msg = go#lsp#message#Hover(l:fname, l:line, l:col) + let l:msg = go#lsp#message#DocLink(l:fname) let l:state = s:newHandlerState('doc url') - let l:resultHandler = go#promise#New(function('s:docLinkFromHoverResult', [], l:state), 10000, '') + let l:resultHandler = go#promise#New(function('s:handleDocLink', [l:line, l:col], l:state), 10000, '') let l:state.handleResult = l:resultHandler.wrapper let l:state.error = l:resultHandler.wrapper call l:lsp.sendMessage(l:msg, l:state) return l:resultHandler.await() endfunction -function! s:docLinkFromHoverResult(msg) abort dict +function! s:handleDocLink(line, character, msg) abort dict if type(a:msg) is type('') return [a:msg, 1] endif - if a:msg is v:null || !has_key(a:msg, 'contents') + if a:msg is v:null || (type(a:msg) is type([]) && len(a:msg) == 0) return ['', 0] endif - let l:doc = json_decode(a:msg.contents.value) - " for backward compatibility with older gopls - if has_key(l:doc, 'link') - let l:link = l:doc.link - return [l:doc.link, 0] - endif + let l:link = '' + for l:item in a:msg + if !s:within(l:item.range, a:line, a:character) + continue + endif + let l:link = l:item.target + break + endfor - if !has_key(l:doc, 'linkPath') || empty(l:doc.linkPath) - return ['', 0] - endif - let l:link = l:doc.linkPath . "#" . l:doc.linkAnchor return [l:link, 0] endfunction @@ -1183,7 +1255,7 @@ function! s:info(show, msg) abort dict return endif - let l:value = json_decode(a:msg.contents.value) + let l:value = s:structuredHoverResult(a:msg.contents) let l:content = [l:value.singleLine] let l:content = s:infoFromHoverContent(l:content) @@ -1678,7 +1750,7 @@ function! go#lsp#FillStruct() abort endfunction " Extract executes the refactor.extract code action for the current buffer -" and configures the handler to only apply the fillstruct command for the +" and configures the handler to only apply the extract_function command for the " current location. function! go#lsp#Extract(line1, line2) abort let l:fname = expand('%:p') diff --git a/autoload/go/lsp/message.vim b/autoload/go/lsp/message.vim index aae6623a82..04d7810aa8 100644 --- a/autoload/go/lsp/message.vim +++ b/autoload/go/lsp/message.vim @@ -269,6 +269,15 @@ function! go#lsp#message#Hover(file, line, col) abort \ } endfunction +function! go#lsp#message#DocLink(file) abort + let l:params = s:textDocumentParams(a:file) + return { + \ 'notification': 0, + \ 'method': 'textDocument/documentLink', + \ 'params': l:params, + \ } +endfunction + function! go#lsp#message#Rename(file, line, col, newName) abort let l:params = s:textDocumentPositionParams(a:file, a:line, a:col) let l:params.newName = a:newName @@ -304,7 +313,8 @@ function! go#lsp#message#ConfigurationResult(items) abort let l:workspace = go#path#FromURI(l:item.scopeUri) let l:config = { \ 'buildFlags': [], - \ 'hoverKind': 'Structured', + \ 'hoverKind': 'FullDocumentation', + \ 'linkTarget': substitute(go#config#DocUrl(), 'https\?://', '', ''), \ } let l:buildtags = go#config#BuildTags() if buildtags isnot '' @@ -429,6 +439,14 @@ function! s:position(line, col) abort return {'line': a:line, 'character': a:col} endfunction +function! s:textDocumentParams(fname) abort + return { + \ 'textDocument': { + \ 'uri': go#path#ToURI(a:fname) + \ }, + \ } +endfunction + function! s:textDocumentPositionParams(fname, line, col) abort return { \ 'textDocument': {