1111//===----------------------------------------------------------------------===//
1212
1313import Foundation
14+ import LSPLogging
1415import LanguageServerProtocol
1516import SKSupport
1617import SwiftExtensions
@@ -19,89 +20,155 @@ import SwiftExtensions
1920///
2021/// The work done progress is started when the object is created and ended when the object is destroyed.
2122/// In between, updates can be sent to the client.
22- final class WorkDoneProgressManager {
23- private let token : ProgressToken
24- private let queue = AsyncQueue < Serial > ( )
25- private let server : SourceKitLSPServer
26- /// `true` if the client returned without an error from the `CreateWorkDoneProgressRequest`.
23+ final actor WorkDoneProgressManager {
24+ private enum Status : Equatable {
25+ case inProgress( message: String ? , percentage: Int ? )
26+ case done
27+ }
28+
29+ /// The token with which the work done progress has been created. `nil` if no work done progress has been created yet,
30+ /// either because we didn't send the `WorkDoneProgress` request yet, because the work done progress creation failed,
31+ /// or because the work done progress has been ended.
32+ private var token : ProgressToken ?
33+
34+ /// The queue on which progress updates are sent to the client.
35+ private let progressUpdateQueue = AsyncQueue < Serial > ( )
36+
37+ private weak var server : SourceKitLSPServer ?
38+
39+ private let title : String
40+
41+ /// The next status that should be sent to the client by `sendProgressUpdateImpl`.
2742 ///
28- /// Since all work done progress reports are being sent on `queue`, we never access it in a state where the
29- /// `CreateWorkDoneProgressRequest` is still in progress .
43+ /// While progress updates are being queued in `progressUpdateQueue` this status can evolve. The next
44+ /// `sendProgressUpdateImpl` call will pick up the latest status .
3045 ///
31- /// Must be a reference because `deinit` captures it and wants to observe changes to it from `init` eg. in the
32- /// following:
33- /// - `init` is called
34- /// - `deinit` is called
35- /// - The task from `init` gets executed
36- /// - The task from `deinit` gets executed
37- /// - This should have `workDoneProgressCreated == true` so that it can send the work progress end.
38- private let workDoneProgressCreated : ThreadSafeBox < Bool > & AnyObject = ThreadSafeBox < Bool > ( initialValue: false )
46+ /// For example, if we receive two update calls to 25% and 50% in quick succession the `sendProgressUpdateImpl`
47+ /// scheduled from the 25% update will already pick up the new 50% status. The `sendProgressUpdateImpl` call scheduled
48+ /// from the 50% update will then realize that the `lastStatus` is already up-to-date and be a no-op.
49+ private var pendingStatus : Status
3950
40- /// The last message and percentage so we don't send a new report notification to the client if `update` is called
41- /// without any actual change.
42- private var lastStatus : ( message: String ? , percentage: Int ? )
51+ /// The last status that was sent to the client. Used so we don't send no-op updates to the client.
52+ private var lastStatus : Status ? = nil
4353
44- convenience init ? ( server: SourceKitLSPServer , title: String , message: String ? = nil , percentage: Int ? = nil ) async {
54+ init ? (
55+ server: SourceKitLSPServer ,
56+ initialDebounce: Duration ? = nil ,
57+ title: String ,
58+ message: String ? = nil ,
59+ percentage: Int ? = nil
60+ ) async {
4561 guard let capabilityRegistry = await server. capabilityRegistry else {
4662 return nil
4763 }
48- self . init ( server: server, capabilityRegistry: capabilityRegistry, title: title, message: message)
64+ self . init (
65+ server: server,
66+ capabilityRegistry: capabilityRegistry,
67+ initialDebounce: initialDebounce,
68+ title: title,
69+ message: message,
70+ percentage: percentage
71+ )
4972 }
5073
5174 init ? (
5275 server: SourceKitLSPServer ,
5376 capabilityRegistry: CapabilityRegistry ,
77+ initialDebounce: Duration ? = nil ,
5478 title: String ,
5579 message: String ? = nil ,
5680 percentage: Int ? = nil
5781 ) {
5882 guard capabilityRegistry. clientCapabilities. window? . workDoneProgress ?? false else {
5983 return nil
6084 }
61- self . token = . string( " WorkDoneProgress- \( UUID ( ) ) " )
6285 self . server = server
63- queue. async { [ server, token, workDoneProgressCreated] in
64- await server. waitUntilInitialized ( )
65- do {
66- _ = try await server. client. send ( CreateWorkDoneProgressRequest ( token: token) )
67- } catch {
68- return
86+ self . title = title
87+ self . pendingStatus = . inProgress( message: message, percentage: percentage)
88+ progressUpdateQueue. async {
89+ if let initialDebounce {
90+ try ? await Task . sleep ( for: initialDebounce)
6991 }
70- server. sendNotificationToClient (
71- WorkDoneProgress (
72- token: token,
73- value: . begin( WorkDoneProgressBegin ( title: title, message: message, percentage: percentage) )
74- )
75- )
76- workDoneProgressCreated. value = true
77- self . lastStatus = ( message, percentage)
92+ await self . sendProgressUpdateAssumingOnProgressUpdateQueue ( )
7893 }
7994 }
8095
81- func update( message: String ? = nil , percentage: Int ? = nil ) {
82- queue. async { [ server, token, workDoneProgressCreated] in
83- guard workDoneProgressCreated. value else {
84- return
96+ /// Send the necessary messages to the client to update the work done progress to `status`.
97+ ///
98+ /// Must be called on `progressUpdateQueue`
99+ private func sendProgressUpdateAssumingOnProgressUpdateQueue( ) async {
100+ let statusToSend = pendingStatus
101+ guard statusToSend != lastStatus else {
102+ return
103+ }
104+ guard let server else {
105+ // SourceKitLSPServer has been destroyed, we don't have a way to send notifications to the client anymore.
106+ return
107+ }
108+ await server. waitUntilInitialized ( )
109+ switch statusToSend {
110+ case . inProgress( message: let message, percentage: let percentage) :
111+ if let token {
112+ server. sendNotificationToClient (
113+ WorkDoneProgress (
114+ token: token,
115+ value: . report( WorkDoneProgressReport ( cancellable: false , message: message, percentage: percentage) )
116+ )
117+ )
118+ } else {
119+ let token = ProgressToken . string ( UUID ( ) . uuidString)
120+ do {
121+ _ = try await server. client. send ( CreateWorkDoneProgressRequest ( token: token) )
122+ } catch {
123+ return
124+ }
125+ server. sendNotificationToClient (
126+ WorkDoneProgress (
127+ token: token,
128+ value: . begin( WorkDoneProgressBegin ( title: title, message: message, percentage: percentage) )
129+ )
130+ )
131+ self . token = token
85132 }
86- guard ( message, percentage) != self . lastStatus else {
87- return
133+ case . done:
134+ if let token {
135+ server. sendNotificationToClient ( WorkDoneProgress ( token: token, value: . end( WorkDoneProgressEnd ( ) ) ) )
136+ self . token = nil
88137 }
89- self . lastStatus = ( message, percentage)
90- server. sendNotificationToClient (
91- WorkDoneProgress (
92- token: token,
93- value: . report( WorkDoneProgressReport ( cancellable: false , message: message, percentage: percentage) )
94- )
95- )
138+ }
139+ lastStatus = statusToSend
140+ }
141+
142+ func update( message: String ? = nil , percentage: Int ? = nil ) {
143+ pendingStatus = . inProgress( message: message, percentage: percentage)
144+ progressUpdateQueue. async {
145+ await self . sendProgressUpdateAssumingOnProgressUpdateQueue ( )
146+ }
147+ }
148+
149+ /// Ends the work done progress. Any further update calls are no-ops.
150+ ///
151+ /// `end` must be should be called before the `WorkDoneProgressManager` is deallocated.
152+ func end( ) {
153+ pendingStatus = . done
154+ progressUpdateQueue. async {
155+ await self . sendProgressUpdateAssumingOnProgressUpdateQueue ( )
96156 }
97157 }
98158
99159 deinit {
100- queue. async { [ server, token, workDoneProgressCreated] in
101- guard workDoneProgressCreated. value else {
102- return
160+ if pendingStatus != . done {
161+ // If there is still a pending work done progress, end it. We know that we don't have any pending updates on
162+ // `progressUpdateQueue` because they would capture `self` strongly and thus we wouldn't be deallocating this
163+ // object.
164+ // This is a fallback logic to ensure we don't leave pending work done progresses in the editor if the
165+ // `WorkDoneProgressManager` is destroyed without a call to `end` (eg. because its owning object is destroyed).
166+ // Calling `end()` is preferred because it ends the work done progress even if there are pending status updates
167+ // in `progressUpdateQueue`, which keep the `WorkDoneProgressManager` alive and thus prevent the work done
168+ // progress to be implicitly ended by the deinitializer.
169+ if let token {
170+ server? . sendNotificationToClient ( WorkDoneProgress ( token: token, value: . end( WorkDoneProgressEnd ( ) ) ) )
103171 }
104- server. sendNotificationToClient ( WorkDoneProgress ( token: token, value: . end( WorkDoneProgressEnd ( ) ) ) )
105172 }
106173 }
107174}
0 commit comments