]> Sergey Matveev's repositories - vim-lsp.git/commitdiff
Add support for browsing the call hierarchy tree.
authorYegappan Lakshmanan <yegappan@yahoo.com>
Thu, 24 Nov 2022 06:48:07 +0000 (22:48 -0800)
committerYegappan Lakshmanan <yegappan@yahoo.com>
Thu, 24 Nov 2022 06:48:07 +0000 (22:48 -0800)
autoload/lsp/callhierarchy.vim
autoload/lsp/lspserver.vim
autoload/lsp/util.vim
doc/lsp.txt
test/unit_tests.vim

index d2615cab6bf022df82efc505fb1e58bf3f50655f..3a4ef33b59d8a173b2e65f8bf985903f37fd455b 100644 (file)
@@ -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<dict<any>>, incoming: bool)
   var qflist: list<dict<any>> = []
@@ -53,22 +54,190 @@ def CreateLoclistWithCalls(calls: list<dict<any>>, incoming: bool)
   save_winid->win_gotoid()
 enddef
 
-export def IncomingCalls(calls: list<dict<any>>)
-  if calls->empty()
+# Jump to the location of the symbol under the cursor in the call hierarchy
+# tree window.
+def CallHierarchyItemJump()
+  var item: dict<any> = 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<any> = 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<any> = 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<any>, 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<any>,
+                         items: list<dict<any>>)
+  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 <buffer> <CR> <ScriptCmd>CallHierarchyItemJump()<CR>
+    nnoremap <buffer> - <ScriptCmd>CallHierarchyTreeItemOpen()<CR>
+    nnoremap <buffer> + <ScriptCmd>CallHierarchyTreeItemClose()<CR>
+    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<any>)
+  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<dict<any>>)
-  if calls->empty()
+export def OutgoingCalls(lspserver: dict<any>)
+  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
index 1319a9555b3c66027c12cfdcc38289434871bd23..53294522f20644da36e00d50873ad64d4fa3f857 100644 (file)
@@ -1174,23 +1174,19 @@ def IncomingCalls(lspserver: dict<any>, 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<any>, item: dict<any>): 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<any>, 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<any>, item: dict<any>): 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<string>, 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]),
index a3d72db60bb984d2176a0846b9787093b1704a8d..023d8d8a69f7f90551e30c1de51e886815405889 100644 (file)
@@ -169,7 +169,7 @@ export def JumpToLspLocation(location: dict<any>, 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<any>, 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
index 36abd1ef06f2ff09949fb3d7418e6993e1bf08e6..d85090561f869c2e3e76b41f55b99d781a0d6122 100644 (file)
@@ -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:
+
+<Enter>                                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
index 9903b17a79112a662bc0e27266dd315b1c6be21a..3283661a3af06840242628ea1b4384acd5c02710 100644 (file)
@@ -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