Skip to content

Commit 06f58db

Browse files
committed
Use build/taskStart, build/taskProgress and build/taskFinish to communicate progress from a BSP server to the client
Instead of defining BSP extensions for `window/workDoneProgress/create` and `$/progress`, we should be able to use the standard `build/taskStart`, `build/taskProgress` and `build/taskFinish` messages to the same effect, as suggested by https://forums.swift.org/t/extending-functionality-of-build-server-protocol-with-sourcekit-lsp/74400/9. Fixes #1783 rdar://138653131
1 parent c4e246b commit 06f58db

File tree

18 files changed

+194
-124
lines changed

18 files changed

+194
-124
lines changed

Contributor Documentation/BSP Extensions.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export interface SourceKitInitializeBuildResponseData {
3535
}
3636
```
3737

38+
## `build/taskStart`
39+
40+
If `data` contains a string value for the `workDoneProgressTitle` key, then the task's message will be displayed in the client as a work done progress with that title.
41+
3842
## `buildTarget/didChange`
3943

4044
`changes` can be `null` to indicate that all targets have changed.
@@ -119,12 +123,6 @@ export interface TextDocumentSourceKitOptionsResult {
119123
}
120124
```
121125
122-
## `window/workDoneProgress/create`
123-
124-
Request from the build server to SourceKit-LSP to create a work done progress.
125-
126-
Definition is the same as in LSP.
127-
128126
## `workspace/buildTargets`
129127
130128
`BuildTargetTag` can have the following additional values
@@ -172,9 +170,3 @@ This request is a no-op and doesn't have any effects.
172170
If the build system is currently updating the build graph, this request should return after those updates have finished processing.
173171
174172
- method: `workspace/waitForBuildSystemUpdates`
175-
176-
## `$/progress`
177-
178-
Notification from the build server to SourceKit-LSP to update a work done progress created using `window/workDoneProgress/create`.
179-
180-
Definition is the same as in LSP.

Contributor Documentation/Implementing a BSP server.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ If the build system does not have a notion of targets, eg. because it provides b
2727

2828
If the build system loads the entire build graph during initialization, it may immediately return from `workspace/waitForBuildSystemUpdates`.
2929

30-
3130
## Supporting background indexing
3231

