Skip to content

Commit 0ed683b

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 0ed683b

File tree

2 files changed

+111
-3
lines changed

2 files changed

+111
-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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,71 @@ def _Check_Distance( point, start, end, expected ):
103103

104104

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

0 commit comments

Comments
 (0)