Skip to content

Commit e1548a0

Browse files
authored
Merge pull request #841 from ahoppen/ahoppen/build-settings-pull
Keep track of build settings in `BuildSystemManager` instead of `SourceKitServer`
2 parents 8f859c5 + c642b37 commit e1548a0

14 files changed

+146
-545
lines changed

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,7 @@ public actor BuildServerBuildSystem: MessageHandler {
202202
/// about the changed build settings.
203203
private func buildSettingsChanged(for document: DocumentURI, settings: FileBuildSettings?) async {
204204
buildSettings[document] = settings
205-
if let settings {
206-
await self.delegate?.fileBuildSettingsChanged([document: .modified(settings)])
207-
} else {
208-
await self.delegate?.fileBuildSettingsChanged([document: .removedOrUnavailable])
209-
}
205+
await self.delegate?.fileBuildSettingsChanged([document])
210206
}
211207
}
212208

Sources/SKCore/BuildSystemDelegate.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ public protocol BuildSystemDelegate: AnyObject {
2121
func buildTargetsChanged(_ changes: [BuildTargetEvent]) async
2222

2323
/// Notify the delegate that the given files' build settings have changed.
24-
///
25-
/// The delegate should cache the new build settings for any of the given
26-
/// files that they are interested in.
27-
func fileBuildSettingsChanged(
28-
_ changedFiles: [DocumentURI: FileBuildSettingsChange]) async
24+
func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async
2925

3026
/// Notify the delegate that the dependencies of the given files have changed
3127
/// and that ASTs may need to be refreshed. If the given set is empty, assume

Sources/SKCore/BuildSystemManager.swift

Lines changed: 35 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ public actor BuildSystemManager {
8989
/// The set of watched files, along with their main file and language.
9090
var watchedFiles: [DocumentURI: (mainFile: DocumentURI, language: Language)] = [:]
9191

92-
/// Statuses for each main file, containing build settings from the build systems.
93-
var mainFileStatuses: [DocumentURI: MainFileStatus] = [:]
94-
9592
/// The underlying primary build system.
9693
let buildSystem: BuildSystem?
9794

@@ -148,16 +145,7 @@ extension BuildSystemManager {
148145
self.mainFilesProvider = mainFilesProvider
149146
}
150147

151-
/// Get the build settings for the given document, assuming it has the given
152-
/// language.
153-
///
154-
/// Returns `nil` if no build settings are available in the build system and
155-
/// no fallback build settings can be computed.
156-
///
157-
/// `isFallback` is `true` if the build settings couldn't be computed and
158-
/// fallback settings are used. These fallback settings are most likely not
159-
/// correct and provide limited semantic functionality.
160-
public func buildSettings(
148+
private func buildSettings(
161149
for document: DocumentURI,
162150
language: Language
163151
) async -> (buildSettings: FileBuildSettings, isFallback: Bool)? {
@@ -183,6 +171,28 @@ extension BuildSystemManager {
183171
}
184172
}
185173

174+
/// Returns the build settings for the given document.
175+
///
176+
/// If the document doesn't have builds settings by itself, eg. because it is
177+
/// a C header file, the build settings will be inferred from the primary main
178+
/// file of the document. In practice this means that we will compute the build
179+
/// settings of a C file that includes the header and replace any file
180+
/// references to that C file in the build settings by the header file.
181+
public func buildSettingsInferredFromMainFile(
182+
for document: DocumentURI,
183+
language: Language
184+
) 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+
}
192+
}
193+
return await buildSettings(for: document, language: language)
194+
}
195+
186196
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
187197
log("registerForChangeNotifications(\(uri.pseudoPath))")
188198
let mainFile: DocumentURI
@@ -195,13 +205,8 @@ extension BuildSystemManager {
195205
self.watchedFiles[uri] = (mainFile, language)
196206
}
197207

198-
let newStatus = await self.cachedStatusOrRegisterForSettings(for: mainFile, language: language)
199-
200-
if let mainChange = newStatus.buildSettingsChange,
201-
let delegate = self._delegate {
202-
let change = self.convert(change: mainChange, ofMainFile: mainFile, to: uri)
203-
await delegate.fileBuildSettingsChanged([uri: change])
204-
}
208+
await buildSystem?.registerForChangeNotifications(for: mainFile, language: language)
209+
fallbackBuildSystem?.registerForChangeNotifications(for: mainFile, language: language)
205210
}
206211

