@@ -20,7 +20,7 @@ import SwiftExtensions
2020///
2121/// The work done progress is started when the object is created and ended when the object is destroyed.
2222/// In between, updates can be sent to the client.
23- final actor WorkDoneProgressManager {
23+ actor WorkDoneProgressManager {
2424 private enum Status : Equatable {
2525 case inProgress( message: String ? , percentage: Int ? )
2626 case done
@@ -36,6 +36,15 @@ final actor WorkDoneProgressManager {
3636
3737 private weak var server : SourceKitLSPServer ?
3838
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+
3948 private let title : String
4049
4150 /// The next status that should be sent to the client by `sendProgressUpdateImpl`.
@@ -58,6 +67,7 @@ final actor WorkDoneProgressManager {
5867
5968 init ? (
6069 server: SourceKitLSPServer ,
70+ tokenPrefix: String ,
6171 initialDebounce: Duration ? = nil ,
6272 title: String ,
6373 message: String ? = nil ,
@@ -69,6 +79,7 @@ final actor WorkDoneProgressManager {
6979 self . init (
7080 server: server,
7181 capabilityRegistry: capabilityRegistry,
82+ tokenPrefix: tokenPrefix,
7283 initialDebounce: initialDebounce,
7384 title: title,
7485 message: message,
@@ -79,6 +90,7 @@ final actor WorkDoneProgressManager {
7990 init ? (
8091 server: SourceKitLSPServer ,
8192 capabilityRegistry: CapabilityRegistry ,
93+ tokenPrefix: String ,
8294 initialDebounce: Duration ? = nil ,
8395 title: String ,
8496 message: String ? = nil ,
@@ -87,6 +99,7 @@ final actor WorkDoneProgressManager {
8799 guard capabilityRegistry. clientCapabilities. window? . workDoneProgress ?? false else {
88100 return nil
89101 }
102+ self . tokenPrefix = tokenPrefix
90103 self . server = server
91104 self . title = title
92105 self . pendingStatus = . inProgress( message: message, percentage: percentage)
@@ -121,7 +134,7 @@ final actor WorkDoneProgressManager {
121134 )
122135 )
123136 } else {
124- let token = ProgressToken . string ( UUID ( ) . uuidString)
137+ let token = ProgressToken . string ( " \( tokenPrefix ) . \( UUID ( ) . uuidString) " )
125138 do {
126139 _ = try await server. client. send ( CreateWorkDoneProgressRequest ( token: token) )
127140 } catch {
@@ -177,3 +190,68 @@ final actor WorkDoneProgressManager {
177190 }
178191 }
179192}
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