From ab0f3d1419f6b6a87c79dba395e34c240bb5622d Mon Sep 17 00:00:00 2001 From: Billie Cleek Date: Thu, 12 Nov 2020 21:28:06 -0800 Subject: [PATCH] calls: update :GoCallers to use gopls Closes #2711 --- autoload/go/calls.vim | 31 +++++++++++ autoload/go/calls_test.vim | 48 +++++++++++++++++ autoload/go/guru.vim | 12 ----- autoload/go/list.vim | 1 + autoload/go/lsp.vim | 101 +++++++++++++++++++++++++++++------- autoload/go/lsp/message.vim | 29 +++++++++-- doc/vim-go.txt | 6 +-- ftplugin/go/commands.vim | 4 +- 8 files changed, 193 insertions(+), 39 deletions(-) create mode 100644 autoload/go/calls.vim create mode 100644 autoload/go/calls_test.vim diff --git a/autoload/go/calls.vim b/autoload/go/calls.vim new file mode 100644 index 0000000000..aee45be85f --- /dev/null +++ b/autoload/go/calls.vim @@ -0,0 +1,31 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +function! go#calls#Callers() abort + if !go#config#GoplsEnabled() + call go#util#EchoError("go_referrers_mode is 'gopls', but gopls is disabled") + endif + let [l:line, l:col] = getpos('.')[1:2] + let [l:line, l:col] = go#lsp#lsp#Position(l:line, l:col) + let l:fname = expand('%:p') + call go#lsp#Callers(l:fname, l:line, l:col, funcref('s:parse_output', ['callers'])) + return +endfunction + +" This uses Vim's errorformat to parse the output and put it into a quickfix +" or locationlist. +function! s:parse_output(mode, output) abort + let errformat = ",%f:%l:%c:\ %m" + let l:listtype = go#list#Type("GoCallers") + call go#list#ParseFormat(l:listtype, errformat, a:output, a:mode, 0) + + let errors = go#list#Get(l:listtype) + call go#list#Window(l:listtype, len(errors)) +endfunction + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/autoload/go/calls_test.vim b/autoload/go/calls_test.vim new file mode 100644 index 0000000000..1851e77a15 --- /dev/null +++ b/autoload/go/calls_test.vim @@ -0,0 +1,48 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +scriptencoding utf-8 + +func! Test_Callers() abort + try + let l:tmp = gotest#write_file('calls/caller.go', [ + \ 'package main', + \ '', + \ 'import "fmt"', + \ '', + \ 'func Quux() {}', + \ '', + \ 'func main() {', + \ "\tQuux()", + \ "\tQuux()", + \ '', + \ "\tfmt.Println(\"vim-go\")", + \ '}', + \ ]) + + let l:expected = [ + \ {'lnum': 8, 'bufnr': bufnr(''), 'col': 2, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'module': '', 'text': 'main'}, + \ {'lnum': 9, 'bufnr': bufnr(''), 'col': 2, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'module': '', 'text': 'main'}, + \ ] + + call go#calls#Callers() + + let l:actual = getloclist(0) + let l:start = reltime() + while len(l:actual) != len(l:expected) && reltimefloat(reltime(l:start)) < 10 + sleep 100m + let l:actual = getloclist(0) + endwhile + + call gotest#assert_quickfix(l:actual, l:expected) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/autoload/go/guru.vim b/autoload/go/guru.vim index 7302f1025e..b7ea7ea531 100644 --- a/autoload/go/guru.vim +++ b/autoload/go/guru.vim @@ -352,18 +352,6 @@ function! go#guru#Callees(selected) abort call s:run_guru(args) endfunction -" Show possible callers of selected function -function! go#guru#Callers(selected) abort - let args = { - \ 'mode': 'callers', - \ 'format': 'plain', - \ 'selected': a:selected, - \ 'needs_scope': 1, - \ } - - call s:run_guru(args) -endfunction - " Show path from callgraph root to selected function function! go#guru#Callstack(selected) abort let args = { diff --git a/autoload/go/list.vim b/autoload/go/list.vim index e700203461..f365280ecc 100644 --- a/autoload/go/list.vim +++ b/autoload/go/list.vim @@ -165,6 +165,7 @@ let s:default_list_type_commands = { \ "GoVet": "quickfix", \ "GoReferrers": "locationlist", \ "GoImplements": "locationlist", + \ "GoCallers": "locationlist", \ "_guru": "locationlist", \ "_term": "locationlist", \ "_job": "locationlist", diff --git a/autoload/go/lsp.vim b/autoload/go/lsp.vim index 4c16dfbd80..76b47c381f 100644 --- a/autoload/go/lsp.vim +++ b/autoload/go/lsp.vim @@ -610,7 +610,7 @@ function! go#lsp#TypeDef(fname, line, col, handler) abort let l:state = s:newHandlerState('type definition') let l:msg = go#lsp#message#TypeDefinition(fnamemodify(a:fname, ':p'), a:line, a:col) let l:state.handleResult = funcref('s:typeDefinitionHandler', [function(a:handler, [], l:state)], l:state) - return l:lsp.sendMessage(l:msg, l:state) + return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:typeDefinitionHandler(next, msg) abort dict @@ -624,6 +624,59 @@ function! s:typeDefinitionHandler(next, msg) abort dict call call(a:next, l:args) endfunction +" go#lsp#Callers calls gopls to get callers of the identifier at +" line and col in fname. handler should be a dictionary function that takes a +" list of strings in the form 'file:line:col: message'. handler will be +" attached to a dictionary that manages state (statuslines, sets the winid, +" etc.) +function! go#lsp#Callers(fname, line, col, handler) abort + call go#lsp#DidChange(a:fname) + + let l:lsp = s:lspfactory.get() + let l:state = s:newHandlerState('callers') + let l:msg = go#lsp#message#PrepareCallHierarchy(fnamemodify(a:fname, ':p'), a:line, a:col) + let l:state.handleResult = funcref('s:prepareCallHierarchyHandler', [function(a:handler, [], l:state)], l:state) + return l:lsp.sendMessage(l:msg, l:state) +endfunction + +function! s:prepareCallHierarchyHandler(next, msg) abort dict + if a:msg is v:null || len(a:msg) == 0 + return + endif + + let l:lsp = s:lspfactory.get() + let l:state = s:newHandlerState('callers') + let l:msg = go#lsp#message#IncomingCalls(a:msg[0]) + let l:state.handleResult = funcref('s:incomingCallsHandler', [function(a:next, [], l:state)], l:state) + return l:lsp.sendMessage(l:msg, l:state) +endfunction + +function! s:incomingCallsHandler(next, msg) abort dict + if a:msg is v:null || len(a:msg) == 0 + return + endif + + let l:locations = [] + for l:item in a:msg + try + let l:fname = go#path#FromURI(l:item.from.uri) + + for l:fromRange in l:item.fromRanges + let l:line = l:fromRange.start.line+1 + let l:content = s:lineinfile(l:fname, l:line) + if l:content is -1 + continue + endif + let l:locations = add(l:locations, printf('%s:%s:%s: %s', l:fname, l:line, go#lsp#lsp#PositionOf(content, l:fromRange.start.character), l:item.from.name)) + endfor + catch + endtry + endfor + + call call(a:next, [l:locations]) + return +endfunction + function! go#lsp#DidOpen(fname) abort if get(b:, 'go_lsp_did_open', 0) return @@ -852,26 +905,12 @@ function! s:handleLocations(next, msg) abort for l:loc in l:msg let l:fname = go#path#FromURI(l:loc.uri) let l:line = l:loc.range.start.line+1 - let l:bufnr = bufnr(l:fname) - let l:bufinfo = getbufinfo(l:fname) - - try - if l:bufnr == -1 || len(l:bufinfo) == 0 || l:bufinfo[0].loaded == 0 - let l:filecontents = readfile(l:fname, '', l:line) - else - let l:filecontents = getbufline(l:fname, l:line) - endif - - if len(l:filecontents) == 0 - continue - endif - - let l:content = l:filecontents[-1] - catch - call go#util#EchoError(printf('%s (line %s): %s at %s', l:fname, l:line, v:exception, v:throwpoint)) - endtry + let l:content = s:lineinfile(l:fname, l:line) + if l:content is -1 + continue + endif - let l:item = printf('%s:%s:%s: %s', go#path#FromURI(l:loc.uri), l:line, go#lsp#lsp#PositionOf(l:content, l:loc.range.start.character), l:content) + let l:item = printf('%s:%s:%s: %s', l:fname, l:line, go#lsp#lsp#PositionOf(l:content, l:loc.range.start.character), l:content) let l:result = add(l:result, l:item) endfor @@ -1687,6 +1726,28 @@ function! s:dedup(list) return sort(keys(l:dict)) endfunction +function! s:lineinfile(fname, line) abort + let l:bufnr = bufnr(a:fname) + let l:bufinfo = getbufinfo(a:fname) + + try + if l:bufnr == -1 || len(l:bufinfo) == 0 || l:bufinfo[0].loaded == 0 + let l:filecontents = readfile(a:fname, '', a:line) + else + let l:filecontents = getbufline(a:fname, a:line) + endif + + if len(l:filecontents) == 0 + return -1 + endif + + return l:filecontents[-1] + catch + call go#util#EchoError(printf('%s (line %s): %s at %s', a:fname, a:line, v:exception, v:throwpoint)) + return -1 + endtry +endfunction + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/autoload/go/lsp/message.vim b/autoload/go/lsp/message.vim index 633e0d13a4..508f900a25 100644 --- a/autoload/go/lsp/message.vim +++ b/autoload/go/lsp/message.vim @@ -238,6 +238,29 @@ function! go#lsp#message#References(file, line, col) abort \ } endfunction +function! go#lsp#message#PrepareCallHierarchy(file, line, col) abort + return { + \ 'notification': 0, + \ 'method': 'textDocument/prepareCallHierarchy', + \ 'params': { + \ 'textDocument': { + \ 'uri': go#path#ToURI(a:file) + \ }, + \ 'position': s:position(a:line, a:col), + \ } + \ } +endfunction + +function! go#lsp#message#IncomingCalls(item) abort + return { + \ 'notification': 0, + \ 'method': 'callHierarchy/incomingCalls', + \ 'params': { + \ 'item': a:item, + \ } + \ } +endfunction + function! go#lsp#message#Hover(file, line, col) abort return { \ 'notification': 0, @@ -367,7 +390,7 @@ function! go#lsp#message#ConfigurationResult(items) abort return l:result endfunction -function go#lsp#message#ExecuteCommand(cmd, args) abort +function! go#lsp#message#ExecuteCommand(cmd, args) abort return { \ 'notification': 0, \ 'method': 'workspace/executeCommand', @@ -378,13 +401,13 @@ function go#lsp#message#ExecuteCommand(cmd, args) abort \ } endfunction -function go#lsp#message#ApplyWorkspaceEditResponse(ok) abort +function! go#lsp#message#ApplyWorkspaceEditResponse(ok) abort return { \ 'applied': a:ok, \ } endfunction -function s:workspaceFolder(key, val) abort +function! s:workspaceFolder(key, val) abort return {'uri': go#path#ToURI(a:val), 'name': a:val} endfunction diff --git a/doc/vim-go.txt b/doc/vim-go.txt index 29c4585235..cbe66ae1f1 100644 --- a/doc/vim-go.txt +++ b/doc/vim-go.txt @@ -1560,9 +1560,9 @@ default this is enabled. *'g:go_guru_scope'* Use this option to define the scope of the analysis to be passed for guru -related commands, such as |:GoImplements|, |:GoCallers|, etc. You can change -it on-the-fly with |:GoGuruScope|. The input should be a a list of package -pattern. An example input might be: +related commands, such as |:GoImplements|, |:GoChannelPeers|, etc. You can +change it on-the-fly with |:GoGuruScope|. The input should be a a list of +package pattern. An example input might be: `["github.com/fatih/color","github.com/fatih/structs"]` Also see |go-guru-scope|. diff --git a/ftplugin/go/commands.vim b/ftplugin/go/commands.vim index 2235f64ca2..6a00041a3d 100644 --- a/ftplugin/go/commands.vim +++ b/ftplugin/go/commands.vim @@ -9,7 +9,6 @@ if go#package#InGOPATH() command! -range=% GoWhicherrs call go#guru#Whicherrs() command! -range=% GoCallees call go#guru#Callees() command! -range=% GoDescribe call go#guru#Describe() - command! -range=% GoCallers call go#guru#Callers() command! -range=% GoCallstack call go#guru#Callstack() command! -range=% GoFreevars call go#guru#Freevars() command! -range=% GoChannelPeers call go#guru#ChannelPeers() @@ -22,6 +21,9 @@ command! -range=0 GoSameIdsClear call go#guru#ClearSameIds() command! -range=0 GoSameIdsToggle call go#guru#ToggleSameIds() command! -range=0 GoSameIdsAutoToggle call go#guru#AutoToggleSameIds() +" -- calls +command! -range=0 GoCallers call go#calls#Callers() + " -- tags command! -nargs=* -range GoAddTags call go#tags#Add(, , , ) command! -nargs=* -range GoRemoveTags call go#tags#Remove(, , , )