@@ -19,6 +19,7 @@ import LanguageServerProtocol
19
19
import PackageLoading
20
20
import SKCore
21
21
import SKSupport
22
+ import SKSwiftPMWorkspace
22
23
import SourceKitD
23
24
24
25
import struct PackageModel. BuildFlags
@@ -407,20 +408,24 @@ public actor SourceKitServer {
407
408
/// Must only be accessed from `queue`.
408
409
private var uriToWorkspaceCache : [ DocumentURI : WeakWorkspace ] = [ : ]
409
410
410
- private( set) var workspaces : [ Workspace ] = [ ] {
411
+ /// The open workspaces.
412
+ ///
413
+ /// Implicit workspaces are workspaces that weren't actually specified by the client during initialization or by a
414
+ /// `didChangeWorkspaceFolders` request. Instead, they were opened by sourcekit-lsp because a file could not be
415
+ /// handled by any of the open workspaces but one of the file's parent directories had handling capabilities for it.
416
+ private var workspacesAndIsImplicit : [ ( workspace: Workspace , isImplicit: Bool ) ] = [ ] {
411
417
didSet {
412
418
uriToWorkspaceCache = [ : ]
413
419
}
414
420
}
415
421
416
- /// **Public for testing**
417
- public var _workspaces : [ Workspace ] {
418
- get {
419
- return self . workspaces
420
- }
421
- set {
422
- self . workspaces = newValue
423
- }
422
+ var workspaces : [ Workspace ] {
423
+ return workspacesAndIsImplicit. map ( \. workspace)
424
+ }
425
+
426
+ @_spi ( Testing)
427
+ public func setWorkspaces( _ newValue: [ ( workspace: Workspace , isImplicit: Bool ) ] ) {
428
+ self . workspacesAndIsImplicit = newValue
424
429
}
425
430
426
431
/// The requests that we are currently handling.
@@ -454,13 +459,37 @@ public actor SourceKitServer {
454
459
self . client = client
455
460
}
456
461
457
- public func workspaceForDocument( uri: DocumentURI ) async -> Workspace ? {
458
- if workspaces. count == 1 {
459
- // Special handling: If there is only one workspace, open all files in it.
460
- // This retains the behavior of SourceKit-LSP before it supported multiple workspaces.
461
- return workspaces. first
462
+ /// Search through all the parent directories of `uri` and check if any of these directories contain a workspace
463
+ /// capable of handling `uri`.
464
+ ///
465
+ /// The search will not consider any directory that is not a child of any of the directories in `rootUris`. This
466
+ /// prevents us from picking up a workspace that is outside of the folders that the user opened.
467
+ private func findWorkspaceCapableOfHandlingDocument( at uri: DocumentURI ) async -> Workspace ? {
468
+ guard var url = uri. fileURL? . deletingLastPathComponent ( ) else {
469
+ return nil
470
+ }
471
+ let projectRoots = await self . workspacesAndIsImplicit. filter { !$0. isImplicit } . asyncCompactMap {
472
+ await $0. workspace. buildSystemManager. projectRoot
473
+ }
474
+ let rootURLs = workspacesAndIsImplicit. filter { !$0. isImplicit } . compactMap { $0. workspace. rootUri? . fileURL }
475
+ while url. pathComponents. count > 1 && rootURLs. contains ( where: { $0. isPrefix ( of: url) } ) {
476
+ // Ignore workspaces that can't handle this file or that have the same project root as an existing workspace.
477
+ // The latter might happen if there is an existing SwiftPM workspace that hasn't been reloaded after a new file
478
+ // was added to it and thus currently doesn't know that it can handle that file. In that case, we shouldn't open
479
+ // a new workspace for the same root. Instead, the existing workspace's build system needs to be reloaded.
480
+ if let workspace = await self . createWorkspace ( WorkspaceFolder ( uri: DocumentURI ( url) ) ) ,
481
+ await workspace. buildSystemManager. fileHandlingCapability ( for: uri) == . handled,
482
+ let projectRoot = await workspace. buildSystemManager. projectRoot,
483
+ !projectRoots. contains ( projectRoot)
484
+ {
485
+ return workspace
486
+ }
487
+ url. deleteLastPathComponent ( )
462
488
}
489
+ return nil
490
+ }
463
491
492
+ public func workspaceForDocument( uri: DocumentURI ) async -> Workspace ? {
464
493
if let cachedWorkspace = uriToWorkspaceCache [ uri] ? . value {
465
494
return cachedWorkspace
466
495
}
@@ -474,8 +503,41 @@ public actor SourceKitServer {
474
503
bestWorkspace = ( workspace, fileHandlingCapability)
475
504
}
476
505
}
506
+ if bestWorkspace. fileHandlingCapability < . handled {
507
+ // We weren't able to handle the document with any of the known workspaces. See if any of the document's parent
508
+ // directories contain a workspace that can handle the document.
509
+ let rootUris = workspaces. map ( \. rootUri)
510
+ if let workspace = await findWorkspaceCapableOfHandlingDocument ( at: uri) {
511
+ if workspaces. map ( \. rootUri) != rootUris {
512
+ // Workspaces was modified while running `findWorkspaceCapableOfHandlingDocument`, so we raced.
513
+ // This is unlikely to happen unless the user opens many files that in sub-workspaces simultaneously.
514
+ // Try again based on the new data. Very likely the workspace that can handle this document is now in
515
+ // `workspaces` and we will be able to return it without having to search again.
516
+ logger. debug ( " findWorkspaceCapableOfHandlingDocument raced with another workspace creation. Trying again. " )
517
+ return await workspaceForDocument ( uri: uri)
518
+ }
519
+ // Appending a workspace is fine and doesn't require checking if we need to re-open any documents because:
520
+ // - Any currently open documents that have FileHandlingCapability `.handled` will continue to be opened in
521
+ // their current workspace because it occurs further in front inside the workspace list
522
+ // - Any currently open documents that have FileHandlingCapability < `.handled` also went through this check
523
+ // and didn't find any parent workspace that was able to handle them. We assume that a workspace can only
524
+ // properly handle files within its root directory, so those files now also can't be handled by the new
525
+ // workspace.
526
+ logger. log ( " Opening implicit workspace at \( workspace. rootUri. forLogging) to handle \( uri. forLogging) " )
527
+ workspacesAndIsImplicit. append ( ( workspace: workspace, isImplicit: true ) )
528
+ bestWorkspace = ( workspace, . handled)
529
+ }
530
+ }
477
531
uriToWorkspaceCache [ uri] = WeakWorkspace ( bestWorkspace. workspace)
478
- return bestWorkspace. workspace
532
+ if let workspace = bestWorkspace. workspace {
533
+ return workspace
534
+ }
535
+ if let workspace = workspaces. only {
536
+ // Special handling: If there is only one workspace, open all files in it, even it it cannot handle the document.
537
+ // This retains the behavior of SourceKit-LSP before it supported multiple workspaces.
538
+ return workspace
539
+ }
540
+ return nil
479
541
}
480
542
481
543
/// Execute `notificationHandler` with the request as well as the workspace
@@ -1095,16 +1157,21 @@ extension SourceKitServer {
1095
1157
capabilityRegistry = CapabilityRegistry ( clientCapabilities: req. capabilities)
1096
1158
1097
1159
if let workspaceFolders = req. workspaceFolders {
1098
- self . workspaces += await workspaceFolders. asyncCompactMap { await self . createWorkspace ( $0) }
1160
+ self . workspacesAndIsImplicit += await workspaceFolders. asyncCompactMap {
1161
+ guard let workspace = await self . createWorkspace ( $0) else {
1162
+ return nil
1163
+ }
1164
+ return ( workspace: workspace, isImplicit: false )
1165
+ }
1099
1166
} else if let uri = req. rootURI {
1100
1167
let workspaceFolder = WorkspaceFolder ( uri: uri)
1101
1168
if let workspace = await self . createWorkspace ( workspaceFolder) {
1102
- self . workspaces . append ( workspace)
1169
+ self . workspacesAndIsImplicit . append ( ( workspace: workspace , isImplicit : false ) )
1103
1170
}
1104
1171
} else if let path = req. rootPath {
1105
1172
let workspaceFolder = WorkspaceFolder ( uri: DocumentURI ( URL ( fileURLWithPath: path) ) )
1106
1173
if let workspace = await self . createWorkspace ( workspaceFolder) {
1107
- self . workspaces . append ( workspace)
1174
+ self . workspacesAndIsImplicit . append ( ( workspace: workspace , isImplicit : false ) )
1108
1175
}
1109
1176
}
1110
1177
@@ -1127,7 +1194,7 @@ extension SourceKitServer {
1127
1194
// discard the workspace we created here since `workspaces` now isn't
1128
1195
// empty anymore.
1129
1196
if self . workspaces. isEmpty {
1130
- self . workspaces . append ( workspace)
1197
+ self . workspacesAndIsImplicit . append ( ( workspace: workspace , isImplicit : false ) )
1131
1198
}
1132
1199
}
1133
1200
@@ -1447,18 +1514,19 @@ extension SourceKitServer {
1447
1514
preChangeWorkspaces [ docUri] = await self . workspaceForDocument ( uri: docUri)
1448
1515
}
1449
1516
if let removed = notification. event. removed {
1450
- self . workspaces. removeAll { workspace in
1451
- return removed. contains ( where: { workspaceFolder in
1452
- workspace. rootUri == workspaceFolder. uri
1453
- } )
1517
+ self . workspacesAndIsImplicit. removeAll { workspace in
1518
+ // Close all implicit workspaces as well because we could have opened a new explicit workspace that now contains
1519
+ // files from a previous implicit workspace.
1520
+ return workspace. isImplicit
1521
+ || removed. contains ( where: { workspaceFolder in workspace. workspace. rootUri == workspaceFolder. uri } )
1454
1522
}
1455
1523
}
1456
1524
if let added = notification. event. added {
1457
1525
let newWorkspaces = await added. asyncCompactMap { await self . createWorkspace ( $0) }
1458
1526
for workspace in newWorkspaces {
1459
1527
await workspace. buildSystemManager. setDelegate ( self )
1460
1528
}
1461
- self . workspaces . append ( contentsOf : newWorkspaces )
1529
+ self . workspacesAndIsImplicit += newWorkspaces . map { ( workspace : $0 , isImplicit : false ) }
1462
1530
}
1463
1531
1464
1532
// For each document that has moved to a different workspace, close it in
@@ -2402,3 +2470,12 @@ fileprivate extension Sequence where Element: Hashable {
2402
2470
return self . filter { set. insert ( $0) . inserted }
2403
2471
}
2404
2472
}
2473
+
2474
+ fileprivate extension URL {
2475
+ func isPrefix( of other: URL ) -> Bool {
2476
+ guard self . pathComponents. count < other. pathComponents. count else {
2477
+ return false
2478
+ }
2479
+ return other. pathComponents [ 0 ..< self . pathComponents. count] == self . pathComponents [ ... ]
2480
+ }
2481
+ }
0 commit comments