Skip to content

Commit 37f7540

Browse files
authored
Merge pull request #1656 from ahoppen/workspace-delegate
Make `Workspace` the delegate of a `BuildSystemManager`
2 parents e083124 + 57055d4 commit 37f7540

File tree

7 files changed

+88
-89
lines changed

7 files changed

+88
-89
lines changed

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
8787
var mainFilesProvider: MainFilesProvider?
8888

8989
/// Build system delegate that will receive notifications about setting changes, etc.
90-
var delegate: BuildSystemManagerDelegate?
90+
private weak var delegate: BuildSystemManagerDelegate?
9191

9292
/// The list of toolchains that are available.
9393
///
@@ -135,6 +135,11 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
135135

136136
package func filesDidChange(_ events: [FileEvent]) async {
137137
await self.buildSystem?.send(BuildServerProtocol.DidChangeWatchedFilesNotification(changes: events))
138+
if let mainFilesProvider {
139+
var mainFiles = await Set(events.asyncFlatMap { await mainFilesProvider.mainFilesContainingFile($0.uri) })
140+
mainFiles.subtract(events.map(\.uri))
141+
await self.filesDependenciesUpdated(mainFiles)
142+
}
138143
}
139144

140145
/// Implementation of `MessageHandler`, handling notifications from the build system.

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 10 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -842,67 +842,6 @@ extension SourceKitLSPServer: MessageHandler {
842842
}
843843
}
844844

845-
// MARK: - Build System Delegate
846-
847-
extension SourceKitLSPServer: BuildSystemManagerDelegate {
848-
private func affectedOpenDocumentsForChangeSet(
849-
_ changes: Set<DocumentURI>,
850-
_ documentManager: DocumentManager
851-
) -> Set<DocumentURI> {
852-
// An empty change set is treated as if all open files have been modified.
853-
guard !changes.isEmpty else {
854-
return documentManager.openDocuments
855-
}
856-
return documentManager.openDocuments.intersection(changes)
857-
}
858-
859-
/// Handle a build settings change notification from the `BuildSystem`.
860-
/// This has two primary cases:
861-
/// - Initial settings reported for a given file, now we can fully open it
862-
/// - Changed settings for an already open file
863-
package func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
864-
for uri in changedFiles {
865-
guard self.documentManager.openDocuments.contains(uri) else {
866-
continue
867-
}
868-
869-
guard let service = await self.workspaceForDocument(uri: uri)?.documentService(for: uri) else {
870-
continue
871-
}
872-
873-
await service.documentUpdatedBuildSettings(uri)
874-
}
875-
}
876-
877-
/// Handle a dependencies updated notification from the `BuildSystem`.
878-
/// We inform the respective language services as long as the given file is open
879-
/// (not queued for opening).
880-
package func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) async {
881-
// Split the changedFiles into the workspaces they belong to.
882-
// Then invoke affectedOpenDocumentsForChangeSet for each workspace with its affected files.
883-
let changedFilesAndWorkspace = await changedFiles.asyncMap {
884-
return (uri: $0, workspace: await self.workspaceForDocument(uri: $0))
885-
}
886-
for workspace in self.workspaces {
887-
let changedFilesForWorkspace = Set(changedFilesAndWorkspace.filter({ $0.workspace === workspace }).map(\.uri))
888-
if changedFilesForWorkspace.isEmpty {
889-
continue
890-
}
891-
for uri in self.affectedOpenDocumentsForChangeSet(changedFilesForWorkspace, self.documentManager) {
892-
logger.log("Dependencies updated for opened file \(uri.forLogging)")
893-
if let service = workspace.documentService(for: uri) {
894-
await service.documentDependenciesUpdated(uri)
895-
}
896-
}
897-
}
898-
}
899-
900-
package func fileHandlingCapabilityChanged() {
901-
logger.log("Updating URI to workspace because file handling capability of a workspace changed")
902-
self.scheduleUpdateOfUriToWorkspace()
903-
}
904-
}
905-
906845
extension SourceKitLSPServer {
907846
nonisolated func logMessageToIndexLog(taskID: IndexTaskID, message: String) {
908847
var message: Substring = message[...]
@@ -980,6 +919,10 @@ extension SourceKitLSPServer {
980919
},
981920
reloadPackageStatusCallback: { [weak self] status in
982921
await self?.reloadPackageStatusCallback(status)
922+
},
923+
fileHandlingCapabilityChanged: { [weak self] in
924+
logger.log("Updating URI to workspace because file handling capability of a workspace changed")
925+
await self?.scheduleUpdateOfUriToWorkspace()
983926
}
984927
)
985928
if options.backgroundIndexingOrDefault, workspace.semanticIndexManager == nil,
@@ -1111,6 +1054,10 @@ extension SourceKitLSPServer {
11111054
},
11121055
reloadPackageStatusCallback: { [weak self] status in
11131056
await self?.reloadPackageStatusCallback(status)
1057+
},
1058+
fileHandlingCapabilityChanged: { [weak self] in
1059+
logger.log("Updating URI to workspace because file handling capability of a workspace changed")
1060+
await self?.scheduleUpdateOfUriToWorkspace()
11141061
}
11151062
)
11161063

@@ -1119,9 +1066,6 @@ extension SourceKitLSPServer {
11191066
}.value
11201067

11211068
assert(!self.workspaces.isEmpty)
1122-
for workspace in self.workspaces {
1123-
await workspace.buildSystemManager.setDelegate(self)
1124-
}
11251069

11261070
return InitializeResult(
11271071
capabilities: await self.serverCapabilities(
@@ -1340,9 +1284,6 @@ extension SourceKitLSPServer {
13401284
for workspace in self.workspaces {
13411285
await workspace.buildSystemManager.setMainFilesProvider(nil)
13421286
workspace.closeIndex()
1343-
1344-
// Break retain cycle with the BSM.
1345-
await workspace.buildSystemManager.setDelegate(nil)
13461287
}
13471288
}
13481289

@@ -1541,9 +1482,6 @@ extension SourceKitLSPServer {
15411482
)
15421483
}
15431484
}
1544-
for workspace in newWorkspaces {
1545-
await workspace.buildSystemManager.setDelegate(self)
1546-
}
15471485
self.workspacesAndIsImplicit += newWorkspaces.map { (workspace: $0, isImplicit: false) }
15481486
}
15491487
}.value
@@ -1940,7 +1878,7 @@ extension SourceKitLSPServer {
19401878
== canonicalOriginatorLocation
19411879
}
19421880

1943-
var locations = try await symbols.asyncMap { (symbol) -> [Location] in
1881+
var locations = try await symbols.asyncFlatMap { (symbol) -> [Location] in
19441882
var locations: [Location]
19451883
if let bestLocalDeclaration = symbol.bestLocalDeclaration,
19461884
!(symbol.isDynamic ?? true),
@@ -1978,7 +1916,7 @@ extension SourceKitLSPServer {
19781916
}
19791917

19801918
return locations
1981-
}.flatMap { $0 }
1919+
}
19821920

19831921
// Remove any duplicate locations. We might end up with duplicate locations when performing a definition request
19841922
// on eg. `MyStruct()` when no explicit initializer is declared. In this case we get two symbol infos, one for the

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ extension SwiftLanguageService {
417417
}
418418

