Skip to content

Commit

Permalink
Merge pull request #1733 from bstaletic/hierarchies
Browse files Browse the repository at this point in the history
[READY] Implement LSP type/call hierarchies as actual hierarchies
  • Loading branch information
mergify[bot] authored Jun 18, 2024
2 parents 03b2c82 + 5b00c18 commit e81c15e
Show file tree
Hide file tree
Showing 24 changed files with 1,386 additions and 89 deletions.
2 changes: 1 addition & 1 deletion docs/bundle.js.map

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions docs/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/main.css.map

Large diffs are not rendered by default.

87 changes: 86 additions & 1 deletion docs/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,61 @@ definitions:
additionalProperties:
$ref: "#/definitions/FileData"

Hierarchy:
type: object
required:
- kind
- locations
- name
description: |-
An object returned, in a list, as a response to hierarchy requests.
The object may contain additional properties, which are used to identify
the hierarchy tree node, by the subservers.
properties:
kind:
type: string
enum:
- File
- Module
- Namespace
- Package
- Class
- Method
- Property
- Field
- Constructor
- Enum
- Interface
- Function
- Variable
- Constant
- String
- Number
- Boolean
- Array
- Object
- Key
- Null
- EnumMember
- Struct
- Event
- Operator
- TypeParameter
locations:
type: array
items:
$ref: "#/defintions/Location"
name:
type: string
description: |-
The name of the symbol represented by the hierarchy item.
root_location:
type: object
description: |-
In call hierarchies, it is useful to differentiate between the call
site (which end up in the `locations` property) and the calling
function. This property, if present, represents the latter.
$ref: "#/definitions/Location"
# Due to the way the API combines keys at the top level, we are not able to
# compose this item per-request. So this definition must be copy-pasted for
# some requests.
Expand Down Expand Up @@ -368,6 +422,22 @@ definitions:
If present, this is a single *GoTo response* and this value
contains the absolute path of the buffer containing the
target location (identified in `line_num` and `column_num`).
kind:
type: string
description: |-
If present, this is a *Hierarchy response* and this value
describes the kind of symbol a node in the call/type hierarchy
tree is referring to.
hierarchy details:
$ref: "#/definitions/Hierarchy"
locations:
type: array
description: |-
If present, this is a *Hierarchy response* and this value
contains a list of locations that a node in the call/type
hierarchy tree is referring to.
hierarchy details:
$ref: "#/definitions/Hierarchy"
line_num:
$ref: "#/definitions/LineNumber"
column_num:
Expand Down Expand Up @@ -886,6 +956,21 @@ paths:
messages are typically multiple lines (such as the documentation and
signature for a method) and are best displayed in a panel or preview
area (or equivalent).
- A *Hierarchy* response. This is identified where the type of the
response is a list of objects containing `kind` and `locations`
properties.
Hierarchy requests work similarly to those in the LSP specification.
If a completer supports call/type hierarchies, the initial request
should be sent through `TypeHierarchy`/`CallHierarchy` subcommand.
Resolving any item in the hierarchy tree is done by subsequent
`ResolveCallHierarchyItem` and `ResolveTypeHierarchyItem` requests.
When resolving a hierarchy item, the arguments following
the subcommand name are the item being resolved and the direction in
which the item should be resolved.
For call hierarchies, the directions are `incoming` and `outgoing`.
For type hierarchies, the directions are `subtypes` and `supertypes`.
Note that, except for the root node of the hierarchy, resolving an
item in both directions is discouraged.
- A *GoTo* response. This is identified where the response type cannot
be determined by any of the above methods. A GoTo response may contain
either a single location (e.g. for `GoToDeclaration`), or a list of
Expand Down
5 changes: 0 additions & 5 deletions update_api_docs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
#!/usr/bin/env python3

from __future__ import print_function
from __future__ import division
from __future__ import unicode_literals
from __future__ import absolute_import

import os
import platform
import sys
Expand Down
15 changes: 15 additions & 0 deletions ycmd/completers/java/java_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,18 @@ def GetServerName( self ):

def GetCommandLine( self ):
return self._command


