Skip to content

Commit 0ce532e

Browse files
authored
Merge pull request #1731 from bstaletic/workspace-dirs
[READY] Add support for didChangeWorkspaceFolders
2 parents f11e293 + 3de31d7 commit 0ce532e

File tree

19 files changed

+323
-129
lines changed

19 files changed

+323
-129
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ wouldn't usually know about. The value is a list of dictionaries containing:
285285
- `capabilities'`: Overrides the default LSP capabilities of ycmd.
286286
- If you enable `workspace/configuration` support, check the extra conf
287287
details, relevant to LSP servers.
288+
- `additional_workspace_dirs`: Specifies statically known workspaces that should
289+
be open on LSP server startup.
288290
- `triggerCharacters`: Override the LSP server's trigger characters for
289291
completion. This can be useful when the server obnoxiously requests completion
290292
on every character or for example on whitespace characters.

ycmd/completers/java/java_completer.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,6 @@ def GetCustomSubcommands( self ):
358358
'OrganizeImports': (
359359
lambda self, request_data, args: self.OrganizeImports( request_data )
360360
),
361-
'OpenProject': (
362-
lambda self, request_data, args: self._OpenProject( request_data, args )
363-
),
364361
'WipeWorkspace': (
365362
lambda self, request_data, args: self._WipeWorkspace( request_data,
366363
args )
@@ -404,6 +401,10 @@ def GetProjectDirectory( self, *args, **kwargs ):
404401
return self._java_project_dir
405402

406403

404+
def GetWorkspaceForFilepath( self, filepath ):
405+
return _FindProjectDir( os.path.dirname( filepath ) )
406+
407+
407408
def _WipeWorkspace( self, request_data, args ):
408409
with_config = False
409410
if len( args ) > 0 and '--with-config' in args:
@@ -414,25 +415,6 @@ def _WipeWorkspace( self, request_data, args ):
414415
wipe_config = with_config )
415416

416417

417-
def _OpenProject( self, request_data, args ):
418-
if len( args ) != 1:
419-
raise ValueError( "Usage: OpenProject <project directory>" )
420-
421-
project_directory = args[ 0 ]
422-
423-
# If the dir is not absolute, calculate it relative to the working dir of
424-
# the client (if supplied).
425-
if not os.path.isabs( project_directory ):
426-
if 'working_dir' not in request_data:
427-
raise ValueError( "Project directory must be absolute" )
428-
429-
project_directory = os.path.normpath( os.path.join(
430-
request_data[ 'working_dir' ],
431-
project_directory ) )
432-
433-
self._RestartServer( request_data, project_directory = project_directory )
434-
435-
436418
def _Reset( self ):
437419
if self._workspace_path and self._use_clean_workspace:
438420
try:

ycmd/completers/language_server/language_server_completer.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ def _ServerToClientRequest( self, request ):
620620
elif method == 'workspace/workspaceFolders':
621621
self.SendResponse(
622622
lsp.Accept( request,
623-
lsp.WorkspaceFolders( self._project_directory ) ) )
623+
lsp.WorkspaceFolders( *self._server_workspace_dirs ) ) )
624624
else: # method unknown - reject
625625
self.SendResponse( lsp.Reject( request, lsp.Errors.MethodNotFound ) )
626626
return
@@ -890,6 +890,7 @@ class LanguageServerCompleter( Completer ):
890890
- ConvertNotificationToMessage
891891
- GetCompleterName
892892
- GetProjectDirectory
893+
- GetWorkspaceForFilepath
893894
- GetProjectRootFiles
894895
- GetTriggerCharacters
895896
- GetDefaultNotificationHandler
@@ -1043,12 +1044,14 @@ def ServerReset( self ):
10431044
self._initialize_event = threading.Event()
10441045
self._on_initialize_complete_handlers = []
10451046
self._server_capabilities = None
1047+
self._workspace_notification_supported = False
10461048
self._is_completion_provider = False
10471049
self._resolve_completion_items = False
10481050
self._project_directory = None
10491051
self._settings = {}
10501052
self._extra_conf_dir = None
10511053
self._semantic_token_atlas = None
1054+
self._server_workspace_dirs = set()
10521055

