Skip to content

Commit facffef

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 facffef

File tree

2 files changed

+112
-3
lines changed

2 files changed

+112
-3
lines changed

ycmd/completers/language_server/language_server_completer.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,10 +2655,10 @@ 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 )
2661+
return _DocumentSymboListToGoTo( request_data, result )
26622662

26632663
return _SymbolInfoListToGoTo( request_data, result )
26642664

@@ -3427,6 +3427,49 @@ def BuildGoToLocationFromSymbol( symbol ):
34273427
return locations
34283428

34293429

3430+
def _FlattenDocumentSymbolHierarchy( symbols ):
3431+
result = []
3432+
for s in symbols:
3433+
partial_results = [ s ]
3434+
if s.get( 'children' ):
3435+
partial_results.extend(
3436+
_FlattenDocumentSymbolHierarchy( s[ 'children' ] ) )
3437+
result.extend( partial_results )
3438+
return result
3439+
3440+
3441+
def _DocumentSymboListToGoTo( request_data, symbols ):
3442+
"""Convert a list of LSP DocumentSymbol into a YCM GoTo response"""
3443+
3444+
def BuildGoToLocationFromSymbol( symbol ):
3445+
symbol[ 'uri' ] = lsp.FilePathToUri( request_data[ 'filepath' ] )
3446+
location, line_value = _LspLocationToLocationAndDescription(
3447+
request_data,
3448+
symbol )
3449+
3450+
description = ( f'{ lsp.SYMBOL_KIND[ symbol[ "kind" ] ] }: '
3451+
f'{ symbol[ "name" ] }' )
3452+
3453+
goto = responses.BuildGoToResponseFromLocation( location,
3454+
description )
3455+
goto[ 'extra_data' ] = {
3456+
'kind': lsp.SYMBOL_KIND[ symbol[ 'kind' ] ],
3457+
'name': symbol[ 'name' ],
3458+
}
3459+
return goto
3460+
3461+
locations = [ BuildGoToLocationFromSymbol( s ) for s in
3462+
sorted( symbols,
3463+
key = lambda s: ( s[ 'kind' ], s[ 'name' ] ) ) ]
3464+
3465+
if not locations:
3466+
raise RuntimeError( "Symbol not found" )
3467+
elif len( locations ) == 1:
3468+
return locations[ 0 ]
3469+
else:
3470+
return locations
3471+
3472+
34303473
def _LspLocationToLocationAndDescription( request_data,
34313474
location,
34323475
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)