def Hierarchy( self, request_data, args ):
# JDT.LS is a special snowflake and needs special snowflake treatement
# See: https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3184
result = super().Hierarchy( request_data, args )
preparation_item, direction, kind = args
if kind == 'call' and direction == 'incoming':
for item in result:
# The base class does almost the same,
# but uses `selectionRange` instead of `range`.
item[ 'root_location' ] = responses.BuildGoToResponseFromLocation(
*language_server_completer._LspLocationToLocationAndDescription(
request_data, item[ 'from' ] ) )
return result
127 changes: 122 additions & 5 deletions ycmd/completers/language_server/language_server_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1685,7 +1685,7 @@ def GetDetailedDiagnostic( self, request_data ):
message = diagnostic[ 'message' ]
try:
code = diagnostic[ 'code' ]
message += f' [{ code }]'
message += f' [{ code }]' # noqa
except KeyError:
pass

Expand Down Expand Up @@ -1785,6 +1785,18 @@ def GetSubcommandsMap( self ):
lambda self, request_data, args: self.GetType( request_data )
)

if ( self._server_capabilities and
_IsCapabilityProvided( self._server_capabilities,
'typeHierarchyProvider' ) ):
commands[ 'TypeHierarchy' ] = (
lambda self, request_data, args:
self.InitialHierarchy( request_data, [ 'type' ] )
)
commands[ 'ResolveTypeHierarchyItem' ] = (
lambda self, request_data, args:
self.Hierarchy( request_data, [ *args, 'type' ] )
)

if ( self._server_capabilities and
_IsCapabilityProvided( self._server_capabilities,
'callHierarchyProvider' ) ):
Expand All @@ -1796,6 +1808,14 @@ def GetSubcommandsMap( self ):
lambda self, request_data, args:
self.CallHierarchy( request_data, [ 'incoming' ] )
)
commands[ 'CallHierarchy' ] = (
lambda self, request_data, args:
self.InitialHierarchy( request_data, [ 'call' ] )
)
commands[ 'ResolveCallHierarchyItem' ] = (
lambda self, request_data, args:
self.Hierarchy( request_data, [ *args, 'call' ] )
)

commands.update( self.GetCustomSubcommands() )

Expand Down Expand Up @@ -2643,14 +2663,105 @@ def GoToDocumentOutline( self, request_data ):
return _SymbolInfoListToGoTo( request_data, result )


def InitialHierarchy( self, request_data, args ):
if not self.ServerIsReady():
raise RuntimeError( 'Server is initializing. Please wait.' )

kind = args[ 0 ]

self._UpdateServerWithFileContents( request_data )
request_id = self.GetConnection().NextRequestId()
message = lsp.PrepareHierarchy( request_id, request_data, kind.title() )
prepare_response = self.GetConnection().GetResponse(
request_id,
message,
REQUEST_TIMEOUT_COMMAND )
preparation_item = prepare_response.get( 'result' ) or []
if not preparation_item:
raise RuntimeError( f'No { kind } hierarchy found.' )

assert len( preparation_item ) == 1, (
'Not available: Multiple hierarchies were received, '
'this is not currently supported.' )

preparation_item[ 0 ][ 'locations' ] = [
responses.BuildGoToResponseFromLocation(
*_LspLocationToLocationAndDescription( request_data,
location,
'selectionRange' ) )
for location in preparation_item ]
kind_string = lsp.SYMBOL_KIND[ preparation_item[ 0 ][ 'kind' ] ]
preparation_item[ 0 ][ 'kind' ] = kind_string
return preparation_item


def Hierarchy( self, request_data, args ):
if not self.ServerIsReady():
raise RuntimeError( 'Server is initializing. Please wait.' )

preparation_item, direction, kind = args

if item := ( preparation_item.get( 'from' ) or
preparation_item.get( 'to' ) ):
preparation_item = item
else:
del preparation_item[ 'locations' ]
kind_number = lsp.SYMBOL_KIND.index( preparation_item[ 'kind' ] )
preparation_item[ 'kind' ] = kind_number

if kind == 'call':
direction += 'Calls'
self._UpdateServerWithFileContents( request_data )
request_id = self.GetConnection().NextRequestId()
message = lsp.Hierarchy( request_id, kind, direction, preparation_item )
response = self.GetConnection().GetResponse( request_id,
message,
REQUEST_TIMEOUT_COMMAND )