3332
To support background indexing, the build system must set `data.prepareProvider: true` in the `build/initialize` response and implement the `buildTarget/prepare` method.
@@ -37,9 +36,8 @@ To support background indexing, the build system must set `data.prepareProvider:
3736
The following methods are not necessary to implement for SourceKit-LSP to work but might help with the implementation of the build server.
3837

3938
- `build/logMessage`
40-
- `window/workDoneProgress/create`
39+
- `build/taskStart`, `build/taskProgress`, and `build/taskFinish`
4140
- `workspace/didChangeWatchedFiles`
42-
- `$/progress`
4341

4442
## Build server discovery
4543

Sources/BuildServerProtocol/CMakeLists.txt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
add_library(BuildServerProtocol STATIC
22
Messages.swift
33

4-
Messages/TextDocumentSourceKitOptionsRequest.swift
5-
Messages/OnBuildTargetDidChangeNotification.swift
6-
Messages/InitializeBuildRequest.swift
4+
Messages/BuildShutdownRequest.swift
5+
Messages/BuildTargetPrepareRequest.swift
76
Messages/BuildTargetSourcesRequest.swift
7+
Messages/InitializeBuildRequest.swift
88
Messages/OnBuildExitNotification.swift
9-
Messages/RegisterForChangeNotifications.swift
9+
Messages/OnBuildInitializedNotification.swift
1010
Messages/OnBuildLogMessageNotification.swift
11-
Messages/WorkDoneProgress.swift
11+
Messages/OnBuildTargetDidChangeNotification.swift
1212
Messages/OnWatchedFilesDidChangeNotification.swift
13-
Messages/OnBuildInitializedNotification.swift
14-
Messages/WorkspaceWaitForBuildSystemUpdates.swift
15-
Messages/BuildShutdownRequest.swift
13+
Messages/RegisterForChangeNotifications.swift
14+
Messages/TaskFinishNotification.swift
15+
Messages/TaskProgressNotification.swift
16+
Messages/TaskStartNotification.swift
17+
Messages/TextDocumentSourceKitOptionsRequest.swift
1618
Messages/WorkspaceBuildTargetsRequest.swift
17-
Messages/BuildTargetPrepareRequest.swift
19+
Messages/WorkspaceWaitForBuildSystemUpdates.swift
1820

19-
SupportTypes/TextDocumentIdentifier.swift
20-
SupportTypes/TaskId.swift
2121
SupportTypes/BuildTarget.swift
22-
SupportTypes/MessageType.swift)
22+
SupportTypes/MessageType.swift
23+
SupportTypes/MillisecondsSince1970Date.swift
24+
SupportTypes/StatusCode.swift
25+
SupportTypes/TaskId.swift
26+
SupportTypes/TextDocumentIdentifier.swift)
2327
set_target_properties(BuildServerProtocol PROPERTIES
2428
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
2529
target_link_libraries(BuildServerProtocol PRIVATE

Sources/BuildServerProtocol/Messages/TaskStartNotification.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,27 @@ public struct TestTaskData: Codable, Hashable, Sendable {
137137
self.target = target
138138
}
139139
}
140+
141+
/// If `data` contains a string value for the `workDoneProgressTitle` key, then the task's message will be displayed in
142+
/// the client as a work done progress with that title.
143+
public struct WorkDoneProgressTask: LSPAnyCodable {
144+
/// The title with which the work done progress should be created in the client.
145+
public let title: String
146+
147+
public init(title: String) {
148+
self.title = title
149+
}
150+
151+
public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) {
152+
guard case .string(let title) = dictionary["workDoneProgressTitle"] else {
153+
return nil
154+
}
155+
self.title = title
156+
}
157+
158+
public func encodeToLSPAny() -> LanguageServerProtocol.LSPAny {
159+
return .dictionary([
160+
"workDoneProgressTitle": .string(title)
161+
])
162+
}
163+
}

Sources/BuildServerProtocol/Messages/WorkDoneProgress.swift

Lines changed: 0 additions & 20 deletions
This file was deleted.

Sources/BuildServerProtocol/SupportTypes/TaskId.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import LanguageServerProtocol
1414

1515
public typealias TaskIdentifier = String
1616

17-
public struct TaskId: Sendable, Codable {
17+
public struct TaskId: Sendable, Codable, Hashable {
1818
/// A unique identifier
1919
public var id: TaskIdentifier
2020

21-
/// The parent task ids, if any. A non-empty parents field means
21+
/// The parent task ids, if any. A non-empty parents field means
2222
/// this task is a sub-task of every parent task id. The child-parent
2323
/// relationship of tasks makes it possible to render tasks in
2424
/// a tree-like user interface or inspect what caused a certain task

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,19 @@ private struct BuildTargetInfo {
7878

7979
fileprivate extension SourceItem {
8080
var sourceKitData: SourceKitSourceItemData? {
81-
guard dataKind == .sourceKit, case .dictionary(let data) = data else {
81+
guard dataKind == .sourceKit else {
8282
return nil
8383
}
84-
return SourceKitSourceItemData(fromLSPDictionary: data)
84+
return SourceKitSourceItemData(fromLSPAny: data)
8585
}
8686
}
8787

8888
fileprivate extension BuildTarget {
8989
var sourceKitData: SourceKitBuildTarget? {
90-
guard dataKind == .sourceKit, case .dictionary(let data) = data else {
90+
guard dataKind == .sourceKit else {
9191
return nil
9292
}
93-
return SourceKitBuildTarget(fromLSPDictionary: data)
93+
return SourceKitBuildTarget(fromLSPAny: data)
9494
}
9595
}
9696

@@ -99,10 +99,7 @@ fileprivate extension InitializeBuildResponse {
9999
guard dataKind == nil || dataKind == .sourceKit else {
100100
return nil
101101
}
102-
guard case .dictionary(let data) = data else {
103-
return nil
104-
}
105-
return SourceKitInitializeBuildResponseData(fromLSPDictionary: data)
102+
return SourceKitInitializeBuildResponseData(fromLSPAny: data)
106103
}
107104
}
108105

@@ -250,7 +247,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
250247
/// get `fileBuildSettingsChanged` and `filesDependenciesUpdated` callbacks.
251248
private var watchedFiles: [DocumentURI: (mainFile: DocumentURI, language: Language)] = [:]
252249

253-
private var connectionToClient: BuildSystemManagerConnectionToClient?
250+
private var connectionToClient: BuildSystemManagerConnectionToClient
254251

255252
/// The build system adapter that is used to answer build system queries.
256253
private var buildSystemAdapter: BuildSystemAdapter?
@@ -302,6 +299,10 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
302299
}
303300
}
304301

302+
/// For tasks from the build system that should create a work done progress in the client, a mapping from the `TaskId`
303+
/// in the build system to a `WorkDoneProgressManager` that manages that work done progress in the client.
304+
private var workDoneProgressManagers: [TaskIdentifier: WorkDoneProgressManager] = [:]
305+
305306
/// Debounces calls to `delegate.filesDependenciesUpdated`.
306307
///
307308
/// This is to ensure we don't call `filesDependenciesUpdated` for the same file multiple time if the client does not
@@ -443,6 +444,8 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
443444
/// which could result in the connection being reported as a leak. To avoid this problem, we want to explicitly shut
444445
/// down the build server when the `SourceKitLSPServer` gets shut down.
445446
package func shutdown() async {
447+
// Clear any pending work done progresses from the build server.
448+
self.workDoneProgressManagers.removeAll()
446449
guard let buildSystemAdapter = await self.buildSystemAdapterAfterInitialized else {
447450
return
448451
}
@@ -488,8 +491,12 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
488491
await self.didChangeBuildTarget(notification: notification)
489492
case let notification as OnBuildLogMessageNotification:
490493
await self.logMessage(notification: notification)
491-
case let notification as BuildServerProtocol.WorkDoneProgress:
492-
await self.workDoneProgress(notification: notification)
494+
case let notification as TaskFinishNotification:
495+
await self.taskFinish(notification: notification)
496+
case let notification as TaskProgressNotification:
497+
await self.taskProgress(notification: notification)
498+
case let notification as TaskStartNotification:
499+
await self.taskStart(notification: notification)
493500
default:
494501
logger.error("Ignoring unknown notification \(type(of: notification).method)")
495502
}
@@ -502,8 +509,6 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
502509
) async {
503510
let request = RequestAndReply(request, reply: reply)
504511
switch request {
505-
case let request as RequestAndReply<BuildServerProtocol.CreateWorkDoneProgressRequest>:
506-
await request.reply { try await self.createWorkDoneProgress(request: request.params) }
507512
default:
508513
await request.reply { throw ResponseError.methodNotFound(Request.method) }
509514
}
@@ -544,29 +549,51 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
544549
} else {
545550
notification.message
546551
}
547-
await connectionToClient?.send(
552+
await connectionToClient.waitUntilInitialized()
553+
connectionToClient.send(
548554
LanguageServerProtocol.LogMessageNotification(type: .info, message: message, logName: "SourceKit-LSP: Indexing")
549555
)
550556
}
551557

552-
private func workDoneProgress(notification: BuildServerProtocol.WorkDoneProgress) async {
553-
guard let connectionToClient else {
554-
logger.fault("Ignoring work done progress from build system because connection to client closed")
558+
private func taskStart(notification: TaskStartNotification) async {
559+
guard let workDoneProgressTitle = WorkDoneProgressTask(fromLSPAny: notification.data)?.title,
560+
await connectionToClient.clientSupportsWorkDoneProgress
561+
else {
562+
return
563+
}
564+
565+
guard workDoneProgressManagers[notification.taskId.id] == nil else {
566+
logger.error("Client is already tracking a work done progress for task \(notification.taskId.id)")
555567
return
556568
}
557-
await connectionToClient.send(notification as LanguageServerProtocol.WorkDoneProgress)
569+
workDoneProgressManagers[notification.taskId.id] = WorkDoneProgressManager(
570+
connectionToClient: connectionToClient,
571+
waitUntilClientInitialized: connectionToClient.waitUntilInitialized,
572+
tokenPrefix: notification.taskId.id,
573+
initialDebounce: options.workDoneProgressDebounceDurationOrDefault,
574+
title: workDoneProgressTitle
575+
)
558576
}
559577

560-
private func createWorkDoneProgress(
561-
request: BuildServerProtocol.CreateWorkDoneProgressRequest
562-
) async throws -> BuildServerProtocol.CreateWorkDoneProgressRequest.Response {
563-
guard let connectionToClient else {
564-
throw ResponseError.unknown("Connection to client closed")
578+
private func taskProgress(notification: TaskProgressNotification) async {
579+
guard let progressManager = workDoneProgressManagers[notification.taskId.id] else {
580+
return
565581
}
566-
guard await connectionToClient.clientSupportsWorkDoneProgress else {
567-
throw ResponseError.unknown("Client does not support work done progress")
582+
let percentage: Int? =
583+
if let progress = notification.progress, let total = notification.total {
584+
Int((Double(progress) / Double(total) * 100).rounded())
585+
} else {
586+
nil
587+
}
588+
await progressManager.update(message: notification.message, percentage: percentage)
589+
}
590+
591+
private func taskFinish(notification: TaskFinishNotification) async {
592+
guard let progressManager = workDoneProgressManagers[notification.taskId.id] else {
593+
return
568594
}
569-
return try await connectionToClient.send(request as LanguageServerProtocol.CreateWorkDoneProgressRequest)
595+
await progressManager.end()
596+
workDoneProgressManagers[notification.taskId.id] = nil
570597
}
571598

572599
// MARK: Build System queries

Sources/BuildSystemIntegration/BuildSystemManagerDelegate.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ package protocol BuildSystemManagerDelegate: AnyObject, Sendable {
4242
/// This is distinct from `BuildSystemManagerDelegate` because the delegate only gets set on the build system after the
4343
/// workspace that created it has been initialized (see `BuildSystemManager.setDelegate`). But the `BuildSystemManager`
4444
/// can send notifications to the client immediately.
45-
package protocol BuildSystemManagerConnectionToClient: Sendable {
45+
package protocol BuildSystemManagerConnectionToClient: Sendable, Connection {
4646
/// Whether the client can handle `WorkDoneProgress` requests.
4747
var clientSupportsWorkDoneProgress: Bool { get async }
4848

49-
func send(_ notification: some NotificationType) async
50-
51-
func send<R: RequestType>(_ request: R) async throws -> R.Response
49+
/// Wait until the connection to the client has been initialized.
50+
///
51+
/// No messages should be sent on this connection before this returns.
52+
func waitUntilInitialized() async
5253

5354
/// Start watching for file changes with the given glob patterns.
5455
func watchFiles(_ fileWatchers: [FileSystemWatcher]) async

Sources/BuildSystemIntegration/BuildSystemMessageDependencyTracker.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ package enum BuildSystemMessageDependencyTracker: QueueBasedMessageHandlerDepend
3636

3737
/// A task that is responsible for logging information to the client. They can be run concurrently to any state read
3838
/// and changes but logging tasks must be ordered among each other.
39-
case logging
39+
case taskProgress
4040

4141
/// Whether this request needs to finish before `other` can start executing.
4242
package func isDependency(of other: BuildSystemMessageDependencyTracker) -> Bool {
@@ -45,9 +45,9 @@ package enum BuildSystemMessageDependencyTracker: QueueBasedMessageHandlerDepend
4545
case (.stateChange, .stateRead): return true
4646
case (.stateRead, .stateChange): return true
4747
case (.stateRead, .stateRead): return false
48-
case (.logging, .logging): return true
49-
case (.logging, _): return false
50-
case (_, .logging): return false
48+
case (.taskProgress, .taskProgress): return true
49+
case (.taskProgress, _): return false
50+
case (_, .taskProgress): return false
5151
}
5252
}
5353

@@ -60,7 +60,7 @@ package enum BuildSystemMessageDependencyTracker: QueueBasedMessageHandlerDepend
6060
case is OnBuildInitializedNotification:
6161
self = .stateChange
6262
case is OnBuildLogMessageNotification:
63-
self = .logging
63+
self = .taskProgress
6464
case is OnBuildTargetDidChangeNotification:
6565
self = .stateChange
6666
case is OnWatchedFilesDidChangeNotification:
@@ -84,8 +84,8 @@ package enum BuildSystemMessageDependencyTracker: QueueBasedMessageHandlerDepend
8484
self = .stateRead
8585
case is BuildTargetSourcesRequest:
8686
self = .stateRead
87-
case is BuildServerProtocol.CreateWorkDoneProgressRequest:
88-
self = .logging
87+
case is TaskStartNotification, is TaskProgressNotification, is TaskFinishNotification:
88+
self = .taskProgress
8989
case is InitializeBuildRequest:
9090
self = .stateChange
9191
case is RegisterForChanges:

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,17 +394,18 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
394394
///
395395
/// - Important: Must only be called on `packageLoadingQueue`.
396396
private func reloadPackageAssumingOnPackageLoadingQueue() async throws {
397-
let progressManager = WorkDoneProgressManager(
398-
connectionToClient: self.connectionToSourceKitLSP,
399-
waitUntilClientInitialized: {},
400-
tokenPrefix: "package-reloading",
401-
initialDebounce: options.workDoneProgressDebounceDurationOrDefault,
402-
title: "SourceKit-LSP: Reloading Package"
397+
self.connectionToSourceKitLSP.send(
398+
TaskStartNotification(
399+
taskId: TaskId(id: "package-reloading"),
400+
data: WorkDoneProgressTask(title: "SourceKit-LSP: Reloading Package").encodeToLSPAny()
401+
)
403402
)
404403
await testHooks.reloadPackageDidStart?()
405404
defer {
406405
Task {
407-
await progressManager?.end()
406+
self.connectionToSourceKitLSP.send(
407+
TaskFinishNotification(taskId: TaskId(id: "package-reloading"), status: .ok)
408+
)
408409
await testHooks.reloadPackageDidFinish?()
409410
}
410411
}

0 commit comments

Comments
 (0)