From d071466d3310bdec801559a97347e112282165c1 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Wed, 23 Nov 2022 22:48:07 -0800 Subject: [PATCH] Add support for browsing the call hierarchy tree. --- autoload/lsp/callhierarchy.vim | 181 +++++++++++++++++++++++++++++++-- autoload/lsp/lspserver.vim | 43 ++++---- autoload/lsp/util.vim | 4 +- doc/lsp.txt | 57 +++++++++-- test/unit_tests.vim | 13 ++- 5 files changed, 249 insertions(+), 49 deletions(-) diff --git a/autoload/lsp/callhierarchy.vim b/autoload/lsp/callhierarchy.vim index d2615ca..3a4ef33 100644 --- a/autoload/lsp/callhierarchy.vim +++ b/autoload/lsp/callhierarchy.vim @@ -3,6 +3,7 @@ vim9script # Functions for dealing with call hierarchy (incoming/outgoing calls) import './util.vim' +import './buffer.vim' as buf def CreateLoclistWithCalls(calls: list>, incoming: bool) var qflist: list> = [] @@ -53,22 +54,190 @@ def CreateLoclistWithCalls(calls: list>, incoming: bool) save_winid->win_gotoid() enddef -export def IncomingCalls(calls: list>) - if calls->empty() +# Jump to the location of the symbol under the cursor in the call hierarchy +# tree window. +def CallHierarchyItemJump() + var item: dict = w:LspCallHierItemMap[line('.')].item + util.JumpToLspLocation(item, '') +enddef + +# Refresh the call hierarchy tree for the symbol at index 'idx'. +def CallHierarchyTreeItemRefresh(idx: number) + var treeItem: dict = w:LspCallHierItemMap[idx] + + if treeItem.open + # Already retrieved the children for this item + return + endif + + if !treeItem->has_key('children') + # First time retrieving the children for the item at index 'idx' + var lspserver = buf.BufLspServerGet(w:LspBufnr) + if lspserver->empty() || !lspserver.running + return + endif + + var reply: any + if w:LspCallHierIncoming + reply = lspserver.getIncomingCalls(treeItem.item) + else + reply = lspserver.getOutgoingCalls(treeItem.item) + endif + + treeItem.children = [] + if !reply->empty() + for item in reply + treeItem.children->add({item: w:LspCallHierIncoming ? item.from : + item.to, open: false}) + endfor + endif + endif + + # Clear and redisplay the tree in the window + treeItem.open = true + var save_cursor = getcurpos() + CallHierarchyTreeRefresh() + setpos('.', save_cursor) +enddef + +# Open the call hierarchy tree item under the cursor +def CallHierarchyTreeItemOpen() + CallHierarchyTreeItemRefresh(line('.')) +enddef + +# Refresh the entire call hierarchy tree +def CallHierarchyTreeRefreshCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + CallHierarchyTreeItemRefresh(2) +enddef + +# Display the incoming call hierarchy tree +def CallHierarchyTreeIncomingCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + w:LspCallHierIncoming = true + CallHierarchyTreeItemRefresh(2) +enddef + +# Display the outgoing call hierarchy tree +def CallHierarchyTreeOutgoingCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + w:LspCallHierIncoming = false + CallHierarchyTreeItemRefresh(2) +enddef + +# Close the call hierarchy tree item under the cursor +def CallHierarchyTreeItemClose() + var treeItem: dict = w:LspCallHierItemMap[line('.')] + treeItem.open = false + var save_cursor = getcurpos() + CallHierarchyTreeRefresh() + setpos('.', save_cursor) +enddef + +# Recursively add the call hierarchy items to w:LspCallHierItemMap +def CallHierarchyTreeItemShow(incoming: bool, treeItem: dict, pfx: string) + var item = treeItem.item + var treePfx: string + if treeItem.open && treeItem->has_key('children') + treePfx = '▼' + else + treePfx = '▶' + endif + var fname = util.LspUriToFile(item.uri) + var s = $'{pfx}{treePfx} {item.name} ({fname->fnamemodify(":t")} [{fname->fnamemodify(":h")}])' + append('$', s) + w:LspCallHierItemMap->add(treeItem) + if treeItem.open && treeItem->has_key('children') + for child in treeItem.children + CallHierarchyTreeItemShow(incoming, child, $'{pfx} ') + endfor + endif +enddef + +def CallHierarchyTreeRefresh() + :setlocal modifiable + :silent! :%d _ + + setline(1, $'# {w:LspCallHierIncoming ? "Incoming calls to" : "Outgoing calls from"} "{w:LspCallHierarchyTree.item.name}"') + w:LspCallHierItemMap = [{}, {}] + CallHierarchyTreeItemShow(w:LspCallHierIncoming, w:LspCallHierarchyTree, '') + :setlocal nomodifiable +enddef + +def CallHierarchyTreeShow(incoming: bool, prepareItem: dict, + items: list>) + var save_bufnr = bufnr() + var wid = bufwinid('LSP-CallHierarchy') + if wid != -1 + wid->win_gotoid() + else + :new LSP-CallHierarchy + :setlocal buftype=nofile + :setlocal bufhidden=wipe + :setlocal noswapfile + :setlocal nonumber nornu + :setlocal fdc=0 signcolumn=no + + nnoremap CallHierarchyItemJump() + nnoremap - CallHierarchyTreeItemOpen() + nnoremap + CallHierarchyTreeItemClose() + command -buffer LspCallHierarchyRefresh CallHierarchyTreeRefreshCmd() + command -buffer LspCallHierarchyIncoming CallHierarchyTreeIncomingCmd() + command -buffer LspCallHierarchyOutgoing CallHierarchyTreeOutgoingCmd() + + syntax match Comment '^#.*$' + syntax match Directory '(.*)$' + endif + + w:LspBufnr = save_bufnr + w:LspCallHierIncoming = incoming + w:LspCallHierarchyTree = {} + w:LspCallHierarchyTree.item = prepareItem + w:LspCallHierarchyTree.open = true + w:LspCallHierarchyTree.children = [] + for item in items + w:LspCallHierarchyTree.children->add({item: incoming ? item.from : item.to, open: false}) + endfor + + CallHierarchyTreeRefresh() + + setlocal nomodified + setlocal nomodifiable +enddef + +export def IncomingCalls(lspserver: dict) + var prepareReply = lspserver.prepareCallHierarchy() + if prepareReply->empty() + util.WarnMsg('No incoming calls') + return + endif + + var reply = lspserver.getIncomingCalls(prepareReply) + if reply->empty() util.WarnMsg('No incoming calls') return endif - CreateLoclistWithCalls(calls, true) + CallHierarchyTreeShow(true, prepareReply, reply) enddef -export def OutgoingCalls(calls: list>) - if calls->empty() +export def OutgoingCalls(lspserver: dict) + var prepareReply = lspserver.prepareCallHierarchy() + if prepareReply->empty() + util.WarnMsg('No outgoing calls') + return + endif + + var reply = lspserver.getOutgoingCalls(prepareReply) + if reply->empty() util.WarnMsg('No outgoing calls') return endif - CreateLoclistWithCalls(calls, false) + CallHierarchyTreeShow(false, prepareReply, reply) enddef # vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/autoload/lsp/lspserver.vim b/autoload/lsp/lspserver.vim index 1319a95..5329452 100644 --- a/autoload/lsp/lspserver.vim +++ b/autoload/lsp/lspserver.vim @@ -1174,23 +1174,19 @@ def IncomingCalls(lspserver: dict, fname: string) return endif - var reply = PrepareCallHierarchy(lspserver) - if reply->empty() - util.WarnMsg('No incoming calls') - return - endif + callhier.IncomingCalls(lspserver) +enddef +def GetIncomingCalls(lspserver: dict, item: dict): any # Request: "callHierarchy/incomingCalls" # Param: CallHierarchyIncomingCallsParams var param = {} - param.item = reply - reply = lspserver.rpc('callHierarchy/incomingCalls', param) - if reply->empty() || reply.result->empty() - util.WarnMsg('No incoming calls') - return + param.item = item + var reply = lspserver.rpc('callHierarchy/incomingCalls', param) + if reply->empty() + return null endif - - callhier.IncomingCalls(reply.result) + return reply.result enddef # Request: "callHierarchy/outgoingCalls" @@ -1201,23 +1197,19 @@ def OutgoingCalls(lspserver: dict, fname: string) return endif - var reply = PrepareCallHierarchy(lspserver) - if reply->empty() - util.WarnMsg('No outgoing calls') - return - endif + callhier.OutgoingCalls(lspserver) +enddef +def GetOutgoingCalls(lspserver: dict, item: dict): any # Request: "callHierarchy/outgoingCalls" # Param: CallHierarchyOutgoingCallsParams var param = {} - param.item = reply - reply = lspserver.rpc('callHierarchy/outgoingCalls', param) - if reply->empty() || reply.result->empty() - util.WarnMsg('No outgoing calls') - return + param.item = item + var reply = lspserver.rpc('callHierarchy/outgoingCalls', param) + if reply->empty() + return null endif - - callhier.OutgoingCalls(reply.result) + return reply.result enddef # Request: "textDocument/typehierarchy" @@ -1588,8 +1580,11 @@ export def NewLspServer(path: string, args: list, isSync: bool, initiali docHighlight: function(DocHighlight, [lspserver]), getDocSymbols: function(GetDocSymbols, [lspserver]), textDocFormat: function(TextDocFormat, [lspserver]), + prepareCallHierarchy: function(PrepareCallHierarchy, [lspserver]), incomingCalls: function(IncomingCalls, [lspserver]), + getIncomingCalls: function(GetIncomingCalls, [lspserver]), outgoingCalls: function(OutgoingCalls, [lspserver]), + getOutgoingCalls: function(GetOutgoingCalls, [lspserver]), typeHierarchy: function(TypeHiearchy, [lspserver]), renameSymbol: function(RenameSymbol, [lspserver]), codeAction: function(CodeAction, [lspserver]), diff --git a/autoload/lsp/util.vim b/autoload/lsp/util.vim index a3d72db..023d8d8 100644 --- a/autoload/lsp/util.vim +++ b/autoload/lsp/util.vim @@ -169,7 +169,7 @@ export def JumpToLspLocation(location: dict, cmdmods: string) # and 'hidden' is not set or if the current buffer is a special # buffer, then open the buffer in a new window. if (&modified && !&hidden) || &buftype != '' - exe $'sbuffer {bnr}' + exe $'belowright sbuffer {bnr}' else exe $'buf {bnr}' endif @@ -178,7 +178,7 @@ export def JumpToLspLocation(location: dict, cmdmods: string) # if the current buffer has unsaved changes and 'hidden' is not set, # or if the current buffer is a special buffer, then open the file # in a new window - exe $'split {fname}' + exe $'belowright split {fname}' else exe $'edit {fname}' endif diff --git a/doc/lsp.txt b/doc/lsp.txt index 36abd1e..d850905 100644 --- a/doc/lsp.txt +++ b/doc/lsp.txt @@ -2,7 +2,7 @@ Author: Yegappan Lakshmanan (yegappan AT yahoo DOT com) For Vim version 9.0 and above -Last change: Nov 19, 2022 +Last change: Nov 23, 2022 ============================================================================== *lsp-license* @@ -94,9 +94,9 @@ The following commands are provided: :LspHighlight Highlight all the matches for the keyword under cursor :LspHighlightClear Clear all the matches highlighted by :LspHighlight :LspIncomingCalls Display the list of symbols calling the current symbol - in a new location list. + in a window. :LspOutgoingCalls Display the list of symbols called by the current - symbol in a new location list. + symbol in a window. :LspOutline Show the list of symbols defined in the current file in a separate window. :LspPeekDeclaration Open the declaration of the symbol under cursor in a @@ -482,12 +482,14 @@ To get a particular option value you can use the following: > file using the LSP server. *:LspIncomingCalls* -:LspIncomingCalls Creates a new location list with the location of the - list of symbols calling the current symbol. +:LspIncomingCalls Display a hierarchy of symbols calling the symbol + under the cursor in a window. See + |lsp-call-hierarchy| for more information. *:LspOutoingCalls* -:LspOutoingCalls Creates a new location list with the location of - the list of symbols called by the current symbol. +:LspOutoingCalls Display a hierarchy of symbols called by the symbol + under the cursor in a window. See + |lsp-call-hierarchy| for more information. *:LspRename* :LspRename Rename the current symbol. You will be prompted to @@ -745,7 +747,42 @@ Note that most of the language servers return only one symbol location even if the symbol is defined in multiple places in the code. ============================================================================== -9. Autocommands *lsp-autocmds* +9. Call Hierarchy *lsp-call-hierarchy* + +The |:LspIncomingCalls| and the |:LspOutoingCalls| commands can be used to +display the call hierarchy of a symbol. For example, the functions calling a +function or the functions called by a function. These two commands open a +window containing the call hierarchy tree. You can use the Vim motion +commands to browse the call hierarchy. + +In the call hierarchy tree window, the following keys are supported: + + Jump to the location of the symbol under the + cursor. +- Expand and show the symbols calling or called + by the symbol under the cursor. ++ Close the call hierarchy for the symbol under + the cursor. + +You can display either the incoming call hierarchy or the outgoing call +hierarchy in this window. You cannot display both at the same time. + +In the call hierarchy tree window, the following commands are supported: + +:LspCallHierarchyRefresh Query the language server again for the top + level symbol and refresh the call hierarchy + tree. +:LspCallHierarchyIncoming Display the incoming call hierarchy for the + top level symbol. If the window is currently + displaying the outgoing calls, then it is + refreshed to display the incoming calls. +:LspCallHierarchyOutgoing Display the outgoing call hierarchy for the + top level symbol. If the window is currently + displaying the incoming calls, then it is + refreshed to display the outgoing calls. + +============================================================================== +10. Autocommands *lsp-autocmds* *LspAttached* LspAttached A |User| autocommand fired when the LSP client @@ -759,7 +796,7 @@ LspDiagsUpdated A |User| autocommand invoked when new has processed the diagnostics. ============================================================================== -10. Debugging *lsp-debug* +11. Debugging *lsp-debug* To debug this plugin, you can log the language server protocol messages sent and received by the plugin from the language server. The following command @@ -791,7 +828,7 @@ use the :LspServerTrace command to set the trace value: > :LspServerTrace { off | messages | verbose } < ============================================================================== -11. Custom Command Handlers *lsp-custom-commands* +12. Custom Command Handlers *lsp-custom-commands* When applying a code action, the language server may issue a non-standard command. For example, the Java language server uses non-standard commands diff --git a/test/unit_tests.vim b/test/unit_tests.vim index 9903b17..3283661 100644 --- a/test/unit_tests.vim +++ b/test/unit_tests.vim @@ -812,13 +812,12 @@ def Test_LspIncomingCalls() :sleep 1 cursor(1, 6) :LspIncomingCalls - assert_equal(2, winnr('$')) - var l = getloclist(0) - assert_equal([7, 3], [l[0].lnum, l[0].col]) - assert_equal('aFunc: xFunc();', l[0].text) - assert_equal([12, 3], [l[1].lnum, l[1].col]) - assert_equal('bFunc: xFunc();', l[1].text) - setloclist(0, [], 'f') + assert_equal([1, 2], [winnr(), winnr('$')]) + var l = getline(1, '$') + assert_equal('# Incoming calls to "xFunc"', l[0]) + assert_match('▼ xFunc (Xtest.c \[.*\])', l[1]) + assert_match(' ▶ aFunc (Xtest.c \[.*\])', l[2]) + assert_match(' ▶ bFunc (Xtest.c \[.*\])', l[3]) :%bw! enddef -- 2.48.1