Skip to content

Always try to render some documentation for Swift files #2198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Sources/DocCDocumentation/DoccDocumentationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ import Foundation
package import LanguageServerProtocol

package enum DocCDocumentationError: LocalizedError {
case noDocumentation
case unsupportedLanguage(String)
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 .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:
return "The index is not availble to complete the request"
case .symbolNotFound(let symbolName):
Expand Down
7 changes: 6 additions & 1 deletion Sources/SourceKitLSP/Clang/ClangLanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ extension DocumentationLanguageService {
catalogURL: catalogURL
)
default:
throw ResponseError.requestFailed(doccDocumentationError: .noDocumentation)
throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language.description))
}
}
}
Expand Down
64 changes: 45 additions & 19 deletions Sources/SourceKitLSP/Swift/DoccDocumentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Loading