From ed6f6761905eea903dd14912d54fdfa570a78535 Mon Sep 17 00:00:00 2001 From: Owen Voorhees Date: Sat, 30 Aug 2025 22:10:09 -0700 Subject: [PATCH] Add a ShowFocusedDiagnosticsRequest LSP extension and wire it up to inferred types remarks --- Contributor Documentation/LSP Extensions.md | 29 +++++ .../ShowFocusedDiagnosticsRequest.swift | 48 +++++++++ Sources/SourceKitLSP/SourceKitLSPServer.swift | 1 + .../FocusedRemarksCommand.swift | 101 ++++++++++++++++++ .../SwiftLanguageService/SwiftCommand.swift | 1 + .../SwiftLanguageService.swift | 54 ++++++++++ .../ExecuteCommandTests.swift | 37 +++++++ 7 files changed, 271 insertions(+) create mode 100644 Sources/LanguageServerProtocol/Requests/ShowFocusedDiagnosticsRequest.swift create mode 100644 Sources/SwiftLanguageService/FocusedRemarksCommand.swift diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index f8d2e5b2c..f0f0314e9 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -770,6 +770,35 @@ export interface PeekDocumentsResult { } ``` +## `workspace/showFocusedDiagnostics` + +Request from the server to the client to display focused diagnostics for a specific subset of the source. + +It requires the experimental client capability `"workspace/showFocusedDiagnostics"` to use. + +- params: `ShowFocusedDiagnosticsParams` +- result: `ShowFocusedDiagnosticsResult` + +```ts +export interface ShowFocusedDiagnosticsParams { + /** + * Array of diagnostics to display + */ + diagnostics: Diagnostic[]; + /** + * The `DocumentUri` of the text document in which to present the diagnostics. + */ + uri: DocumentUri; +} + +/** + * Response to indicate the `success` of the `ShowFocusedDiagnosticsRequest` + */ +export interface ShowFocusedDiagnosticsResult { + success: boolean; +} +``` + ## `workspace/synchronize` Request from the client to the server to wait for SourceKit-LSP to handle all ongoing requests and, optionally, wait for background activity to finish. diff --git a/Sources/LanguageServerProtocol/Requests/ShowFocusedDiagnosticsRequest.swift b/Sources/LanguageServerProtocol/Requests/ShowFocusedDiagnosticsRequest.swift new file mode 100644 index 000000000..98dc54bfd --- /dev/null +++ b/Sources/LanguageServerProtocol/Requests/ShowFocusedDiagnosticsRequest.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Request from the server to the client to show focused diagnostics **(LSP Extension)** +/// +/// This request is handled by the client to display focused diagnostic information +/// related to a subset of the source. +/// +/// - Parameters: +/// - diagnostics: Array of diagnostics to display +/// - uri: Document URI in which to present the diagnostics +/// +/// - Returns: `ShowFocusedDiagnosticsResponse` which indicates the `success` of the request. +/// +/// ### LSP Extension +/// +/// This request is an extension to LSP supported by SourceKit-LSP. +/// It requires the experimental client capability `workspace/showFocusedDiagnostics` to use. +public struct ShowFocusedDiagnosticsRequest: RequestType { + public static let method: String = "workspace/showFocusedDiagnostics" + public typealias Response = ShowFocusedDiagnosticsResponse + + public var diagnostics: [Diagnostic] + public var uri: DocumentURI + + public init(diagnostics: [Diagnostic], uri: DocumentURI) { + self.diagnostics = diagnostics + self.uri = uri + } +} + +/// Response to indicate the `success` of the `ShowFocusedDiagnosticsRequest` +public struct ShowFocusedDiagnosticsResponse: ResponseType { + public var success: Bool + + public init(success: Bool) { + self.success = success + } +} diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 1ddd7099b..e0a78d3f5 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -963,6 +963,7 @@ extension SourceKitLSPServer { PeekDocumentsRequest.method, GetReferenceDocumentRequest.method, DidChangeActiveDocumentNotification.method, + ShowFocusedDiagnosticsRequest.method, ] for capabilityName in experimentalClientCapabilities { guard let experimentalCapability = initializationOptions[capabilityName] else { diff --git a/Sources/SwiftLanguageService/FocusedRemarksCommand.swift b/Sources/SwiftLanguageService/FocusedRemarksCommand.swift new file mode 100644 index 000000000..df0255214 --- /dev/null +++ b/Sources/SwiftLanguageService/FocusedRemarksCommand.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +package import LanguageServerProtocol +import SourceKitD + +/// Describes a kind of focused remarks supported by the compiler. Remarks should be exposed via +/// a flag which accepts a position in the document as a pair and only emits +/// diagnostics relavant to that position (e.g. expressions or function bodies with source ranges that +/// contain the position). +package enum FocusedRemarksKind: String, CaseIterable, Codable { + case showInferredTypes + + package var defaultTitle: String { + switch self { + case .showInferredTypes: + return "Show Inferred Types" + } + } + + package func additionalCompilerArgs(line: Int, column: Int) -> [String] { + switch self { + case .showInferredTypes: + return [ + "-Xfrontend", + "-Rinferred-types-at", + "-Xfrontend", + "\(line):\(column)", + ] + } + } +} + +package struct FocusedRemarksCommand: SwiftCommand { + package static let identifier: String = "focused.remarks.command" + + package let commandType: FocusedRemarksKind + package var title: String + package let position: Position + package let textDocument: TextDocumentIdentifier + + package init(commandType: FocusedRemarksKind, position: Position, textDocument: TextDocumentIdentifier) { + self.commandType = commandType + self.position = position + self.textDocument = textDocument + self.title = commandType.defaultTitle + } + + package init?(fromLSPDictionary dictionary: [String: LSPAny]) { + guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue], + case .string(let title)? = dictionary[CodingKeys.title.stringValue], + case .dictionary(let positionDict)? = dictionary[CodingKeys.position.stringValue], + case .string(let commandTypeString)? = dictionary[CodingKeys.commandType.stringValue] + else { + return nil + } + guard let position = Position(fromLSPDictionary: positionDict), + let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict), + let commandType = FocusedRemarksKind(rawValue: commandTypeString) + else { + return nil + } + + self.init( + commandType: commandType, + title: title, + position: position, + textDocument: textDocument + ) + } + + package init( + commandType: FocusedRemarksKind, + title: String, + position: Position, + textDocument: TextDocumentIdentifier + ) { + self.commandType = commandType + self.title = title + self.position = position + self.textDocument = textDocument + } + + package func encodeToLSPAny() -> LSPAny { + return .dictionary([ + CodingKeys.title.stringValue: .string(title), + CodingKeys.position.stringValue: position.encodeToLSPAny(), + CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(), + CodingKeys.commandType.stringValue: .string(commandType.rawValue), + ]) + } +} diff --git a/Sources/SwiftLanguageService/SwiftCommand.swift b/Sources/SwiftLanguageService/SwiftCommand.swift index 54677077f..13a395b25 100644 --- a/Sources/SwiftLanguageService/SwiftCommand.swift +++ b/Sources/SwiftLanguageService/SwiftCommand.swift @@ -51,6 +51,7 @@ extension SwiftLanguageService { [ SemanticRefactorCommand.self, ExpandMacroCommand.self, + FocusedRemarksCommand.self, ].map { (command: any SwiftCommand.Type) in command.identifier } diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index 759102a4e..43fabaa9e 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -892,6 +892,7 @@ extension SwiftLanguageService { (retrieveSyntaxCodeActions, nil), (retrieveRefactorCodeActions, .refactor), (retrieveQuickFixCodeActions, .quickFix), + (retrieveFocusedRemarksCodeActions, nil), ] let wantedActionKinds = req.context.only let providers: [CodeActionProvider] = providersAndKinds.compactMap { @@ -1028,6 +1029,21 @@ extension SwiftLanguageService { return codeActions } + func retrieveFocusedRemarksCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] { + guard self.capabilityRegistry.clientHasExperimentalCapability(ShowFocusedDiagnosticsRequest.method) else { + return [] + } + return FocusedRemarksKind.allCases.map { commandType in + let command = FocusedRemarksCommand( + commandType: commandType, + position: params.range.lowerBound, + textDocument: params.textDocument + ) + .asCommand() + return CodeAction(title: command.title, kind: nil, command: command) + } + } + package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { let uri = req.textDocument.uri let infos = try await variableTypeInfos(uri, req.range) @@ -1119,6 +1135,8 @@ extension SwiftLanguageService { try await semanticRefactoring(command) } else if let command = req.swiftCommand(ofType: ExpandMacroCommand.self) { try await expandMacro(command) + } else if let command = req.swiftCommand(ofType: FocusedRemarksCommand.self) { + try await executeFocusedRemarksCommand(command) } else { throw ResponseError.unknown("unknown command \(req.command)") } @@ -1126,6 +1144,42 @@ extension SwiftLanguageService { return nil } + package func executeFocusedRemarksCommand(_ command: FocusedRemarksCommand) async throws { + let snapshot = try self.documentManager.latestSnapshot(command.textDocument.uri) + let buildSettings = await self.buildSettings( + for: command.textDocument.uri, + fallbackAfterTimeout: true + ) + + guard var buildSettings else { + throw ResponseError.unknown("Unable to get build settings for '\(command.textDocument.uri)'") + } + + let line = command.position.line + 1 + let column = command.position.utf16index + 1 + buildSettings.compilerArguments.append( + contentsOf: command.commandType.additionalCompilerArgs( + line: line, + column: column + ) + ) + + let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport( + for: snapshot, + buildSettings: SwiftCompileCommand(buildSettings) + ) + + let showFocusedDiagnosticsRequest = ShowFocusedDiagnosticsRequest( + diagnostics: diagnosticReport.items, + uri: snapshot.uri + ) + + guard let sourceKitLSPServer else { + throw ResponseError.unknown("Connection to the editor closed") + } + _ = try await sourceKitLSPServer.sendRequestToClient(showFocusedDiagnosticsRequest) + } + package func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse { let referenceDocumentURL = try ReferenceDocumentURL(from: req.uri) diff --git a/Tests/SourceKitLSPTests/ExecuteCommandTests.swift b/Tests/SourceKitLSPTests/ExecuteCommandTests.swift index 4eaf60696..ab9b6485e 100644 --- a/Tests/SourceKitLSPTests/ExecuteCommandTests.swift +++ b/Tests/SourceKitLSPTests/ExecuteCommandTests.swift @@ -189,4 +189,41 @@ final class ExecuteCommandTests: XCTestCase { req.arguments = [1, 2, "", metadata.encodeToLSPAny()] XCTAssertEqual([1, 2, ""], req.argumentsWithoutSourceKitMetadata) } + + func testShowInferredTypesCommand() async throws { + let testClient = try await TestSourceKitLSPClient( + capabilities: ClientCapabilities(experimental: [ + "workspace/showFocusedDiagnostics": .dictionary(["supported": .bool(true)]) + ]) + ) + let uri = DocumentURI(for: .swift) + let positions = testClient.openDocument( + """ + func test() -> Int { + 11️⃣0 + } + """, + uri: uri + ) + let textDocument = TextDocumentIdentifier(uri) + let command = FocusedRemarksCommand( + commandType: .showInferredTypes, + position: positions["1️⃣"], + textDocument: textDocument + ) + let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) + let request = ExecuteCommandRequest( + command: FocusedRemarksCommand.identifier, + arguments: [command.encodeToLSPAny(), metadata.encodeToLSPAny()] + ) + let expectation = self.expectation(description: "Handle ShowFocusedDiagnosticsRequest") + + testClient.handleSingleRequest { (req: ShowFocusedDiagnosticsRequest) in + expectation.fulfill() + XCTAssertEqual(req.diagnostics.map(\.message), ["integer literal was inferred to be of type 'Int'"]) + return ShowFocusedDiagnosticsResponse(success: true) + } + _ = try await testClient.send(request) + try await fulfillmentOfOrThrow(expectation) + } }