@@ -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
}
@@ -252,13 +246,21 @@ extension BuildSystemManager {
252
246
}
253
247
254
248
extension BuildSystemManager : BuildSystemDelegate {
255
- public func fileBuildSettingsChanged( _ changedFiles: Set < DocumentURI > ) async {
256
- let changedWatchedFiles = changedFiles. flatMap ( { mainFile in
257
- self . watchedFiles. filter { $1. mainFile == mainFile } . keys
249
+ private func watchedFilesReferencing( mainFiles: Set < DocumentURI > ) -> Set < DocumentURI > {
250
+ return Set ( watchedFiles. compactMap { ( watchedFile, mainFileAndLanguage) in
251
+ if mainFiles. contains ( mainFileAndLanguage. mainFile) {
252
+ return watchedFile
253
+ } else {
254
+ return nil
255
+ }
258
256
} )
257
+ }
258
+
259
+ public func fileBuildSettingsChanged( _ changedFiles: Set < DocumentURI > ) async {
260
+ let changedWatchedFiles = watchedFilesReferencing ( mainFiles: changedFiles)
259
261
260
262
if !changedWatchedFiles. isEmpty, let delegate = self . _delegate {
261
- await delegate. fileBuildSettingsChanged ( Set ( changedWatchedFiles) )
263
+ await delegate. fileBuildSettingsChanged ( changedWatchedFiles)
262
264
}
263
265
}
264
266
@@ -272,10 +274,9 @@ extension BuildSystemManager: BuildSystemDelegate {
272
274
}
273
275
274
276
// Need to map the changed main files back into changed watch files.
275
- let changedWatchedFiles = self . watchedFiles. filter { changedFiles. contains ( $1. mainFile) }
276
- let newChangedFiles = Set ( changedWatchedFiles. map { $0. key } )
277
- if let delegate = self . _delegate, !newChangedFiles. isEmpty {
278
- await delegate. filesDependenciesUpdated ( newChangedFiles)
277
+ let changedWatchedFiles = watchedFilesReferencing ( mainFiles: changedFiles)
278
+ if let delegate, !changedWatchedFiles. isEmpty {
279
+ await delegate. filesDependenciesUpdated ( changedWatchedFiles)
279
280
}
280
281
}
281
282
@@ -294,28 +295,63 @@ extension BuildSystemManager: BuildSystemDelegate {
294
295
295
296
extension BuildSystemManager : MainFilesDelegate {
296
297
// FIXME: Consider debouncing/limiting this, seems to trigger often during a build.
298
+ /// Checks if there are any files in `mainFileAssociations` where the main file
299
+ /// that we have stored has changed.
300
+ ///
301
+ /// For all of these files, re-associate the file with the new main file and
302
+ /// inform the delegate that the build settings for it might have changed.
297
303
public func mainFilesChanged( ) async {
298
- let origWatched = self . watchedFiles
299
- self . watchedFiles = [ : ]
300
- var buildSettingsChanges = Set < DocumentURI > ( )
301
-
302
- for (uri, state) in origWatched {
303
- let mainFiles = self . _mainFilesProvider? . mainFilesContainingFile ( uri) ?? [ ]
304
- let newMainFile = chooseMainFile ( for: uri, previous: state. mainFile, from: mainFiles)
305
- let language = state. language
306
-
307
- self . watchedFiles [ uri] = ( newMainFile, language)
308
-
309
- if state. mainFile != newMainFile {
310
- log ( " main file for ' \( uri) ' changed old: ' \( state. mainFile) ' -> new: ' \( newMainFile) ' " , level: . info)
311
- await self . checkUnreferencedMainFile ( state. mainFile)
304
+ var changedMainFileAssociations : Set < DocumentURI > = [ ]
305
+ for (file, ( oldMainFile, language) ) in self . watchedFiles {
306
+ let newMainFile = self . mainFile ( for: file, useCache: false )
307
+ if newMainFile != oldMainFile {
308
+ self . watchedFiles [ file] = ( newMainFile, language)
309
+ changedMainFileAssociations. insert ( file)
310
+ }
311
+ }
312
312
313
- buildSettingsChanges. insert ( uri)
313
+ for file in changedMainFileAssociations {
314
+ guard let language = watchedFiles [ file] ? . language else {
315
+ continue
314
316
}
317
+ // Re-register for notifications of this file within the build system.
318
+ // This is the easiest way to make sure we are watching for build setting
319
+ // changes of the new main file and stop watching for build setting
320
+ // changes in the old main file if no other watched file depends on it.
321
+ await self . unregisterForChangeNotifications ( for: file)
322
+ await self . registerForChangeNotifications ( for: file, language: language)
315
323
}
316
324
317
- if let delegate = self . _delegate, !buildSettingsChanges. isEmpty {
318
- await delegate. fileBuildSettingsChanged ( buildSettingsChanges)
325
+ if let delegate, !changedMainFileAssociations. isEmpty {
326
+ await delegate. fileBuildSettingsChanged ( changedMainFileAssociations)
327
+ }
328
+ }
329
+
330
+ /// Return the main file that should be used to get build settings for `uri`.
331
+ ///
332
+ /// For Swift or normal C files, this will be the file itself. For header
333
+ /// files, we pick a main file that includes the header since header files
334
+ /// don't have build settings by themselves.
335
+ private func mainFile( for uri: DocumentURI , useCache: Bool = true ) -> DocumentURI {
336
+ if useCache, let mainFile = self . watchedFiles [ uri] ? . mainFile {
337
+ // Performance optimization: We did already compute the main file and have
338
+ // it cached. We can just return it.
339
+ return mainFile
340
+ }
341
+ guard let mainFilesProvider else {
342
+ return uri
343
+ }
344
+
345
+ let mainFiles = mainFilesProvider. mainFilesContainingFile ( uri)
346
+ if mainFiles. contains ( uri) {
347
+ // If the main files contain the file itself, prefer to use that one
348
+ return uri
349
+ } else if let mainFile = mainFiles. min ( by: { $0. pseudoPath < $1. pseudoPath } ) {
350
+ // Pick the lexicographically first main file if it exists.
351
+ // This makes sure that picking a main file is deterministic.
352
+ return mainFile
353
+ } else {
354
+ return uri
319
355
}
320
356
}
321
357
}
@@ -324,23 +360,6 @@ extension BuildSystemManager {
324
360
325
361
/// *For Testing* Returns the main file used for `uri`, if this is a registered file.
326
362
public func _cachedMainFile( for uri: DocumentURI ) -> DocumentURI ? {
327
- watchedFiles [ uri] ? . mainFile
328
- }
329
- }
330
-
331
- /// Choose a new main file for the given uri, preferring to use a previous main file if still
332
- /// available, to avoid thrashing the settings unnecessarily, and falling back to `uri` itself if
333
- /// there are no main files found at all.
334
- private func chooseMainFile(
335
- for uri: DocumentURI ,
336
- previous: DocumentURI ? = nil ,
337
- from mainFiles: Set < DocumentURI > ) -> DocumentURI
338
- {
339
- if let previous = previous, mainFiles. contains ( previous) {
340
- return previous
341
- } else if mainFiles. isEmpty || mainFiles. contains ( uri) {
342
- return uri
343
- } else {
344
- return mainFiles. first!
363
+ return self . watchedFiles [ uri] ? . mainFile
345
364
}
346
365
}
0 commit comments