Skip to content

Commit 022a6de

Browse files
committed
Handle filesDependenciesUpdated in BuildSystemManager instead of the build system
The implementation of which file’s dependencies have been updated is common across all build systems and thus build systems shouldn’t need to implement this logic. This also allows us to remove `BuildSystemDelegate`.
1 parent 9d94fe3 commit 022a6de

10 files changed

+76
-190
lines changed

Sources/BuildSystemIntegration/BuildServerBuildSystem.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,6 @@ package actor BuildServerBuildSystem: MessageHandler {
6767
package private(set) var indexDatabasePath: AbsolutePath?
6868
package private(set) var indexStorePath: AbsolutePath?
6969

70-
/// Delegate to handle any build system events.
71-
package weak var delegate: BuildSystemDelegate?
72-
73-
/// - Note: Needed to set the delegate from a different actor isolation context
74-
package func setDelegate(_ delegate: BuildSystemDelegate?) async {
75-
self.delegate = delegate
76-
}
77-
7870
package weak var messageHandler: BuiltInBuildSystemMessageHandler?
7971

8072
/// The build settings that have been received from the build server.

Sources/BuildSystemIntegration/BuildSystemDelegate.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,10 @@
99
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
1010
//
1111
//===----------------------------------------------------------------------===//
12+
1213
import BuildServerProtocol
1314
import LanguageServerProtocol
1415

15-
/// Handles build system events, such as file build settings changes.
16-
// FIXME: (BSP migration) The build system should exclusively communicate back to SourceKit-LSP using BSP and this protocol should be deleted.
17-
package protocol BuildSystemDelegate: AnyObject, Sendable {
18-
/// Notify the delegate that the dependencies of the given files have changed
19-
/// and that ASTs may need to be refreshed. If the given set is empty, assume
20-
/// that all watched files are affected.
21-
///
22-
/// The callee should refresh ASTs unless it is able to determine that a
23-
/// refresh is not necessary.
24-
func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) async
25-
}
26-
2716
/// Handles build system events, such as file build settings changes.
2817
package protocol BuildSystemManagerDelegate: AnyObject, Sendable {
2918
/// Notify the delegate that the given files' build settings have changed.

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
9595
/// Used to determine which toolchain to use for a given document.
9696
private let toolchainRegistry: ToolchainRegistry
9797

98+
private let options: SourceKitLSPOptions
99+
100+
/// Debounces calls to `delegate.filesDependenciesUpdated`.
101+
///
102+
/// This is to ensure we don't call `filesDependenciesUpdated` for the same file multiple time if the client does not
103+
/// debounce `workspace/didChangeWatchedFiles` and sends a separate notification eg. for every file within a target as
104+
/// it's being updated by a git checkout, which would cause other files within that target to receive a
105+
/// `fileDependenciesUpdated` call once for every updated file within the target.
106+
///
107+
/// Force-unwrapped optional because initializing it requires access to `self`.
108+
private var filesDependenciesUpdatedDebouncer: Debouncer<Set<DocumentURI>>! = nil
109+
98110
private var cachedTargetsForDocument = RequestCache<InverseSourcesRequest>()
99111

100112
private var cachedSourceKitOptions = RequestCache<SourceKitOptionsRequest>()
@@ -123,6 +135,7 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
123135
) async {
124136
self.fallbackBuildSystem = FallbackBuildSystem(options: options.fallbackBuildSystemOrDefault)
125137
self.toolchainRegistry = toolchainRegistry
138+
self.options = options
126139
self.projectRoot = buildSystemKind?.projectRoot
127140
self.buildSystem = await BuiltInBuildSystemAdapter(
128141
buildSystemKind: buildSystemKind,
@@ -132,16 +145,68 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
132145
reloadPackageStatusCallback: reloadPackageStatusCallback,
133146
messageHandler: self
134147
)
135-
await self.buildSystem?.underlyingBuildSystem.setDelegate(self)
148+
// The debounce duration of 500ms was chosen arbitrarily without any measurements.
149+
self.filesDependenciesUpdatedDebouncer = Debouncer(
150+
debounceDuration: .milliseconds(500),
151+
combineResults: { $0.union($1) }
152+
) {
153+
[weak self] (filesWithUpdatedDependencies) in
154+
guard let self, let delegate = await self.delegate else {
155+
logger.fault("Not calling filesDependenciesUpdated because no delegate exists in SwiftPMBuildSystem")
156+
return
157+
}
158+
let changedWatchedFiles = await self.watchedFilesReferencing(mainFiles: filesWithUpdatedDependencies)
159+
guard !changedWatchedFiles.isEmpty else {
160+
return
161+
}
162+
await delegate.filesDependenciesUpdated(changedWatchedFiles)
163+
}
136164
}
137165

138166
package func filesDidChange(_ events: [FileEvent]) async {
139167
await self.buildSystem?.send(BuildServerProtocol.DidChangeWatchedFilesNotification(changes: events))
168+
169+
var targetsWithUpdatedDependencies: Set<BuildTargetIdentifier> = []
170+
// If a Swift file within a target is updated, reload all the other files within the target since they might be
171+
// referring to a function in the updated file.
172+
let targetsWithChangedSwiftFiles =
173+
await events
174+
.filter { $0.uri.fileURL?.pathExtension == "swift" }
175+
.asyncFlatMap { await self.targets(for: $0.uri) }
176+
targetsWithUpdatedDependencies.formUnion(targetsWithChangedSwiftFiles)
177+
178+
// If a `.swiftmodule` file is updated, this means that we have performed a build / are
179+
// performing a build and files that depend on this module have updated dependencies.
180+
// We don't have access to the build graph from the SwiftPM API offered to SourceKit-LSP to figure out which files
181+
// depend on the updated module, so assume that all files have updated dependencies.
182+
// The file watching here is somewhat fragile as well because it assumes that the `.swiftmodule` files are being
183+
// written to a directory within the workspace root. This is not necessarily true if the user specifies a build
184+
// directory outside the source tree.
185+
// If we have background indexing enabled, this is not necessary because we call `fileDependenciesUpdated` when
186+
// preparation of a target finishes.
187+
if !options.backgroundIndexingOrDefault,
188+
events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" })
189+
{
190+
let targets = await orLog("Getting build targets") {
191+
try await self.buildTargets()
192+
}
193+
targetsWithUpdatedDependencies.formUnion(targets?.map(\.id) ?? [])
194+
}
195+
196+
var filesWithUpdatedDependencies: Set<DocumentURI> = []
197+
198+
await orLog("Getting source files in targets") {
199+
let sourceFiles = try await self.sourceFiles(in: Array(Set(targetsWithUpdatedDependencies)))
200+
filesWithUpdatedDependencies.formUnion(sourceFiles.flatMap(\.sources).map(\.uri))
201+
}
202+
140203
if let mainFilesProvider {
141204
var mainFiles = await Set(events.asyncFlatMap { await mainFilesProvider.mainFilesContainingFile($0.uri) })
142205
mainFiles.subtract(events.map(\.uri))
143-
await self.filesDependenciesUpdated(mainFiles)
206+
filesWithUpdatedDependencies.formUnion(mainFiles)
144207
}
208+
209+
await self.filesDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies)
145210
}
146211

147212
/// Implementation of `MessageHandler`, handling notifications from the build system.
@@ -404,6 +469,10 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
404469
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void
405470
) async throws {
406471
let _: VoidResponse? = try await buildSystem?.send(PrepareTargetsRequest(targets: targets))
472+
await orLog("Calling fileDependenciesUpdated") {
473+
let filesInPreparedTargets = try await self.sourceFiles(in: targets).flatMap(\.sources).map(\.uri)
474+
await filesDependenciesUpdatedDebouncer.scheduleCall(Set(filesInPreparedTargets))
475+
}
407476
}
408477

409478
package func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
@@ -471,7 +540,7 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
471540
}
472541
}
473542

474-
extension BuildSystemManager: BuildSystemDelegate {
543+
extension BuildSystemManager {
475544
private func watchedFilesReferencing(mainFiles: Set<DocumentURI>) -> Set<DocumentURI> {
476545
return Set(
477546
watchedFiles.compactMap { (watchedFile, mainFileAndLanguage) in
@@ -484,22 +553,6 @@ extension BuildSystemManager: BuildSystemDelegate {
484553
)
485554
}
486555

487-
package func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) async {
488-
// Empty changes --> assume everything has changed.
489-
guard !changedFiles.isEmpty else {
490-
if let delegate = self.delegate {
491-
await delegate.filesDependenciesUpdated(changedFiles)
492-
}
493-
return
494-
}
495-
496-
// Need to map the changed main files back into changed watch files.
497-
let changedWatchedFiles = watchedFilesReferencing(mainFiles: changedFiles)
498-
if let delegate, !changedWatchedFiles.isEmpty {
499-
await delegate.filesDependenciesUpdated(changedWatchedFiles)
500-
}
501-
}
502-
503556
private func didChangeBuildTarget(notification: DidChangeBuildTargetNotification) async {
504557
// Every `DidChangeBuildTargetNotification` notification needs to invalidate the cache since the changed target
505558
// might gained a source file.

Sources/BuildSystemIntegration/BuiltInBuildSystem.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,6 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable {
7575
/// The path to put the index database, if any.
7676
var indexDatabasePath: AbsolutePath? { get async }
7777

78-
/// Delegate to handle any build system events such as file build settings initial reports as well as changes.
79-
///
80-
/// The build system must not retain the delegate because the delegate can be the `BuildSystemManager`, which could
81-
/// result in a retain cycle `BuildSystemManager` -> `BuildSystem` -> `BuildSystemManager`.
82-
var delegate: BuildSystemDelegate? { get async }
83-
84-
/// Set the build system's delegate.
85-
///
86-
/// - Note: Needed so we can set the delegate from a different actor isolation
87-
/// context.
88-
func setDelegate(_ delegate: BuildSystemDelegate?) async
89-
9078
/// Whether the build system is capable of preparing a target for indexing, ie. if the `prepare` methods has been
9179
/// implemented.
9280
var supportsPreparation: Bool { get }

Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,9 @@ package actor CompilationDatabaseBuildSystem {
3838
}
3939
}
4040

41-
/// Delegate to handle any build system events.
42-
package weak var delegate: BuildSystemDelegate? = nil
43-
4441
/// Callbacks that should be called if the list of possible test files has changed.
4542
package var testFilesDidChangeCallbacks: [() async -> Void] = []
4643

47-
package func setDelegate(_ delegate: BuildSystemDelegate?) async {
48-
self.delegate = delegate
49-
}
50-
5144
package weak var messageHandler: BuiltInBuildSystemMessageHandler?
5245

5346
package let projectRoot: AbsolutePath

Sources/BuildSystemIntegration/FallbackBuildSystem.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,6 @@ package actor FallbackBuildSystem {
3838
)
3939
}()
4040

41-
/// Delegate to handle any build system events.
42-
package weak var delegate: BuildSystemDelegate? = nil
43-
44-
package func setDelegate(_ delegate: BuildSystemDelegate?) async {
45-
self.delegate = delegate
46-
}
47-
4841
package var indexStorePath: AbsolutePath? { return nil }
4942

5043
package var indexDatabasePath: AbsolutePath? { return nil }

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,6 @@ package actor SwiftPMBuildSystem {
173173
/// issues in SwiftPM.
174174
private let packageLoadingQueue = AsyncQueue<Serial>()
175175

176-
/// Delegate to handle any build system events.
177-
package weak var delegate: BuildSystemIntegration.BuildSystemDelegate? = nil
178-
179-
package func setDelegate(_ delegate: BuildSystemIntegration.BuildSystemDelegate?) async {
180-
self.delegate = delegate
181-
}
182-
183176
package weak var messageHandler: BuiltInBuildSystemMessageHandler?
184177

185178
/// This callback is informed when `reloadPackage` starts and ends executing.
@@ -188,16 +181,6 @@ package actor SwiftPMBuildSystem {
188181
/// Callbacks that should be called if the list of possible test files has changed.
189182
private var testFilesDidChangeCallbacks: [() async -> Void] = []
190183

191-
/// Debounces calls to `delegate.filesDependenciesUpdated`.
192-
///
193-
/// This is to ensure we don't call `filesDependenciesUpdated` for the same file multiple time if the client does not
194-
/// debounce `workspace/didChangeWatchedFiles` and sends a separate notification eg. for every file within a target as
195-
/// it's being updated by a git checkout, which would cause other files within that target to receive a
196-
/// `fileDependenciesUpdated` call once for every updated file within the target.
197-
///
198-
/// Force-unwrapped optional because initializing it requires access to `self`.
199-
private var fileDependenciesUpdatedDebouncer: Debouncer<Set<DocumentURI>>! = nil
200-
201184
/// Whether the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the
202185
/// user's build.
203186
private var isForIndexBuild: Bool { options.backgroundIndexingOrDefault }
@@ -378,19 +361,6 @@ package actor SwiftPMBuildSystem {
378361
)
379362

380363
self.reloadPackageStatusCallback = reloadPackageStatusCallback
381-
382-
// The debounce duration of 500ms was chosen arbitrarily without scientific research.
383-
self.fileDependenciesUpdatedDebouncer = Debouncer(
384-
debounceDuration: .milliseconds(500),
385-
combineResults: { $0.union($1) }
386-
) {
387-
[weak self] (filesWithUpdatedDependencies) in
388-
guard let delegate = await self?.delegate else {
389-
logger.fault("Not calling filesDependenciesUpdated because no delegate exists in SwiftPMBuildSystem")
390-
return
391-
}
392-
await delegate.filesDependenciesUpdated(filesWithUpdatedDependencies)
393-
}
394364
}
395365

396366
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
@@ -692,8 +662,6 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
692662
for target in request.targets {
693663
await orLog("Preparing") { try await prepare(singleTarget: target) }
694664
}
695-
let filesInPreparedTargets = request.targets.flatMap { self.targets[$0]?.buildTarget.sources ?? [] }
696-
await fileDependenciesUpdatedDebouncer.scheduleCall(Set(filesInPreparedTargets.map(DocumentURI.init)))
697665
return VoidResponse()
698666
}
699667

@@ -846,32 +814,6 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem {
846814
try await self.schedulePackageReload().value
847815
}
848816
}
849-
850-
var filesWithUpdatedDependencies: Set<DocumentURI> = []
851-
// If a Swift file within a target is updated, reload all the other files within the target since they might be
852-
// referring to a function in the updated file.
853-
for event in notification.changes {
854-
guard event.uri.fileURL?.pathExtension == "swift", let targets = fileToTargets[event.uri] else {
855-
continue
856-
}
857-
for target in targets {
858-
filesWithUpdatedDependencies.formUnion(self.targets[target]?.buildTarget.sources.map(DocumentURI.init) ?? [])
859-
}
860-
}
861-
862-
// If a `.swiftmodule` file is updated, this means that we have performed a build / are
863-
// performing a build and files that depend on this module have updated dependencies.
864-
// We don't have access to the build graph from the SwiftPM API offered to SourceKit-LSP to figure out which files
865-
// depend on the updated module, so assume that all files have updated dependencies.
866-
// The file watching here is somewhat fragile as well because it assumes that the `.swiftmodule` files are being
867-
// written to a directory within the workspace root. This is not necessarily true if the user specifies a build
868-
// directory outside the source tree.
869-
// If we have background indexing enabled, this is not necessary because we call `fileDependenciesUpdated` when
870-
// preparation of a target finishes.
871-
if !isForIndexBuild, notification.changes.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" }) {
872-
filesWithUpdatedDependencies.formUnion(self.fileToTargets.keys)
873-
}
874-
await self.fileDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies)
875817
}
876818