207212
/// Return settings for `file` based on the `change` settings for `mainFile`.
@@ -222,116 +227,6 @@ extension BuildSystemManager {
222227
}
223228
}
224229

225-
/// Handle a request for `FileBuildSettings` on `mainFile`.
226-
///
227-
/// Updates and returns the new `MainFileStatus` for `mainFile`.
228-
func cachedStatusOrRegisterForSettings(
229-
for mainFile: DocumentURI,
230-
language: Language
231-
) async -> MainFileStatus {
232-
// If we already have a status for the main file, use that.
233-
// Don't update any existing timeout.
234-
if let status = self.mainFileStatuses[mainFile] {
235-
return status
236-
}
237-
// This is a new `mainFile` that we need to handle. We need to fetch the
238-
// build settings.
239-
let newStatus: MainFileStatus
240-
if let buildSystem = self.buildSystem {
241-
// Register the timeout if it's applicable.
242-
if let fallback = self.fallbackBuildSystem {
243-
Task {
244-
try await Task.sleep(nanoseconds: UInt64(self.fallbackSettingsTimeout.nanoseconds()!))
245-
await self.handleFallbackTimer(for: mainFile, language: language, fallback)
246-
}
247-
}
248-
249-
// Intentionally register with the `BuildSystem` after setting the fallback to allow for
250-
// testing of the fallback system triggering before the `BuildSystem` can reply (e.g. if a
251-
// fallback time of 0 is specified).
252-
await buildSystem.registerForChangeNotifications(for: mainFile, language: language)
253-
254-
255-
newStatus = .waiting
256-
} else if let fallback = self.fallbackBuildSystem {
257-
// Only have a fallback build system. We consider it be a primary build
258-
// system that functions synchronously.
259-
if let settings = fallback.buildSettings(for: mainFile, language: language) {
260-
newStatus = .primary(settings)
261-
} else {
262-
newStatus = .unsupported
263-
}
264-
} else { // Don't have any build systems.
265-
newStatus = .unsupported
266-
}
267-
268-
if let status = self.mainFileStatuses[mainFile] {
269-
// Since we await above, another call to `cachedStatusOrRegisterForSettings`
270-
// might have set the main file status of `mainFile`. If this race happened,
271-
// return the value set by the concurrently executing function. This is safe
272-
// since all calls from this function are either side-effect free or
273-
// idempotent.
274-
return status
275-
}
276-
277-
self.mainFileStatuses[mainFile] = newStatus
278-
return newStatus
279-
}
280-
281-
/// Update and notify our delegate for the given main file changes if they are
282-
/// convertible into `FileBuildSettingsChange`.
283-
func updateAndNotifyStatuses(changes: [DocumentURI: MainFileStatus]) async {
284-
var changedWatchedFiles = [DocumentURI: FileBuildSettingsChange]()
285-
for (mainFile, status) in changes {
286-
let watches = self.watchedFiles.filter { $1.mainFile == mainFile }
287-
guard !watches.isEmpty else {
288-
// Possible notification after the file was unregistered. Ignore.
289-
continue
290-
}
291-
let prevStatus = self.mainFileStatuses[mainFile]
292-
self.mainFileStatuses[mainFile] = status
293-
294-
// It's possible that the command line arguments didn't change
295-
// (waitingFallback --> fallback), in that case we don't need to report a change.
296-
// If we were waiting though, we need to emit an initial change.
297-
guard prevStatus == .waiting || status.buildSettings != prevStatus?.buildSettings else {
298-
continue
299-
}
300-
if let change = status.buildSettingsChange {
301-
for watch in watches {
302-
let newChange =
303-
self.convert(change: change, ofMainFile: mainFile, to: watch.key)
304-
changedWatchedFiles[watch.key] = newChange
305-
}
306-
}
307-
}
308-
309-
if !changedWatchedFiles.isEmpty, let delegate = self._delegate {
310-
await delegate.fileBuildSettingsChanged(changedWatchedFiles)
311-
}
312-
}
313-
314-
/// Handle the fallback timer firing for a given `mainFile`.
315-
///
316-
/// Since this doesn't occur immediately it's possible that the `mainFile` is
317-
/// no longer referenced or is referenced by multiple watched files.
318-
func handleFallbackTimer(
319-
for mainFile: DocumentURI,
320-
language: Language,
321-
_ fallback: FallbackBuildSystem
322-
) async {
323-
// There won't be a current status if it's unreferenced by any watched file.
324-
// Similarly, if the status isn't `waiting` then there's nothing to do.
325-
guard let status = self.mainFileStatuses[mainFile], status == .waiting else {
326-
return
327-
}
328-
if let settings = fallback.buildSettings(for: mainFile, language: language) {
329-
await self.updateAndNotifyStatuses(changes: [mainFile: .waitingUsingFallback(settings)])
330-
} else {
331-
// Keep the status as waiting.
332-
}
333-
}
334-
335230
public func unregisterForChangeNotifications(for uri: DocumentURI) async {
336231
guard let mainFile = self.watchedFiles[uri]?.mainFile else {
337232
log("Unbalanced calls for registerForChangeNotifications and unregisterForChangeNotifications", level: .warning)
@@ -347,7 +242,6 @@ extension BuildSystemManager {
347242
if !self.watchedFiles.values.lazy.map({ $0.mainFile }).contains(mainFile) {
348243
// This was the last reference to the main file. Remove it.
349244
await self.buildSystem?.unregisterForChangeNotifications(for: mainFile)
350-
self.mainFileStatuses[mainFile] = nil
351245
}
352246
}
353247

@@ -361,41 +255,20 @@ extension BuildSystemManager {
361255

362256
extension BuildSystemManager: BuildSystemDelegate {
363257
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified
364-
public nonisolated func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) {
258+
public nonisolated func fileBuildSettingsChanged(_ changes: Set<DocumentURI>) {
365259
Task {
366260
await fileBuildSettingsChangedImpl(changes)
367261
}
368262
}
369263

370-
public func fileBuildSettingsChangedImpl(_ changes: [DocumentURI: FileBuildSettingsChange]) async {
371-
let statusChanges: [DocumentURI: MainFileStatus] =
372-
changes.reduce(into: [:]) { (result, entry) in
373-
let mainFile = entry.key
374-
let settingsChange = entry.value
375-
let watches = self.watchedFiles.filter { $1.mainFile == mainFile }
376-
guard let firstWatch = watches.first else {
377-
// Possible notification after the file was unregistered. Ignore.
378-
return
379-
}
380-
let newStatus: MainFileStatus
381-
382-
if let newSettings = settingsChange.newSettings {
383-
newStatus = settingsChange.isFallback ? .fallback(newSettings) : .primary(newSettings)
384-
} else if let fallback = self.fallbackBuildSystem {
385-
// FIXME: we need to stop threading the language everywhere, or we need the build system
386-
// itself to pass it in here. Or alternatively cache the fallback settings/language earlier?
387-
let language = firstWatch.value.language
388-
if let settings = fallback.buildSettings(for: mainFile, language: language) {
389-
newStatus = .fallback(settings)
390-
} else {
391-
newStatus = .unsupported
392-
}
393-
} else {
394-
newStatus = .unsupported
395-
}
396-
result[mainFile] = newStatus
264+
public func fileBuildSettingsChangedImpl(_ changedFiles: Set<DocumentURI>) async {
265+
let changedWatchedFiles = changedFiles.flatMap({ mainFile in
266+
self.watchedFiles.filter { $1.mainFile == mainFile }.keys
267+
})
268+
269+
if !changedWatchedFiles.isEmpty, let delegate = self._delegate {
270+
await delegate.fileBuildSettingsChanged(Set(changedWatchedFiles))
397271
}
398-
await self.updateAndNotifyStatuses(changes: statusChanges)
399272
}
400273

401274
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified
@@ -461,7 +334,7 @@ extension BuildSystemManager: MainFilesDelegate {
461334
public func mainFilesChangedImpl() async {
462335
let origWatched = self.watchedFiles
463336
self.watchedFiles = [:]
464-
var buildSettingsChanges = [DocumentURI: FileBuildSettingsChange]()
337+
var buildSettingsChanges = Set<DocumentURI>()
465338

466339
for (uri, state) in origWatched {
467340
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? []
@@ -474,12 +347,7 @@ extension BuildSystemManager: MainFilesDelegate {
474347
log("main file for '\(uri)' changed old: '\(state.mainFile)' -> new: '\(newMainFile)'", level: .info)
475348
await self.checkUnreferencedMainFile(state.mainFile)
476349

477-
let newStatus = await self.cachedStatusOrRegisterForSettings(
478-
for: newMainFile, language: language)
479-
if let change = newStatus.buildSettingsChange {
480-
let newChange = self.convert(change: change, ofMainFile: newMainFile, to: uri)
481-
buildSettingsChanges[uri] = newChange
482-
}
350+
buildSettingsChanges.insert(uri)
483351
}
484352
}
485353

@@ -495,11 +363,6 @@ extension BuildSystemManager {
495363
public func _cachedMainFile(for uri: DocumentURI) -> DocumentURI? {
496364
watchedFiles[uri]?.mainFile
497365
}
498-
499-
/// *For Testing* Returns the main file used for `uri`, if this is a registered file.
500-
public func _cachedMainFileSettings(for uri: DocumentURI) -> FileBuildSettings?? {
501-
mainFileStatuses[uri]?.buildSettings
502-
}
503366
}
504367

505368
/// Choose a new main file for the given uri, preferring to use a previous main file if still

Sources/SKCore/CompilationDatabaseBuildSystem.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,6 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
9393

9494
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
9595
self.watchedFiles[uri] = language
96-
97-
guard let delegate = self.delegate else { return }
98-
99-
let settings = self.settings(for: uri)
100-
await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
10196
}
10297

10398
/// We don't support change watching.
@@ -148,13 +143,9 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
148143
self.compdb = tryLoadCompilationDatabase(directory: projectRoot, self.fileSystem)
149144

150145
if let delegate = self.delegate {
151-
var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:]
146+
var changedFiles = Set<DocumentURI>()
152147
for (uri, _) in self.watchedFiles {
153-
if let settings = self.settings(for: uri) {
154-
changedFiles[uri] = FileBuildSettingsChange(settings)
155-
} else {
156-
changedFiles[uri] = .removedOrUnavailable
157-
}
148+
changedFiles.insert(uri)
158149
}
159150
await delegate.fileBuildSettingsChanged(changedFiles)
160151
}

Sources/SKCore/FallbackBuildSystem.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,7 @@ public final class FallbackBuildSystem: BuildSystem {
6060
}
6161

6262
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
63-
guard let delegate = self.delegate else { return }
64-
65-
let settings = self.buildSettings(for: uri, language: language)
66-
Task {
67-
await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
68-
}
63+
// Fallback build systems never change.
6964
}
7065

7166
/// We don't support change watching.

Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -242,16 +242,7 @@ extension SwiftPMWorkspace {
242242
})
243243

244244
guard let delegate = self.delegate else { return }
245-
var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:]
246-
for (uri, language) in self.watchedFiles {
247-
orLog {
248-
if let settings = try self.buildSettings(for: uri, language: language) {
249-
changedFiles[uri] = FileBuildSettingsChange(settings)
250-
} else {
251-
changedFiles[uri] = .removedOrUnavailable
252-
}
253-
}
254-
}
245+
let changedFiles = Set<DocumentURI>(self.watchedFiles.keys)
255246
await delegate.fileBuildSettingsChanged(changedFiles)
256247
await delegate.fileHandlingCapabilityChanged()
257248
}
@@ -307,20 +298,7 @@ extension SwiftPMWorkspace: SKCore.BuildSystem {
307298

308299
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
309300
assert(self.watchedFiles[uri] == nil, "Registered twice for change notifications of the same URI")
310-
guard let delegate = self.delegate else { return }
311301
self.watchedFiles[uri] = language
312-
313-
var settings: FileBuildSettings? = nil
314-
do {
315-
settings = try self.buildSettings(for: uri, language: language)
316-
} catch {
317-
log("error computing settings: \(error)")
318-
}
319-
if let settings = settings {
320-
await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
321-
} else {
322-
await delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable])
323-
}
324302
}
325303

326304
/// Unregister the given file for build-system level change notifications, such as command

0 commit comments

Comments
 (0)