Skip to content

Commit e75fac5

Browse files
authored
Merge pull request #1493 from ahoppen/work-done-progress-token-prefix
Allow prefixing of the token for a `WorkDoneProgress` with a custom string
2 parents f2fe9dc + 3003fe1 commit e75fac5

File tree

4 files changed

+135
-57
lines changed

4 files changed

+135
-57
lines changed

Sources/SourceKitLSP/IndexProgressManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ actor IndexProgressManager {
127127
} else {
128128
workDoneProgress = await WorkDoneProgressManager(
129129
server: sourceKitLSPServer,
130+
tokenPrefix: "indexing",
130131
initialDebounce: sourceKitLSPServer.options.workDoneProgressDebounceDuration,
131132
title: "Indexing",
132133
message: message,

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,17 @@ public actor SourceKitLSPServer {
126126
/// initializer.
127127
private nonisolated(unsafe) var indexProgressManager: IndexProgressManager!
128128

129-
/// Number of workspaces that are currently reloading swift package. When this is not 0, a
130-
/// `packageLoadingWorkDoneProgress` is created to show a work done progress indicator in the client.
131-
private var inProgressPackageLoadingOperations = 0
132-
private var packageLoadingWorkDoneProgress: WorkDoneProgressManager?
129+
/// Implicitly unwrapped optional so we can create an `SharedWorkDoneProgressManager` that has a weak reference to
130+
/// `SourceKitLSPServer`.
131+
/// `nonisolated(unsafe)` because `packageLoadingWorkDoneProgress` will not be modified after it is assigned from the
132+
/// initializer.
133+
private nonisolated(unsafe) var packageLoadingWorkDoneProgress: SharedWorkDoneProgressManager!
134+
135+
/// Implicitly unwrapped optional so we can create an `SharedWorkDoneProgressManager` that has a weak reference to
136+
/// `SourceKitLSPServer`.
137+
/// `nonisolated(unsafe)` because `sourcekitdCrashedWorkDoneProgress` will not be modified after it is assigned from
138+
/// the initializer.
139+
nonisolated(unsafe) var sourcekitdCrashedWorkDoneProgress: SharedWorkDoneProgressManager!
133140

134141
/// Caches which workspace a document with the given URI should be opened in.
135142
///
@@ -210,6 +217,17 @@ public actor SourceKitLSPServer {
210217
])
211218
self.indexProgressManager = nil
212219
self.indexProgressManager = IndexProgressManager(sourceKitLSPServer: self)
220+
self.packageLoadingWorkDoneProgress = SharedWorkDoneProgressManager(
221+
sourceKitLSPServer: self,
222+
tokenPrefix: "package-reloading",
223+
title: "SourceKit-LSP: Reloading Package"
224+
)
225+
self.sourcekitdCrashedWorkDoneProgress = SharedWorkDoneProgressManager(
226+
sourceKitLSPServer: self,
227+
tokenPrefix: "sourcekitd-crashed",
228+
title: "SourceKit-LSP: Restoring functionality",
229+
message: "Please run 'sourcekit-lsp diagnose' to file an issue"
230+
)
213231
}
214232

215233
/// Await until the server has send the reply to the initialize request.
@@ -885,21 +903,9 @@ extension SourceKitLSPServer {
885903
private func reloadPackageStatusCallback(_ status: ReloadPackageStatus) async {
886904
switch status {
887905
case .start:
888-
inProgressPackageLoadingOperations += 1
889-
if let capabilityRegistry, packageLoadingWorkDoneProgress == nil {
890-
packageLoadingWorkDoneProgress = WorkDoneProgressManager(
891-
server: self,
892-
capabilityRegistry: capabilityRegistry,
893-
initialDebounce: options.workDoneProgressDebounceDuration,
894-
title: "SourceKit-LSP: Reloading Package"
895-
)
896-
}
906+
await packageLoadingWorkDoneProgress.start()
897907
case .end:
898-
inProgressPackageLoadingOperations -= 1
899-
if inProgressPackageLoadingOperations == 0, let packageLoadingWorkDoneProgress {
900-
self.packageLoadingWorkDoneProgress = nil
901-
await packageLoadingWorkDoneProgress.end()
902-
}
908+
await packageLoadingWorkDoneProgress.end()
903909
}
904910
}
905911

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -142,40 +142,8 @@ public actor SwiftLanguageService: LanguageService, Sendable {
142142
nonisolated var requests: sourcekitd_api_requests { return sourcekitd.requests }
143143
nonisolated var values: sourcekitd_api_values { return sourcekitd.values }
144144

145-
/// When sourcekitd is crashed, a `WorkDoneProgressManager` that display the sourcekitd crash status in the client.
146-
private var sourcekitdCrashedWorkDoneProgress: WorkDoneProgressManager?
147-
148-
private var state: LanguageServerState {
149-
didSet {
150-
for handler in stateChangeHandlers {
151-
handler(oldValue, state)
152-
}
153-
154-
guard let sourceKitLSPServer else {
155-
Task {
156-
await sourcekitdCrashedWorkDoneProgress?.end()
157-
}
158-
sourcekitdCrashedWorkDoneProgress = nil
159-
return
160-
}
161-
switch state {
162-
case .connected:
163-
Task {
164-
await sourcekitdCrashedWorkDoneProgress?.end()
165-
}
166-
sourcekitdCrashedWorkDoneProgress = nil
167-
case .connectionInterrupted, .semanticFunctionalityDisabled:
168-
if sourcekitdCrashedWorkDoneProgress == nil {
169-
sourcekitdCrashedWorkDoneProgress = WorkDoneProgressManager(
170-
server: sourceKitLSPServer,
171-
capabilityRegistry: capabilityRegistry,
172-
title: "SourceKit-LSP: Restoring functionality",
173-
message: "Please run 'sourcekit-lsp diagnose' to file an issue"
174-
)
175-
}
176-
}
177-
}
178-
}
145+
/// - Important: Use `setState` to change the state, which notifies the state change handlers
146+
private var state: LanguageServerState
179147

180148
private var stateChangeHandlers: [(_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void] = []
181149

@@ -273,6 +241,30 @@ public actor SwiftLanguageService: LanguageService, Sendable {
273241
return true
274242
}
275243

244+
private func setState(_ newState: LanguageServerState) async {
245+
let oldState = state
246+
state = newState
247+
for handler in stateChangeHandlers {
248+
handler(oldState, newState)
249+
}
250+
251+
guard let sourceKitLSPServer else {
252+
return
253+
}
254+
switch (oldState, newState) {
255+
case (.connected, .connectionInterrupted), (.connected, .semanticFunctionalityDisabled):
256+
await sourceKitLSPServer.sourcekitdCrashedWorkDoneProgress.start()
257+
case (.connectionInterrupted, .connected), (.semanticFunctionalityDisabled, .connected):
258+
await sourceKitLSPServer.sourcekitdCrashedWorkDoneProgress.end()
259+
case (.connected, .connected),
260+
(.connectionInterrupted, .connectionInterrupted),
261+
(.connectionInterrupted, .semanticFunctionalityDisabled),
262+
(.semanticFunctionalityDisabled, .connectionInterrupted),
263+
(.semanticFunctionalityDisabled, .semanticFunctionalityDisabled):
264+
break
265+
}
266+
}
267+
276268
public func addStateChangeHandler(
277269
handler: @Sendable @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void
278270
) {
@@ -985,13 +977,14 @@ extension SwiftLanguageService: SKDNotificationHandler {
985977
)
986978
// Check if we need to update our `state` based on the contents of the notification.
987979
if notification.value?[self.keys.notification] == self.values.semaEnabledNotification {
988-
self.state = .connected
980+
await self.setState(.connected)
981+
return
989982
}
990983

991984
if self.state == .connectionInterrupted {
992985
// If we get a notification while we are restoring the connection, it means that the server has restarted.
993986
// We still need to wait for semantic functionality to come back up.
994-
self.state = .semanticFunctionalityDisabled
987+
await self.setState(.semanticFunctionalityDisabled)
995988

996989
// Ask our parent to re-open all of our documents.
997990
if let sourceKitLSPServer {
@@ -1002,7 +995,7 @@ extension SwiftLanguageService: SKDNotificationHandler {
1002995
}
1003996

1004997
if notification.error == .connectionInterrupted {
1005-
self.state = .connectionInterrupted
998+
await self.setState(.connectionInterrupted)
1006999

10071000
// We don't have any open documents anymore after sourcekitd crashed.
10081001
// Reset the document manager to reflect that.

Sources/SourceKitLSP/WorkDoneProgressManager.swift

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)