@@ -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.
@@ -120,7 +122,6 @@ public actor BuildSystemManager {
120
122
121
123
public func filesDidChange( _ events: [ FileEvent ] ) async {
122
124
await self . buildSystem? . filesDidChange ( events)
123
- self . fallbackBuildSystem? . filesDidChange ( events)
124
125
}
125
126
}
126
127
@@ -182,31 +183,26 @@ extension BuildSystemManager {
182
183
for document: DocumentURI ,
183
184
language: Language
184
185
) async -> ( buildSettings: FileBuildSettings , isFallback: Bool ) ? {
185
- if let mainFile = mainFilesProvider? . mainFilesContainingFile ( document) . first {
186
- if let mainFileBuildSettings = await buildSettings ( for: mainFile, language: language) {
187
- return (
188
- buildSettings: mainFileBuildSettings. buildSettings. patching ( newFile: document. pseudoPath, originalFile: mainFile. pseudoPath) ,
189
- isFallback: mainFileBuildSettings. isFallback
190
- )
191
- }
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)
192
192
}
193
- return await buildSettings ( for : document , language : language )
193
+ return buildSettings
194
194
}
195
195
196
196
public func registerForChangeNotifications( for uri: DocumentURI , language: Language ) async {
197
197
log ( " registerForChangeNotifications( \( uri. pseudoPath) ) " )
198
- let mainFile : DocumentURI
199
-
200
- if let watchedFile = self . watchedFiles [ uri] {
201
- mainFile = watchedFile. mainFile
202
- } else {
203
- let mainFiles = self . _mainFilesProvider? . mainFilesContainingFile ( uri)
204
- mainFile = chooseMainFile ( for: uri, from: mainFiles ?? [ ] )
205
- self . watchedFiles [ uri] = ( mainFile, language)
206
- }
198
+ let mainFile = mainFile ( for: uri)
199
+ self . watchedFiles [ uri] = ( mainFile, language)
207
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.
208
205
await buildSystem? . registerForChangeNotifications ( for: mainFile, language: language)
209
- fallbackBuildSystem? . registerForChangeNotifications ( for: mainFile, language: language)
210
206
}
211
207
212
208
/// Return settings for `file` based on the `change` settings for `mainFile`.
@@ -233,34 +229,38 @@ extension BuildSystemManager {
233
229
return
234
230
}
235
231
self . watchedFiles [ uri] = nil
236
- await self . checkUnreferencedMainFile ( mainFile)
237
- }
238
232
239
- /// If the given main file is no longer referenced by any watched files,
240
- /// remove it and unregister it at the underlying build system.
241
- func checkUnreferencedMainFile( _ mainFile: DocumentURI ) async {
242
- if !self . watchedFiles. values. lazy. map ( { $0. mainFile } ) . contains ( mainFile) {
243
- // 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.
244
236
await self . buildSystem? . unregisterForChangeNotifications ( for: mainFile)
245
237
}
246
238
}
247
239
248
240
public func fileHandlingCapability( for uri: DocumentURI ) async -> FileHandlingCapability {
249
241
return max (
250
242
await buildSystem? . fileHandlingCapability ( for: uri) ?? . unhandled,
251
- fallbackBuildSystem? . fileHandlingCapability ( for : uri ) ?? . unhandled
243
+ fallbackBuildSystem != nil ? . fallback : . unhandled
252
244
)
253
245
}
254
246
}
255
247
256
248
extension BuildSystemManager : BuildSystemDelegate {
257
- public func fileBuildSettingsChanged( _ changedFiles: Set < DocumentURI > ) async {
258
- let changedWatchedFiles = changedFiles. flatMap ( { mainFile in
259
- 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
+ }
260
256
} )
257
+ }
258
+
259
+ public func fileBuildSettingsChanged( _ changedFiles: Set < DocumentURI > ) async {
260
+ let changedWatchedFiles = watchedFilesReferencing ( mainFiles: changedFiles)
261
261
262
262
if !changedWatchedFiles. isEmpty, let delegate = self . _delegate {
263
- await delegate. fileBuildSettingsChanged ( Set ( changedWatchedFiles) )
263
+ await delegate. fileBuildSettingsChanged ( changedWatchedFiles)
264
264
}
265
265
}
266
266
@@ -274,10 +274,9 @@ extension BuildSystemManager: BuildSystemDelegate {
274
274
}
275
275
276
276
// Need to map the changed main files back into changed watch files.
277
- let changedWatchedFiles = self . watchedFiles. filter { changedFiles. contains ( $1. mainFile) }
278
- let newChangedFiles = Set ( changedWatchedFiles. map { $0. key } )
279
- if let delegate = self . _delegate, !newChangedFiles. isEmpty {
280
- await delegate. filesDependenciesUpdated ( newChangedFiles)
277
+ let changedWatchedFiles = watchedFilesReferencing ( mainFiles: changedFiles)
278
+ if let delegate, !changedWatchedFiles. isEmpty {
279
+ await delegate. filesDependenciesUpdated ( changedWatchedFiles)
281
280
}
282
281
}
283
282
@@ -296,28 +295,63 @@ extension BuildSystemManager: BuildSystemDelegate {
296
295
297
296
extension BuildSystemManager : MainFilesDelegate {
298
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.
299
303
public func mainFilesChanged( ) async {
300
- let origWatched = self . watchedFiles
301
- self . watchedFiles = [ : ]
302
- var buildSettingsChanges = Set < DocumentURI > ( )
303
-
304
- for (uri, state) in origWatched {
305
- let mainFiles = self . _mainFilesProvider? . mainFilesContainingFile ( uri) ?? [ ]
306
- let newMainFile = chooseMainFile ( for: uri, previous: state. mainFile, from: mainFiles)
307
- let language = state. language
308
-
309
- self . watchedFiles [ uri] = ( newMainFile, language)
310
-
311
- if state. mainFile != newMainFile {
312
- log ( " main file for ' \( uri) ' changed old: ' \( state. mainFile) ' -> new: ' \( newMainFile) ' " , level: . info)
313
- 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
+ }
314
312
315
- buildSettingsChanges. insert ( uri)
313
+ for file in changedMainFileAssociations {
314
+ guard let language = watchedFiles [ file] ? . language else {
315
+ continue
316
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)
317
323
}
318
324
319
- if let delegate = self . _delegate, !buildSettingsChanges. isEmpty {
320
- 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
321
355
}
322
356
}
323
357
}
@@ -326,23 +360,6 @@ extension BuildSystemManager {
326
360
327
361
/// *For Testing* Returns the main file used for `uri`, if this is a registered file.
328
362
public func _cachedMainFile( for uri: DocumentURI ) -> DocumentURI ? {
329
- watchedFiles [ uri] ? . mainFile
330
- }
331
- }
332
-
333
- /// Choose a new main file for the given uri, preferring to use a previous main file if still
334
- /// available, to avoid thrashing the settings unnecessarily, and falling back to `uri` itself if
335
- /// there are no main files found at all.
336
- private func chooseMainFile(
337
- for uri: DocumentURI ,
338
- previous: DocumentURI ? = nil ,
339
- from mainFiles: Set < DocumentURI > ) -> DocumentURI
340
- {
341
- if let previous = previous, mainFiles. contains ( previous) {
342
- return previous
343
- } else if mainFiles. isEmpty || mainFiles. contains ( uri) {
344
- return uri
345
- } else {
346
- return mainFiles. first!
363
+ return self . watchedFiles [ uri] ? . mainFile
347
364
}
348
365
}
0 commit comments