From f4e4e06e4d272990426d2e98e2fe5dc70bce6f8a Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Fri, 22 Nov 2024 16:43:01 -0500 Subject: [PATCH 1/7] add `textDocument/convertDocumentation` request to SourceKit-LSP --- Contributor Documentation/LSP Extensions.md | 88 ++++ Package.swift | 3 + Sources/LanguageServerProtocol/CMakeLists.txt | 1 + Sources/LanguageServerProtocol/Messages.swift | 1 + .../ConvertDocumentationRequest.swift | 148 ++++++ .../TestSourceKitLSPClient.swift | 8 +- .../Documentation/DocCServer.swift | 175 ++++++++ .../Documentation/DocumentationManager.swift | 242 ++++++++++ Sources/SourceKitLSP/SourceKitLSPServer.swift | 39 +- Sources/SourceKitLSP/Swift/CursorInfo.swift | 9 +- .../ConvertDocumentationTests.swift | 422 ++++++++++++++++++ 11 files changed, 1124 insertions(+), 12 deletions(-) create mode 100644 Sources/LanguageServerProtocol/Requests/ConvertDocumentationRequest.swift create mode 100644 Sources/SourceKitLSP/Documentation/DocCServer.swift create mode 100644 Sources/SourceKitLSP/Documentation/DocumentationManager.swift create mode 100644 Tests/SourceKitLSPTests/ConvertDocumentationTests.swift diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index 3130f8615..c9f2b5ca4 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -169,6 +169,94 @@ interface SKCompletionOptions { } ``` +## `textDocument/convertDocumentation` + +New request that returns a RenderNode for a symbol at a given location that can then be +rendered in an editor by `swiftlang/swift-docc-render`. + +This request uses Swift DocC's convert service to convert documentation for a Swift symbol +or Markup/Tutorial file. It responds with a string containing a JSON encoded `RenderNode` +or an error if the documentation could not be converted. This error message can be displayed +to the user in the live preview editor. + +At the moment this request is only available on macOS and Linux. If SourceKit-LSP supports +this request it will add `textDocument/convertDocumentation` to its experimental server +capabilities. + +```ts +export interface ConvertDocumentationParams { + /** + * The document to render documentation for. + */ + textDocument: TextDocumentIdentifier; + + /** + * The document location at which to lookup symbol information. + * + * This parameter is only used in Swift files to determine which symbol to render. + * The position is ignored for markdown and tutorial documents. + */ + position: Position; +} + +export type ConvertDocumentationResponse = RenderNodeResponse | ErrorResponse; + +interface RenderNodeResponse { + /** + * The type of this response: either a RenderNode or error. + */ + type: "renderNode"; + + /** + * The JSON encoded RenderNode that can be rendered by swift-docc-render. + */ + renderNode: string; +} + +interface ErrorResponse { + /** + * The type of this response: either a RenderNode or error. + */ + type: "error"; + + /** + * The error that occurred. + */ + error: ConvertDocumentationError; +} + +export type ConvertDocumentationError = ErrorWithNoParams | SymbolNotFoundError; + +interface ErrorWithNoParams { + /** + * The kind of error that occurred. + */ + kind: "indexNotAvailable" | "noDocumentation"; + + /** + * A human readable error message that can be shown to the user. + */ + message: string; +} + +interface SymbolNotFoundError { + /** + * The kind of error that occurred. + */ + kind: "symbolNotFound"; + + /** + * The name of the symbol that could not be found. + */ + symbolName: string; + + /** + * A human readable error message that can be shown to the user. + */ + message: string; +} +``` + ## `textDocument/symbolInfo` New request for semantic information about the symbol at a given location. diff --git a/Package.swift b/Package.swift index 2d87941e6..448c3370d 100644 --- a/Package.swift +++ b/Package.swift @@ -384,6 +384,7 @@ var targets: [Target] = [ "SwiftExtensions", "ToolchainRegistry", "TSCExtensions", + .product(name: "SwiftDocC", package: "swift-docc"), .product(name: "IndexStoreDB", package: "indexstore-db"), .product(name: "Crypto", package: "swift-crypto"), .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), @@ -570,6 +571,7 @@ var dependencies: [Package.Dependency] { } else if useLocalDependencies { return [ .package(path: "../indexstore-db"), + .package(path: "../swift-docc"), .package(name: "swift-package-manager", path: "../swiftpm"), .package(path: "../swift-tools-support-core"), .package(path: "../swift-argument-parser"), @@ -581,6 +583,7 @@ var dependencies: [Package.Dependency] { return [ .package(url: "https://github.com/swiftlang/indexstore-db.git", branch: relatedDependenciesBranch), + .package(url: "https://github.com/swiftlang/swift-docc.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/swiftlang/swift-package-manager.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/apple/swift-tools-support-core.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"), diff --git a/Sources/LanguageServerProtocol/CMakeLists.txt b/Sources/LanguageServerProtocol/CMakeLists.txt index 3b6483686..79258a3a2 100644 --- a/Sources/LanguageServerProtocol/CMakeLists.txt +++ b/Sources/LanguageServerProtocol/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(LanguageServerProtocol STATIC Requests/ColorPresentationRequest.swift Requests/CompletionItemResolveRequest.swift Requests/CompletionRequest.swift + Requests/ConvertDocumentationRequest.swift Requests/CreateWorkDoneProgressRequest.swift Requests/DeclarationRequest.swift Requests/DefinitionRequest.swift diff --git a/Sources/LanguageServerProtocol/Messages.swift b/Sources/LanguageServerProtocol/Messages.swift index e0bd03a27..02aa62a6c 100644 --- a/Sources/LanguageServerProtocol/Messages.swift +++ b/Sources/LanguageServerProtocol/Messages.swift @@ -70,6 +70,7 @@ public let builtinRequests: [_RequestType.Type] = [ ShowMessageRequest.self, ShutdownRequest.self, SignatureHelpRequest.self, + ConvertDocumentationRequest.self, SymbolInfoRequest.self, TriggerReindexRequest.self, TypeDefinitionRequest.self, diff --git a/Sources/LanguageServerProtocol/Requests/ConvertDocumentationRequest.swift b/Sources/LanguageServerProtocol/Requests/ConvertDocumentationRequest.swift new file mode 100644 index 000000000..362dbfe82 --- /dev/null +++ b/Sources/LanguageServerProtocol/Requests/ConvertDocumentationRequest.swift @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 for converting documentation for the symbol at a given location **(LSP Extension)**. +/// +/// This request looks up the symbol (if any) at a given text document location and returns a +/// ``ConvertDocumentationResponse`` for that location. This request is primarily designed for editors +/// to support live preview of Swift documentation. +/// +/// - Parameters: +/// - textDocument: The document to render documentation for. +/// - position: The document location at which to lookup symbol information. +/// +/// - Returns: A ``ConvertDocumentationResponse`` for the given location, which may contain an error +/// message if documentation could not be converted. This error message can be displayed to the user +/// in the live preview editor. +/// +/// ### LSP Extension +/// +/// This request is an extension to LSP supported by SourceKit-LSP. +/// The client is expected to display the documentation in an editor using swift-docc-render. +public struct ConvertDocumentationRequest: TextDocumentRequest, Hashable { + public static let method: String = "textDocument/convertDocumentation" + public typealias Response = ConvertDocumentationResponse + + /// The document in which to lookup the symbol location. + public var textDocument: TextDocumentIdentifier + + /// The document location at which to lookup symbol information. + public var position: Position + + public init(textDocument: TextDocumentIdentifier, position: Position) { + self.textDocument = textDocument + self.position = position + } +} + +public enum ConvertDocumentationResponse: ResponseType { + case renderNode(String) + case error(ConvertDocumentationError) +} + +public enum ConvertDocumentationError: ResponseType, Equatable { + case indexNotAvailable + case noDocumentation + case symbolNotFound(String) + + var message: String { + switch self { + case .indexNotAvailable: + return "The index is not availble to complete the request" + case .noDocumentation: + return "No documentation could be rendered for the position in this document" + case .symbolNotFound(let symbolName): + return "Could not find symbol \(symbolName) in the project" + } + } +} + +extension ConvertDocumentationError: Codable { + enum CodingKeys: String, CodingKey { + case kind + case message + case symbolName + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + let kind = try values.decode(String.self, forKey: .kind) + switch kind { + case "indexNotAvailable": + self = .indexNotAvailable + case "noDocumentation": + self = .noDocumentation + case "symbolNotFound": + let symbolName = try values.decode(String.self, forKey: .symbolName) + self = .symbolNotFound(symbolName) + default: + throw DecodingError.dataCorruptedError( + forKey: CodingKeys.kind, + in: values, + debugDescription: "Invalid error kind: \(kind)" + ) + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .indexNotAvailable: + try container.encode("indexNotAvailable", forKey: .kind) + case .noDocumentation: + try container.encode("noDocumentation", forKey: .kind) + case .symbolNotFound(let symbolName): + try container.encode("symbolNotFound", forKey: .kind) + try container.encode(symbolName, forKey: .symbolName) + } + try container.encode(message, forKey: .message) + } +} + +extension ConvertDocumentationResponse: Codable { + enum CodingKeys: String, CodingKey { + case type + case renderNode + case error + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + let type = try values.decode(String.self, forKey: .type) + switch type { + case "renderNode": + let renderNode = try values.decode(String.self, forKey: .renderNode) + self = .renderNode(renderNode) + case "error": + let error = try values.decode(ConvertDocumentationError.self, forKey: .error) + self = .error(error) + default: + throw DecodingError.dataCorruptedError( + forKey: CodingKeys.type, + in: values, + debugDescription: "Invalid type: \(type)" + ) + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .renderNode(let renderNode): + try container.encode("renderNode", forKey: .type) + try container.encode(renderNode, forKey: .renderNode) + case .error(let error): + try container.encode("error", forKey: .type) + try container.encode(error, forKey: .error) + } + } +} diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift index b902705a8..5ef0e8f98 100644 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -228,12 +228,12 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable { // MARK: - Sending messages /// Send the request to `server` and return the request result. - package func send(_ request: R) async throws -> R.Response { - return try await withCheckedThrowingContinuation { continuation in + package func send(_ request: R) async throws(ResponseError) -> R.Response { + return try await withCheckedContinuation { continuation in self.send(request) { result in - continuation.resume(with: result) + continuation.resume(returning: result) } - } + }.get() } /// Variant of `send` above that allows the response to be discarded if it is a `VoidResponse`. diff --git a/Sources/SourceKitLSP/Documentation/DocCServer.swift b/Sources/SourceKitLSP/Documentation/DocCServer.swift new file mode 100644 index 000000000..7172b0053 --- /dev/null +++ b/Sources/SourceKitLSP/Documentation/DocCServer.swift @@ -0,0 +1,175 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(SwiftDocC) +import Foundation +@preconcurrency import SwiftDocC + +package struct DocCServer { + private let server: DocumentationServer + private let jsonEncoder = JSONEncoder() + private let jsonDecoder = JSONDecoder() + + init(peer peerServer: DocumentationServer? = nil, qualityOfService: DispatchQoS) { + server = DocumentationServer.createDefaultServer(qualityOfService: qualityOfService, peer: peerServer) + } + + func convert( + externalIDsToConvert: [String]?, + documentPathsToConvert: [String]?, + includeRenderReferenceStore: Bool, + documentationBundleLocation: URL?, + documentationBundleDisplayName: String, + documentationBundleIdentifier: String, + symbolGraphs: [Data], + emitSymbolSourceFileURIs: Bool, + markupFiles: [Data], + tutorialFiles: [Data], + convertRequestIdentifier: String, + completion: @escaping (_: Result) -> Void + ) { + let request = ConvertRequest( + bundleInfo: DocumentationBundle.Info( + displayName: documentationBundleDisplayName, + identifier: documentationBundleIdentifier, + defaultCodeListingLanguage: nil, + defaultAvailability: nil, + defaultModuleKind: nil, + ), + externalIDsToConvert: externalIDsToConvert, + documentPathsToConvert: documentPathsToConvert, + includeRenderReferenceStore: includeRenderReferenceStore, + bundleLocation: documentationBundleLocation, + symbolGraphs: symbolGraphs, + overridingDocumentationComments: nil, + knownDisambiguatedSymbolPathComponents: nil, + emitSymbolSourceFileURIs: emitSymbolSourceFileURIs, + markupFiles: markupFiles, + tutorialFiles: tutorialFiles, + miscResourceURLs: [], + symbolIdentifiersWithExpandedDocumentation: nil + ) + + makeRequest( + messageType: ConvertService.convertMessageType, + messageIdentifier: convertRequestIdentifier, + request: request + ) { response in + completion( + response.flatMap { + message -> Result in + guard let messagePayload = message.payload else { + return .failure(.unexpectedlyNilPayload(message.type.rawValue)) + } + + guard message.type != ConvertService.convertResponseErrorMessageType else { + return Result { + try self.jsonDecoder.decode(ConvertServiceError.self, from: messagePayload) + } + .flatMapError { + .failure( + DocCServerError.messagePayloadDecodingFailure( + messageType: message.type.rawValue, + decodingError: $0 + ) + ) + } + .flatMap { .failure(.internalError($0)) } + } + + guard message.type == ConvertService.convertResponseMessageType else { + return .failure(.unknownMessageType(message.type.rawValue)) + } + + return .success(messagePayload) + } + .flatMap { convertMessagePayload -> Result in + return Result { + try self.jsonDecoder.decode(ConvertResponse.self, from: convertMessagePayload) + } + .flatMapError { decodingError -> Result in + return .failure( + DocCServerError.messagePayloadDecodingFailure( + messageType: ConvertService.convertResponseMessageType.rawValue, + decodingError: decodingError + ) + ) + } + } + ) + } + } + + private func makeRequest( + messageType: DocumentationServer.MessageType, + messageIdentifier: String, + request: Request, + completion: @escaping (_: Result) -> Void + ) { + let encodedMessageResult: Result = Result { try jsonEncoder.encode(request) } + .mapError { .encodingFailure($0) } + .flatMap { encodedPayload in + Result { + let message = DocumentationServer.Message( + type: messageType, + identifier: messageIdentifier, + payload: encodedPayload + ) + return try jsonEncoder.encode(message) + }.mapError { encodingError -> DocCServerError in + return .encodingFailure(encodingError) + } + } + + switch encodedMessageResult { + case .success(let encodedMessage): + server.process(encodedMessage) { response in + let decodeMessageResult: Result = Result { + try self.jsonDecoder.decode(DocumentationServer.Message.self, from: response) + } + .flatMapError { .failure(.decodingFailure($0)) } + completion(decodeMessageResult) + } + case .failure(let encodingError): + completion(.failure(encodingError)) + } + } +} + +/// Represents a potential error that the ``DocCServer`` could encounter while processing requests +enum DocCServerError: LocalizedError { + case encodingFailure(_ encodingError: Error) + case decodingFailure(_ decodingError: Error) + case messagePayloadDecodingFailure(messageType: String, decodingError: Error) + case unknownMessageType(_ messageType: String) + case unexpectedlyNilPayload(_ messageType: String) + case internalError(_ underlyingError: DescribedError) + + var errorDescription: String? { + switch self { + case .encodingFailure(let encodingError): + return "Failed to encode message: \(encodingError.localizedDescription)" + case .decodingFailure(let decodingError): + return "Failed to decode a received message: \(decodingError.localizedDescription)" + case .messagePayloadDecodingFailure(let messageType, let decodingError): + return + "Received a message of type '\(messageType)' and failed to decode its payload: \(decodingError.localizedDescription)." + case .unknownMessageType(let messageType): + return "Received an unknown message type: '\(messageType)'." + case .unexpectedlyNilPayload(let messageType): + return "Received a message of type '\(messageType)' with a 'nil' payload." + case .internalError(underlyingError: let underlyingError): + return underlyingError.errorDescription + } + } +} +#endif diff --git a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift new file mode 100644 index 000000000..0335a4420 --- /dev/null +++ b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift @@ -0,0 +1,242 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(SwiftDocC) +import BuildSystemIntegration +import Foundation +import IndexStoreDB +import LanguageServerProtocol +import SemanticIndex +import SwiftExtensions +import SwiftSyntax + +package final actor DocumentationManager { + private weak var sourceKitLSPServer: SourceKitLSPServer? + + private let doccServer: DocCServer + + init(sourceKitLSPServer: SourceKitLSPServer) { + self.sourceKitLSPServer = sourceKitLSPServer + self.doccServer = DocCServer(peer: nil, qualityOfService: .background) + } + + func convertDocumentation( + _ documentURI: DocumentURI, + at position: Position = Position(line: 0, utf16index: 0) + ) async throws -> ConvertDocumentationResponse { + guard let sourceKitLSPServer = sourceKitLSPServer else { + throw ResponseError.internalError("SourceKit-LSP is shutting down") + } + guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: documentURI) else { + throw ResponseError.workspaceNotOpen(documentURI) + } + + let snapshot = try sourceKitLSPServer.documentManager.latestSnapshot(documentURI) + let targetId = await workspace.buildSystemManager.canonicalTarget(for: documentURI) + var moduleName: String? = nil + if let targetId = targetId { + moduleName = await workspace.buildSystemManager.buildTarget(named: targetId)?.displayName + } + + var externalIDsToConvert: [String]? + var symbolGraphs = [Data]() + switch snapshot.language { + case .swift: + guard let languageService = await sourceKitLSPServer.languageService(for: documentURI, .swift, in: workspace), + let swiftLanguageService = languageService as? SwiftLanguageService + else { + throw ResponseError.internalError("Unable to find Swift language service for \(documentURI)") + } + // Search for the nearest documentable symbol at this location + let syntaxTree = await swiftLanguageService.syntaxTreeManager.syntaxTree(for: snapshot) + guard + let absoluteSymbolPosition = DocumentableSymbolFinder.find( + in: [Syntax(syntaxTree)], + at: snapshot.absolutePosition(of: position) + ) + else { + return .error(.noDocumentation) + } + // Retrieve the symbol graph as well as information about the symbol + let position = await swiftLanguageService.adjustPositionToStartOfIdentifier( + snapshot.position(of: absoluteSymbolPosition), + in: snapshot + ) + let (cursorInfo, _, symbolGraph) = try await swiftLanguageService.cursorInfo( + documentURI, + position.., + at cursorPosition: AbsolutePosition + ) -> AbsolutePosition? { + let visitor = DocumentableSymbolFinder(cursorPosition) + for node in nodes { + visitor.walk(node) + } + return visitor.result + } + + @discardableResult private func setResult(_ symbolPosition: AbsolutePosition) -> SyntaxVisitorContinueKind { + if result == nil { + result = symbolPosition + } + return .skipChildren + } + + private func visitNamedDeclWithMemberBlock( + node: some SyntaxProtocol, + name: TokenSyntax, + memberBlock: MemberBlockSyntax + ) -> SyntaxVisitorContinueKind { + if cursorPosition <= memberBlock.leftBrace.positionAfterSkippingLeadingTrivia { + setResult(name.positionAfterSkippingLeadingTrivia) + } else if let child = DocumentableSymbolFinder.find( + in: memberBlock.children(viewMode: .sourceAccurate), + at: cursorPosition + ) { + setResult(child) + } else if node.range.contains(cursorPosition) { + setResult(name.positionAfterSkippingLeadingTrivia) + } + return .skipChildren + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + } + + override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + } + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + } + + override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { + return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + } + + override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { + let symbolPosition = node.name.positionAfterSkippingLeadingTrivia + if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { + setResult(symbolPosition) + } + return .skipChildren + } + + override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind { + let range = node.leftBrace.endPositionBeforeTrailingTrivia.. SyntaxVisitorContinueKind { + let symbolPosition = node.initKeyword.positionAfterSkippingLeadingTrivia + if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { + setResult(symbolPosition) + } + return .skipChildren + } + + override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind { + let symbolPosition = node.name.positionAfterSkippingLeadingTrivia + if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { + setResult(symbolPosition) + } + return .skipChildren + } + + override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { + // A variable declaration is only documentable if there is only one pattern binding + guard node.bindings.count == 1, + let identifier = node.bindings.first!.pattern.as(IdentifierPatternSyntax.self) + else { + return .skipChildren + } + let symbolPosition = identifier.positionAfterSkippingLeadingTrivia + if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { + setResult(symbolPosition) + } + return .skipChildren + } +} +#endif diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 323d6fb12..b97183060 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -96,6 +96,19 @@ package actor SourceKitLSPServer { package let documentManager = DocumentManager() + #if canImport(SwiftDocC) + /// The documentation manager + private var documentationManager: DocumentationManager { + guard let cachedDocumentationManager = cachedDocumentationManager else { + let documentationManager = DocumentationManager(sourceKitLSPServer: self) + cachedDocumentationManager = documentationManager + return documentationManager + } + return cachedDocumentationManager + } + private var cachedDocumentationManager: DocumentationManager? = nil + #endif + /// The `TaskScheduler` that schedules all background indexing tasks. /// /// Shared process-wide to ensure the scheduled index operations across multiple workspaces don't exceed the maximum @@ -772,6 +785,15 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { await request.reply { try await rename(request.params) } case let request as RequestAndReply: await request.reply { try await shutdown(request.params) } + #if canImport(SwiftDocC) + case let request as RequestAndReply: + await request.reply { + try await documentationManager.convertDocumentation( + request.params.textDocument.uri, + at: request.params.position + ) + } + #endif case let request as RequestAndReply: await self.handleRequest(for: request, requestHandler: self.symbolInfo) case let request as RequestAndReply: @@ -1019,6 +1041,16 @@ extension SourceKitLSPServer { ? nil : ExecuteCommandOptions(commands: builtinSwiftCommands) + var experimentalCapabilities: [String: LSPAny] = [ + "workspace/tests": .dictionary(["version": .int(2)]), + "textDocument/tests": .dictionary(["version": .int(2)]), + "workspace/triggerReindex": .dictionary(["version": .int(1)]), + "workspace/getReferenceDocument": .dictionary(["version": .int(1)]), + ] + #if canImport(SwiftDocC) + experimentalCapabilities["textDocument/convertDocumentation"] = .dictionary(["version": .int(1)]) + #endif + return ServerCapabilities( textDocumentSync: .options( TextDocumentSyncOptions( @@ -1060,12 +1092,7 @@ extension SourceKitLSPServer { typeHierarchyProvider: .bool(true), semanticTokensProvider: semanticTokensOptions, inlayHintProvider: inlayHintOptions, - experimental: .dictionary([ - "workspace/tests": .dictionary(["version": .int(2)]), - "textDocument/tests": .dictionary(["version": .int(2)]), - "workspace/triggerReindex": .dictionary(["version": .int(1)]), - "workspace/getReferenceDocument": .dictionary(["version": .int(1)]), - ]) + experimental: .dictionary(experimentalCapabilities) ) } diff --git a/Sources/SourceKitLSP/Swift/CursorInfo.swift b/Sources/SourceKitLSP/Swift/CursorInfo.swift index 3d97488e8..3643ebbae 100644 --- a/Sources/SourceKitLSP/Swift/CursorInfo.swift +++ b/Sources/SourceKitLSP/Swift/CursorInfo.swift @@ -141,14 +141,16 @@ extension SwiftLanguageService { /// - Parameters: /// - url: Document URI in which to perform the request. Must be an open document. /// - range: The position range within the document to lookup the symbol at. + /// - includeSymbolGraph: Whether or not to ask sourcekitd for the complete symbol graph. /// - fallbackSettingsAfterTimeout: Whether fallback build settings should be used for the cursor info request if no /// build settings can be retrieved within a timeout. func cursorInfo( _ uri: DocumentURI, _ range: Range, + includeSymbolGraph: Bool = false, fallbackSettingsAfterTimeout: Bool, additionalParameters appendAdditionalParameters: ((SKDRequestDictionary) -> Void)? = nil - ) async throws -> (cursorInfo: [CursorInfo], refactorActions: [SemanticRefactorCommand]) { + ) async throws -> (cursorInfo: [CursorInfo], refactorActions: [SemanticRefactorCommand], symbolGraph: String?) { let documentManager = try self.documentManager let snapshot = try await self.latestSnapshot(for: uri) @@ -163,6 +165,7 @@ extension SwiftLanguageService { keys.length: offsetRange.upperBound != offsetRange.lowerBound ? offsetRange.count : nil, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, + keys.retrieveSymbolGraph: includeSymbolGraph ? 1 : 0, keys.compilerArgs: await self.buildSettings(for: uri, fallbackAfterTimeout: fallbackSettingsAfterTimeout)? .compilerArgs as [SKDRequestValue]?, ]) @@ -186,6 +189,8 @@ extension SwiftLanguageService { keys, self.sourcekitd.api ) ?? [] - return (cursorInfoResults, refactorActions) + let symbolGraph: String? = dict[keys.symbolGraph] + + return (cursorInfoResults, refactorActions, symbolGraph) } } diff --git a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift new file mode 100644 index 000000000..802788b82 --- /dev/null +++ b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift @@ -0,0 +1,422 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(SwiftDocC) +import LanguageServerProtocol +import SKLogging +import SKTestSupport +import SourceKitLSP +import SwiftDocC +import XCTest + +final class ConvertDocumentationTests: XCTestCase { + func testEmptySwiftFile() async throws { + try await convertDocumentation( + swiftFile: "0️⃣", + expectedResponses: [ + .error(.noDocumentation) + ] + ) + } + + func testFunction() async throws { + try await convertDocumentation( + swiftFile: """ + /// A function that do0️⃣es some important stuff. + func func1️⃣tion() { + // Some import2️⃣ant function contents. + }3️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/function()")), + .renderNode(.init(kind: .symbol, path: "test/function()")), + .renderNode(.init(kind: .symbol, path: "test/function()")), + .error(.noDocumentation), + ] + ) + } + + func testStructure() async throws { + try await convertDocumentation( + swiftFile: """ + /// A structure contain0️⃣ing important information. + public struct Struc1️⃣ture { + /// The inte2️⃣ger `foo` + var foo: I3️⃣nt + + /// The other integer `bar`4️⃣ + v5️⃣ar bar: Int + + /// Initiali6️⃣ze the structure. + init(_ foo: Int,7️⃣ bar: Int) { + self.foo = foo + self.bar = bar + } + }8️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Structure")), + .renderNode(.init(kind: .symbol, path: "test/Structure")), + .renderNode(.init(kind: .symbol, path: "test/Structure/foo")), + .renderNode(.init(kind: .symbol, path: "test/Structure/foo")), + .renderNode(.init(kind: .symbol, path: "test/Structure/bar")), + .renderNode(.init(kind: .symbol, path: "test/Structure/bar")), + .renderNode(.init(kind: .symbol, path: "test/Structure/init(_:bar:)")), + .renderNode(.init(kind: .symbol, path: "test/Structure/init(_:bar:)")), + .error(.noDocumentation), + ] + ) + } + + func testEmptyStructure() async throws { + try await convertDocumentation( + swiftFile: """ + pub0️⃣lic struct Struc1️⃣ture { + 2️⃣ + }3️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Structure")), + .renderNode(.init(kind: .symbol, path: "test/Structure")), + .renderNode(.init(kind: .symbol, path: "test/Structure")), + .error(.noDocumentation), + ] + ) + } + + func testClass() async throws { + try await convertDocumentation( + swiftFile: """ + /// A class contain0️⃣ing important information. + public class Cla1️⃣ss { + /// The inte2️⃣ger `foo` + var foo: I3️⃣nt + + /// The other integer `bar`4️⃣ + v5️⃣ar bar: Int + + /// Initiali6️⃣ze the class. + init(_ foo: Int,7️⃣ bar: Int) { + self.foo = foo + self.bar = bar + }8️⃣ + }9️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Class")), + .renderNode(.init(kind: .symbol, path: "test/Class")), + .renderNode(.init(kind: .symbol, path: "test/Class/foo")), + .renderNode(.init(kind: .symbol, path: "test/Class/foo")), + .renderNode(.init(kind: .symbol, path: "test/Class/bar")), + .renderNode(.init(kind: .symbol, path: "test/Class/bar")), + .renderNode(.init(kind: .symbol, path: "test/Class/init(_:bar:)")), + .renderNode(.init(kind: .symbol, path: "test/Class/init(_:bar:)")), + .renderNode(.init(kind: .symbol, path: "test/Class")), + .error(.noDocumentation), + ] + ) + } + + func testEmptyClass() async throws { + try await convertDocumentation( + swiftFile: """ + pub0️⃣lic class Cla1️⃣ss { + 2️⃣ + }3️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Class")), + .renderNode(.init(kind: .symbol, path: "test/Class")), + .renderNode(.init(kind: .symbol, path: "test/Class")), + .error(.noDocumentation), + ] + ) + } + + func testActor() async throws { + try await convertDocumentation( + swiftFile: """ + /// An actor contain0️⃣ing important information. + public actor Ac1️⃣tor { + /// The inte2️⃣ger `foo` + var foo: I3️⃣nt + + /// The other integer `bar`4️⃣ + v5️⃣ar bar: Int + + /// Initiali6️⃣ze the actor. + init(_ foo: Int,7️⃣ bar: Int) { + self.foo = foo + self.bar = bar + } + }8️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Actor")), + .renderNode(.init(kind: .symbol, path: "test/Actor")), + .renderNode(.init(kind: .symbol, path: "test/Actor/foo")), + .renderNode(.init(kind: .symbol, path: "test/Actor/foo")), + .renderNode(.init(kind: .symbol, path: "test/Actor/bar")), + .renderNode(.init(kind: .symbol, path: "test/Actor/bar")), + .renderNode(.init(kind: .symbol, path: "test/Actor/init(_:bar:)")), + .renderNode(.init(kind: .symbol, path: "test/Actor/init(_:bar:)")), + .error(.noDocumentation), + ] + ) + } + + func testEmptyActor() async throws { + try await convertDocumentation( + swiftFile: """ + pub0️⃣lic class Act1️⃣or { + 2️⃣ + }3️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Actor")), + .renderNode(.init(kind: .symbol, path: "test/Actor")), + .renderNode(.init(kind: .symbol, path: "test/Actor")), + .error(.noDocumentation), + ] + ) + } + + func testEnumeration() async throws { + try await convertDocumentation( + swiftFile: """ + /// An enumeration contain0️⃣ing important information. + public enum En1️⃣um { + /// The 2️⃣first case. + case fi3️⃣rst + + //4️⃣/ The second case. + ca5️⃣se second + + // The third case.6️⃣ + case third(In7️⃣t) + }8️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Enum")), + .renderNode(.init(kind: .symbol, path: "test/Enum")), + .renderNode(.init(kind: .symbol, path: "test/Enum/first")), + .renderNode(.init(kind: .symbol, path: "test/Enum/first")), + .renderNode(.init(kind: .symbol, path: "test/Enum/second")), + .renderNode(.init(kind: .symbol, path: "test/Enum/second")), + .renderNode(.init(kind: .symbol, path: "test/Enum/third(_:)")), + .renderNode(.init(kind: .symbol, path: "test/Enum/third(_:)")), + .error(.noDocumentation), + ] + ) + } + + func testProtocol() async throws { + try await convertDocumentation( + swiftFile: """ + /// A protocol contain0️⃣ing important information. + public protocol Proto1️⃣col { + /// The inte2️⃣ger `foo` + var foo: I3️⃣nt + + /// The other integer `bar`4️⃣ + v5️⃣ar bar: Int + }6️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Protocol")), + .renderNode(.init(kind: .symbol, path: "test/Protocol")), + .renderNode(.init(kind: .symbol, path: "test/Protocol/foo")), + .renderNode(.init(kind: .symbol, path: "test/Protocol/foo")), + .renderNode(.init(kind: .symbol, path: "test/Protocol/bar")), + .renderNode(.init(kind: .symbol, path: "test/Protocol/bar")), + .error(.noDocumentation), + ] + ) + } + + func testEmptyProtocol() async throws { + try await convertDocumentation( + swiftFile: """ + pub0️⃣lic struct Prot1️⃣ocol { + 2️⃣ + }3️⃣ + """, + expectedResponses: [ + .renderNode(.init(kind: .symbol, path: "test/Protocol")), + .renderNode(.init(kind: .symbol, path: "test/Protocol")), + .renderNode(.init(kind: .symbol, path: "test/Protocol")), + .error(.noDocumentation), + ] + ) + } + + func testExtension() async throws { + try await convertDocumentation( + swiftFile: """ + /// A structure containing important information + public struct Structure { + let number: Int + } + + extension Stru0️⃣cture { + /// One more than the number + var numberPlusOne: Int {1️⃣ number + 1 } + + /// The kind of2️⃣ this structure + enum Kind { + /// The fi3️⃣rst kind + case first + /// The se4️⃣cond kind + case second + } + }5️⃣ + """, + expectedResponses: [ + .error(.noDocumentation), + .renderNode(.init(kind: .symbol, path: "test/Structure/numberPlusOne")), + .renderNode(.init(kind: .symbol, path: "test/Structure/Kind")), + .renderNode(.init(kind: .symbol, path: "test/Structure/Kind/first")), + .renderNode(.init(kind: .symbol, path: "test/Structure/Kind/second")), + .error(.noDocumentation), + ] + ) + } +} + +fileprivate struct PartialRenderNode { + let kind: RenderNode.Kind + let path: String? + + init(kind: RenderNode.Kind, path: String? = nil) { + self.kind = kind + self.path = path + } +} + +fileprivate enum PartialConvertResponse { + case renderNode(PartialRenderNode) + case error(ConvertDocumentationError) +} + +fileprivate func convertDocumentation( + swiftFile markedText: String, + expectedResponses: [PartialConvertResponse], + file: StaticString = #filePath, + line: UInt = #line +) async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + let positions = testClient.openDocument(markedText, uri: uri) + + await convertDocumentation( + testClient: testClient, + uri: uri, + positions: positions, + expectedResponses: expectedResponses, + file: file, + line: line + ) +} + +fileprivate func convertDocumentation( + testClient: TestSourceKitLSPClient, + uri: DocumentURI, + positions: DocumentPositions, + expectedResponses: [PartialConvertResponse], + file: StaticString = #filePath, + line: UInt = #line +) async { + guard expectedResponses.count == positions.allMarkers.count else { + XCTFail( + "the number of expected render nodes did not match the number of positions in the text document", + file: file, + line: line + ) + return + } + + for (index, marker) in positions.allMarkers.enumerated() { + let response: ConvertDocumentationResponse + do { + response = try await testClient.send( + ConvertDocumentationRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions[marker] + ) + ) + } catch { + XCTFail( + "textDocument/convertDocumentation failed at position \(marker): \(error.message)", + file: file, + line: line + ) + return + } + switch response { + case .renderNode(let renderNodeString): + guard let renderNode = try? JSONDecoder().decode(RenderNode.self, from: renderNodeString) else { + XCTFail("failed to decode response from textDocument/convertDocumentation at position \(marker)") + return + } + switch expectedResponses[index] { + case .renderNode(let expectedRenderNode): + XCTAssertEqual( + renderNode.kind, + expectedRenderNode.kind, + "render node kind did not match expected value at position \(marker)", + file: file, + line: line + ) + if let expectedPath = expectedRenderNode.path { + XCTAssertEqual( + renderNode.identifier.path, + "/documentation/\(expectedPath)", + "render node path did not match expected value at position \(marker)", + file: file, + line: line + ) + } + case .error(let error): + XCTFail( + "expected error \(error.rawValue), but received a render node at position \(marker)", + file: file, + line: line + ) + } + case .error(let error): + switch expectedResponses[index] { + case .renderNode: + XCTFail( + "expected a render node, but received an error \(error.rawValue) at position \(marker)", + file: file, + line: line + ) + case .error(let expectedError): + XCTAssertEqual(error, expectedError, file: file, line: line) + } + } + } +} + +fileprivate extension ConvertDocumentationError { + var rawValue: String { + switch self { + case .indexNotAvailable: + return "indexNotAvailable" + case .noDocumentation: + return "noDocumentation" + case .symbolNotFound: + return "symbolNotFound" + } + } +} +#endif From 2e4aee15d7e843d00d45dbbe099dd5c7fcb4c9bf Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Mon, 16 Dec 2024 17:13:52 -0500 Subject: [PATCH 2/7] use overridingDocumentationComments to grab the latest comments directly from the snapshot --- .../Documentation/DocCServer.swift | 5 +- .../Documentation/DocumentationManager.swift | 55 +++-- .../ConvertDocumentationTests.swift | 219 ++++++++++++------ 3 files changed, 196 insertions(+), 83 deletions(-) diff --git a/Sources/SourceKitLSP/Documentation/DocCServer.swift b/Sources/SourceKitLSP/Documentation/DocCServer.swift index 7172b0053..e0ed8ec01 100644 --- a/Sources/SourceKitLSP/Documentation/DocCServer.swift +++ b/Sources/SourceKitLSP/Documentation/DocCServer.swift @@ -31,6 +31,7 @@ package struct DocCServer { documentationBundleDisplayName: String, documentationBundleIdentifier: String, symbolGraphs: [Data], + overridingDocumentationComments: [String: [String]] = [:], emitSymbolSourceFileURIs: Bool, markupFiles: [Data], tutorialFiles: [Data], @@ -50,7 +51,9 @@ package struct DocCServer { includeRenderReferenceStore: includeRenderReferenceStore, bundleLocation: documentationBundleLocation, symbolGraphs: symbolGraphs, - overridingDocumentationComments: nil, + overridingDocumentationComments: overridingDocumentationComments.mapValues { + $0.map { ConvertRequest.Line(text: $0) } + }, knownDisambiguatedSymbolPathComponents: nil, emitSymbolSourceFileURIs: emitSymbolSourceFileURIs, markupFiles: markupFiles, diff --git a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift index 0335a4420..62ec6722b 100644 --- a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift +++ b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift @@ -49,6 +49,7 @@ package final actor DocumentationManager { var externalIDsToConvert: [String]? var symbolGraphs = [Data]() + var overridingDocumentationComments = [String: [String]]() switch snapshot.language { case .swift: guard let languageService = await sourceKitLSPServer.languageService(for: documentURI, .swift, in: workspace), @@ -59,7 +60,7 @@ package final actor DocumentationManager { // Search for the nearest documentable symbol at this location let syntaxTree = await swiftLanguageService.syntaxTreeManager.syntaxTree(for: snapshot) guard - let absoluteSymbolPosition = DocumentableSymbolFinder.find( + let nearestDocumentableSymbol = DocumentableSymbolFinder.find( in: [Syntax(syntaxTree)], at: snapshot.absolutePosition(of: position) ) @@ -68,7 +69,7 @@ package final actor DocumentationManager { } // Retrieve the symbol graph as well as information about the symbol let position = await swiftLanguageService.adjustPositionToStartOfIdentifier( - snapshot.position(of: absoluteSymbolPosition), + snapshot.position(of: nearestDocumentableSymbol.position), in: snapshot ) let (cursorInfo, _, symbolGraph) = try await swiftLanguageService.cursorInfo( @@ -88,6 +89,7 @@ package final actor DocumentationManager { } externalIDsToConvert = [symbolUSR] symbolGraphs.append(rawSymbolGraph) + overridingDocumentationComments[symbolUSR] = nearestDocumentableSymbol.documentationComments default: return .error(.noDocumentation) } @@ -101,6 +103,7 @@ package final actor DocumentationManager { documentationBundleDisplayName: moduleName ?? "Unknown", documentationBundleIdentifier: "unknown", symbolGraphs: symbolGraphs, + overridingDocumentationComments: overridingDocumentationComments, emitSymbolSourceFileURIs: false, markupFiles: [], tutorialFiles: [], @@ -126,10 +129,15 @@ package final actor DocumentationManager { } fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { + struct Symbol { + let position: AbsolutePosition + let documentationComments: [String] + } + private let cursorPosition: AbsolutePosition /// Accumulating the result in here. - private var result: AbsolutePosition? = nil + private var result: Symbol? = nil private init(_ cursorPosition: AbsolutePosition) { self.cursorPosition = cursorPosition @@ -140,7 +148,7 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { static func find( in nodes: some Sequence, at cursorPosition: AbsolutePosition - ) -> AbsolutePosition? { + ) -> Symbol? { let visitor = DocumentableSymbolFinder(cursorPosition) for node in nodes { visitor.walk(node) @@ -148,9 +156,29 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { return visitor.result } - @discardableResult private func setResult(_ symbolPosition: AbsolutePosition) -> SyntaxVisitorContinueKind { + @discardableResult private func setResult( + node: some SyntaxProtocol, + position: AbsolutePosition + ) -> SyntaxVisitorContinueKind { + return setResult( + position, + node.leadingTrivia.compactMap { trivia in + switch trivia { + case .docLineComment(let comment): + return String(comment.dropFirst(3).trimmingCharacters(in: .whitespaces)) + default: + return nil + } + } + ) + } + + @discardableResult private func setResult( + _ symbolPosition: AbsolutePosition, + _ documentationComments: [String] + ) -> SyntaxVisitorContinueKind { if result == nil { - result = symbolPosition + result = Symbol(position: symbolPosition, documentationComments: documentationComments) } return .skipChildren } @@ -160,15 +188,16 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { name: TokenSyntax, memberBlock: MemberBlockSyntax ) -> SyntaxVisitorContinueKind { + if cursorPosition <= memberBlock.leftBrace.positionAfterSkippingLeadingTrivia { - setResult(name.positionAfterSkippingLeadingTrivia) + setResult(node: node, position: name.positionAfterSkippingLeadingTrivia) } else if let child = DocumentableSymbolFinder.find( in: memberBlock.children(viewMode: .sourceAccurate), at: cursorPosition ) { - setResult(child) + setResult(child.position, child.documentationComments) } else if node.range.contains(cursorPosition) { - setResult(name.positionAfterSkippingLeadingTrivia) + setResult(node: node, position: name.positionAfterSkippingLeadingTrivia) } return .skipChildren } @@ -196,7 +225,7 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { let symbolPosition = node.name.positionAfterSkippingLeadingTrivia if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { - setResult(symbolPosition) + setResult(node: node, position: symbolPosition) } return .skipChildren } @@ -212,7 +241,7 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { let symbolPosition = node.initKeyword.positionAfterSkippingLeadingTrivia if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { - setResult(symbolPosition) + setResult(node: node, position: symbolPosition) } return .skipChildren } @@ -220,7 +249,7 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind { let symbolPosition = node.name.positionAfterSkippingLeadingTrivia if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { - setResult(symbolPosition) + setResult(node: node, position: symbolPosition) } return .skipChildren } @@ -234,7 +263,7 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { } let symbolPosition = identifier.positionAfterSkippingLeadingTrivia if node.range.contains(cursorPosition) || cursorPosition < symbolPosition { - setResult(symbolPosition) + setResult(node: node, position: symbolPosition) } return .skipChildren } diff --git a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift index 802788b82..8aed05d97 100644 --- a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift @@ -37,9 +37,9 @@ final class ConvertDocumentationTests: XCTestCase { }3️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/function()")), - .renderNode(.init(kind: .symbol, path: "test/function()")), - .renderNode(.init(kind: .symbol, path: "test/function()")), + .renderNode(kind: .symbol, path: "test/function()"), + .renderNode(kind: .symbol, path: "test/function()"), + .renderNode(kind: .symbol, path: "test/function()"), .error(.noDocumentation), ] ) @@ -64,14 +64,14 @@ final class ConvertDocumentationTests: XCTestCase { }8️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Structure")), - .renderNode(.init(kind: .symbol, path: "test/Structure")), - .renderNode(.init(kind: .symbol, path: "test/Structure/foo")), - .renderNode(.init(kind: .symbol, path: "test/Structure/foo")), - .renderNode(.init(kind: .symbol, path: "test/Structure/bar")), - .renderNode(.init(kind: .symbol, path: "test/Structure/bar")), - .renderNode(.init(kind: .symbol, path: "test/Structure/init(_:bar:)")), - .renderNode(.init(kind: .symbol, path: "test/Structure/init(_:bar:)")), + .renderNode(kind: .symbol, path: "test/Structure"), + .renderNode(kind: .symbol, path: "test/Structure"), + .renderNode(kind: .symbol, path: "test/Structure/foo"), + .renderNode(kind: .symbol, path: "test/Structure/foo"), + .renderNode(kind: .symbol, path: "test/Structure/bar"), + .renderNode(kind: .symbol, path: "test/Structure/bar"), + .renderNode(kind: .symbol, path: "test/Structure/init(_:bar:)"), + .renderNode(kind: .symbol, path: "test/Structure/init(_:bar:)"), .error(.noDocumentation), ] ) @@ -85,9 +85,9 @@ final class ConvertDocumentationTests: XCTestCase { }3️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Structure")), - .renderNode(.init(kind: .symbol, path: "test/Structure")), - .renderNode(.init(kind: .symbol, path: "test/Structure")), + .renderNode(kind: .symbol, path: "test/Structure"), + .renderNode(kind: .symbol, path: "test/Structure"), + .renderNode(kind: .symbol, path: "test/Structure"), .error(.noDocumentation), ] ) @@ -112,15 +112,15 @@ final class ConvertDocumentationTests: XCTestCase { }9️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Class")), - .renderNode(.init(kind: .symbol, path: "test/Class")), - .renderNode(.init(kind: .symbol, path: "test/Class/foo")), - .renderNode(.init(kind: .symbol, path: "test/Class/foo")), - .renderNode(.init(kind: .symbol, path: "test/Class/bar")), - .renderNode(.init(kind: .symbol, path: "test/Class/bar")), - .renderNode(.init(kind: .symbol, path: "test/Class/init(_:bar:)")), - .renderNode(.init(kind: .symbol, path: "test/Class/init(_:bar:)")), - .renderNode(.init(kind: .symbol, path: "test/Class")), + .renderNode(kind: .symbol, path: "test/Class"), + .renderNode(kind: .symbol, path: "test/Class"), + .renderNode(kind: .symbol, path: "test/Class/foo"), + .renderNode(kind: .symbol, path: "test/Class/foo"), + .renderNode(kind: .symbol, path: "test/Class/bar"), + .renderNode(kind: .symbol, path: "test/Class/bar"), + .renderNode(kind: .symbol, path: "test/Class/init(_:bar:)"), + .renderNode(kind: .symbol, path: "test/Class/init(_:bar:)"), + .renderNode(kind: .symbol, path: "test/Class"), .error(.noDocumentation), ] ) @@ -134,9 +134,9 @@ final class ConvertDocumentationTests: XCTestCase { }3️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Class")), - .renderNode(.init(kind: .symbol, path: "test/Class")), - .renderNode(.init(kind: .symbol, path: "test/Class")), + .renderNode(kind: .symbol, path: "test/Class"), + .renderNode(kind: .symbol, path: "test/Class"), + .renderNode(kind: .symbol, path: "test/Class"), .error(.noDocumentation), ] ) @@ -161,14 +161,14 @@ final class ConvertDocumentationTests: XCTestCase { }8️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Actor")), - .renderNode(.init(kind: .symbol, path: "test/Actor")), - .renderNode(.init(kind: .symbol, path: "test/Actor/foo")), - .renderNode(.init(kind: .symbol, path: "test/Actor/foo")), - .renderNode(.init(kind: .symbol, path: "test/Actor/bar")), - .renderNode(.init(kind: .symbol, path: "test/Actor/bar")), - .renderNode(.init(kind: .symbol, path: "test/Actor/init(_:bar:)")), - .renderNode(.init(kind: .symbol, path: "test/Actor/init(_:bar:)")), + .renderNode(kind: .symbol, path: "test/Actor"), + .renderNode(kind: .symbol, path: "test/Actor"), + .renderNode(kind: .symbol, path: "test/Actor/foo"), + .renderNode(kind: .symbol, path: "test/Actor/foo"), + .renderNode(kind: .symbol, path: "test/Actor/bar"), + .renderNode(kind: .symbol, path: "test/Actor/bar"), + .renderNode(kind: .symbol, path: "test/Actor/init(_:bar:)"), + .renderNode(kind: .symbol, path: "test/Actor/init(_:bar:)"), .error(.noDocumentation), ] ) @@ -182,9 +182,9 @@ final class ConvertDocumentationTests: XCTestCase { }3️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Actor")), - .renderNode(.init(kind: .symbol, path: "test/Actor")), - .renderNode(.init(kind: .symbol, path: "test/Actor")), + .renderNode(kind: .symbol, path: "test/Actor"), + .renderNode(kind: .symbol, path: "test/Actor"), + .renderNode(kind: .symbol, path: "test/Actor"), .error(.noDocumentation), ] ) @@ -206,14 +206,14 @@ final class ConvertDocumentationTests: XCTestCase { }8️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Enum")), - .renderNode(.init(kind: .symbol, path: "test/Enum")), - .renderNode(.init(kind: .symbol, path: "test/Enum/first")), - .renderNode(.init(kind: .symbol, path: "test/Enum/first")), - .renderNode(.init(kind: .symbol, path: "test/Enum/second")), - .renderNode(.init(kind: .symbol, path: "test/Enum/second")), - .renderNode(.init(kind: .symbol, path: "test/Enum/third(_:)")), - .renderNode(.init(kind: .symbol, path: "test/Enum/third(_:)")), + .renderNode(kind: .symbol, path: "test/Enum"), + .renderNode(kind: .symbol, path: "test/Enum"), + .renderNode(kind: .symbol, path: "test/Enum/first"), + .renderNode(kind: .symbol, path: "test/Enum/first"), + .renderNode(kind: .symbol, path: "test/Enum/second"), + .renderNode(kind: .symbol, path: "test/Enum/second"), + .renderNode(kind: .symbol, path: "test/Enum/third(_:)"), + .renderNode(kind: .symbol, path: "test/Enum/third(_:)"), .error(.noDocumentation), ] ) @@ -232,12 +232,12 @@ final class ConvertDocumentationTests: XCTestCase { }6️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Protocol")), - .renderNode(.init(kind: .symbol, path: "test/Protocol")), - .renderNode(.init(kind: .symbol, path: "test/Protocol/foo")), - .renderNode(.init(kind: .symbol, path: "test/Protocol/foo")), - .renderNode(.init(kind: .symbol, path: "test/Protocol/bar")), - .renderNode(.init(kind: .symbol, path: "test/Protocol/bar")), + .renderNode(kind: .symbol, path: "test/Protocol"), + .renderNode(kind: .symbol, path: "test/Protocol"), + .renderNode(kind: .symbol, path: "test/Protocol/foo"), + .renderNode(kind: .symbol, path: "test/Protocol/foo"), + .renderNode(kind: .symbol, path: "test/Protocol/bar"), + .renderNode(kind: .symbol, path: "test/Protocol/bar"), .error(.noDocumentation), ] ) @@ -246,14 +246,15 @@ final class ConvertDocumentationTests: XCTestCase { func testEmptyProtocol() async throws { try await convertDocumentation( swiftFile: """ + /// A protocol containing important information pub0️⃣lic struct Prot1️⃣ocol { 2️⃣ }3️⃣ """, expectedResponses: [ - .renderNode(.init(kind: .symbol, path: "test/Protocol")), - .renderNode(.init(kind: .symbol, path: "test/Protocol")), - .renderNode(.init(kind: .symbol, path: "test/Protocol")), + .renderNode(kind: .symbol, path: "test/Protocol"), + .renderNode(kind: .symbol, path: "test/Protocol"), + .renderNode(kind: .symbol, path: "test/Protocol"), .error(.noDocumentation), ] ) @@ -282,28 +283,100 @@ final class ConvertDocumentationTests: XCTestCase { """, expectedResponses: [ .error(.noDocumentation), - .renderNode(.init(kind: .symbol, path: "test/Structure/numberPlusOne")), - .renderNode(.init(kind: .symbol, path: "test/Structure/Kind")), - .renderNode(.init(kind: .symbol, path: "test/Structure/Kind/first")), - .renderNode(.init(kind: .symbol, path: "test/Structure/Kind/second")), + .renderNode(kind: .symbol, path: "test/Structure/numberPlusOne"), + .renderNode(kind: .symbol, path: "test/Structure/Kind"), + .renderNode(kind: .symbol, path: "test/Structure/Kind/first"), + .renderNode(kind: .symbol, path: "test/Structure/Kind/second"), .error(.noDocumentation), ] ) } -} -fileprivate struct PartialRenderNode { - let kind: RenderNode.Kind - let path: String? + func testEditCommentInSwiftFile() async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + let positions = testClient.openDocument( + """ + /// A structure containing0️⃣ important information + public struct Structure { + let number: Int + } + """, + uri: uri + ) + + // Make sure that the initial documentation comment is present in the response + await convertDocumentation( + testClient: testClient, + uri: uri, + positions: positions, + expectedResponses: [.renderNode(kind: .symbol, containing: "A structure containing important information")] + ) + + // Change the content of the documentation comment + testClient.send( + DidChangeTextDocumentNotification( + textDocument: VersionedTextDocumentIdentifier(uri, version: 2), + contentChanges: [ + TextDocumentContentChangeEvent(range: positions["0️⃣"].. Date: Tue, 17 Dec 2024 10:29:03 -0500 Subject: [PATCH 3/7] support documentation block comments --- .../Documentation/DocumentationManager.swift | 40 ++++++++-------- .../ConvertDocumentationTests.swift | 48 ++++++++++++++++++- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift index 62ec6722b..cb86ca8b7 100644 --- a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift +++ b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift @@ -156,31 +156,31 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { return visitor.result } - @discardableResult private func setResult( - node: some SyntaxProtocol, - position: AbsolutePosition - ) -> SyntaxVisitorContinueKind { - return setResult( - position, - node.leadingTrivia.compactMap { trivia in - switch trivia { - case .docLineComment(let comment): - return String(comment.dropFirst(3).trimmingCharacters(in: .whitespaces)) - default: - return nil + private func setResult(node: some SyntaxProtocol, position: AbsolutePosition) { + setResult( + result: Symbol( + position: position, + documentationComments: node.leadingTrivia.flatMap { trivia -> [String] in + switch trivia { + case .docLineComment(let comment): + return [String(comment.dropFirst(3).trimmingCharacters(in: .whitespaces))] + case .docBlockComment(let comment): + return comment.dropFirst(3) + .dropLast(2) + .split(separator: "\n") + .map { String($0).trimmingCharacters(in: .whitespaces) } + default: + return [] + } } - } + ) ) } - @discardableResult private func setResult( - _ symbolPosition: AbsolutePosition, - _ documentationComments: [String] - ) -> SyntaxVisitorContinueKind { + private func setResult(result symbol: Symbol) { if result == nil { - result = Symbol(position: symbolPosition, documentationComments: documentationComments) + result = symbol } - return .skipChildren } private func visitNamedDeclWithMemberBlock( @@ -195,7 +195,7 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { in: memberBlock.children(viewMode: .sourceAccurate), at: cursorPosition ) { - setResult(child.position, child.documentationComments) + setResult(result: child) } else if node.range.contains(cursorPosition) { setResult(node: node, position: name.positionAfterSkippingLeadingTrivia) } diff --git a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift index 8aed05d97..9c4cd3721 100644 --- a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift @@ -292,7 +292,7 @@ final class ConvertDocumentationTests: XCTestCase { ) } - func testEditCommentInSwiftFile() async throws { + func testEditDocLineCommentInSwiftFile() async throws { let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( @@ -332,7 +332,7 @@ final class ConvertDocumentationTests: XCTestCase { ) } - func testEditMultiLineCommentInSwiftFile() async throws { + func testEditMultipleDocLineCommentsInSwiftFile() async throws { let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( @@ -373,6 +373,50 @@ final class ConvertDocumentationTests: XCTestCase { expectedResponses: [.renderNode(kind: .symbol, containing: "This is an amazing description")] ) } + + func testEditDocBlockCommentInSwiftFile() async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + let positions = testClient.openDocument( + """ + /** + A structure containing important information + + This is a0️⃣ description + */ + public struct Structure { + let number: Int + } + """, + uri: uri + ) + + // Make sure that the initial documentation comment is present in the response + await convertDocumentation( + testClient: testClient, + uri: uri, + positions: positions, + expectedResponses: [.renderNode(kind: .symbol, containing: "This is a description")] + ) + + // Change the content of the documentation comment + testClient.send( + DidChangeTextDocumentNotification( + textDocument: VersionedTextDocumentIdentifier(uri, version: 2), + contentChanges: [ + TextDocumentContentChangeEvent(range: positions["0️⃣"].. Date: Tue, 17 Dec 2024 10:34:03 -0500 Subject: [PATCH 4/7] fix failure message in test --- Tests/SourceKitLSPTests/ConvertDocumentationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift index 9c4cd3721..cb8fb7ff2 100644 --- a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift @@ -454,7 +454,7 @@ fileprivate func convertDocumentation( ) async { guard expectedResponses.count == positions.allMarkers.count else { XCTFail( - "the number of expected render nodes did not match the number of positions in the text document", + "the number of expected responses did not match the number of positions in the text document", file: file, line: line ) From bb04186601f3cfe124229d1470f59f1419e645e9 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Tue, 17 Dec 2024 14:34:28 -0500 Subject: [PATCH 5/7] minor code quality improvements --- .../Documentation/DocumentationManager.swift | 11 +++++----- Sources/SourceKitLSP/SourceKitLSPServer.swift | 21 ++++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift index cb86ca8b7..2bd6bce2b 100644 --- a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift +++ b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift @@ -188,7 +188,6 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { name: TokenSyntax, memberBlock: MemberBlockSyntax ) -> SyntaxVisitorContinueKind { - if cursorPosition <= memberBlock.leftBrace.positionAfterSkippingLeadingTrivia { setResult(node: node, position: name.positionAfterSkippingLeadingTrivia) } else if let child = DocumentableSymbolFinder.find( @@ -203,23 +202,23 @@ fileprivate final class DocumentableSymbolFinder: SyntaxAnyVisitor { } override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) } override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) } override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { - return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) } override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { - return visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) + visitNamedDeclWithMemberBlock(node: node, name: node.name, memberBlock: node.memberBlock) } override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index b97183060..b4e4d9ad6 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -97,16 +97,13 @@ package actor SourceKitLSPServer { package let documentManager = DocumentManager() #if canImport(SwiftDocC) - /// The documentation manager - private var documentationManager: DocumentationManager { - guard let cachedDocumentationManager = cachedDocumentationManager else { - let documentationManager = DocumentationManager(sourceKitLSPServer: self) - cachedDocumentationManager = documentationManager - return documentationManager - } - return cachedDocumentationManager - } - private var cachedDocumentationManager: DocumentationManager? = nil + /// The `DocumentationManager` that handles all documentation related requests + /// + /// Implicitly unwrapped optional so we can create an `DocumentationManager` that has a weak reference to + /// `SourceKitLSPServer`. + /// `nonisolated(unsafe)` because `documentationManager` will not be modified after it is assigned from the + /// initializer. + private(set) nonisolated(unsafe) var documentationManager: DocumentationManager! #endif /// The `TaskScheduler` that schedules all background indexing tasks. @@ -188,6 +185,10 @@ package actor SourceKitLSPServer { (TaskPriority.low, max(Int(lowPriorityCores), 1)), ]) self.indexProgressManager = nil + #if canImport(SwiftDocC) + self.documentationManager = nil + self.documentationManager = DocumentationManager(sourceKitLSPServer: self) + #endif self.indexProgressManager = IndexProgressManager(sourceKitLSPServer: self) self.sourcekitdCrashedWorkDoneProgress = SharedWorkDoneProgressManager( sourceKitLSPServer: self, From edb915510d95378384dc8619ab8141f0aa69eee1 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 19 Dec 2024 23:24:22 -0500 Subject: [PATCH 6/7] fix build errors --- Sources/SourceKitLSP/Documentation/DocumentationManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift index 2bd6bce2b..6e3c33005 100644 --- a/Sources/SourceKitLSP/Documentation/DocumentationManager.swift +++ b/Sources/SourceKitLSP/Documentation/DocumentationManager.swift @@ -12,10 +12,12 @@ #if canImport(SwiftDocC) import BuildSystemIntegration +import BuildServerProtocol import Foundation import IndexStoreDB import LanguageServerProtocol import SemanticIndex +import SwiftDocC import SwiftExtensions import SwiftSyntax From ce904ba4603e002cf5d390fe536af1ee61fa8e42 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Fri, 20 Dec 2024 09:51:09 -0500 Subject: [PATCH 7/7] revert changes to TestSourceKitLSPClient --- Sources/SKTestSupport/TestSourceKitLSPClient.swift | 8 ++++---- Tests/SourceKitLSPTests/ConvertDocumentationTests.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift index 5ef0e8f98..b902705a8 100644 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -228,12 +228,12 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable { // MARK: - Sending messages /// Send the request to `server` and return the request result. - package func send(_ request: R) async throws(ResponseError) -> R.Response { - return try await withCheckedContinuation { continuation in + package func send(_ request: R) async throws -> R.Response { + return try await withCheckedThrowingContinuation { continuation in self.send(request) { result in - continuation.resume(returning: result) + continuation.resume(with: result) } - }.get() + } } /// Variant of `send` above that allows the response to be discarded if it is a `VoidResponse`. diff --git a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift index cb8fb7ff2..11ee520dc 100644 --- a/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/ConvertDocumentationTests.swift @@ -472,7 +472,7 @@ fileprivate func convertDocumentation( ) } catch { XCTFail( - "textDocument/convertDocumentation failed at position \(marker): \(error.message)", + "textDocument/convertDocumentation failed at position \(marker): \(error.localizedDescription)", file: file, line: line )