Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[READY] Implement LSP type/call hierarchies as actual hierarchies #1733

Merged
merged 13 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading