11
11
//===----------------------------------------------------------------------===//
12
12
13
13
import Foundation
14
+ import LSPLogging
14
15
import LanguageServerProtocol
15
16
import SKSupport
16
17
import SwiftExtensions
@@ -19,29 +20,44 @@ import SwiftExtensions
19
20
///
20
21
/// The work done progress is started when the object is created and ended when the object is destroyed.
21
22
/// 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`.
27
42
///
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 .
30
45
///
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
39
50
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
43
53
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 {
45
61
guard let capabilityRegistry = await server. capabilityRegistry else {
46
62
return nil
47
63
}
@@ -51,57 +67,101 @@ final class WorkDoneProgressManager {
51
67
init ? (
52
68
server: SourceKitLSPServer ,
53
69
capabilityRegistry: CapabilityRegistry ,
70
+ initialDebounce: Duration ? = nil ,
54
71
title: String ,
55
72
message: String ? = nil ,
56
73
percentage: Int ? = nil
57
74
) {
58
75
guard capabilityRegistry. clientCapabilities. window? . workDoneProgress ?? false else {
59
76
return nil
60
77
}
61
- self . token = . string( " WorkDoneProgress- \( UUID ( ) ) " )
62
78
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
79
+ self . title = title
80
+ self . pendingStatus = . inProgress( message: message, percentage: percentage)
81
+ progressUpdateQueue. async {
82
+ if let initialDebounce {
83
+ try ? await Task . sleep ( for: initialDebounce)
69
84
}
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)
85
+ await self . sendProgressUpdateAssumingOnProgressUpdateQueue ( )
78
86
}
79
87
}
80
88
81
- func update( message: String ? = nil , percentage: Int ? = nil ) {
82
- queue. async { [ server, token, workDoneProgressCreated] in
83
- guard workDoneProgressCreated. value else {
84
- return
89
+ /// Send the necessary messages to the client to update the work done progress to `status`.
90
+ ///
91
+ /// Must be called on `progressUpdateQueue`
92
+ private func sendProgressUpdateAssumingOnProgressUpdateQueue( ) async {
93
+ let statusToSend = pendingStatus
94
+ guard statusToSend != lastStatus else {
95
+ return
96
+ }
97
+ guard let server else {
98
+ // SourceKitLSPServer has been destroyed, we don't have a way to send notifications to the client anymore.
99
+ return
100
+ }
101
+ await server. waitUntilInitialized ( )
102
+ switch statusToSend {
103
+ case . inProgress( message: let message, percentage: let percentage) :
104
+ if let token {
105
+ server. sendNotificationToClient (
106
+ WorkDoneProgress (
107
+ token: token,
108
+ value: . report( WorkDoneProgressReport ( cancellable: false , message: message, percentage: percentage) )
109
+ )
110
+ )
111
+ } else {
112
+ let token = ProgressToken . string ( " WorkDoneProgress- \( UUID ( ) ) " )
113
+ do {
114
+ _ = try await server. client. send ( CreateWorkDoneProgressRequest ( token: token) )
115
+ } catch {
116
+ return
117
+ }
118
+ server. sendNotificationToClient (
119
+ WorkDoneProgress (
120
+ token: token,
121
+ value: . begin( WorkDoneProgressBegin ( title: title, message: message, percentage: percentage) )
122
+ )
123
+ )
124
+ self . token = token
85
125
}
86
- guard ( message, percentage) != self . lastStatus else {
87
- return
126
+ case . done:
127
+ if let token {
128
+ server. sendNotificationToClient ( WorkDoneProgress ( token: token, value: . end( WorkDoneProgressEnd ( ) ) ) )
129
+ self . token = nil
88
130
}
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
- )
131
+ }
132
+ lastStatus = statusToSend
133
+ }
134
+
135
+ func update( message: String ? = nil , percentage: Int ? = nil ) {
136
+ pendingStatus = . inProgress( message: message, percentage: percentage)
137
+ progressUpdateQueue. async {
138
+ await self . sendProgressUpdateAssumingOnProgressUpdateQueue ( )
139
+ }
140
+ }
141
+
142
+ /// Ends the work done progress. Any further update calls are no-ops.
143
+ ///
144
+ /// `end` must be should be called before the `WorkDoneProgressManager` is deallocated.
145
+ func end( ) {
146
+ pendingStatus = . done
147
+ progressUpdateQueue. async {
148
+ await self . sendProgressUpdateAssumingOnProgressUpdateQueue ( )
96
149
}
97
150
}
98
151
99
152
deinit {
100
- queue. async { [ server, token, workDoneProgressCreated] in
101
- guard workDoneProgressCreated. value else {
102
- return
153
+ if pendingStatus != . done {
154
+ // If there is still a pending work done progress, end it. We know that we don't have any pending updates on
155
+ // `progressUpdateQueue` because they would capture `self` strongly and thus we wouldn't be deallocating this
156
+ // object.
157
+ // This is a fallback logic to ensure we don't leave pending work done progresses in the editor if the
158
+ // `WorkDoneProgressManager` is destroyed without a call to `end` (eg. because its owning object is destroyed).
159
+ // Calling `end()` is preferred because it ends the work done progress even if there are pending status updates
160
+ // in `progressUpdateQueue`, which keep the `WorkDoneProgressManager` alive and thus prevent the work done
161
+ // progress to be implicitly ended by the deinitializer.
162
+ if let token {
163
+ server? . sendNotificationToClient ( WorkDoneProgress ( token: token, value: . end( WorkDoneProgressEnd ( ) ) ) )
103
164
}
104
- server. sendNotificationToClient ( WorkDoneProgress ( token: token, value: . end( WorkDoneProgressEnd ( ) ) ) )
105
165
}
106
166
}
107
167
}
0 commit comments