Skip to content

Commit 5de7052

Browse files
committed
Add support for DocumentSymbol in document outline requests
We are intentionally not advertising the capability. We do want a flat response, so receiving a DocumentSymbol is a pessimisation. Not advertising the capability means that conforming servers take the faster code path and the likes of OmniSharp, that assume capabilities, still work. Yes, it's messy, but so is LSP.
1 parent 1026c83 commit 5de7052

File tree

2 files changed

+104
-23
lines changed

2 files changed

+104
-23
lines changed

ycmd/completers/language_server/language_server_completer.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2638,7 +2638,7 @@ def GoToSymbol( self, request_data, args ):
26382638
REQUEST_TIMEOUT_COMMAND )
26392639

26402640
result = response.get( 'result' ) or []
2641-
return _SymbolInfoListToGoTo( request_data, result )
2641+
return _LspSymbolListToGoTo( request_data, result )
26422642

26432643

26442644
def GoToDocumentOutline( self, request_data ):
@@ -2655,12 +2655,11 @@ def GoToDocumentOutline( self, request_data ):
26552655

26562656
result = response.get( 'result' ) or []
26572657

2658-
# We should only receive SymbolInformation (not DocumentSymbol)
26592658
if any( 'range' in s for s in result ):
2660-
raise ValueError(
2661-
"Invalid server response; DocumentSymbol not supported" )
2659+
LOGGER.debug( 'Hierarchical DocumentSymbol not supported.' )
2660+
result = _FlattenDocumentSymbolHierarchy( result )
26622661

2663-
return _SymbolInfoListToGoTo( request_data, result )
2662+
return _LspSymbolListToGoTo( request_data, result )
26642663

26652664

26662665
def InitialHierarchy( self, request_data, args ):
@@ -3396,26 +3395,10 @@ def _LocationListToGoTo( request_data, positions ):
33963395
raise RuntimeError( 'Cannot jump to location' )
33973396

33983397

3399-
def _SymbolInfoListToGoTo( request_data, symbols ):
3398+
def _LspSymbolListToGoTo( request_data, symbols ):
34003399
"""Convert a list of LSP SymbolInformation into a YCM GoTo response"""
34013400

3402-
def BuildGoToLocationFromSymbol( symbol ):
3403-
location, line_value = _LspLocationToLocationAndDescription(
3404-
request_data,
3405-
symbol[ 'location' ] )
3406-
3407-
description = ( f'{ lsp.SYMBOL_KIND[ symbol[ "kind" ] ] }: '
3408-
f'{ symbol[ "name" ] }' )
3409-
3410-
goto = responses.BuildGoToResponseFromLocation( location,
3411-
description )
3412-
goto[ 'extra_data' ] = {
3413-
'kind': lsp.SYMBOL_KIND[ symbol[ 'kind' ] ],
3414-
'name': symbol[ 'name' ],
3415-
}
3416-
return goto
3417-
3418-
locations = [ BuildGoToLocationFromSymbol( s ) for s in
3401+
locations = [ _BuildGoToLocationFromSymbol( s, request_data ) for s in
34193402
sorted( symbols,
34203403
key = lambda s: ( s[ 'kind' ], s[ 'name' ] ) ) ]
34213404

@@ -3427,6 +3410,38 @@ def BuildGoToLocationFromSymbol( symbol ):
34273410
return locations
34283411

34293412

3413+
def _FlattenDocumentSymbolHierarchy( symbols ):
3414+
result = []
3415+
for s in symbols:
3416+
result.append( s )
3417+
if children := s.get( 'children' ):
3418+
result.extend( _FlattenDocumentSymbolHierarchy( children ) )
3419+
return result
3420+
3421+
3422+
def _BuildGoToLocationFromSymbol( symbol, request_data ):
3423+
""" Convert a LSP SymbolInfo or DocumentSymbol into a YCM GoTo response"""
3424+
lsp_location = symbol.get( 'location' )
3425+
if not lsp_location: # This is a DocumentSymbol
3426+
lsp_location = symbol
3427+
lsp_location[ 'uri' ] = lsp.FilePathToUri( request_data[ 'filepath' ] )
3428+
3429+
location, line_value = _LspLocationToLocationAndDescription(
3430+
request_data,
3431+
lsp_location )
3432+
3433+
description = ( f'{ lsp.SYMBOL_KIND[ symbol[ "kind" ] ] }: '
3434+
f'{ symbol[ "name" ] }' )
3435+
3436+
goto = responses.BuildGoToResponseFromLocation( location,
3437+
description )
3438+
goto[ 'extra_data' ] = {
3439+
'kind': lsp.SYMBOL_KIND[ symbol[ 'kind' ] ],
3440+
'name': symbol[ 'name' ],
3441+
}
3442+
return goto
3443+
3444+
34303445
def _LspLocationToLocationAndDescription( request_data,
34313446
location,
34323447
range_property = 'range' ):

ycmd/tests/language_server/language_server_completer_test.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
empty,
2424
ends_with,
2525
equal_to,
26+
instance_of,
2627
contains_exactly,
2728
has_entries,
2829
has_entry,
@@ -103,6 +104,71 @@ def _Check_Distance( point, start, end, expected ):
103104

104105

105106
class LanguageServerCompleterTest( TestCase ):
107+
@IsolatedYcmd()
108+
def test_LanguageServerCompleter_DocumentSymbol_Hierarchical( self, app ):
109+
completer = MockCompleter()
110+
completer._server_capabilities = { 'documentSymbolProvider': True }
111+
request_data = RequestWrap( BuildRequest( filepath = '/foo' ) )
112+
server_response = {
113+
'result': [
114+
{
115+
"name": "testy",
116+
"kind": 3,
117+
"range": {
118+
"start": { "line": 2, "character": 0 },
119+
"end": { "line": 12, "character": 1 }
120+
},
121+
"children": [
122+
{
123+
"name": "MainClass",
124+
"kind": 5,
125+
"range": {
126+
"start": { "line": 4, "character": 1 },
127+
"end": { "line": 11, "character": 2 }
128+
}
129+
}
130+
]
131+
},
132+
{
133+
"name": "other",
134+
"kind": 3,
135+
"range": {
136+
"start": { "line": 14, "character": 0 },
137+
"end": { "line": 15, "character": 1 }
138+
},
139+
"children": []
140+
}
141+
]
142+
}
143+
144+
with patch.object( completer, '_ServerIsInitialized', return_value = True ):
145+
with patch.object( completer.GetConnection(),
146+
'GetResponse',
147+
return_value = server_response ):
148+
document_outline = completer.GoToDocumentOutline( request_data )
149+
print( f'result: { document_outline }' )
150+
assert_that( document_outline, contains_exactly(
151+
has_entries( {
152+
'line_num': 15,
153+
'column_num': 1,
154+
'filepath': instance_of( str ),
155+
'description': 'Namespace: other',
156+
} ),
157+
has_entries( {
158+
'line_num': 3,
159+
'column_num': 1,
160+
'filepath': instance_of( str ),
161+
'description': 'Namespace: testy',
162+
} ),
163+
has_entries( {
164+
'line_num': 5,
165+
'column_num': 2,
166+
'filepath': instance_of( str ),
167+
'description': 'Class: MainClass',
168+
} )
169+
) )
170+
171+
106172
@IsolatedYcmd( { 'global_ycm_extra_conf':
107173
PathToTestFile( 'extra_confs', 'settings_extra_conf.py' ) } )
108174
def test_LanguageServerCompleter_ExtraConf_ServerReset( self, app ):

0 commit comments

Comments
 (0)