10531056

10541057
def GetCompleterName( self ):
@@ -1076,6 +1079,7 @@ def _StartServerNoLock( self, request_data ):
10761079
self.GetCommandLine() )
10771080

10781081
self._project_directory = self.GetProjectDirectory( request_data )
1082+
self._server_workspace_dirs.add( self._project_directory )
10791083

10801084
if self._connection_type == 'tcp':
10811085
if self.GetCommandLine():
@@ -2147,6 +2151,14 @@ def _RefreshFileContentsUnderLock( self, file_name, contents, file_types ):
21472151
action )
21482152

21492153
if action == lsp.ServerFileState.OPEN_FILE:
2154+
# First check if we need to inform the server of a new workspace.
2155+
if self._workspace_notification_supported:
2156+
workspace_for_file = self.GetWorkspaceForFilepath( file_name )
2157+
if workspace_for_file not in self._server_workspace_dirs:
2158+
self._server_workspace_dirs.add( workspace_for_file )
2159+
msg = lsp.DidChangeWorkspaceFolders( workspace_for_file )
2160+
self.GetConnection().SendNotification( msg )
2161+
21502162
msg = lsp.DidOpenTextDocument( file_state, file_types, contents )
21512163

21522164
self.GetConnection().SendNotification( msg )
@@ -2283,7 +2295,7 @@ def GetProjectDirectory( self, request_data ):
22832295
- If the user specified 'project_directory' in their extra conf
22842296
'Settings', use that.
22852297
- try to find files from GetProjectRootFiles and use the
2286-
first directory from there
2298+
first directory from there. (From GetWorkspaceForFilepath)
22872299
- if there's an extra_conf file, use that directory
22882300
- otherwise if we know the client's cwd, use that
22892301
- otherwise use the directory of the file that we just opened
@@ -2298,20 +2310,36 @@ def GetProjectDirectory( self, request_data ):
22982310
return utils.AbsolutePath( self._settings[ 'project_directory' ],
22992311
self._extra_conf_dir )
23002312

2301-
project_root_files = self.GetProjectRootFiles()
2302-
if project_root_files:
2303-
for folder in utils.PathsToAllParentFolders( request_data[ 'filepath' ] ):
2304-
for root_file in project_root_files:
2305-
if os.path.isfile( os.path.join( folder, root_file ) ):
2306-
return folder
2313+
filepath = request_data[ 'filepath' ]
2314+
workspace_path = self.GetWorkspaceForFilepath( filepath, strict = True )
2315+
if workspace_path:
2316+
return workspace_path
23072317

23082318
if self._extra_conf_dir:
23092319
return self._extra_conf_dir
23102320

23112321
if 'working_dir' in request_data:
23122322
return request_data[ 'working_dir' ]
23132323

2314-
return os.path.dirname( request_data[ 'filepath' ] )
2324+
return os.path.dirname( filepath )
2325+
2326+
2327+
def GetWorkspaceForFilepath( self, filepath, strict = False ):
2328+
"""Return the workspace of the provided filepath. This could be a subproject
2329+
or a completely unrelated project to the root directory.
2330+
Like GetProjectDirectory, can be overridden by a concrete LSP completer.
2331+
By default we try to find the first parent directory that contains any file
2332+
mentioned in GetProjectRootFiles().
2333+
`strict` function argument was useful for allowing GetProjectDirectory to
2334+
reuse this implementation.
2335+
"""
2336+
project_root_files = self.GetProjectRootFiles()
2337+
if project_root_files:
2338+
for folder in utils.PathsToAllParentFolders( filepath ):
2339+
for root_file in project_root_files:
2340+
if os.path.isfile( os.path.join( folder, root_file ) ):
2341+
return folder
2342+
return None if strict else os.path.dirname( filepath )
23152343

23162344

