11
11
//===----------------------------------------------------------------------===//
12
12
13
13
import Foundation
14
+ import LSPLogging
14
15
import LanguageServerProtocol
15
16
import SKSupport
16
17
import SwiftExtensions
@@ -19,89 +20,155 @@ 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
}
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
+ )
49
72
}
50
73
51
74
init ? (
52
75
server: SourceKitLSPServer ,
53
76
capabilityRegistry: CapabilityRegistry ,
77
+ initialDebounce: Duration ? = nil ,
54
78
title: String ,
55
79
message: String ? = nil ,
56
80
percentage: Int ? = nil
57
81
) {
58
82
guard capabilityRegistry. clientCapabilities. window? . workDoneProgress ?? false else {
59
83
return nil
60
84
}
61
- self . token = . string( " WorkDoneProgress- \( UUID ( ) ) " )
62
85
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)
69
91
}
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 ( )
78
93
}
79
94
}
80
95
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
85
132
}
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
88
137
}
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 ( )
96
156
}
97
157
}
98
158
99
159
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 ( ) ) ) )
103
171
}
104
- server. sendNotificationToClient ( WorkDoneProgress ( token: token, value: . end( WorkDoneProgressEnd ( ) ) ) )
105
172
}
106
173
}
107
174
}
0 commit comments