From d73433128def788d7cc69069fc20774d90074078 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Mon, 1 Sep 2025 19:50:43 +0200 Subject: [PATCH] Make the `build/logMessage` conform to the BSP spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When I added the log structure to `build/logMessage` in #2022 I must have assumed that the entire BSP notifciation was an extension defined by SourceKit-LSP and didn’t realized that this was actually a change that made the notification non-compliant with BSP. Change it up a little bit to make it compliant again. --- Contributor Documentation/BSP Extensions.md | 45 ++++++ Contributor Documentation/LSP Extensions.md | 12 +- .../BuildServerManager.swift | 22 ++- .../BuildServerManagerDelegate.swift | 6 +- .../SwiftPMBuildServer.swift | 23 +-- .../OnBuildLogMessageNotification.swift | 146 +++++++++++++++++- .../PreparationTaskDescription.swift | 6 +- .../SemanticIndex/SemanticIndexManager.swift | 6 +- .../UpdateIndexStoreTaskDescription.swift | 6 +- Sources/SourceKitLSP/SourceKitLSPServer.swift | 2 +- Sources/SourceKitLSP/Workspace.swift | 6 +- 11 files changed, 246 insertions(+), 34 deletions(-) diff --git a/Contributor Documentation/BSP Extensions.md b/Contributor Documentation/BSP Extensions.md index bb3cd71e2..c8497f039 100644 --- a/Contributor Documentation/BSP Extensions.md +++ b/Contributor Documentation/BSP Extensions.md @@ -38,6 +38,51 @@ export interface SourceKitInitializeBuildResponseData { } ``` +## `build/logMessage` + +Added fields: + +```ts +/** + * Extends BSPs log message grouping by explicitly starting and ending the log for a specific task ID. + */ +structure?: StructuredLogBegin | StructuredLogReport | StructuredLogEnd; +``` + +With + +```ts +/** + * Indicates the beginning of a new task that may receive updates with `StructuredLogReport` or `StructuredLogEnd` + * payloads. + */ +export interface StructuredLogBegin { + kind: 'begin' + + /** + * A succinct title that can be used to describe the task that started this structured. + */ + title: string; +} + + +/** + * Adds a new log message to a structured log without ending it. + */ +export interface StructuredLogReport { + kind: 'report'; +} + +/** + * Ends a structured log. No more `StructuredLogReport` updates should be sent for this task ID. + * + * The task ID may be re-used for new structured logs by beginning a new structured log for that task. + */ +export interface StructuredLogEnd { + kind: 'end'; +} +``` + ## `build/taskStart` 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. diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index 0ecfb52cc..d8552e66f 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -517,7 +517,7 @@ With * payloads. */ export interface StructuredLogBegin { - kind: 'begin' + kind: 'begin'; /** * A succinct title that can be used to describe the task that started this structured. @@ -535,10 +535,7 @@ export interface StructuredLogBegin { * Adds a new log message to a structured log without ending it. */ export interface StructuredLogReport { - /* - * A unique identifier, identifying the task this structured log message belongs to. - */ - taskID: string; + kind: 'report'; } /** @@ -547,10 +544,7 @@ export interface StructuredLogReport { * The task ID may be re-used for new structured logs by beginning a new structured log for that task. */ export interface StructuredLogEnd { - /* - * A unique identifier, identifying the task this structured log message belongs to. - */ - taskID: string; + kind: 'end'; } ``` diff --git a/Sources/BuildServerIntegration/BuildServerManager.swift b/Sources/BuildServerIntegration/BuildServerManager.swift index b71285afc..44a03768e 100644 --- a/Sources/BuildServerIntegration/BuildServerManager.swift +++ b/Sources/BuildServerIntegration/BuildServerManager.swift @@ -698,7 +698,7 @@ package actor BuildServerManager: QueueBasedMessageHandler { await filesBuildSettingsChangedDebouncer.scheduleCall(Set(watchedFiles.keys)) } - private func logMessage(notification: BuildServerProtocol.OnBuildLogMessageNotification) async { + private func logMessage(notification: OnBuildLogMessageNotification) async { await connectionToClient.waitUntilInitialized() let type: WindowMessageType = switch notification.type { @@ -710,7 +710,7 @@ package actor BuildServerManager: QueueBasedMessageHandler { connectionToClient.logMessageToIndexLog( message: notification.message, type: type, - structure: notification.structure + structure: notification.lspStructure ) } @@ -1739,3 +1739,21 @@ private let supplementalClangIndexingArgs: [String] = [ "-Wno-non-modular-include-in-framework-module", "-Wno-incomplete-umbrella", ] + +private extension OnBuildLogMessageNotification { + var lspStructure: LanguageServerProtocol.StructuredLogKind? { + guard let taskId = self.task?.id else { + return nil + } + switch structure { + case .begin(let info): + return .begin(StructuredLogBegin(title: info.title, taskID: taskId)) + case .report: + return .report(StructuredLogReport(taskID: taskId)) + case .end: + return .end(StructuredLogEnd(taskID: taskId)) + case nil: + return nil + } + } +} diff --git a/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift b/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift index d03c8e44c..37d0cef91 100644 --- a/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift +++ b/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift @@ -50,5 +50,9 @@ package protocol BuildServerManagerConnectionToClient: Sendable, Connection { func watchFiles(_ fileWatchers: [FileSystemWatcher]) async /// Log a message in the client's index log. - func logMessageToIndexLog(message: String, type: WindowMessageType, structure: StructuredLogKind?) + func logMessageToIndexLog( + message: String, + type: WindowMessageType, + structure: LanguageServerProtocol.StructuredLogKind? + ) } diff --git a/Sources/BuildServerIntegration/SwiftPMBuildServer.swift b/Sources/BuildServerIntegration/SwiftPMBuildServer.swift index 2b6186a4c..ed1291820 100644 --- a/Sources/BuildServerIntegration/SwiftPMBuildServer.swift +++ b/Sources/BuildServerIntegration/SwiftPMBuildServer.swift @@ -184,12 +184,13 @@ package actor SwiftPMBuildServer: BuiltInBuildServer { self.connectionToSourceKitLSP = connectionToSourceKitLSP // Start an open-ended log for messages that we receive during package loading. We never end this log. - let logTaskID = "swiftpm-log-\(UUID())" + let logTaskID = TaskId(id: "swiftpm-log-\(UUID())") connectionToSourceKitLSP.send( OnBuildLogMessageNotification( type: .info, + task: logTaskID, message: "", - structure: .begin(StructuredLogBegin(title: "SwiftPM log for \(projectRoot.path)", taskID: logTaskID)) + structure: .begin(StructuredLogBegin(title: "SwiftPM log for \(projectRoot.path)")) ) ) @@ -197,8 +198,9 @@ package actor SwiftPMBuildServer: BuiltInBuildServer { connectionToSourceKitLSP.send( OnBuildLogMessageNotification( type: .info, + task: logTaskID, message: diagnostic.description, - structure: .report(StructuredLogReport(taskID: logTaskID)) + structure: .report(StructuredLogReport()) ) ) logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)") @@ -750,12 +752,10 @@ package actor SwiftPMBuildServer: BuiltInBuildServer { connectionToSourceKitLSP.send( BuildServerProtocol.OnBuildLogMessageNotification( type: .info, + task: taskID, message: "\(arguments.joined(separator: " "))", structure: .begin( - StructuredLogBegin( - title: "Preparing \(self.swiftPMTargets[target]?.name ?? target.uri.stringValue)", - taskID: taskID.id - ) + StructuredLogBegin(title: "Preparing \(self.swiftPMTargets[target]?.name ?? target.uri.stringValue)") ) ) ) @@ -763,8 +763,9 @@ package actor SwiftPMBuildServer: BuiltInBuildServer { self.connectionToSourceKitLSP.send( BuildServerProtocol.OnBuildLogMessageNotification( type: .info, + task: taskID, message: message, - structure: .report(StructuredLogReport(taskID: taskID.id)) + structure: .report(StructuredLogReport()) ) ) } @@ -772,8 +773,9 @@ package actor SwiftPMBuildServer: BuiltInBuildServer { self.connectionToSourceKitLSP.send( BuildServerProtocol.OnBuildLogMessageNotification( type: .info, + task: taskID, message: message, - structure: .report(StructuredLogReport(taskID: taskID.id)) + structure: .report(StructuredLogReport()) ) ) } @@ -790,8 +792,9 @@ package actor SwiftPMBuildServer: BuiltInBuildServer { self.connectionToSourceKitLSP.send( BuildServerProtocol.OnBuildLogMessageNotification( type: exitStatus.isSuccess ? .info : .error, + task: taskID, message: "Finished with \(exitStatus.description) in \(start.duration(to: .now))", - structure: .end(StructuredLogEnd(taskID: taskID.id)) + structure: .end(StructuredLogEnd()) ) ) switch exitStatus { diff --git a/Sources/BuildServerProtocol/Messages/OnBuildLogMessageNotification.swift b/Sources/BuildServerProtocol/Messages/OnBuildLogMessageNotification.swift index 94677c466..b9b5bd5dd 100644 --- a/Sources/BuildServerProtocol/Messages/OnBuildLogMessageNotification.swift +++ b/Sources/BuildServerProtocol/Messages/OnBuildLogMessageNotification.swift @@ -14,23 +14,161 @@ public import LanguageServerProtocol /// The log message notification is sent from a server to a client to ask the client to log a particular message in its console. /// -/// A `build/logMessage`` notification is similar to LSP's `window/logMessage``. +/// A `build/logMessage`` notification is similar to LSP's `window/logMessage``, except for a few additions like id and originId. public struct OnBuildLogMessageNotification: NotificationType { public static let method: String = "build/logMessage" /// The message type. public var type: MessageType + /// The task id if any. + public var task: TaskId? + + /// The request id that originated this notification. + /// The originId field helps clients know which request originated a notification in case several requests are handled by the + /// client at the same time. It will only be populated if the client defined it in the request that triggered this notification. + public var originId: OriginId? + /// The actual message. public var message: String - /// If specified, allows grouping log messages that belong to the same originating task together instead of logging - /// them in chronological order in which they were produced. + /// Extends BSPs log message grouping by explicitly starting and ending the log for a specific task ID. + /// + /// **(BSP Extension)*** public var structure: StructuredLogKind? - public init(type: MessageType, message: String, structure: StructuredLogKind?) { + public init( + type: MessageType, + task: TaskId? = nil, + originId: OriginId? = nil, + message: String, + structure: StructuredLogKind? = nil + ) { self.type = type + self.task = task + self.originId = originId self.message = message self.structure = structure } } + +public enum StructuredLogKind: Codable, Hashable, Sendable { + case begin(StructuredLogBegin) + case report(StructuredLogReport) + case end(StructuredLogEnd) + + public init(from decoder: Decoder) throws { + if let begin = try? StructuredLogBegin(from: decoder) { + self = .begin(begin) + } else if let report = try? StructuredLogReport(from: decoder) { + self = .report(report) + } else if let end = try? StructuredLogEnd(from: decoder) { + self = .end(end) + } else { + let context = DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected StructuredLogBegin, StructuredLogReport, or StructuredLogEnd" + ) + throw DecodingError.dataCorrupted(context) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .begin(let begin): + try begin.encode(to: encoder) + case .report(let report): + try report.encode(to: encoder) + case .end(let end): + try end.encode(to: encoder) + } + } +} + +/// Indicates the beginning of a new task that may receive updates with `StructuredLogReport` or `StructuredLogEnd` +/// payloads. +public struct StructuredLogBegin: Codable, Hashable, Sendable { + /// A succinct title that can be used to describe the task that started this structured. + public var title: String + + private enum CodingKeys: CodingKey { + case kind + case title + } + + public init(title: String) { + self.title = title + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + guard try container.decode(String.self, forKey: .kind) == "begin" else { + throw DecodingError.dataCorruptedError( + forKey: .kind, + in: container, + debugDescription: "Kind of StructuredLogBegin is not 'begin'" + ) + } + + self.title = try container.decode(String.self, forKey: .title) + + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("begin", forKey: .kind) + try container.encode(self.title, forKey: .title) + } +} + +/// Adds a new log message to a structured log without ending it. +public struct StructuredLogReport: Codable, Hashable, Sendable { + private enum CodingKeys: CodingKey { + case kind + } + + public init() {} + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + guard try container.decode(String.self, forKey: .kind) == "report" else { + throw DecodingError.dataCorruptedError( + forKey: .kind, + in: container, + debugDescription: "Kind of StructuredLogReport is not 'report'" + ) + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("report", forKey: .kind) + } +} + +/// Ends a structured log. No more `StructuredLogReport` updates should be sent for this task ID. +/// +/// The task ID may be re-used for new structured logs by beginning a new structured log for that task. +public struct StructuredLogEnd: Codable, Hashable, Sendable { + private enum CodingKeys: CodingKey { + case kind + } + + public init() {} + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + guard try container.decode(String.self, forKey: .kind) == "end" else { + throw DecodingError.dataCorruptedError( + forKey: .kind, + in: container, + debugDescription: "Kind of StructuredLogEnd is not 'end'" + ) + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("end", forKey: .kind) + } +} diff --git a/Sources/SemanticIndex/PreparationTaskDescription.swift b/Sources/SemanticIndex/PreparationTaskDescription.swift index 4d45be438..5e5d932b1 100644 --- a/Sources/SemanticIndex/PreparationTaskDescription.swift +++ b/Sources/SemanticIndex/PreparationTaskDescription.swift @@ -41,7 +41,9 @@ package struct PreparationTaskDescription: IndexTaskDescription { /// See `SemanticIndexManager.logMessageToIndexLog`. private let logMessageToIndexLog: - @Sendable (_ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind) -> Void + @Sendable ( + _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind + ) -> Void /// Hooks that should be called when the preparation task finishes. private let hooks: IndexHooks @@ -65,7 +67,7 @@ package struct PreparationTaskDescription: IndexTaskDescription { preparationUpToDateTracker: UpToDateTracker, logMessageToIndexLog: @escaping @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind + _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind ) -> Void, hooks: IndexHooks ) { diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 3534c1709..c11386601 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -215,7 +215,9 @@ package final actor SemanticIndexManager { /// Callback that is called when an indexing task produces output it wants to log to the index log. private let logMessageToIndexLog: - @Sendable (_ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind) -> Void + @Sendable ( + _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind + ) -> Void /// Called when files are scheduled to be indexed. /// @@ -263,7 +265,7 @@ package final actor SemanticIndexManager { indexTaskScheduler: TaskScheduler, logMessageToIndexLog: @escaping @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind + _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind ) -> Void, indexTasksWereScheduled: @escaping @Sendable (Int) -> Void, indexProgressStatusDidChange: @escaping @Sendable () -> Void diff --git a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift index 253dce7ee..c43bdafe8 100644 --- a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift +++ b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift @@ -113,7 +113,9 @@ package struct UpdateIndexStoreTaskDescription: IndexTaskDescription { /// See `SemanticIndexManager.logMessageToIndexLog`. private let logMessageToIndexLog: - @Sendable (_ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind) -> Void + @Sendable ( + _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind + ) -> Void /// How long to wait until we cancel an update indexstore task. This timeout should be long enough that all /// `swift-frontend` tasks finish within it. It prevents us from blocking the index if the type checker gets stuck on @@ -148,7 +150,7 @@ package struct UpdateIndexStoreTaskDescription: IndexTaskDescription { indexFilesWithUpToDateUnit: Bool, logMessageToIndexLog: @escaping @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: StructuredLogKind + _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind ) -> Void, timeout: Duration, hooks: IndexHooks diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index eb5cd177d..b06eed916 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -870,7 +870,7 @@ extension SourceKitLSPServer { nonisolated package func logMessageToIndexLog( message: String, type: WindowMessageType, - structure: StructuredLogKind? + structure: LanguageServerProtocol.StructuredLogKind? ) { self.sendNotificationToClient( LogMessageNotification( diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 0b0e1bf08..0fe7095e2 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -253,7 +253,11 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { await sourceKitLSPServer?.watchFiles(fileWatchers) } - func logMessageToIndexLog(message: String, type: WindowMessageType, structure: StructuredLogKind?) { + func logMessageToIndexLog( + message: String, + type: WindowMessageType, + structure: LanguageServerProtocol.StructuredLogKind? + ) { guard let sourceKitLSPServer else { // `SourceKitLSPServer` has been destructed. We are tearing down the // language server. Nothing left to do.