result = response.get( 'result' )
if result:
for item in result:
if kind == 'call':
name_and_kind_key = 'to' if direction == 'outgoingCalls' else 'from'
hierarchy_item = item[ name_and_kind_key ]
kind_string = lsp.SYMBOL_KIND[ hierarchy_item[ 'kind' ] ]
item[ 'kind' ] = kind_string
item[ 'name' ] = hierarchy_item[ 'name' ]
lsp_locations = [ {
'uri': hierarchy_item[ 'uri' ],
'range': r }
for r in item[ 'fromRanges' ] ]
item[ 'locations' ] = [
responses.BuildGoToResponseFromLocation(
*_LspLocationToLocationAndDescription( request_data, location ) )
for location in lsp_locations ]

if direction == 'incomingCalls':
item[ 'root_location' ] = responses.BuildGoToResponseFromLocation(
*_LspLocationToLocationAndDescription( request_data,
hierarchy_item,
'selectionRange' ) )
else:
item[ 'kind' ] = lsp.SYMBOL_KIND[ item[ 'kind' ] ]
item[ 'locations' ] = [
responses.BuildGoToResponseFromLocation(
*_LspLocationToLocationAndDescription( request_data, location ) )
for location in [ item ] ]
return result
if kind == 'call':
raise RuntimeError(
f'No { direction.rstrip( "Calls" ) } { kind }s found.' )
else:
raise RuntimeError( f'No { direction } found.' )


def CallHierarchy( self, request_data, args ):
if not self.ServerIsReady():
raise RuntimeError( 'Server is initializing. Please wait.' )

self._UpdateServerWithFileContents( request_data )
request_id = self.GetConnection().NextRequestId()
message = lsp.PrepareCallHierarchy( request_id, request_data )
message = lsp.PrepareHierarchy( request_id, request_data, 'Call' )
prepare_response = self.GetConnection().GetResponse(
request_id,
message,
Expand All @@ -2666,7 +2777,10 @@ def CallHierarchy( self, request_data, args ):
preparation_item = preparation_item[ 0 ]

request_id = self.GetConnection().NextRequestId()
message = lsp.CallHierarchy( request_id, args[ 0 ], preparation_item )
message = lsp.Hierarchy( request_id,
'call',
args[ 0 ] + 'Calls',
preparation_item )
response = self.GetConnection().GetResponse( request_id,
message,
REQUEST_TIMEOUT_COMMAND )
Expand Down Expand Up @@ -3282,7 +3396,9 @@ def BuildGoToLocationFromSymbol( symbol ):
return locations


def _LspLocationToLocationAndDescription( request_data, location ):
def _LspLocationToLocationAndDescription( request_data,
location,
range_property = 'range' ):
"""Convert a LSP Location to a ycmd location."""
try:
filename = lsp.UriToFilePath( location[ 'uri' ] )
Expand All @@ -3299,9 +3415,10 @@ def _LspLocationToLocationAndDescription( request_data, location ):
'GoTo location' )
file_contents = []

range = location[ range_property ]
return _BuildLocationAndDescription( filename,
file_contents,
location[ 'range' ][ 'start' ] )
range[ 'start' ] )


def _LspToYcmdLocation( file_contents, location ):
Expand Down
8 changes: 4 additions & 4 deletions ycmd/completers/language_server/language_server_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,8 @@ def Position( line_num, line_value, column_codepoint ):
}


def PrepareCallHierarchy( request_id, request_data ):
return BuildRequest( request_id, 'textDocument/prepareCallHierarchy', {
def PrepareHierarchy( request_id, request_data, kind ):
return BuildRequest( request_id, f'textDocument/prepare{ kind }Hierarchy', {
'textDocument': {
'uri': FilePathToUri( request_data[ 'filepath' ] ),
},
Expand All @@ -644,8 +644,8 @@ def PrepareCallHierarchy( request_id, request_data ):
} )


def CallHierarchy( request_id, direction, item ):
return BuildRequest( request_id, f'callHierarchy/{ direction }Calls', {
def Hierarchy( request_id, kind, direction, item ):
return BuildRequest( request_id, f'{ kind }Hierarchy/{ direction }', {
'item': item
} )

Expand Down
Loading

0 comments on commit e81c15e

Please sign in to comment.