877819
package func sourceFiles() -> [SourceFileInfo] {

Sources/BuildSystemIntegration/TestBuildSystem.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,8 @@ package actor TestBuildSystem: BuiltInBuildSystem {
2828
package let indexStorePath: AbsolutePath? = nil
2929
package let indexDatabasePath: AbsolutePath? = nil
3030

31-
package weak var delegate: BuildSystemDelegate?
32-
3331
private weak var messageHandler: BuiltInBuildSystemMessageHandler?
3432

35-
package func setDelegate(_ delegate: BuildSystemDelegate?) async {
36-
self.delegate = delegate
37-
}
38-
3933
/// Build settings by file.
4034
private var buildSettingsByFile: [DocumentURI: SourceKitOptionsResponse] = [:]
4135

Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ final class BuildServerBuildSystemTests: XCTestCase {
7676
_fixLifetime(buildSystemDelegate)
7777
}
7878
let buildSystem = try await BuildServerBuildSystem(projectRoot: root, messageHandler: buildSystemDelegate)
79-
await buildSystem.setDelegate(buildSystemDelegate)
8079
_ = try await buildSystem.sourceKitOptions(
8180
request: SourceKitOptionsRequest(
8281
textDocument: TextDocumentIdentifier(uri: uri),
@@ -126,16 +125,11 @@ final class BuildServerBuildSystemTests: XCTestCase {
126125
}
127126
}
128127

129-
final class TestDelegate: BuildSystemDelegate, BuiltInBuildSystemMessageHandler {
128+
final class TestDelegate: BuiltInBuildSystemMessageHandler {
130129
let targetExpectations: [(DidChangeBuildTargetNotification, XCTestExpectation)]
131-
let dependenciesUpdatedExpectations: [DocumentURI: XCTestExpectation]
132130

133-
package init(
134-
targetExpectations: [(DidChangeBuildTargetNotification, XCTestExpectation)] = [],
135-
dependenciesUpdatedExpectations: [DocumentURI: XCTestExpectation] = [:]
136-
) {
131+
package init(targetExpectations: [(DidChangeBuildTargetNotification, XCTestExpectation)] = []) {
137132
self.targetExpectations = targetExpectations
138-
self.dependenciesUpdatedExpectations = dependenciesUpdatedExpectations
139133
}
140134

141135
func didChangeBuildTarget(notification: DidChangeBuildTargetNotification) {
@@ -146,12 +140,6 @@ final class TestDelegate: BuildSystemDelegate, BuiltInBuildSystemMessageHandler
146140
}
147141
}
148142

149-
package func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) {
150-
for uri in changedFiles {
151-
dependenciesUpdatedExpectations[uri]?.fulfill()
152-
}
153-
}
154-
155143
func sendRequestToSourceKitLSP<R: RequestType>(_ request: R) async throws -> R.Response {
156144
throw ResponseError.methodNotFound(R.method)
157145
}

0 commit comments

Comments
 (0)