23172345
def _SendInitialize( self, request_data ):
@@ -2333,10 +2361,15 @@ def _SendInitialize( self, request_data ):
23332361
# the settings on the Initialize request are somehow subtly different from
23342362
# the settings supplied in didChangeConfiguration, though it's not exactly
23352363
# clear how/where that is specified.
2364+
additional_workspace_dirs = self._settings.get(
2365+
'additional_workspace_dirs',
2366+
[] )
2367+
self._server_workspace_dirs.update( additional_workspace_dirs )
23362368
msg = lsp.Initialize( request_id,
23372369
self._project_directory,
23382370
self.ExtraCapabilities(),
2339-
self._settings.get( 'ls', {} ) )
2371+
self._settings.get( 'ls', {} ),
2372+
additional_workspace_dirs )
23402373

23412374
def response_handler( response, message ):
23422375
if message is None:
@@ -2382,6 +2415,9 @@ def _HandleInitializeInPollThread( self, response ):
23822415
when the initialize request receives a response."""
23832416
with self._server_info_mutex:
23842417
self._server_capabilities = response[ 'result' ][ 'capabilities' ]
2418+
self._workspace_notification_supported = (
2419+
_ServerSupportsWorkspaceFoldersChangeNotif(
2420+
self._server_capabilities ) )
23852421
self._resolve_completion_items = self._ShouldResolveCompletionItems()
23862422

23872423
if self._resolve_completion_items:
@@ -2925,6 +2961,8 @@ def ServerStateDescription():
29252961
ServerStateDescription() ),
29262962
responses.DebugInfoItem( 'Project Directory',
29272963
self._project_directory ),
2964+
responses.DebugInfoItem( 'Open Workspaces',
2965+
self._server_workspace_dirs ),
29282966
responses.DebugInfoItem(
29292967
'Settings',
29302968
json.dumps( self._settings.get( 'ls', {} ),
@@ -3565,6 +3603,12 @@ def DecodeModifiers( self, tokenModifiers ):
35653603
return tokens
35663604

35673605

3606+
def _ServerSupportsWorkspaceFoldersChangeNotif( server_capabilities ):
3607+
workspace = server_capabilities.get( 'workspace', {} )
3608+
workspace_folders = workspace.get( 'workspaceFolders', {} )
3609+
return _IsCapabilityProvided( workspace_folders, 'changeNotifications' )
3610+
3611+
35683612
def _IsCapabilityProvided( capabilities, query ):
35693613
capability = capabilities.get( query )
35703614
return bool( capability ) or capability == {}

ycmd/completers/language_server/language_server_protocol.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,11 @@ def BuildResponse( request, parameters ):
294294
return _BuildMessageData( message )
295295

296296

297-
def Initialize( request_id, project_directory, extra_capabilities, settings ):
297+
def Initialize( request_id,
298+
project_directory,
299+
extra_capabilities,
300+
settings,
301+
workspaces ):
298302
"""Build the Language Server initialize request"""
299303

300304
capabilities = {
@@ -393,7 +397,7 @@ def Initialize( request_id, project_directory, extra_capabilities, settings ):
393397
'rootUri': FilePathToUri( project_directory ),
394398
'initializationOptions': settings,
395399
'capabilities': UpdateDict( capabilities, extra_capabilities ),
396-
'workspaceFolders': WorkspaceFolders( project_directory ),
400+
'workspaceFolders': WorkspaceFolders( *workspaces ),
397401
} )
398402

399403

@@ -447,6 +451,15 @@ def ApplyEditResponse( request, applied ):
447451
return Accept( request, msg )
448452

449453

454+
def DidChangeWorkspaceFolders( new_folder ):
455+
return BuildNotification( 'workspace/didChangeWorkspaceFolders', {
456+
'event': {
457+
'removed': [],
458+
'added': WorkspaceFolders( new_folder )
459+
}
460+
} )
461+
462+
450463
def DidChangeWatchedFiles( path, kind ):
451464
return BuildNotification( 'workspace/didChangeWatchedFiles', {
452465
'changes': [ {

ycmd/tests/clangd/debug_info_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def test_DebugInfo_NotInitialized( self, app ):
5858
'key': 'Project Directory',
5959
'value': None,
6060
} ),
61+
has_entries( {
62+
'key': 'Open Workspaces',
63+
'value': has_items()
64+
} ),
6165
has_entries( {
6266
'key': 'Settings',
6367
'value': '{}',
@@ -95,6 +99,10 @@ def test_DebugInfo_Initialized( self, app ):
9599
'key': 'Project Directory',
96100
'value': PathToTestFile(),
97101
} ),
102+
has_entries( {
103+
'key': 'Open Workspaces',
104+
'value': has_items()
105+
} ),
98106
has_entries( {
99107
'key': 'Settings',
100108
'value': '{}',
@@ -134,6 +142,10 @@ def test_DebugInfo_ExtraConf_ReturningFlags( self, app ):
134142
'key': 'Project Directory',
135143
'value': PathToTestFile( 'extra_conf' ),
136144
} ),
145+
has_entries( {
146+
'key': 'Open Workspaces',
147+
'value': has_items()
148+
} ),
137149
has_entries( {
138150
'key': 'Settings',
139151
'value': '{}',
@@ -174,6 +186,10 @@ def test_DebugInfo_ExtraConf_NotReturningFlags( self, app ):
174186
'key': 'Project Directory',
175187
'value': PathToTestFile( 'extra_conf' ),
176188
} ),
189+
has_entries( {
190+
'key': 'Open Workspaces',
191+
'value': has_items()
192+
} ),
177193
has_entries( {
178194
'key': 'Settings',
179195
'value': '{}',
@@ -216,6 +232,10 @@ def test_DebugInfo_ExtraConf_Global( self, app ):
216232
'key': 'Project Directory',
217233
'value': PathToTestFile(),
218234
} ),
235+
has_entries( {
236+
'key': 'Open Workspaces',
237+
'value': has_items()
238+
} ),
219239
has_entries( {
220240
'key': 'Settings',
221241
'value': '{}',
@@ -259,6 +279,10 @@ def test_DebugInfo_ExtraConf_LocalOverGlobal( self, app ):
259279
'key': 'Project Directory',
260280
'value': PathToTestFile( 'extra_conf' ),
261281
} ),
282+
has_entries( {
283+
'key': 'Open Workspaces',
284+
'value': has_items()
285+
} ),
262286
has_entries( {
263287
'key': 'Settings',
264288
'value': '{}',
@@ -308,6 +332,10 @@ def test_DebugInfo_ExtraConf_Database( self, app ):
308332
'key': 'Project Directory',
309333
'value': tmp_dir,
310334
} ),
335+
has_entries( {
336+
'key': 'Open Workspaces',
337+
'value': has_items()
338+
} ),
311339
has_entries( {
312340
'key': 'Settings',
313341
'value': '{}',
@@ -365,6 +393,10 @@ def Settings( **kwargs ):
365393
'key': 'Project Directory',
366394
'value': tmp_dir,
367395
} ),
396+
has_entries( {
397+
'key': 'Open Workspaces',
398+
'value': has_items()
399+
} ),
368400
has_entries( {
369401
'key': 'Settings',
370402
'value': '{}',
@@ -419,6 +451,10 @@ def test_DebugInfo_ExtraConf_UseDatabaseOverGlobal( self, app ):
419451
'key': 'Project Directory',
420452
'value': tmp_dir,
421453
} ),
454+
has_entries( {
455+
'key': 'Open Workspaces',
456+
'value': has_items()
457+
} ),
422458
has_entries( {
423459
'key': 'Settings',
424460
'value': '{}',
@@ -459,6 +495,10 @@ def test_DebugInfo_ExtraConf_MacIncludeFlags( self, app ):
459495
'key': 'Project Directory',
460496
'value': PathToTestFile( 'extra_conf' ),
461497
} ),
498+
has_entries( {
499+
'key': 'Open Workspaces',
500+
'value': has_items()
501+
} ),
462502
has_entries( {
463503
'key': 'Settings',
464504
'value': '{}',

ycmd/tests/go/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def Wrapper( test_case_instance, *args, **kwargs ):
7373
return Decorator
7474

7575

76-
def PathToTestFile( *args ):
76+
def PathToTestFile( *args, module_dir = 'go_module' ):
7777
dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
7878
# GOPLS doesn't work if any parent directory is named "testdata"
79-
return os.path.join( dir_of_current_script, 'go_module', *args )
79+
return os.path.join( dir_of_current_script, module_dir, *args )

0 commit comments

Comments
 (0)