Skip to content

Commit 5fb71b7

Browse files
authored
Merge pull request #1465 from ahoppen/prepare-progress
Rewrite how “Preparing current file” is differentiated from the generic “Preparing targets”
2 parents 518aac4 + 60ce3b2 commit 5fb71b7

File tree

2 files changed

+53
-49
lines changed

2 files changed

+53
-49
lines changed

Sources/SKCore/Debouncer.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@ public actor Debouncer<Parameter> {
3333
private let makeCall: (Parameter) async -> Void
3434

3535
/// In the time between the call to `scheduleCall` and the call actually being committed (ie. in the time that the
36-
/// call can be debounced), the task that would commit the call (unless cancelled) and the parameter with which this
37-
/// call should be made.
38-
private var inProgressData: (Parameter, Task<Void, Never>)?
36+
/// call can be debounced), the task that would commit the call (unless cancelled), the parameter with which this
37+
/// call should be made and the time at which the call should be made. Keeping track of the time ensures that we don't
38+
/// indefinitely debounce if a new `scheduleCall` is made every 0.4s but we debounce for 0.5s.
39+
private var inProgressData: (Parameter, ContinuousClock.Instant, Task<Void, Never>)?
3940

4041
public init(
4142
debounceDuration: Duration,
4243
combineResults: @escaping (Parameter, Parameter) -> Parameter,
43-
_ makeCall: @escaping (Parameter) async -> Void
44+
_ makeCall: @Sendable @escaping (Parameter) async -> Void
4445
) {
4546
self.debounceDuration = debounceDuration
4647
self.combineParameters = combineResults
@@ -52,26 +53,28 @@ public actor Debouncer<Parameter> {
5253
/// `debounceDuration` after the second `scheduleCall` call.
5354
public func scheduleCall(_ parameter: Parameter) {
5455
var parameter = parameter
55-
if let (inProgressParameter, inProgressTask) = inProgressData {
56+
var targetDate = ContinuousClock.now + debounceDuration
57+
if let (inProgressParameter, inProgressTargetDate, inProgressTask) = inProgressData {
5658
inProgressTask.cancel()
5759
parameter = combineParameters(inProgressParameter, parameter)
60+
targetDate = inProgressTargetDate
5861
}
5962
let task = Task {
6063
do {
61-
try await Task.sleep(for: debounceDuration)
64+
try await Task.sleep(until: targetDate)
6265
try Task.checkCancellation()
6366
} catch {
6467
return
6568
}
6669
inProgressData = nil
6770
await makeCall(parameter)
6871
}
69-
inProgressData = (parameter, task)
72+
inProgressData = (parameter, ContinuousClock.now + debounceDuration, task)
7073
}
7174
}
7275

7376
extension Debouncer<Void> {
74-
public init(debounceDuration: Duration, _ makeCall: @escaping () async -> Void) {
77+
public init(debounceDuration: Duration, _ makeCall: @Sendable @escaping () async -> Void) {
7578
self.init(debounceDuration: debounceDuration, combineResults: { _, _ in }, makeCall)
7679
}
7780

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,6 @@ public enum IndexProgressStatus {
9898

9999
/// See `SemanticIndexManager.inProgressPrepareForEditorTask`.
100100
fileprivate struct InProgressPrepareForEditorTask {
101-
fileprivate enum State {
102-
case determiningCanonicalConfiguredTarget
103-
case preparingTarget
104-
}
105101
/// A unique ID that identifies the preparation task and is used to set
106102
/// `SemanticIndexManager.inProgressPrepareForEditorTask` to `nil` when the current in progress task finishes.
107103
let id: UUID
@@ -111,9 +107,21 @@ fileprivate struct InProgressPrepareForEditorTask {
111107

112108
/// The task that prepares the document. Should never be awaited and only be used to cancel the task.
113109
let task: Task<Void, Never>
110+
}
111+
112+
/// The reason why a target is being prepared. This is used to determine the `IndexProgressStatus`.
113+
fileprivate enum TargetPreparationPurpose: Comparable {
114+
/// We are preparing the target so we can index files in it.
115+
case forIndexing
116+
117+
/// We are preparing the target to provide semantic functionality in one of its files.
118+
case forEditorFunctionality
119+
}
114120

115-
/// Whether the task is currently determining the file's target or actually preparing the document.
116-
var state: State
121+
/// An entry in `SemanticIndexManager.inProgressPreparationTasks`.
122+
fileprivate struct InProgressPreparationTask {
123+
let task: OpaqueQueuedIndexTask
124+
let purpose: TargetPreparationPurpose
117125
}
118126

119127
/// Schedules index tasks and keeps track of the index status of files.
@@ -139,7 +147,7 @@ public final actor SemanticIndexManager {
139147
/// executing.
140148
///
141149
/// After a preparation task finishes, it is removed from this dictionary.
142-
private var inProgressPreparationTasks: [ConfiguredTarget: OpaqueQueuedIndexTask] = [:]
150+
private var inProgressPreparationTasks: [ConfiguredTarget: InProgressPreparationTask] = [:]
143151

144152
/// The files that are currently being index, either waiting for their target to be prepared, waiting for the index
145153
/// store update task to be scheduled in the task scheduler or which currently have an index store update running.
@@ -175,14 +183,14 @@ public final actor SemanticIndexManager {
175183

176184
/// A summary of the tasks that this `SemanticIndexManager` has currently scheduled or is currently indexing.
177185
public var progressStatus: IndexProgressStatus {
178-
if let inProgressPrepareForEditorTask, inProgressPrepareForEditorTask.state == .preparingTarget {
186+
if inProgressPreparationTasks.values.contains(where: { $0.purpose == .forEditorFunctionality }) {
179187
return .preparingFileForEditorFunctionality
180188
}
181189
if generateBuildGraphTask != nil {
182190
return .generatingBuildGraph
183191
}
184-
let preparationTasks = inProgressPreparationTasks.mapValues { queuedTask in
185-
return queuedTask.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled
192+
let preparationTasks = inProgressPreparationTasks.mapValues { inProgressTask in
193+
return inProgressTask.task.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled
186194
}
187195
let indexTasks = inProgressIndexTasks.mapValues { status in
188196
switch status {
@@ -385,38 +393,18 @@ public final actor SemanticIndexManager {
385393
let id = UUID()
386394
let task = Task(priority: priority) {
387395
await withLoggingScope("preparation") {
388-
defer {
389-
if inProgressPrepareForEditorTask?.id == id {
390-
inProgressPrepareForEditorTask = nil
391-
self.indexProgressStatusDidChange()
392-
}
393-
}
394-
// Should be kept in sync with `prepareFileForEditorFunctionality`
395-
guard let target = await buildSystemManager.canonicalConfiguredTarget(for: uri) else {
396-
return
397-
}
398-
if Task.isCancelled {
399-
return
400-
}
401-
if await preparationUpToDateTracker.isUpToDate(target) {
402-
// If the target is up-to-date, there is nothing to prepare.
403-
return
404-
}
396+
await prepareFileForEditorFunctionality(uri)
405397
if inProgressPrepareForEditorTask?.id == id {
406-
if inProgressPrepareForEditorTask?.state != .determiningCanonicalConfiguredTarget {
407-
logger.fault("inProgressPrepareForEditorTask is in unexpected state")
408-
}
409-
inProgressPrepareForEditorTask?.state = .preparingTarget
398+
inProgressPrepareForEditorTask = nil
399+
self.indexProgressStatusDidChange()
410400
}
411-
await self.prepare(targets: [target], priority: nil)
412401
}
413402
}
414403
inProgressPrepareForEditorTask?.task.cancel()
415404
inProgressPrepareForEditorTask = InProgressPrepareForEditorTask(
416405
id: id,
417406
document: uri,
418-
task: task,
419-
state: .determiningCanonicalConfiguredTarget
407+
task: task
420408
)
421409
self.indexProgressStatusDidChange()
422410
}
@@ -426,20 +414,22 @@ public final actor SemanticIndexManager {
426414
///
427415
/// If file's target is known to be up-to-date, this returns almost immediately.
428416
public func prepareFileForEditorFunctionality(_ uri: DocumentURI) async {
429-
// Should be kept in sync with `schedulePreparationForEditorFunctionality`.
430417
guard let target = await buildSystemManager.canonicalConfiguredTarget(for: uri) else {
431418
return
432419
}
433420
if Task.isCancelled {
434421
return
435422
}
436-
await self.prepare(targets: [target], priority: nil)
423+
if await preparationUpToDateTracker.isUpToDate(target) {
424+
// If the target is up-to-date, there is nothing to prepare.
425+
return
426+
}
427+
await self.prepare(targets: [target], purpose: .forEditorFunctionality, priority: nil)
437428
}
438429

439430
// MARK: - Helper functions
440431

441-
/// Prepare the given targets for indexing.
442-
private func prepare(targets: [ConfiguredTarget], priority: TaskPriority?) async {
432+
private func prepare(targets: [ConfiguredTarget], purpose: TargetPreparationPurpose, priority: TaskPriority?) async {
443433
// Perform a quick initial check whether the target is up-to-date, in which case we don't need to schedule a
444434
// preparation operation at all.
445435
// We will check the up-to-date status again in `PreparationTaskDescription.execute`. This ensures that if we
@@ -471,14 +461,25 @@ public final actor SemanticIndexManager {
471461
return
472462
}
473463
for target in targetsToPrepare {
474-
if self.inProgressPreparationTasks[target] == OpaqueQueuedIndexTask(task) {
464+
if self.inProgressPreparationTasks[target]?.task == OpaqueQueuedIndexTask(task) {
475465
self.inProgressPreparationTasks[target] = nil
476466
}
477467
}
478468
self.indexProgressStatusDidChange()
479469
}
480470
for target in targetsToPrepare {
481-
inProgressPreparationTasks[target] = OpaqueQueuedIndexTask(preparationTask)
471+
// If we are preparing the same target for indexing and editor functionality, pick editor functionality as the
472+
// purpose because it is more significant.
473+
let mergedPurpose =
474+
if let existingPurpose = inProgressPreparationTasks[target]?.purpose {
475+
max(existingPurpose, purpose)
476+
} else {
477+
purpose
478+
}
479+
inProgressPreparationTasks[target] = InProgressPreparationTask(
480+
task: OpaqueQueuedIndexTask(preparationTask),
481+
purpose: mergedPurpose
482+
)
482483
}
483484
await withTaskCancellationHandler {
484485
return await preparationTask.waitToFinish()
@@ -603,7 +604,7 @@ public final actor SemanticIndexManager {
603604
let preparationTaskID = UUID()
604605
let indexTask = Task(priority: priority) {
605606
// First prepare the targets.
606-
await prepare(targets: targetsBatch, priority: priority)
607+
await prepare(targets: targetsBatch, purpose: .forIndexing, priority: priority)
607608

608609
// And after preparation is done, index the files in the targets.
609610
await withTaskGroup(of: Void.self) { taskGroup in

0 commit comments

Comments
 (0)