From 09baabbe12e79a539f4f89617f206faa3734c382 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 10 Jul 2025 10:25:08 -0400 Subject: [PATCH 1/6] always try to render some documentation for Swift files --- .../DoccDocumentationError.swift | 6 +- .../Swift/DoccDocumentation.swift | 64 +++++++++++++------ .../DoccDocumentationTests.swift | 64 +++++++++++++------ 3 files changed, 92 insertions(+), 42 deletions(-) diff --git a/Sources/DocCDocumentation/DoccDocumentationError.swift b/Sources/DocCDocumentation/DoccDocumentationError.swift index 35c00b9a4..974d03942 100644 --- a/Sources/DocCDocumentation/DoccDocumentationError.swift +++ b/Sources/DocCDocumentation/DoccDocumentationError.swift @@ -14,14 +14,14 @@ import Foundation package import LanguageServerProtocol package enum DocCDocumentationError: LocalizedError { - case noDocumentation + case noDocumentableSymbols case indexNotAvailable case symbolNotFound(String) var errorDescription: String? { switch self { - case .noDocumentation: - return "No documentation could be rendered for the position in this document" + case .noDocumentableSymbols: + return "No documentable symbols were found in this Swift file" case .indexNotAvailable: return "The index is not availble to complete the request" case .symbolNotFound(let symbolName): diff --git a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift index d57fde7cc..7040d5728 100644 --- a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift +++ b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift @@ -46,9 +46,9 @@ extension SwiftLanguageService { let nearestDocumentableSymbol = DocumentableSymbol.findNearestSymbol( syntaxTree: syntaxTree, position: snapshot.absolutePosition(of: position) - ) + ) ?? DocumentableSymbol.findTopLevelSymbol(syntaxTree: syntaxTree) else { - throw ResponseError.requestFailed(doccDocumentationError: .noDocumentation) + throw ResponseError.requestFailed(doccDocumentationError: .noDocumentableSymbols) } // Retrieve the symbol graph as well as information about the symbol let symbolPosition = await adjustPositionToStartOfIdentifier( @@ -130,30 +130,56 @@ fileprivate struct DocumentableSymbol { } } - static func findNearestSymbol(syntaxTree: SourceFileSyntax, position: AbsolutePosition) -> DocumentableSymbol? { - guard let token = syntaxTree.token(at: position) else { + init?(node: any SyntaxProtocol) { + if let namedDecl = node.asProtocol(NamedDeclSyntax.self) { + self = DocumentableSymbol(node: namedDecl, position: namedDecl.name.positionAfterSkippingLeadingTrivia) + } else if let initDecl = node.as(InitializerDeclSyntax.self) { + self = DocumentableSymbol(node: initDecl, position: initDecl.initKeyword.positionAfterSkippingLeadingTrivia) + } else if let functionDecl = node.as(FunctionDeclSyntax.self) { + self = DocumentableSymbol(node: functionDecl, position: functionDecl.name.positionAfterSkippingLeadingTrivia) + } else if let variableDecl = node.as(VariableDeclSyntax.self) { + guard let identifier = variableDecl.bindings.only?.pattern.as(IdentifierPatternSyntax.self) else { + return nil + } + self = DocumentableSymbol(node: variableDecl, position: identifier.positionAfterSkippingLeadingTrivia) + } else if let enumCaseDecl = node.as(EnumCaseDeclSyntax.self) { + guard let name = enumCaseDecl.elements.only?.name else { + return nil + } + self = DocumentableSymbol(node: enumCaseDecl, position: name.positionAfterSkippingLeadingTrivia) + } else { return nil } - return token.ancestorOrSelf { node in - if let namedDecl = node.asProtocol(NamedDeclSyntax.self) { - return DocumentableSymbol(node: namedDecl, position: namedDecl.name.positionAfterSkippingLeadingTrivia) - } else if let initDecl = node.as(InitializerDeclSyntax.self) { - return DocumentableSymbol(node: initDecl, position: initDecl.initKeyword.positionAfterSkippingLeadingTrivia) - } else if let functionDecl = node.as(FunctionDeclSyntax.self) { - return DocumentableSymbol(node: functionDecl, position: functionDecl.name.positionAfterSkippingLeadingTrivia) - } else if let variableDecl = node.as(VariableDeclSyntax.self) { - guard let identifier = variableDecl.bindings.only?.pattern.as(IdentifierPatternSyntax.self) else { - return nil + } + + static func findTopLevelSymbol(syntaxTree: SourceFileSyntax) -> DocumentableSymbol? { + class Visitor: SyntaxAnyVisitor { + var topLevelSymbol: DocumentableSymbol? = nil + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + guard topLevelSymbol == nil else { + return .skipChildren } - return DocumentableSymbol(node: variableDecl, position: identifier.positionAfterSkippingLeadingTrivia) - } else if let enumCaseDecl = node.as(EnumCaseDeclSyntax.self) { - guard let name = enumCaseDecl.elements.only?.name else { - return nil + + if let symbol = DocumentableSymbol(node: node) { + topLevelSymbol = symbol + return .skipChildren } - return DocumentableSymbol(node: enumCaseDecl, position: name.positionAfterSkippingLeadingTrivia) + return .visitChildren } + } + + let visitor = Visitor(viewMode: .all) + visitor.walk(syntaxTree) + return visitor.topLevelSymbol + } + + static func findNearestSymbol(syntaxTree: SourceFileSyntax, position: AbsolutePosition) -> DocumentableSymbol? { + guard let token = syntaxTree.token(at: position) else { return nil } + // Walk up the tree until we find a documentable symbol + return token.ancestorOrSelf { DocumentableSymbol(node: $0) } } } #endif diff --git a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift index 40320f753..7ac33c25a 100644 --- a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift @@ -27,7 +27,7 @@ final class DoccDocumentationTests: XCTestCase { try await renderDocumentation( swiftFile: "1️⃣", expectedResponses: [ - "1️⃣": .error(.noDocumentation) + "1️⃣": .error(.noDocumentableSymbols) ] ) } @@ -44,7 +44,7 @@ final class DoccDocumentationTests: XCTestCase { "1️⃣": .renderNode(kind: .symbol, path: "test/function()"), "2️⃣": .renderNode(kind: .symbol, path: "test/function()"), "3️⃣": .renderNode(kind: .symbol, path: "test/function()"), - "4️⃣": .error(.noDocumentation), + "4️⃣": .renderNode(kind: .symbol, path: "test/function()"), ] ) } @@ -76,7 +76,7 @@ final class DoccDocumentationTests: XCTestCase { "6️⃣": .renderNode(kind: .symbol, path: "test/Structure/bar"), "7️⃣": .renderNode(kind: .symbol, path: "test/Structure/init(_:bar:)"), "8️⃣": .renderNode(kind: .symbol, path: "test/Structure/init(_:bar:)"), - "9️⃣": .error(.noDocumentation), + "9️⃣": .renderNode(kind: .symbol, path: "test/Structure"), ] ) } @@ -92,7 +92,7 @@ final class DoccDocumentationTests: XCTestCase { "1️⃣": .renderNode(kind: .symbol, path: "test/Structure"), "2️⃣": .renderNode(kind: .symbol, path: "test/Structure"), "3️⃣": .renderNode(kind: .symbol, path: "test/Structure"), - "4️⃣": .error(.noDocumentation), + "4️⃣": .renderNode(kind: .symbol, path: "test/Structure"), ] ) } @@ -125,7 +125,7 @@ final class DoccDocumentationTests: XCTestCase { "7️⃣": .renderNode(kind: .symbol, path: "test/Class/init(_:bar:)"), "8️⃣": .renderNode(kind: .symbol, path: "test/Class/init(_:bar:)"), "9️⃣": .renderNode(kind: .symbol, path: "test/Class"), - "0️⃣": .error(.noDocumentation), + "0️⃣": .renderNode(kind: .symbol, path: "test/Class"), ] ) } @@ -141,7 +141,7 @@ final class DoccDocumentationTests: XCTestCase { "1️⃣": .renderNode(kind: .symbol, path: "test/Class"), "2️⃣": .renderNode(kind: .symbol, path: "test/Class"), "3️⃣": .renderNode(kind: .symbol, path: "test/Class"), - "4️⃣": .error(.noDocumentation), + "4️⃣": .renderNode(kind: .symbol, path: "test/Class"), ] ) } @@ -173,7 +173,7 @@ final class DoccDocumentationTests: XCTestCase { "6️⃣": .renderNode(kind: .symbol, path: "test/Actor/bar"), "7️⃣": .renderNode(kind: .symbol, path: "test/Actor/init(_:bar:)"), "8️⃣": .renderNode(kind: .symbol, path: "test/Actor/init(_:bar:)"), - "9️⃣": .error(.noDocumentation), + "9️⃣": .renderNode(kind: .symbol, path: "test/Actor"), ] ) } @@ -189,7 +189,7 @@ final class DoccDocumentationTests: XCTestCase { "1️⃣": .renderNode(kind: .symbol, path: "test/Actor"), "2️⃣": .renderNode(kind: .symbol, path: "test/Actor"), "3️⃣": .renderNode(kind: .symbol, path: "test/Actor"), - "4️⃣": .error(.noDocumentation), + "4️⃣": .renderNode(kind: .symbol, path: "test/Actor"), ] ) } @@ -218,7 +218,7 @@ final class DoccDocumentationTests: XCTestCase { "6️⃣": .renderNode(kind: .symbol, path: "test/Enum/second"), "7️⃣": .renderNode(kind: .symbol, path: "test/Enum/third(_:)"), "8️⃣": .renderNode(kind: .symbol, path: "test/Enum/third(_:)"), - "9️⃣": .error(.noDocumentation), + "9️⃣": .renderNode(kind: .symbol, path: "test/Enum"), ] ) } @@ -289,7 +289,7 @@ final class DoccDocumentationTests: XCTestCase { "4️⃣": .renderNode(kind: .symbol, path: "test/Protocol/foo"), "5️⃣": .renderNode(kind: .symbol, path: "test/Protocol/bar"), "6️⃣": .renderNode(kind: .symbol, path: "test/Protocol/bar"), - "7️⃣": .error(.noDocumentation), + "7️⃣": .renderNode(kind: .symbol, path: "test/Protocol"), ] ) } @@ -306,19 +306,21 @@ final class DoccDocumentationTests: XCTestCase { "1️⃣": .renderNode(kind: .symbol, path: "test/Protocol"), "2️⃣": .renderNode(kind: .symbol, path: "test/Protocol"), "3️⃣": .renderNode(kind: .symbol, path: "test/Protocol"), - "4️⃣": .error(.noDocumentation), + "4️⃣": .renderNode(kind: .symbol, path: "test/Protocol"), ] ) } func testExtension() async throws { - try await renderDocumentation( - swiftFile: """ + let project = try await SwiftPMTestProject( + files: [ + "MyLibrary/Structure.swift": """ /// A structure containing important information public struct Structure { let number: Int } - + """, + "MyLibrary/Extension.swift": """ extension Stru1️⃣cture { /// One more than the number var numberPlusOne: Int {2️⃣ number + 1 } @@ -332,13 +334,35 @@ final class DoccDocumentationTests: XCTestCase { } }6️⃣ """, + ], + enableBackgroundIndexing: true + ) + try await renderDocumentation( + fileName: "Extension.swift", + project: project, + expectedResponses: [ + "1️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/numberPlusOne"), + "2️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/numberPlusOne"), + "3️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind"), + "4️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind/first"), + "5️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind/second"), + "6️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/numberPlusOne"), + ] + ) + } + + func testCursorInImport() async throws { + try await renderDocumentation( + swiftFile: """ + import Found1️⃣ation + + /// A structure containing important information + public struct Structure { + let number: Int + } + """, expectedResponses: [ - "1️⃣": .error(.noDocumentation), - "2️⃣": .renderNode(kind: .symbol, path: "test/Structure/numberPlusOne"), - "3️⃣": .renderNode(kind: .symbol, path: "test/Structure/Kind"), - "4️⃣": .renderNode(kind: .symbol, path: "test/Structure/Kind/first"), - "5️⃣": .renderNode(kind: .symbol, path: "test/Structure/Kind/second"), - "6️⃣": .error(.noDocumentation), + "1️⃣": .renderNode(kind: .symbol, path: "test/Structure") ] ) } From 31d5dfcd2d943700dbb7b1f76a8592c141080a8e Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 10 Jul 2025 11:22:54 -0400 Subject: [PATCH 2/6] add error message for unsupported languages --- .../DoccDocumentationError.swift | 3 ++ .../Clang/ClangLanguageService.swift | 7 ++- .../DoccDocumentationHandler.swift | 2 +- .../DoccDocumentationTests.swift | 48 +++++++++++++------ 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/Sources/DocCDocumentation/DoccDocumentationError.swift b/Sources/DocCDocumentation/DoccDocumentationError.swift index 974d03942..6915023d9 100644 --- a/Sources/DocCDocumentation/DoccDocumentationError.swift +++ b/Sources/DocCDocumentation/DoccDocumentationError.swift @@ -14,12 +14,15 @@ import Foundation package import LanguageServerProtocol package enum DocCDocumentationError: LocalizedError { + case unsupportedLanguage(String) case noDocumentableSymbols case indexNotAvailable case symbolNotFound(String) var errorDescription: String? { switch self { + case .unsupportedLanguage(let language): + return "Documentation preview is not available for \(language) files" case .noDocumentableSymbols: return "No documentable symbols were found in this Swift file" case .indexNotAvailable: diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift index 6b75bc788..66deb0d13 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift @@ -495,7 +495,12 @@ extension ClangLanguageService { #if canImport(DocCDocumentation) func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse { - throw ResponseError.requestFailed(doccDocumentationError: .noDocumentation) + guard let sourceKitLSPServer else { + throw ResponseError.unknown("Connection to the editor closed") + } + + let snapshot = try sourceKitLSPServer.documentManager.latestSnapshot(req.textDocument.uri) + throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language.description)) } #endif diff --git a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift index 332a335fe..188be2910 100644 --- a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift +++ b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift @@ -105,7 +105,7 @@ extension DocumentationLanguageService { catalogURL: catalogURL ) default: - throw ResponseError.requestFailed(doccDocumentationError: .noDocumentation) + throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language.description)) } } } diff --git a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift index 7ac33c25a..fb5eec861 100644 --- a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift @@ -21,11 +21,30 @@ import SwiftDocC import XCTest final class DoccDocumentationTests: XCTestCase { + + func testUnsupportedLanguage() async throws { + try await renderDocumentation( + markedText: "1️⃣", + language: .c, + expectedResponses: ["1️⃣": .error(.unsupportedLanguage("C"))] + ) + try await renderDocumentation( + markedText: "2️⃣", + language: .cpp, + expectedResponses: ["2️⃣": .error(.unsupportedLanguage("C++"))] + ) + try await renderDocumentation( + markedText: "3️⃣", + language: .objective_c, + expectedResponses: ["3️⃣": .error(.unsupportedLanguage("Objective-C"))] + ) + } + // MARK: Swift Documentation func testEmptySwiftFile() async throws { try await renderDocumentation( - swiftFile: "1️⃣", + markedText: "1️⃣", expectedResponses: [ "1️⃣": .error(.noDocumentableSymbols) ] @@ -34,7 +53,7 @@ final class DoccDocumentationTests: XCTestCase { func testFunction() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ /// A function that do1️⃣es some important stuff. func func2️⃣tion() { // Some import3️⃣ant function contents. @@ -51,7 +70,7 @@ final class DoccDocumentationTests: XCTestCase { func testStructure() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ /// A structure contain1️⃣ing important information. public struct Struc2️⃣ture { /// The inte3️⃣ger `foo` @@ -83,7 +102,7 @@ final class DoccDocumentationTests: XCTestCase { func testEmptyStructure() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ pub1️⃣lic struct Struc2️⃣ture { 3️⃣ }4️⃣ @@ -99,7 +118,7 @@ final class DoccDocumentationTests: XCTestCase { func testClass() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ /// A class contain1️⃣ing important information. public class Cla2️⃣ss { /// The inte3️⃣ger `foo` @@ -132,7 +151,7 @@ final class DoccDocumentationTests: XCTestCase { func testEmptyClass() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ pub1️⃣lic class Cla2️⃣ss { 3️⃣ }4️⃣ @@ -148,7 +167,7 @@ final class DoccDocumentationTests: XCTestCase { func testActor() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ /// An actor contain1️⃣ing important information. public actor Ac2️⃣tor { /// The inte3️⃣ger `foo` @@ -180,7 +199,7 @@ final class DoccDocumentationTests: XCTestCase { func testEmptyActor() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ pub1️⃣lic class Act2️⃣or { 3️⃣ }4️⃣ @@ -196,7 +215,7 @@ final class DoccDocumentationTests: XCTestCase { func testEnumeration() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ /// An enumeration contain1️⃣ing important information. public enum En2️⃣um { /// The 3️⃣first case. @@ -272,7 +291,7 @@ final class DoccDocumentationTests: XCTestCase { func testProtocol() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ /// A protocol contain1️⃣ing important information. public protocol Proto2️⃣col { /// The inte3️⃣ger `foo` @@ -296,7 +315,7 @@ final class DoccDocumentationTests: XCTestCase { func testEmptyProtocol() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ /// A protocol containing important information pub1️⃣lic struct Prot2️⃣ocol { 3️⃣ @@ -353,7 +372,7 @@ final class DoccDocumentationTests: XCTestCase { func testCursorInImport() async throws { try await renderDocumentation( - swiftFile: """ + markedText: """ import Found1️⃣ation /// A structure containing important information @@ -733,13 +752,14 @@ fileprivate func renderDocumentation( } fileprivate func renderDocumentation( - swiftFile markedText: String, + markedText: String, + language: Language = .swift, expectedResponses: [String: PartialConvertResponse], file: StaticString = #filePath, line: UInt = #line ) async throws { let testClient = try await TestSourceKitLSPClient() - let uri = DocumentURI(for: .swift) + let uri = DocumentURI(for: language) let positions = testClient.openDocument(markedText, uri: uri) await renderDocumentation( From 9f907b92fda70ede408db014dc5e3d4851ed9546 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 10 Jul 2025 15:00:19 -0400 Subject: [PATCH 3/6] improve logic for DocumentableSymbol.findNearestSymbol(node:) --- .../Swift/DoccDocumentation.swift | 49 +++++++++---------- .../DoccDocumentationTests.swift | 7 +-- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift index 7040d5728..7a7090c64 100644 --- a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift +++ b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift @@ -46,7 +46,7 @@ extension SwiftLanguageService { let nearestDocumentableSymbol = DocumentableSymbol.findNearestSymbol( syntaxTree: syntaxTree, position: snapshot.absolutePosition(of: position) - ) ?? DocumentableSymbol.findTopLevelSymbol(syntaxTree: syntaxTree) + ) else { throw ResponseError.requestFailed(doccDocumentationError: .noDocumentableSymbols) } @@ -152,34 +152,31 @@ fileprivate struct DocumentableSymbol { } } - static func findTopLevelSymbol(syntaxTree: SourceFileSyntax) -> DocumentableSymbol? { - class Visitor: SyntaxAnyVisitor { - var topLevelSymbol: DocumentableSymbol? = nil - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - guard topLevelSymbol == nil else { - return .skipChildren - } - - if let symbol = DocumentableSymbol(node: node) { - topLevelSymbol = symbol - return .skipChildren - } - return .visitChildren + static func findNearestSymbol(syntaxTree: SourceFileSyntax, position: AbsolutePosition) -> DocumentableSymbol? { + // token(at:) can return nil if the position is at the end of the document. Fall back to using the last token in this case. + let token = syntaxTree.token(at: position) ?? syntaxTree.lastToken(viewMode: .all) + // Check if the current token is within a valid documentable symbol + if let token, let symbol = token.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { + return symbol + } + // Walk forward through the tokens until we find a documentable symbol + var previousToken = token + while let nextToken = previousToken?.nextToken(viewMode: .all) { + if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { + return symbol } + previousToken = nextToken } - - let visitor = Visitor(viewMode: .all) - visitor.walk(syntaxTree) - return visitor.topLevelSymbol - } - - static func findNearestSymbol(syntaxTree: SourceFileSyntax, position: AbsolutePosition) -> DocumentableSymbol? { - guard let token = syntaxTree.token(at: position) else { - return nil + // Walk backwards through the tokens until we find a documentable symbol + previousToken = token + while let nextToken = previousToken?.previousToken(viewMode: .all) { + if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { + return symbol + } + previousToken = nextToken } - // Walk up the tree until we find a documentable symbol - return token.ancestorOrSelf { DocumentableSymbol(node: $0) } + // We couldn't find anything + return nil } } #endif diff --git a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift index fb5eec861..e65dc0517 100644 --- a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift @@ -350,8 +350,8 @@ final class DoccDocumentationTests: XCTestCase { case first /// The se5️⃣cond kind case second - } - }6️⃣ + }6️⃣ + }7️⃣ """, ], enableBackgroundIndexing: true @@ -365,7 +365,8 @@ final class DoccDocumentationTests: XCTestCase { "3️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind"), "4️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind/first"), "5️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind/second"), - "6️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/numberPlusOne"), + "6️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind"), + "7️⃣": .renderNode(kind: .symbol, path: "MyLibrary/Structure/Kind"), ] ) } From 4238f76a6054bb4b40b13e922e980d792f1bcec4 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 10 Jul 2025 15:00:43 -0400 Subject: [PATCH 4/6] use Language instead of String for DoccDocumentationError.unsupportedLanguage --- Sources/DocCDocumentation/DoccDocumentationError.swift | 4 ++-- Sources/SourceKitLSP/Clang/ClangLanguageService.swift | 2 +- .../Documentation/DoccDocumentationHandler.swift | 2 +- Tests/SourceKitLSPTests/DoccDocumentationTests.swift | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/DocCDocumentation/DoccDocumentationError.swift b/Sources/DocCDocumentation/DoccDocumentationError.swift index 6915023d9..263fbea81 100644 --- a/Sources/DocCDocumentation/DoccDocumentationError.swift +++ b/Sources/DocCDocumentation/DoccDocumentationError.swift @@ -14,7 +14,7 @@ import Foundation package import LanguageServerProtocol package enum DocCDocumentationError: LocalizedError { - case unsupportedLanguage(String) + case unsupportedLanguage(Language) case noDocumentableSymbols case indexNotAvailable case symbolNotFound(String) @@ -22,7 +22,7 @@ package enum DocCDocumentationError: LocalizedError { var errorDescription: String? { switch self { case .unsupportedLanguage(let language): - return "Documentation preview is not available for \(language) files" + return "Documentation preview is not available for \(language.description) files" case .noDocumentableSymbols: return "No documentable symbols were found in this Swift file" case .indexNotAvailable: diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift index 66deb0d13..e10eb9eea 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageService.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageService.swift @@ -500,7 +500,7 @@ extension ClangLanguageService { } let snapshot = try sourceKitLSPServer.documentManager.latestSnapshot(req.textDocument.uri) - throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language.description)) + throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language)) } #endif diff --git a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift index 188be2910..078d13dae 100644 --- a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift +++ b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift @@ -105,7 +105,7 @@ extension DocumentationLanguageService { catalogURL: catalogURL ) default: - throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language.description)) + throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language)) } } } diff --git a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift index e65dc0517..5ec33d809 100644 --- a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift @@ -26,17 +26,17 @@ final class DoccDocumentationTests: XCTestCase { try await renderDocumentation( markedText: "1️⃣", language: .c, - expectedResponses: ["1️⃣": .error(.unsupportedLanguage("C"))] + expectedResponses: ["1️⃣": .error(.unsupportedLanguage(.c))] ) try await renderDocumentation( markedText: "2️⃣", language: .cpp, - expectedResponses: ["2️⃣": .error(.unsupportedLanguage("C++"))] + expectedResponses: ["2️⃣": .error(.unsupportedLanguage(.cpp))] ) try await renderDocumentation( markedText: "3️⃣", language: .objective_c, - expectedResponses: ["3️⃣": .error(.unsupportedLanguage("Objective-C"))] + expectedResponses: ["3️⃣": .error(.unsupportedLanguage(.objective_c))] ) } From 6b6703ed233fbdde7acb81e7182cc4902c4a3613 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 10 Jul 2025 15:27:31 -0400 Subject: [PATCH 5/6] add subscripts and deinitializers as documentable Swift symbols --- .../Swift/DoccDocumentation.swift | 4 ++ .../DoccDocumentationTests.swift | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift index 7a7090c64..6b034199a 100644 --- a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift +++ b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift @@ -135,8 +135,12 @@ fileprivate struct DocumentableSymbol { self = DocumentableSymbol(node: namedDecl, position: namedDecl.name.positionAfterSkippingLeadingTrivia) } else if let initDecl = node.as(InitializerDeclSyntax.self) { self = DocumentableSymbol(node: initDecl, position: initDecl.initKeyword.positionAfterSkippingLeadingTrivia) + } else if let deinitDecl = node.as(DeinitializerDeclSyntax.self) { + self = DocumentableSymbol(node: deinitDecl, position: deinitDecl.deinitKeyword.positionAfterSkippingLeadingTrivia) } else if let functionDecl = node.as(FunctionDeclSyntax.self) { self = DocumentableSymbol(node: functionDecl, position: functionDecl.name.positionAfterSkippingLeadingTrivia) + } else if let subscriptDecl = node.as(SubscriptDeclSyntax.self) { + self = DocumentableSymbol(node: subscriptDecl, position: subscriptDecl.positionAfterSkippingLeadingTrivia) } else if let variableDecl = node.as(VariableDeclSyntax.self) { guard let identifier = variableDecl.bindings.only?.pattern.as(IdentifierPatternSyntax.self) else { return nil diff --git a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift index 5ec33d809..11f45ebe0 100644 --- a/Tests/SourceKitLSPTests/DoccDocumentationTests.swift +++ b/Tests/SourceKitLSPTests/DoccDocumentationTests.swift @@ -116,6 +116,25 @@ final class DoccDocumentationTests: XCTestCase { ) } + func testSubscriptDeclaration() async throws { + try await renderDocumentation( + markedText: """ + /// A structure containing important information. + public struct Structure { + // Get the 1️⃣subscript at index + subscript(in2️⃣dex: Int) -> Int { + return i3️⃣ndex + } + } + """, + expectedResponses: [ + "1️⃣": .renderNode(kind: .symbol, path: "test/Structure/subscript(_:)"), + "2️⃣": .renderNode(kind: .symbol, path: "test/Structure/subscript(_:)"), + "3️⃣": .renderNode(kind: .symbol, path: "test/Structure/subscript(_:)"), + ] + ) + } + func testClass() async throws { try await renderDocumentation( markedText: """ @@ -149,6 +168,25 @@ final class DoccDocumentationTests: XCTestCase { ) } + func testClassDeInitializer() async throws { + try await renderDocumentation( + markedText: """ + /// A class containing important information. + public class Class { + /// Initi1️⃣alize the class. + dein2️⃣it { + // De-initi3️⃣alize stuff + } + } + """, + expectedResponses: [ + "1️⃣": .renderNode(kind: .symbol, path: "test/Class/deinit"), + "2️⃣": .renderNode(kind: .symbol, path: "test/Class/deinit"), + "3️⃣": .renderNode(kind: .symbol, path: "test/Class/deinit"), + ] + ) + } + func testEmptyClass() async throws { try await renderDocumentation( markedText: """ From bfbbb2f2c5e55b3714a45899be178a4e068c5916 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Mon, 14 Jul 2025 11:25:55 -0400 Subject: [PATCH 6/6] improve token searching in findNearestSymbol(syntaxTree:position:) --- .../Swift/DoccDocumentation.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift index 6b034199a..728fe73f5 100644 --- a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift +++ b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift @@ -157,15 +157,25 @@ fileprivate struct DocumentableSymbol { } static func findNearestSymbol(syntaxTree: SourceFileSyntax, position: AbsolutePosition) -> DocumentableSymbol? { - // token(at:) can return nil if the position is at the end of the document. Fall back to using the last token in this case. - let token = syntaxTree.token(at: position) ?? syntaxTree.lastToken(viewMode: .all) + let token: TokenSyntax + if let tokenAtPosition = syntaxTree.token(at: position) { + token = tokenAtPosition + } else if position >= syntaxTree.endPosition, let lastToken = syntaxTree.lastToken(viewMode: .sourceAccurate) { + // token(at:) returns nil if position is at the end of the document. + token = lastToken + } else if position < syntaxTree.position, let firstToken = syntaxTree.firstToken(viewMode: .sourceAccurate) { + // No case in practice where this happens but good to cover anyway + token = firstToken + } else { + return nil + } // Check if the current token is within a valid documentable symbol - if let token, let symbol = token.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { + if let symbol = token.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { return symbol } // Walk forward through the tokens until we find a documentable symbol - var previousToken = token - while let nextToken = previousToken?.nextToken(viewMode: .all) { + var previousToken: TokenSyntax? = token + while let nextToken = previousToken?.nextToken(viewMode: .sourceAccurate) { if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { return symbol } @@ -173,7 +183,7 @@ fileprivate struct DocumentableSymbol { } // Walk backwards through the tokens until we find a documentable symbol previousToken = token - while let nextToken = previousToken?.previousToken(viewMode: .all) { + while let nextToken = previousToken?.previousToken(viewMode: .sourceAccurate) { if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { return symbol }