Skip to content

Commit 24dfbd5

Browse files
authored
Merge pull request #1356 from ahoppen/progress-indicator-current-file-preparation
Show work done progress while a source file is being prepared for editor functionality
2 parents aa356cb + ecacd7b commit 24dfbd5

File tree

7 files changed

+131
-84
lines changed

7 files changed

+131
-84
lines changed

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,38 @@ public enum IndexTaskStatus: Comparable {
6464
case executing
6565
}
6666

67+
/// The current index status that should be displayed to the editor.
68+
///
69+
/// In reality, these status are not exclusive. Eg. the index might be preparing one target for editor functionality,
70+
/// re-generating the build graph and indexing files at the same time. To avoid showing too many concurrent status
71+
/// messages to the user, we only show the highest priority task.
72+
public enum IndexProgressStatus {
73+
case preparingFileForEditorFunctionality
74+
case generatingBuildGraph
75+
case indexing(preparationTasks: [ConfiguredTarget: IndexTaskStatus], indexTasks: [DocumentURI: IndexTaskStatus])
76+
case upToDate
77+
78+
public func merging(with other: IndexProgressStatus) -> IndexProgressStatus {
79+
switch (self, other) {
80+
case (_, .preparingFileForEditorFunctionality), (.preparingFileForEditorFunctionality, _):
81+
return .preparingFileForEditorFunctionality
82+
case (_, .generatingBuildGraph), (.generatingBuildGraph, _):
83+
return .generatingBuildGraph
84+
case (
85+
.indexing(let selfPreparationTasks, let selfIndexTasks),
86+
.indexing(let otherPreparationTasks, let otherIndexTasks)
87+
):
88+
return .indexing(
89+
preparationTasks: selfPreparationTasks.merging(otherPreparationTasks) { max($0, $1) },
90+
indexTasks: selfIndexTasks.merging(otherIndexTasks) { max($0, $1) }
91+
)
92+
case (.indexing, .upToDate): return self
93+
case (.upToDate, .indexing): return other
94+
case (.upToDate, .upToDate): return .upToDate
95+
}
96+
}
97+
}
98+
6799
/// Schedules index tasks and keeps track of the index status of files.
68100
public final actor SemanticIndexManager {
69101
/// The underlying index. This is used to check if the index of a file is already up-to-date, in which case it doesn't
@@ -122,24 +154,22 @@ public final actor SemanticIndexManager {
122154
/// The parameter is the number of files that were scheduled to be indexed.
123155
private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void
124156

125-
/// Callback that is called when the progress status of an update indexstore or preparation task finishes.
126-
///
127-
/// An object observing this property probably wants to check `inProgressIndexTasks` when the callback is called to
128-
/// get the current list of in-progress index tasks.
129-
///
130-
/// The number of `indexStatusDidChange` calls does not have to relate to the number of `indexTasksWereScheduled` calls.
131-
private let indexStatusDidChange: @Sendable () -> Void
157+
/// Callback that is called when `progressStatus` might have changed.
158+
private let indexProgressStatusDidChange: @Sendable () -> Void
132159

133160
// MARK: - Public API
134161

135162
/// A summary of the tasks that this `SemanticIndexManager` has currently scheduled or is currently indexing.
136-
public var inProgressTasks:
137-
(
138-
isGeneratingBuildGraph: Bool,
139-
indexTasks: [DocumentURI: IndexTaskStatus],
140-
preparationTasks: [ConfiguredTarget: IndexTaskStatus]
141-
)
142-
{
163+
public var progressStatus: IndexProgressStatus {
164+
if inProgressPrepareForEditorTask != nil {
165+
return .preparingFileForEditorFunctionality
166+
}
167+
if generateBuildGraphTask != nil {
168+
return .generatingBuildGraph
169+
}
170+
let preparationTasks = inProgressPreparationTasks.mapValues { queuedTask in
171+
return queuedTask.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled
172+
}
143173
let indexTasks = inProgressIndexTasks.mapValues { status in
144174
switch status {
145175
case .waitingForPreparation:
@@ -148,10 +178,10 @@ public final actor SemanticIndexManager {
148178
return updateIndexStoreTask.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled
149179
}
150180
}
151-
let preparationTasks = inProgressPreparationTasks.mapValues { queuedTask in
152-
return queuedTask.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled
181+
if preparationTasks.isEmpty && indexTasks.isEmpty {
182+
return .upToDate
153183
}
154-
return (generateBuildGraphTask != nil, indexTasks, preparationTasks)
184+
return .indexing(preparationTasks: preparationTasks, indexTasks: indexTasks)
155185
}
156186

157187
public init(
@@ -161,15 +191,15 @@ public final actor SemanticIndexManager {
161191
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
162192
indexProcessDidProduceResult: @escaping @Sendable (IndexProcessResult) -> Void,
163193
indexTasksWereScheduled: @escaping @Sendable (Int) -> Void,
164-
indexStatusDidChange: @escaping @Sendable () -> Void
194+
indexProgressStatusDidChange: @escaping @Sendable () -> Void
165195
) {
166196
self.index = index
167197
self.buildSystemManager = buildSystemManager
168198
self.testHooks = testHooks
169199
self.indexTaskScheduler = indexTaskScheduler
170200
self.indexProcessDidProduceResult = indexProcessDidProduceResult
171201
self.indexTasksWereScheduled = indexTasksWereScheduled
172-
self.indexStatusDidChange = indexStatusDidChange
202+
self.indexProgressStatusDidChange = indexProgressStatusDidChange
173203
}
174204

175205
/// Schedules a task to index `files`. Files that are known to be up-to-date based on `indexStatus` will
@@ -222,7 +252,7 @@ public final actor SemanticIndexManager {
222252
generateBuildGraphTask = nil
223253
}
224254
}
225-
indexStatusDidChange()
255+
indexProgressStatusDidChange()
226256
}
227257

228258
/// Wait for all in-progress index tasks to finish.
@@ -350,11 +380,13 @@ public final actor SemanticIndexManager {
350380
await self.prepare(targets: [target], priority: priority)
351381
if inProgressPrepareForEditorTask?.id == id {
352382
inProgressPrepareForEditorTask = nil
383+
self.indexProgressStatusDidChange()
353384
}
354385
}
355386
}
356387
inProgressPrepareForEditorTask?.task.cancel()
357388
inProgressPrepareForEditorTask = (id, uri, task)
389+
self.indexProgressStatusDidChange()
358390
}
359391

360392
// MARK: - Helper functions
@@ -388,15 +420,15 @@ public final actor SemanticIndexManager {
388420
}
389421
let preparationTask = await indexTaskScheduler.schedule(priority: priority, taskDescription) { task, newState in
390422
guard case .finished = newState else {
391-
self.indexStatusDidChange()
423+
self.indexProgressStatusDidChange()
392424
return
393425
}
394426
for target in targetsToPrepare {
395427
if self.inProgressPreparationTasks[target] == OpaqueQueuedIndexTask(task) {
396428
self.inProgressPreparationTasks[target] = nil
397429
}
398430
}
399-
self.indexStatusDidChange()
431+
self.indexProgressStatusDidChange()
400432
}
401433
for target in targetsToPrepare {
402434
inProgressPreparationTasks[target] = OpaqueQueuedIndexTask(preparationTask)
@@ -432,7 +464,7 @@ public final actor SemanticIndexManager {
432464
)
433465
let updateIndexTask = await indexTaskScheduler.schedule(priority: priority, taskDescription) { task, newState in
434466
guard case .finished = newState else {
435-
self.indexStatusDidChange()
467+
self.indexProgressStatusDidChange()
436468
return
437469
}
438470
for fileAndTarget in filesAndTargets {
@@ -442,7 +474,7 @@ public final actor SemanticIndexManager {
442474
self.inProgressIndexTasks[fileAndTarget.file.sourceFile] = nil
443475
}
444476
}
445-
self.indexStatusDidChange()
477+
self.indexProgressStatusDidChange()
446478
}
447479
for fileAndTarget in filesAndTargets {
448480
if case .waitingForPreparation(preparationTaskID, let indexTask) = inProgressIndexTasks[

Sources/SourceKitLSP/IndexProgressManager.swift

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import SemanticIndex
1818
/// Listens for index status updates from `SemanticIndexManagers`. From that information, it manages a
1919
/// `WorkDoneProgress` that communicates the index progress to the editor.
2020
actor IndexProgressManager {
21-
/// A queue on which `indexTaskWasQueued` and `indexStatusDidChange` are handled.
21+
/// A queue on which `indexTaskWasQueued` and `indexProgressStatusDidChange` are handled.
2222
///
23-
/// This allows the two functions two be `nonisolated` (and eg. the caller of `indexStatusDidChange` doesn't have to
23+
/// This allows the two functions two be `nonisolated` (and eg. the caller of `indexProgressStatusDidChange` doesn't have to
2424
/// wait for the work done progress to be updated) while still guaranteeing that there is only one
25-
/// `indexStatusDidChangeImpl` running at a time, preventing race conditions that would cause two
25+
/// `indexProgressStatusDidChangeImpl` running at a time, preventing race conditions that would cause two
2626
/// `WorkDoneProgressManager`s to be created.
2727
private let queue = AsyncQueue<Serial>()
2828

@@ -64,74 +64,64 @@ actor IndexProgressManager {
6464

6565
private func indexTasksWereScheduledImpl(count: Int) async {
6666
queuedIndexTasks += count
67-
await indexStatusDidChangeImpl()
67+
await indexProgressStatusDidChangeImpl()
6868
}
6969

7070
/// Called when a `SemanticIndexManager` finishes indexing a file. Adjusts the done index count, eg. the 1 in `1/3`.
71-
nonisolated func indexStatusDidChange() {
71+
nonisolated func indexProgressStatusDidChange() {
7272
queue.async {
73-
await self.indexStatusDidChangeImpl()
73+
await self.indexProgressStatusDidChangeImpl()
7474
}
7575
}
7676

77-
private func indexStatusDidChangeImpl() async {
77+
private func indexProgressStatusDidChangeImpl() async {
7878
guard let sourceKitLSPServer else {
7979
workDoneProgress = nil
8080
return
8181
}
82-
var isGeneratingBuildGraph = false
83-
var indexTasks: [DocumentURI: IndexTaskStatus] = [:]
84-
var preparationTasks: [ConfiguredTarget: IndexTaskStatus] = [:]
82+
var status = IndexProgressStatus.upToDate
8583
for indexManager in await sourceKitLSPServer.workspaces.compactMap({ $0.semanticIndexManager }) {
86-
let inProgress = await indexManager.inProgressTasks
87-
isGeneratingBuildGraph = isGeneratingBuildGraph || inProgress.isGeneratingBuildGraph
88-
indexTasks.merge(inProgress.indexTasks) { lhs, rhs in
89-
return max(lhs, rhs)
90-
}
91-
preparationTasks.merge(inProgress.preparationTasks) { lhs, rhs in
92-
return max(lhs, rhs)
93-
}
94-
}
95-
96-
if indexTasks.isEmpty && !isGeneratingBuildGraph {
97-
// Nothing left to index. Reset the target count and dismiss the work done progress.
98-
queuedIndexTasks = 0
99-
workDoneProgress = nil
100-
return
84+
status = status.merging(with: await indexManager.progressStatus)
10185
}
10286

103-
// We can get into a situation where queuedIndexTasks < indexTasks.count if we haven't processed all
104-
// `indexTasksWereScheduled` calls yet but the semantic index managers already track them in their in-progress tasks.
105-
// Clip the finished tasks to 0 because showing a negative number there looks stupid.
106-
let finishedTasks = max(queuedIndexTasks - indexTasks.count, 0)
10787
var message: String
108-
if isGeneratingBuildGraph {
88+
let percentage: Int
89+
switch status {
90+
case .preparingFileForEditorFunctionality:
91+
message = "Preparing current file"
92+
percentage = 0
93+
case .generatingBuildGraph:
10994
message = "Generating build graph"
110-
} else {
95+
percentage = 0
96+
case .indexing(preparationTasks: let preparationTasks, indexTasks: let indexTasks):
97+
// We can get into a situation where queuedIndexTasks < indexTasks.count if we haven't processed all
98+
// `indexTasksWereScheduled` calls yet but the semantic index managers already track them in their in-progress tasks.
99+
// Clip the finished tasks to 0 because showing a negative number there looks stupid.
100+
let finishedTasks = max(queuedIndexTasks - indexTasks.count, 0)
111101
message = "\(finishedTasks) / \(queuedIndexTasks)"
112-
}
102+
if await sourceKitLSPServer.options.indexOptions.showActivePreparationTasksInProgress {
103+
var inProgressTasks: [String] = []
104+
inProgressTasks += preparationTasks.filter { $0.value == .executing }
105+
.map { "- Preparing \($0.key.targetID)" }
106+
.sorted()
107+
inProgressTasks += indexTasks.filter { $0.value == .executing }
108+
.map { "- Indexing \($0.key.fileURL?.lastPathComponent ?? $0.key.pseudoPath)" }
109+
.sorted()
113110

114-
if await sourceKitLSPServer.options.indexOptions.showActivePreparationTasksInProgress {
115-
var inProgressTasks: [String] = []
116-
if isGeneratingBuildGraph {
117-
inProgressTasks.append("- Generating build graph")
111+
message += "\n\n" + inProgressTasks.joined(separator: "\n")
118112
}
119-
inProgressTasks += preparationTasks.filter { $0.value == .executing }
120-
.map { "- Preparing \($0.key.targetID)" }
121-
.sorted()
122-
inProgressTasks += indexTasks.filter { $0.value == .executing }
123-
.map { "- Indexing \($0.key.fileURL?.lastPathComponent ?? $0.key.pseudoPath)" }
124-
.sorted()
125-
126-
message += "\n\n" + inProgressTasks.joined(separator: "\n")
113+
if queuedIndexTasks != 0 {
114+
percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100)
115+
} else {
116+
percentage = 0
117+
}
118+
case .upToDate:
119+
// Nothing left to index. Reset the target count and dismiss the work done progress.
120+
queuedIndexTasks = 0
121+
workDoneProgress = nil
122+
return
127123
}
128124

129-
let percentage: Int
130-
if queuedIndexTasks != 0 {
131-
percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100)
132-
} else {
133-
percentage = 0
134-
}
135125
if let workDoneProgress {
136126
workDoneProgress.update(message: message, percentage: percentage)
137127
} else {

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public actor SourceKitLSPServer {
150150
uriToWorkspaceCache = [:]
151151
// `indexProgressManager` iterates over all workspaces in the SourceKitLSPServer. Modifying workspaces might thus
152152
// update the index progress status.
153-
indexProgressManager.indexStatusDidChange()
153+
indexProgressManager.indexProgressStatusDidChange()
154154
}
155155
}
156156

@@ -897,8 +897,8 @@ extension SourceKitLSPServer {
897897
indexTasksWereScheduled: { [weak self] count in
898898
self?.indexProgressManager.indexTasksWereScheduled(count: count)
899899
},
900-
indexStatusDidChange: { [weak self] in
901-
self?.indexProgressManager.indexStatusDidChange()
900+
indexProgressStatusDidChange: { [weak self] in
901+
self?.indexProgressManager.indexProgressStatusDidChange()
902902
}
903903
)
904904
}
@@ -963,8 +963,8 @@ extension SourceKitLSPServer {
963963
indexTasksWereScheduled: { [weak self] count in
964964
self?.indexProgressManager.indexTasksWereScheduled(count: count)
965965
},
966-
indexStatusDidChange: { [weak self] in
967-
self?.indexProgressManager.indexStatusDidChange()
966+
indexProgressStatusDidChange: { [weak self] in
967+
self?.indexProgressManager.indexProgressStatusDidChange()
968968
}
969969
)
970970

Sources/SourceKitLSP/WorkDoneProgressManager.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ final class WorkDoneProgressManager {
3636
/// - This should have `workDoneProgressCreated == true` so that it can send the work progress end.
3737
private let workDoneProgressCreated: ThreadSafeBox<Bool> & AnyObject = ThreadSafeBox<Bool>(initialValue: false)
3838

39+
/// The last message and percentage so we don't send a new report notification to the client if `update` is called
40+
/// without any actual change.
41+
private var lastStatus: (message: String?, percentage: Int?)
42+
3943
convenience init?(server: SourceKitLSPServer, title: String, message: String? = nil, percentage: Int? = nil) async {
4044
guard let capabilityRegistry = await server.capabilityRegistry else {
4145
return nil
@@ -69,6 +73,7 @@ final class WorkDoneProgressManager {
6973
)
7074
)
7175
workDoneProgressCreated.value = true
76+
self.lastStatus = (message, percentage)
7277
}
7378
}
7479

@@ -77,6 +82,10 @@ final class WorkDoneProgressManager {
7782
guard workDoneProgressCreated.value else {
7883
return
7984
}
85+
guard (message, percentage) != self.lastStatus else {
86+
return
87+
}
88+
self.lastStatus = (message, percentage)
8089
server.sendNotificationToClient(
8190
WorkDoneProgress(
8291
token: token,

Sources/SourceKitLSP/Workspace.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public final class Workspace: Sendable {
9696
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
9797
indexProcessDidProduceResult: @escaping @Sendable (IndexProcessResult) -> Void,
9898
indexTasksWereScheduled: @escaping @Sendable (Int) -> Void,
99-
indexStatusDidChange: @escaping @Sendable () -> Void
99+
indexProgressStatusDidChange: @escaping @Sendable () -> Void
100100
) async {
101101
self.documentManager = documentManager
102102
self.buildSetup = options.buildSetup
@@ -117,7 +117,7 @@ public final class Workspace: Sendable {
117117
indexTaskScheduler: indexTaskScheduler,
118118
indexProcessDidProduceResult: indexProcessDidProduceResult,
119119
indexTasksWereScheduled: indexTasksWereScheduled,
120-
indexStatusDidChange: indexStatusDidChange
120+
indexProgressStatusDidChange: indexProgressStatusDidChange
121121
)
122122
} else {
123123
self.semanticIndexManager = nil
@@ -156,7 +156,7 @@ public final class Workspace: Sendable {
156156
indexProcessDidProduceResult: @escaping @Sendable (IndexProcessResult) -> Void,
157157
reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void,
158158
indexTasksWereScheduled: @Sendable @escaping (Int) -> Void,
159-
indexStatusDidChange: @Sendable @escaping () -> Void
159+
indexProgressStatusDidChange: @Sendable @escaping () -> Void
160160
) async throws {
161161
var buildSystem: BuildSystem? = nil
162162

@@ -263,7 +263,7 @@ public final class Workspace: Sendable {
263263
indexTaskScheduler: indexTaskScheduler,
264264
indexProcessDidProduceResult: indexProcessDidProduceResult,
265265
indexTasksWereScheduled: indexTasksWereScheduled,
266-
indexStatusDidChange: indexStatusDidChange
266+
indexProgressStatusDidChange: indexProgressStatusDidChange
267267
)
268268
}
269269

0 commit comments

Comments
 (0)