@@ -95,6 +95,18 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
95
95
/// Used to determine which toolchain to use for a given document.
96
96
private let toolchainRegistry : ToolchainRegistry
97
97
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
+
98
110
private var cachedTargetsForDocument = RequestCache < InverseSourcesRequest > ( )
99
111
100
112
private var cachedSourceKitOptions = RequestCache < SourceKitOptionsRequest > ( )
@@ -123,6 +135,7 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
123
135
) async {
124
136
self . fallbackBuildSystem = FallbackBuildSystem ( options: options. fallbackBuildSystemOrDefault)
125
137
self . toolchainRegistry = toolchainRegistry
138
+ self . options = options
126
139
self . projectRoot = buildSystemKind? . projectRoot
127
140
self . buildSystem = await BuiltInBuildSystemAdapter (
128
141
buildSystemKind: buildSystemKind,
@@ -132,16 +145,68 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
132
145
reloadPackageStatusCallback: reloadPackageStatusCallback,
133
146
messageHandler: self
134
147
)
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
+ }
136
164
}
137
165
138
166
package func filesDidChange( _ events: [ FileEvent ] ) async {
139
167
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
+
140
203
if let mainFilesProvider {
141
204
var mainFiles = await Set ( events. asyncFlatMap { await mainFilesProvider. mainFilesContainingFile ( $0. uri) } )
142
205
mainFiles. subtract ( events. map ( \. uri) )
143
- await self . filesDependenciesUpdated ( mainFiles)
206
+ filesWithUpdatedDependencies . formUnion ( mainFiles)
144
207
}
208
+
209
+ await self . filesDependenciesUpdatedDebouncer. scheduleCall ( filesWithUpdatedDependencies)
145
210
}
146
211
147
212
/// Implementation of `MessageHandler`, handling notifications from the build system.
@@ -404,6 +469,10 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
404
469
logMessageToIndexLog: @escaping @Sendable ( _ taskID: IndexTaskID , _ message: String ) -> Void
405
470
) async throws {
406
471
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
+ }
407
476
}
408
477
409
478
package func registerForChangeNotifications( for uri: DocumentURI , language: Language ) async {
@@ -471,7 +540,7 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate {
471
540
}
472
541
}
473
542
474
- extension BuildSystemManager : BuildSystemDelegate {
543
+ extension BuildSystemManager {
475
544
private func watchedFilesReferencing( mainFiles: Set < DocumentURI > ) -> Set < DocumentURI > {
476
545
return Set (
477
546
watchedFiles. compactMap { ( watchedFile, mainFileAndLanguage) in
@@ -484,22 +553,6 @@ extension BuildSystemManager: BuildSystemDelegate {
484
553
)
485
554
}
486
555
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
-
503
556
private func didChangeBuildTarget( notification: DidChangeBuildTargetNotification ) async {
504
557
// Every `DidChangeBuildTargetNotification` notification needs to invalidate the cache since the changed target
505
558
// might gained a source file.
0 commit comments