419419
package func documentUpdatedBuildSettings(_ uri: DocumentURI) async {
420+
guard (try? documentManager.openDocuments.contains(uri)) ?? false else {
421+
return
422+
}
420423
// Close and re-open the document internally to inform sourcekitd to update the compile command. At the moment
421424
// there's no better way to do this.
422425
// Schedule the document re-open in the SourceKit-LSP server. This ensures that the re-open happens exclusively with
@@ -425,6 +428,9 @@ extension SwiftLanguageService {
425428
}
426429

427430
package func documentDependenciesUpdated(_ uri: DocumentURI) async {
431+
guard (try? documentManager.openDocuments.contains(uri)) ?? false else {
432+
return
433+
}
428434
await orLog("Sending dependencyUpdated request to sourcekitd") {
429435
let req = sourcekitd.dictionary([
430436
keys.request: requests.dependencyUpdated

Sources/SourceKitLSP/Workspace.swift

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fileprivate func firstNonNil<T>(
4848
/// "initialize" request has been made.
4949
///
5050
/// Typically a workspace is contained in a root directory.
51-
package final class Workspace: Sendable {
51+
package final class Workspace: Sendable, BuildSystemManagerDelegate {
5252

5353
/// The root directory of the workspace.
5454
package let rootUri: DocumentURI?
@@ -73,9 +73,6 @@ package final class Workspace: Sendable {
7373
/// The index that syntactically scans the workspace for tests.
7474
let syntacticTestIndex = SyntacticTestIndex()
7575

76-
/// Documents open in the SourceKitLSPServer. This may include open documents from other workspaces.
77-
private let documentManager: DocumentManager
78-
7976
/// Language service for an open document, if available.
8077
private let documentService: ThreadSafeBox<[DocumentURI: LanguageService]> = ThreadSafeBox(initialValue: [:])
8178

@@ -85,8 +82,10 @@ package final class Workspace: Sendable {
8582
/// `nil` if background indexing is not enabled.
8683
let semanticIndexManager: SemanticIndexManager?
8784

85+
/// A callback that should be called when the file handling capability of this workspace changes.
86+
private let fileHandlingCapabilityChangedCallback: @Sendable () async -> Void
87+
8888
private init(
89-
documentManager: DocumentManager,
9089
rootUri: DocumentURI?,
9190
capabilityRegistry: CapabilityRegistry,
9291
options: SourceKitLSPOptions,
@@ -97,14 +96,15 @@ package final class Workspace: Sendable {
9796
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
9897
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void,
9998
indexTasksWereScheduled: @escaping @Sendable (Int) -> Void,
100-
indexProgressStatusDidChange: @escaping @Sendable () -> Void
99+
indexProgressStatusDidChange: @escaping @Sendable () -> Void,
100+
fileHandlingCapabilityChanged: @escaping @Sendable () async -> Void
101101
) async {
102-
self.documentManager = documentManager
103102
self.rootUri = rootUri
104103
self.capabilityRegistry = capabilityRegistry
105104
self.options = options
106105
self._uncheckedIndex = ThreadSafeBox(initialValue: uncheckedIndex)
107106
self.buildSystemManager = buildSystemManager
107+
self.fileHandlingCapabilityChangedCallback = fileHandlingCapabilityChanged
108108
if options.backgroundIndexingOrDefault, let uncheckedIndex, await buildSystemManager.supportsPreparation {
109109
self.semanticIndexManager = SemanticIndexManager(
110110
index: uncheckedIndex,
@@ -153,7 +153,8 @@ package final class Workspace: Sendable {
153153
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void,
154154
indexTasksWereScheduled: @Sendable @escaping (Int) -> Void,
155155
indexProgressStatusDidChange: @Sendable @escaping () -> Void,
156-
reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void
156+
reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void,
157+
fileHandlingCapabilityChanged: @Sendable @escaping () async -> Void
157158
) async {
158159
let buildSystemManager = await BuildSystemManager(
159160
buildSystemKind: buildSystemKind,
@@ -216,7 +217,6 @@ package final class Workspace: Sendable {
216217
await buildSystemManager.setMainFilesProvider(UncheckedIndex(index))
217218

218219
await self.init(
219-
documentManager: documentManager,
220220
rootUri: rootUri,
221221
capabilityRegistry: capabilityRegistry,
222222
options: options,
@@ -227,8 +227,10 @@ package final class Workspace: Sendable {
227227
indexTaskScheduler: indexTaskScheduler,
228228
logMessageToIndexLog: logMessageToIndexLog,
229229
indexTasksWereScheduled: indexTasksWereScheduled,
230-
indexProgressStatusDidChange: indexProgressStatusDidChange
230+
indexProgressStatusDidChange: indexProgressStatusDidChange,
231+
fileHandlingCapabilityChanged: fileHandlingCapabilityChanged
231232
)
233+
await buildSystemManager.setDelegate(self)
232234
}
233235

234236
package static func forTesting(
@@ -238,7 +240,6 @@ package final class Workspace: Sendable {
238240
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>
239241
) async -> Workspace {
240242
return await Workspace(
241-
documentManager: DocumentManager(),
242243
rootUri: nil,
243244
capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()),
244245
options: options,
@@ -249,7 +250,8 @@ package final class Workspace: Sendable {
249250
indexTaskScheduler: indexTaskScheduler,
250251
logMessageToIndexLog: { _, _ in },
251252
indexTasksWereScheduled: { _ in },
252-
indexProgressStatusDidChange: {}
253+
indexProgressStatusDidChange: {},
254+
fileHandlingCapabilityChanged: {}
253255
)
254256
}
255257

@@ -291,6 +293,32 @@ package final class Workspace: Sendable {
291293
return newLanguageService
292294
}
293295
}
296+
297+
/// Handle a build settings change notification from the `BuildSystem`.
298+
/// This has two primary cases:
299+
/// - Initial settings reported for a given file, now we can fully open it
300+
/// - Changed settings for an already open file
301+
package func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
302+
for uri in changedFiles {
303+
await self.documentService(for: uri)?.documentUpdatedBuildSettings(uri)
304+
}
305+
}
306+
307+
/// Handle a dependencies updated notification from the `BuildSystem`.
308+
/// We inform the respective language services as long as the given file is open
309+
/// (not queued for opening).
310+
package func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) async {
311+
for uri in changedFiles {
312+
logger.log("Dependencies updated for opened file \(uri.forLogging)")
313+
if let service = documentService(for: uri) {
314+
await service.documentDependenciesUpdated(uri)
315+
}
316+
}
317+
}
318+
319+
package func fileHandlingCapabilityChanged() async {
320+
await self.fileHandlingCapabilityChangedCallback()
321+
}
294322
}
295323

296324
/// Wrapper around a workspace that isn't being retained.

Sources/SwiftExtensions/Sequence+AsyncMap.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ extension Sequence {
2525
return result
2626
}
2727

28+
/// Just like `Sequence.flatMap` but allows an `async` transform function.
29+
package func asyncFlatMap<SegmentOfResult: Sequence>(
30+
@_inheritActorContext _ transform: @Sendable (Element) async throws -> SegmentOfResult
31+
) async rethrows -> [SegmentOfResult.Element] {
32+
var result: [SegmentOfResult.Element] = []
33+
result.reserveCapacity(self.underestimatedCount)
34+
35+
for element in self {
36+
result += try await transform(element)
37+
}
38+
39+
return result
40+
}
41+
2842
/// Just like `Sequence.compactMap` but allows an `async` transform function.
2943
package func asyncCompactMap<T>(
3044
@_inheritActorContext _ transform: @Sendable (Element) async throws -> T?

Tests/SourceKitLSPTests/BuildSystemTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ final class BuildSystemTests: XCTestCase {
6565
)
6666

6767
await server.setWorkspaces([(workspace: workspace, isImplicit: false)])
68-
await workspace.buildSystemManager.setDelegate(server)
68+
await workspace.buildSystemManager.setDelegate(workspace)
6969
}
7070

7171
override func tearDown() {

Tests/SourceKitLSPTests/DependencyTrackingTests.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class DependencyTrackingTests: XCTestCase {
4040
usePullDiagnostics: false
4141
)
4242

43-
let (libBUri, _) = try project.openDocument("LibB.swift")
43+
_ = try project.openDocument("LibB.swift")
4444

4545
let initialDiags = try await project.testClient.nextDiagnosticsNotification()
4646
// Semantic analysis: expect module import error.
@@ -58,7 +58,14 @@ final class DependencyTrackingTests: XCTestCase {
5858

5959
try await SwiftPMTestProject.build(at: project.scratchDirectory)
6060

61-
await project.testClient.server.filesDependenciesUpdated([libBUri])
61+
project.testClient.send(
62+
DidChangeWatchedFilesNotification(
63+
changes:
64+
FileManager.default.findFiles(withExtension: "swiftmodule", in: project.scratchDirectory).map {
65+
FileEvent(uri: DocumentURI($0), type: .created)
66+
}
67+
)
68+
)
6269

6370
let updatedDiags = try await project.testClient.nextDiagnosticsNotification()
6471
// Semantic analysis: no more errors expected, import should resolve since we built.
@@ -102,7 +109,8 @@ final class DependencyTrackingTests: XCTestCase {
102109
let contents = "int libX(int value);"
103110
try contents.write(to: generatedHeaderURL, atomically: true, encoding: .utf8)
104111

105-
await project.testClient.server.filesDependenciesUpdated([mainUri])
112+
let workspace = try await unwrap(project.testClient.server.workspaceForDocument(uri: mainUri))
113+
await workspace.filesDependenciesUpdated([mainUri])
106114

107115
let updatedDiags = try await project.testClient.nextDiagnosticsNotification()
108116
// No more errors expected, import should resolve since we the generated header file

0 commit comments

Comments
 (0)