@@ -20,7 +20,7 @@ import SwiftExtensions
20
20
///
21
21
/// The work done progress is started when the object is created and ended when the object is destroyed.
22
22
/// In between, updates can be sent to the client.
23
- final actor WorkDoneProgressManager {
23
+ actor WorkDoneProgressManager {
24
24
private enum Status: Equatable {
25
25
case inProgress(message: String?, percentage: Int?)
26
26
case done
@@ -36,6 +36,15 @@ final actor WorkDoneProgressManager {
36
36
37
37
private weak var server: SourceKitLSPServer?
38
38
39
+ /// A string with which the `token` of the generated `WorkDoneProgress` sent to the client starts.
40
+ ///
41
+ /// A UUID will be appended to this prefix to make the token unique. The token prefix can be used to classify the work
42
+ /// done progress into a category, which makes debugging easier because the tokens have semantic meaning and also
43
+ /// allows clients to interpret what the `WorkDoneProgress` represents (for example Swift for VS Code explicitly
44
+ /// recognizes work done progress that indicates that sourcekitd has crashed to offer a diagnostic bundle to be
45
+ /// generated).
46
+ private let tokenPrefix: String
47
+
39
48
private let title: String
40
49
41
50
/// The next status that should be sent to the client by `sendProgressUpdateImpl`.
@@ -58,6 +67,7 @@ final actor WorkDoneProgressManager {
58
67
59
68
init?(
60
69
server: SourceKitLSPServer,
70
+ tokenPrefix: String,
61
71
initialDebounce: Duration? = nil,
62
72
title: String,
63
73
message: String? = nil,
@@ -69,6 +79,7 @@ final actor WorkDoneProgressManager {
69
79
self.init(
70
80
server: server,
71
81
capabilityRegistry: capabilityRegistry,
82
+ tokenPrefix: tokenPrefix,
72
83
initialDebounce: initialDebounce,
73
84
title: title,
74
85
message: message,
@@ -79,6 +90,7 @@ final actor WorkDoneProgressManager {
79
90
init?(
80
91
server: SourceKitLSPServer,
81
92
capabilityRegistry: CapabilityRegistry,
93
+ tokenPrefix: String,
82
94
initialDebounce: Duration? = nil,
83
95
title: String,
84
96
message: String? = nil,
@@ -87,6 +99,7 @@ final actor WorkDoneProgressManager {
87
99
guard capabilityRegistry.clientCapabilities.window?.workDoneProgress ?? false else {
88
100
return nil
89
101
}
102
+ self.tokenPrefix = tokenPrefix
90
103
self.server = server
91
104
self.title = title
92
105
self.pendingStatus = .inProgress(message: message, percentage: percentage)
@@ -121,7 +134,7 @@ final actor WorkDoneProgressManager {
121
134
)
122
135
)
123
136
} else {
124
- let token = ProgressToken.string(UUID().uuidString)
137
+ let token = ProgressToken.string("\(tokenPrefix).\( UUID().uuidString)" )
125
138
do {
126
139
_ = try await server.client.send(CreateWorkDoneProgressRequest(token: token))
127
140
} catch {
@@ -177,3 +190,68 @@ final actor WorkDoneProgressManager {
177
190
}
178
191
}
179
192
}
193
+
194
+ /// A `WorkDoneProgressManager` that essentially has two states. If any operation tracked by this type is currently
195
+ /// running, it displays a work done progress in the client. If multiple operations are running at the same time, it
196
+ /// doesn't show multiple work done progress in the client. For example, we only want to show one progress indicator
197
+ /// when sourcekitd has crashed, not one per `SwiftLanguageService`.
198
+ actor SharedWorkDoneProgressManager {
199
+ private weak var sourceKitLSPServer: SourceKitLSPServer?
200
+
201
+ /// The number of in-progress operations. When greater than 0 `workDoneProgress` non-nil and a work done progress is
202
+ /// displayed to the user.
203
+ private var inProgressOperations = 0
204
+ private var workDoneProgress: WorkDoneProgressManager?
205
+
206
+ private let tokenPrefix: String
207
+ private let title: String
208
+ private let message: String?
209
+
210
+ public init(
211
+ sourceKitLSPServer: SourceKitLSPServer,
212
+ tokenPrefix: String,
213
+ title: String,
214
+ message: String? = nil
215
+ ) {
216
+ self.sourceKitLSPServer = sourceKitLSPServer
217
+ self.tokenPrefix = tokenPrefix
218
+ self.title = title
219
+ self.message = message
220
+ }
221
+
222
+ func start() async {
223
+ guard let sourceKitLSPServer else {
224
+ return
225
+ }
226
+ // Do all asynchronous operations up-front so that incrementing `inProgressOperations` and setting `workDoneProgress`
227
+ // cannot be interrupted by an `await` call
228
+ let initialDebounce = await sourceKitLSPServer.options.workDoneProgressDebounceDuration
229
+ let capabilityRegistry = await sourceKitLSPServer.capabilityRegistry
230
+
231
+ inProgressOperations += 1
232
+ if let capabilityRegistry, workDoneProgress == nil {
233
+ workDoneProgress = WorkDoneProgressManager(
234
+ server: sourceKitLSPServer,
235
+ capabilityRegistry: capabilityRegistry,
236
+ tokenPrefix: tokenPrefix,
237
+ initialDebounce: initialDebounce,
238
+ title: title,
239
+ message: message
240
+ )
241
+ }
242
+ }
243
+
244
+ func end() async {
245
+ if inProgressOperations > 0 {
246
+ inProgressOperations -= 1
247
+ } else {
248
+ logger.fault(
249
+ "Unbalanced calls to SharedWorkDoneProgressManager.start and end for \(self.tokenPrefix, privacy: .public)"
250
+ )
251
+ }
252
+ if inProgressOperations == 0, let workDoneProgress {
253
+ self.workDoneProgress = nil
254
+ await workDoneProgress.end()
255
+ }
256
+ }
257
+ }
0 commit comments