@@ -408,6 +408,10 @@ public actor SourceKitLSPServer {
408
408
/// request's task before handling any cancellation request for it.
409
409
private let cancellationMessageHandlingQueue = AsyncQueue < Serial > ( )
410
410
411
+ /// The queue on which all modifications of `uriToWorkspaceCache` happen. This means that the value of
412
+ /// `workspacesAndIsImplicit` and `uriToWorkspaceCache` can't change while executing a closure on `workspaceQueue`.
413
+ private let workspaceQueue = AsyncQueue < Serial > ( )
414
+
411
415
/// The connection to the editor.
412
416
public let client : Connection
413
417
@@ -432,14 +436,19 @@ public actor SourceKitLSPServer {
432
436
}
433
437
434
438
/// Caches which workspace a document with the given URI should be opened in.
435
- /// Must only be accessed from `queue`.
439
+ ///
440
+ /// - Important: Must only be modified from `workspaceQueue`. This means that the value of `uriToWorkspaceCache`
441
+ /// can't change while executing an operation on `workspaceQueue`.
436
442
private var uriToWorkspaceCache : [ DocumentURI : WeakWorkspace ] = [ : ]
437
443
438
444
/// The open workspaces.
439
445
///
440
446
/// Implicit workspaces are workspaces that weren't actually specified by the client during initialization or by a
441
447
/// `didChangeWorkspaceFolders` request. Instead, they were opened by sourcekit-lsp because a file could not be
442
448
/// handled by any of the open workspaces but one of the file's parent directories had handling capabilities for it.
449
+ ///
450
+ /// - Important: Must only be modified from `workspaceQueue`. This means that the value of `workspacesAndIsImplicit`
451
+ /// can't change while executing an operation on `workspaceQueue`.
443
452
private var workspacesAndIsImplicit : [ ( workspace: Workspace , isImplicit: Bool ) ] = [ ] {
444
453
didSet {
445
454
uriToWorkspaceCache = [ : ]
@@ -452,7 +461,9 @@ public actor SourceKitLSPServer {
452
461
453
462
@_spi ( Testing)
454
463
public func setWorkspaces( _ newValue: [ ( workspace: Workspace , isImplicit: Bool ) ] ) {
455
- self . workspacesAndIsImplicit = newValue
464
+ workspaceQueue. async {
465
+ self . workspacesAndIsImplicit = newValue
466
+ }
456
467
}
457
468
458
469
/// The requests that we are currently handling.
@@ -517,54 +528,58 @@ public actor SourceKitLSPServer {
517
528
}
518
529
519
530
public func workspaceForDocument( uri: DocumentURI ) async -> Workspace ? {
520
- if let cachedWorkspace = uriToWorkspaceCache [ uri] ? . value {
531
+ if let cachedWorkspace = self . uriToWorkspaceCache [ uri] ? . value {
521
532
return cachedWorkspace
522
533
}
523
534
524
- // Pick the workspace with the best FileHandlingCapability for this file.
525
- // If there is a tie, use the workspace that occurred first in the list.
526
- var bestWorkspace : ( workspace: Workspace ? , fileHandlingCapability: FileHandlingCapability ) = ( nil , . unhandled)
527
- for workspace in workspaces {
528
- let fileHandlingCapability = await workspace. buildSystemManager. fileHandlingCapability ( for: uri)
529
- if fileHandlingCapability > bestWorkspace. fileHandlingCapability {
530
- bestWorkspace = ( workspace, fileHandlingCapability)
535
+ // Execute the computation of the workspace on `workspaceQueue` to ensure that the file handling capabilities of the
536
+ // workspaces don't change during the computation. Otherwise, we could run into a race condition like the following:
537
+ // 1. We don't have an entry for file `a.swift` in `uriToWorkspaceCache` and start the computation
538
+ // 2. We find that the first workspace in `self.workspaces` can handle this file.
539
+ // 3. During the `await ... .fileHandlingCapability` for a second workspace the file handling capabilities for the
540
+ // first workspace change, meaning it can no longer handle the document. This resets `uriToWorkspaceCache`
541
+ // assuming that the URI to workspace relation will get re-computed.
542
+ // 4. But we then set `uriToWorkspaceCache[uri]` to the workspace found in step (2), caching an out-of-date result.
543
+ //
544
+ // Furthermore, the computation of the workspace for a URI can create a new implicit workspace, which modifies
545
+ // `workspacesAndIsImplicit` and which must only be modified on `workspaceQueue`.
546
+ return await self . workspaceQueue. async {
547
+ // Pick the workspace with the best FileHandlingCapability for this file.
548
+ // If there is a tie, use the workspace that occurred first in the list.
549
+ var bestWorkspace : ( workspace: Workspace ? , fileHandlingCapability: FileHandlingCapability ) = ( nil , . unhandled)
550
+ for workspace in self . workspaces {
551
+ let fileHandlingCapability = await workspace. buildSystemManager. fileHandlingCapability ( for: uri)
552
+ if fileHandlingCapability > bestWorkspace. fileHandlingCapability {
553
+ bestWorkspace = ( workspace, fileHandlingCapability)
554
+ }
531
555
}
532
- }
533
- if bestWorkspace. fileHandlingCapability < . handled {
534
- // We weren't able to handle the document with any of the known workspaces. See if any of the document's parent
535
- // directories contain a workspace that can handle the document.
536
- let rootUris = workspaces. map ( \. rootUri)
537
- if let workspace = await findWorkspaceCapableOfHandlingDocument ( at: uri) {
538
- if workspaces. map ( \. rootUri) != rootUris {
539
- // Workspaces was modified while running `findWorkspaceCapableOfHandlingDocument`, so we raced.
540
- // This is unlikely to happen unless the user opens many files that in sub-workspaces simultaneously.
541
- // Try again based on the new data. Very likely the workspace that can handle this document is now in
542
- // `workspaces` and we will be able to return it without having to search again.
543
- logger. debug ( " findWorkspaceCapableOfHandlingDocument raced with another workspace creation. Trying again. " )
544
- return await workspaceForDocument ( uri: uri)
556
+ if bestWorkspace. fileHandlingCapability < . handled {
557
+ // We weren't able to handle the document with any of the known workspaces. See if any of the document's parent
558
+ // directories contain a workspace that can handle the document.
559
+ if let workspace = await self . findWorkspaceCapableOfHandlingDocument ( at: uri) {
560
+ // Appending a workspace is fine and doesn't require checking if we need to re-open any documents because:
561
+ // - Any currently open documents that have FileHandlingCapability `.handled` will continue to be opened in
562
+ // their current workspace because it occurs further in front inside the workspace list
563
+ // - Any currently open documents that have FileHandlingCapability < `.handled` also went through this check
564
+ // and didn't find any parent workspace that was able to handle them. We assume that a workspace can only
565
+ // properly handle files within its root directory, so those files now also can't be handled by the new
566
+ // workspace.
567
+ logger. log ( " Opening implicit workspace at \( workspace. rootUri. forLogging) to handle \( uri. forLogging) " )
568
+ self . workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: true ) )
569
+ bestWorkspace = ( workspace, . handled)
545
570
}
546
- // Appending a workspace is fine and doesn't require checking if we need to re-open any documents because:
547
- // - Any currently open documents that have FileHandlingCapability `.handled` will continue to be opened in
548
- // their current workspace because it occurs further in front inside the workspace list
549
- // - Any currently open documents that have FileHandlingCapability < `.handled` also went through this check
550
- // and didn't find any parent workspace that was able to handle them. We assume that a workspace can only
551
- // properly handle files within its root directory, so those files now also can't be handled by the new
552
- // workspace.
553
- logger. log ( " Opening implicit workspace at \( workspace. rootUri. forLogging) to handle \( uri. forLogging) " )
554
- workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: true ) )
555
- bestWorkspace = ( workspace, . handled)
556
571
}
557
- }
558
- uriToWorkspaceCache [ uri ] = WeakWorkspace ( bestWorkspace. workspace)
559
- if let workspace = bestWorkspace . workspace {
560
- return workspace
561
- }
562
- if let workspace = workspaces . only {
563
- // Special handling: If there is only one workspace, open all files in it, even it it cannot handle the document .
564
- // This retains the behavior of SourceKit-LSP before it supported multiple workspaces.
565
- return workspace
566
- }
567
- return nil
572
+ self . uriToWorkspaceCache [ uri ] = WeakWorkspace ( bestWorkspace . workspace )
573
+ if let workspace = bestWorkspace. workspace {
574
+ return workspace
575
+ }
576
+ if let workspace = self . workspaces . only {
577
+ // Special handling: If there is only one workspace, open all files in it, even it it cannot handle the document.
578
+ // This retains the behavior of SourceKit-LSP before it supported multiple workspaces .
579
+ return workspace
580
+ }
581
+ return nil
582
+ } . valuePropagatingCancellation
568
583
}
569
584
570
585
/// Execute `notificationHandler` with the request as well as the workspace
@@ -1077,8 +1092,10 @@ extension SourceKitLSPServer: BuildSystemDelegate {
1077
1092
}
1078
1093
1079
1094
public func fileHandlingCapabilityChanged( ) {
1080
- logger. log ( " Resetting URI to workspace cache because file handling capability of a workspace changed " )
1081
- self . uriToWorkspaceCache = [ : ]
1095
+ workspaceQueue. async {
1096
+ logger. log ( " Resetting URI to workspace cache because file handling capability of a workspace changed " )
1097
+ self . uriToWorkspaceCache = [ : ]
1098
+ }
1082
1099
}
1083
1100
}
1084
1101
@@ -1184,47 +1201,43 @@ extension SourceKitLSPServer {
1184
1201
1185
1202
capabilityRegistry = CapabilityRegistry ( clientCapabilities: req. capabilities)
1186
1203
1187
- if let workspaceFolders = req. workspaceFolders {
1188
- self . workspacesAndIsImplicit += await workspaceFolders. asyncCompactMap {
1189
- guard let workspace = await self . createWorkspace ( $0) else {
1190
- return nil
1204
+ await workspaceQueue. async {
1205
+ if let workspaceFolders = req. workspaceFolders {
1206
+ self . workspacesAndIsImplicit += await workspaceFolders. asyncCompactMap {
1207
+ guard let workspace = await self . createWorkspace ( $0) else {
1208
+ return nil
1209
+ }
1210
+ return ( workspace: workspace, isImplicit: false )
1211
+ }
1212
+ } else if let uri = req. rootURI {
1213
+ let workspaceFolder = WorkspaceFolder ( uri: uri)
1214
+ if let workspace = await self . createWorkspace ( workspaceFolder) {
1215
+ self . workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: false ) )
1216
+ }
1217
+ } else if let path = req. rootPath {
1218
+ let workspaceFolder = WorkspaceFolder ( uri: DocumentURI ( URL ( fileURLWithPath: path) ) )
1219
+ if let workspace = await self . createWorkspace ( workspaceFolder) {
1220
+ self . workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: false ) )
1191
1221
}
1192
- return ( workspace: workspace, isImplicit: false )
1193
- }
1194
- } else if let uri = req. rootURI {
1195
- let workspaceFolder = WorkspaceFolder ( uri: uri)
1196
- if let workspace = await self . createWorkspace ( workspaceFolder) {
1197
- self . workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: false ) )
1198
- }
1199
- } else if let path = req. rootPath {
1200
- let workspaceFolder = WorkspaceFolder ( uri: DocumentURI ( URL ( fileURLWithPath: path) ) )
1201
- if let workspace = await self . createWorkspace ( workspaceFolder) {
1202
- self . workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: false ) )
1203
1222
}
1204
- }
1205
1223
1206
- if self . workspaces. isEmpty {
1207
- logger. error ( " no workspace found " )
1208
-
1209
- let workspace = await Workspace (
1210
- documentManager: self . documentManager,
1211
- rootUri: req. rootURI,
1212
- capabilityRegistry: self . capabilityRegistry!,
1213
- toolchainRegistry: self . toolchainRegistry,
1214
- buildSetup: self . options. buildSetup,
1215
- underlyingBuildSystem: nil ,
1216
- index: nil ,
1217
- indexDelegate: nil
1218
- )
1219
-
1220
- // Another workspace might have been added while we awaited the
1221
- // construction of the workspace above. If that race happened, just
1222
- // discard the workspace we created here since `workspaces` now isn't
1223
- // empty anymore.
1224
1224
if self . workspaces. isEmpty {
1225
+ logger. error ( " no workspace found " )
1226
+
1227
+ let workspace = await Workspace (
1228
+ documentManager: self . documentManager,
1229
+ rootUri: req. rootURI,
1230
+ capabilityRegistry: self . capabilityRegistry!,
1231
+ toolchainRegistry: self . toolchainRegistry,
1232
+ buildSetup: self . options. buildSetup,
1233
+ underlyingBuildSystem: nil ,
1234
+ index: nil ,
1235
+ indexDelegate: nil
1236
+ )
1237
+
1225
1238
self . workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: false ) )
1226
1239
}
1227
- }
1240
+ } . value
1228
1241
1229
1242
assert ( !self . workspaces. isEmpty)
1230
1243
for workspace in self . workspaces {
@@ -1552,21 +1565,23 @@ extension SourceKitLSPServer {
1552
1565
for docUri in self . documentManager. openDocuments {
1553
1566
preChangeWorkspaces [ docUri] = await self . workspaceForDocument ( uri: docUri)
1554
1567
}
1555
- if let removed = notification. event. removed {
1556
- self . workspacesAndIsImplicit. removeAll { workspace in
1557
- // Close all implicit workspaces as well because we could have opened a new explicit workspace that now contains
1558
- // files from a previous implicit workspace.
1559
- return workspace. isImplicit
1560
- || removed. contains ( where: { workspaceFolder in workspace. workspace. rootUri == workspaceFolder. uri } )
1568
+ await workspaceQueue. async {
1569
+ if let removed = notification. event. removed {
1570
+ self . workspacesAndIsImplicit. removeAll { workspace in
1571
+ // Close all implicit workspaces as well because we could have opened a new explicit workspace that now contains
1572
+ // files from a previous implicit workspace.
1573
+ return workspace. isImplicit
1574
+ || removed. contains ( where: { workspaceFolder in workspace. workspace. rootUri == workspaceFolder. uri } )
1575
+ }
1561
1576
}
1562
- }
1563
- if let added = notification. event. added {
1564
- let newWorkspaces = await added. asyncCompactMap { await self . createWorkspace ( $0) }
1565
- for workspace in newWorkspaces {
1566
- await workspace. buildSystemManager. setDelegate ( self )
1577
+ if let added = notification. event. added {
1578
+ let newWorkspaces = await added. asyncCompactMap { await self . createWorkspace ( $0) }
1579
+ for workspace in newWorkspaces {
1580
+ await workspace. buildSystemManager. setDelegate ( self )
1581
+ }
1582
+ self . workspacesAndIsImplicit += newWorkspaces. map { ( workspace: $0, isImplicit: false ) }
1567
1583
}
1568
- self . workspacesAndIsImplicit += newWorkspaces. map { ( workspace: $0, isImplicit: false ) }
1569
- }
1584
+ } . value
1570
1585
1571
1586
// For each document that has moved to a different workspace, close it in
1572
1587
// the old workspace and open it in the new workspace.
0 commit comments