@@ -86,7 +86,9 @@ extension MainFileStatus {
86
86
/// this class has a configurable `buildSettings` timeout which denotes the amount of time to give
87
87
/// the build system before applying the fallback arguments.
88
88
public actor BuildSystemManager {
89
- /// The set of watched files, along with their main file and language.
89
+ /// The files for which the delegate has requested change notifications, ie.
90
+ /// the files for which the delegate wants to get `filesDependenciesUpdated`
91
+ /// callbacks if the file's build settings.
90
92
var watchedFiles : [ DocumentURI : ( mainFile: DocumentURI , language: Language ) ] = [ : ]
91
93
92
94
/// The underlying primary build system.
@@ -181,29 +183,25 @@ extension BuildSystemManager {
181
183
for document: DocumentURI ,
182
184
language: Language
183
185
) async -> ( buildSettings: FileBuildSettings , isFallback: Bool ) ? {
184
- if let mainFile = mainFilesProvider? . mainFilesContainingFile ( document) . first {
185
- if let mainFileBuildSettings = await buildSettings ( for: mainFile, language: language) {
186
- return (
187
- buildSettings: mainFileBuildSettings. buildSettings. patching ( newFile: document. pseudoPath, originalFile: mainFile. pseudoPath) ,
188
- isFallback: mainFileBuildSettings. isFallback
189
- )
190
- }
186
+ let mainFile = mainFile ( for: document)
187
+ var buildSettings = await buildSettings ( for: mainFile, language: language)
188
+ if mainFile != document, let settings = buildSettings? . buildSettings {
189
+ // If the main file isn't the file itself, we need to patch the build settings
190
+ // to reference `document` instead of `mainFile`.
191
+ buildSettings? . buildSettings = settings. patching ( newFile: document. pseudoPath, originalFile: mainFile. pseudoPath)
191
192
}
192
- return await buildSettings ( for : document , language : language )
193
+ return buildSettings
193
194
}
194
195
195
196
public func registerForChangeNotifications( for uri: DocumentURI , language: Language ) async {
196
197
log ( " registerForChangeNotifications( \( uri. pseudoPath) ) " )
197
- let mainFile : DocumentURI
198
-
199
- if let watchedFile = self . watchedFiles [ uri] {
200
- mainFile = watchedFile. mainFile
201
- } else {
202
- let mainFiles = self . _mainFilesProvider? . mainFilesContainingFile ( uri)
203
- mainFile = chooseMainFile ( for: uri, from: mainFiles ?? [ ] )
204
- self . watchedFiles [ uri] = ( mainFile, language)
205
- }
198
+ let mainFile = mainFile ( for: uri)
199
+ self . watchedFiles [ uri] = ( mainFile, language)
206
200
201
+ // Register for change notifications of the main file in the underlying build
202
+ // system. That way, iff the main file changes, we will also notify the
203
+ // delegate about build setting changes of all header files that are based
204
+ // on that main file.
207
205
await buildSystem? . registerForChangeNotifications ( for: mainFile, language: language)
208
206
}
209
207
@@ -231,14 +229,10 @@ extension BuildSystemManager {
231
229
return
232
230
}
233
231
self . watchedFiles [ uri] = nil
234
- await self . checkUnreferencedMainFile ( mainFile)
235
- }
236
232
237
- /// If the given main file is no longer referenced by any watched files,
238
- /// remove it and unregister it at the underlying build system.
239
- func checkUnreferencedMainFile( _ mainFile: DocumentURI ) async {
240
- if !self . watchedFiles. values. lazy. map ( { $0. mainFile } ) . contains ( mainFile) {
241
- // This was the last reference to the main file. Remove it.
233
+ if watchedFilesReferencing ( mainFiles: [ mainFile] ) . isEmpty {
234
+ // Nobody is interested in this main file anymore.
235
+ // We are no longer interested in change notifications for it.
242
236
await self . buildSystem? . unregisterForChangeNotifications ( for: mainFile)
243
237
}
244
238
}
@@ -259,13 +253,21 @@ extension BuildSystemManager: BuildSystemDelegate {
259
253
}
260
254
}
261
255
262
- public func fileBuildSettingsChangedImpl( _ changedFiles: Set < DocumentURI > ) async {
263
- let changedWatchedFiles = changedFiles. flatMap ( { mainFile in
264
- self . watchedFiles. filter { $1. mainFile == mainFile } . keys
256
+ private func watchedFilesReferencing( mainFiles: Set < DocumentURI > ) -> Set < DocumentURI > {
257
+ return Set ( watchedFiles. compactMap { ( watchedFile, mainFileAndLanguage) in
258
+ if mainFiles. contains ( mainFileAndLanguage. mainFile) {
259
+ return watchedFile
260
+ } else {
261
+ return nil
262
+ }
265
263
} )
264
+ }
265
+
266
+ public func fileBuildSettingsChangedImpl( _ changedFiles: Set < DocumentURI > ) async {
267
+ let changedWatchedFiles = watchedFilesReferencing ( mainFiles: changedFiles)
266
268
267
269
if !changedWatchedFiles. isEmpty, let delegate = self . _delegate {
268
- await delegate. fileBuildSettingsChanged ( Set ( changedWatchedFiles) )
270
+ await delegate. fileBuildSettingsChanged ( changedWatchedFiles)
269
271
}
270
272
}
271
273
@@ -286,10 +288,9 @@ extension BuildSystemManager: BuildSystemDelegate {
286
288
}
287
289
288
290
// Need to map the changed main files back into changed watch files.
289
- let changedWatchedFiles = self . watchedFiles. filter { changedFiles. contains ( $1. mainFile) }
290
- let newChangedFiles = Set ( changedWatchedFiles. map { $0. key } )
291
- if let delegate = self . _delegate, !newChangedFiles. isEmpty {
292
- await delegate. filesDependenciesUpdated ( newChangedFiles)
291
+ let changedWatchedFiles = watchedFilesReferencing ( mainFiles: changedFiles)
292
+ if let delegate, !changedWatchedFiles. isEmpty {
293
+ await delegate. filesDependenciesUpdated ( changedWatchedFiles)
293
294
}
294
295
}
295
296
@@ -329,28 +330,63 @@ extension BuildSystemManager: MainFilesDelegate {
329
330
}
330
331
331
332
// FIXME: Consider debouncing/limiting this, seems to trigger often during a build.
333
+ /// Checks if there are any files in `mainFileAssociations` where the main file
334
+ /// that we have stored has changed.
335
+ ///
336
+ /// For all of these files, re-associate the file with the new main file and
337
+ /// inform the delegate that the build settings for it might have changed.
332
338
public func mainFilesChangedImpl( ) async {
333
- let origWatched = self . watchedFiles
334
- self . watchedFiles = [ : ]
335
- var buildSettingsChanges = Set < DocumentURI > ( )
336
-
337
- for (uri, state) in origWatched {
338
- let mainFiles = self . _mainFilesProvider? . mainFilesContainingFile ( uri) ?? [ ]
339
- let newMainFile = chooseMainFile ( for: uri, previous: state. mainFile, from: mainFiles)
340
- let language = state. language
341
-
342
- self . watchedFiles [ uri] = ( newMainFile, language)
343
-
344
- if state. mainFile != newMainFile {
345
- log ( " main file for ' \( uri) ' changed old: ' \( state. mainFile) ' -> new: ' \( newMainFile) ' " , level: . info)
346
- await self . checkUnreferencedMainFile ( state. mainFile)
339
+ var changedMainFileAssociations : Set < DocumentURI > = [ ]
340
+ for (file, ( oldMainFile, language) ) in self . watchedFiles {
341
+ let newMainFile = self . mainFile ( for: file, useCache: false )
342
+ if newMainFile != oldMainFile {
343
+ self . watchedFiles [ file] = ( newMainFile, language)
344
+ changedMainFileAssociations. insert ( file)
345
+ }
346
+ }
347
347
348
- buildSettingsChanges. insert ( uri)
348
+ for file in changedMainFileAssociations {
349
+ guard let language = watchedFiles [ file] ? . language else {
350
+ continue
349
351
}
352
+ // Re-register for notifications of this file within the build system.
353
+ // This is the easiest way to make sure we are watching for build setting
354
+ // changes of the new main file and stop watching for build setting
355
+ // changes in the old main file if no other watched file depends on it.
356
+ await self . unregisterForChangeNotifications ( for: file)
357
+ await self . registerForChangeNotifications ( for: file, language: language)
350
358
}
351
359
352
- if let delegate = self . _delegate, !buildSettingsChanges. isEmpty {
353
- await delegate. fileBuildSettingsChanged ( buildSettingsChanges)
360
+ if let delegate, !changedMainFileAssociations. isEmpty {
361
+ await delegate. fileBuildSettingsChanged ( changedMainFileAssociations)
362
+ }
363
+ }
364
+
365
+ /// Return the main file that should be used to get build settings for `uri`.
366
+ ///
367
+ /// For Swift or normal C files, this will be the file itself. For header
368
+ /// files, we pick a main file that includes the header since header files
369
+ /// don't have build settings by themselves.
370
+ private func mainFile( for uri: DocumentURI , useCache: Bool = true ) -> DocumentURI {
371
+ if useCache, let mainFile = self . watchedFiles [ uri] ? . mainFile {
372
+ // Performance optimization: We did already compute the main file and have
373
+ // it cached. We can just return it.
374
+ return mainFile
375
+ }
376
+ guard let mainFilesProvider else {
377
+ return uri
378
+ }
379
+
380
+ let mainFiles = mainFilesProvider. mainFilesContainingFile ( uri)
381
+ if mainFiles. contains ( uri) {
382
+ // If the main files contain the file itself, prefer to use that one
383
+ return uri
384
+ } else if let mainFile = mainFiles. sorted ( by: { $0. pseudoPath < $1. pseudoPath } ) . first {
385
+ // Pick the lexicographically first main file if it exists.
386
+ // This makes sure that picking a main file is deterministic.
387
+ return mainFile
388
+ } else {
389
+ return uri
354
390
}
355
391
}
356
392
}
@@ -359,23 +395,6 @@ extension BuildSystemManager {
359
395
360
396
/// *For Testing* Returns the main file used for `uri`, if this is a registered file.
361
397
public func _cachedMainFile( for uri: DocumentURI ) -> DocumentURI ? {
362
- watchedFiles [ uri] ? . mainFile
363
- }
364
- }
365
-
366
- /// Choose a new main file for the given uri, preferring to use a previous main file if still
367
- /// available, to avoid thrashing the settings unnecessarily, and falling back to `uri` itself if
368
- /// there are no main files found at all.
369
- private func chooseMainFile(
370
- for uri: DocumentURI ,
371
- previous: DocumentURI ? = nil ,
372
- from mainFiles: Set < DocumentURI > ) -> DocumentURI
373
- {
374
- if let previous = previous, mainFiles. contains ( previous) {
375
- return previous
376
- } else if mainFiles. isEmpty || mainFiles. contains ( uri) {
377
- return uri
378
- } else {
379
- return mainFiles. first!
398
+ return self . watchedFiles [ uri] ? . mainFile
380
399
}
381
400
}
0 commit comments