diff --git a/Sources/ClangLanguageService/ClangLanguageService.swift b/Sources/ClangLanguageService/ClangLanguageService.swift index b56258a94..080e9e9f7 100644 --- a/Sources/ClangLanguageService/ClangLanguageService.swift +++ b/Sources/ClangLanguageService/ClangLanguageService.swift @@ -511,8 +511,15 @@ extension ClangLanguageService { package func symbolGraph( forOnDiskContentsOf symbolDocumentUri: DocumentURI, at location: SymbolLocation - ) async throws -> String? { - return nil + ) async throws -> String { + throw ResponseError.internalError("Symbol graph is currently not supported for clang files") + } + + package func symbolGraph( + for snapshot: SourceKitLSP.DocumentSnapshot, + at position: LanguageServerProtocol.Position + ) async throws -> (symbolGraph: String, usr: String, overrideDocComments: [String]) { + throw ResponseError.internalError("Symbol graph is currently not supported for clang files") } package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { diff --git a/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift b/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift index a87a5a9a0..89fabbc1b 100644 --- a/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift +++ b/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift @@ -20,6 +20,7 @@ import Markdown import SKUtilities import SourceKitLSP import SemanticIndex +import SKLogging extension DocumentationLanguageService { package func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse { @@ -29,7 +30,6 @@ extension DocumentationLanguageService { guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: req.textDocument.uri) else { throw ResponseError.workspaceNotOpen(req.textDocument.uri) } - let documentationManager = workspace.doccDocumentationManager let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) var moduleName: String? = nil var catalogURL: URL? = nil @@ -40,81 +40,197 @@ extension DocumentationLanguageService { switch snapshot.language { case .tutorial: - return try await documentationManager.renderDocCDocumentation( - tutorialFile: snapshot.text, + return try await tutorialDocumentation( + for: snapshot, + in: workspace, moduleName: moduleName, catalogURL: catalogURL ) case .markdown: - guard case .symbol(let symbolName) = MarkdownTitleFinder.find(parsing: snapshot.text) else { - // This is an article that can be rendered on its own - return try await documentationManager.renderDocCDocumentation( - markupFile: snapshot.text, - moduleName: moduleName, - catalogURL: catalogURL - ) + return try await markdownDocumentation( + for: snapshot, + in: workspace, + moduleName: moduleName, + catalogURL: catalogURL + ) + case .swift: + guard let position = req.position else { + throw ResponseError.invalidParams("A position must be provided for Swift files") } - guard let moduleName, symbolName == moduleName else { - // This is a symbol extension page. Find the symbol so that we can include it in the request. - guard let index = workspace.index(checkedFor: .deletedFiles) else { - throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable) - } - guard let symbolLink = DocCSymbolLink(linkString: symbolName), - let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence( - ofDocCSymbolLink: symbolLink, - fetchSymbolGraph: { location in - guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri) else { - throw ResponseError.internalError("Unable to find language service for \(location.documentUri)") - } - let languageService = try await sourceKitLSPServer.primaryLanguageService( - for: location.documentUri, - .swift, - in: symbolWorkspace - ) - return try await languageService.symbolGraph(forOnDiskContentsOf: location.documentUri, at: location) + + return try await swiftDocumentation( + for: snapshot, + at: position, + in: workspace, + moduleName: moduleName, + catalogURL: catalogURL + ) + default: + throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language)) + } + } + + private func tutorialDocumentation( + for snapshot: DocumentSnapshot, + in workspace: Workspace, + moduleName: String?, + catalogURL: URL? + ) async throws -> DoccDocumentationResponse { + return try await workspace.doccDocumentationManager.renderDocCDocumentation( + tutorialFile: snapshot.text, + moduleName: moduleName, + catalogURL: catalogURL + ) + } + + private func markdownDocumentation( + for snapshot: DocumentSnapshot, + in workspace: Workspace, + moduleName: String?, + catalogURL: URL? + ) async throws -> DoccDocumentationResponse { + guard let sourceKitLSPServer else { + throw ResponseError.internalError("SourceKit-LSP is shutting down") + } + let documentationManager = workspace.doccDocumentationManager + guard case .symbol(let symbolName) = MarkdownTitleFinder.find(parsing: snapshot.text) else { + // This is an article that can be rendered on its own + return try await documentationManager.renderDocCDocumentation( + markupFile: snapshot.text, + moduleName: moduleName, + catalogURL: catalogURL + ) + } + guard let moduleName, symbolName == moduleName else { + // This is a symbol extension page. Find the symbol so that we can include it in the request. + guard let index = workspace.index(checkedFor: .deletedFiles) else { + throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable) + } + guard let symbolLink = DocCSymbolLink(linkString: symbolName), + let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence( + ofDocCSymbolLink: symbolLink, + fetchSymbolGraph: { location in + guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri) else { + throw ResponseError.internalError("Unable to find language service for \(location.documentUri)") } - ) - else { - throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName)) - } - let symbolDocumentUri = symbolOccurrence.location.documentUri - guard let symbolWorkspace = try await workspaceForDocument(uri: symbolDocumentUri) else { - throw ResponseError.internalError("Unable to find language service for \(symbolDocumentUri)") - } - let languageService = try await sourceKitLSPServer.primaryLanguageService( - for: symbolDocumentUri, - .swift, - in: symbolWorkspace - ) - let symbolGraph = try await languageService.symbolGraph( - forOnDiskContentsOf: symbolDocumentUri, - at: symbolOccurrence.location - ) - guard let symbolGraph else { - throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)") - } - return try await documentationManager.renderDocCDocumentation( - symbolUSR: symbolOccurrence.symbol.usr, - symbolGraph: symbolGraph, - markupFile: snapshot.text, - moduleName: moduleName, - catalogURL: catalogURL + let languageService = try await sourceKitLSPServer.primaryLanguageService( + for: location.documentUri, + .swift, + in: symbolWorkspace + ) + return try await languageService.symbolGraph( + forOnDiskContentsOf: location.documentUri, + at: location + ) + } ) + else { + throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName)) } - // This is a page representing the module itself. - // Create a dummy symbol graph and tell SwiftDocC to convert the module name. - // The version information isn't really all that important since we're creating - // what is essentially an empty symbol graph. + let symbolDocumentUri = symbolOccurrence.location.documentUri + guard let symbolWorkspace = try await workspaceForDocument(uri: symbolDocumentUri) else { + throw ResponseError.internalError("Unable to find language service for \(symbolDocumentUri)") + } + let languageService = try await sourceKitLSPServer.primaryLanguageService( + for: symbolDocumentUri, + .swift, + in: symbolWorkspace + ) + let symbolGraph = try await languageService.symbolGraph( + forOnDiskContentsOf: symbolDocumentUri, + at: symbolOccurrence.location + ) return try await documentationManager.renderDocCDocumentation( - symbolUSR: moduleName, - symbolGraph: emptySymbolGraph(forModule: moduleName), + symbolUSR: symbolOccurrence.symbol.usr, + symbolGraph: symbolGraph, markupFile: snapshot.text, moduleName: moduleName, catalogURL: catalogURL ) - default: - throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language)) } + // This is a page representing the module itself. + // Create a dummy symbol graph and tell SwiftDocC to convert the module name. + // The version information isn't really all that important since we're creating + // what is essentially an empty symbol graph. + return try await documentationManager.renderDocCDocumentation( + symbolUSR: moduleName, + symbolGraph: emptySymbolGraph(forModule: moduleName), + markupFile: snapshot.text, + moduleName: moduleName, + catalogURL: catalogURL + ) + } + + private func swiftDocumentation( + for snapshot: DocumentSnapshot, + at position: Position, + in workspace: Workspace, + moduleName: String?, + catalogURL: URL? + ) async throws -> DoccDocumentationResponse { + guard let sourceKitLSPServer else { + throw ResponseError.internalError("SourceKit-LSP is shutting down") + } + let documentationManager = workspace.doccDocumentationManager + let (symbolGraph, symbolUSR, overrideDocComments) = try await sourceKitLSPServer.primaryLanguageService( + for: snapshot.uri, + snapshot.language, + in: workspace + ).symbolGraph(for: snapshot, at: position) + // Locate the documentation extension and include it in the request if one exists + let markupExtensionFile = await orLog("Finding markup extension file for symbol \(symbolUSR)") { + try await findMarkupExtensionFile( + workspace: workspace, + documentationManager: documentationManager, + catalogURL: catalogURL, + for: symbolUSR, + fetchSymbolGraph: { location in + guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri) else { + throw ResponseError.internalError("Unable to find language service for \(location.documentUri)") + } + let languageService = try await sourceKitLSPServer.primaryLanguageService( + for: location.documentUri, + .swift, + in: symbolWorkspace + ) + return try await languageService.symbolGraph(forOnDiskContentsOf: location.documentUri, at: location) + } + ) + } + return try await documentationManager.renderDocCDocumentation( + symbolUSR: symbolUSR, + symbolGraph: symbolGraph, + overrideDocComments: overrideDocComments, + markupFile: markupExtensionFile, + moduleName: moduleName, + catalogURL: catalogURL + ) + } + + private func findMarkupExtensionFile( + workspace: Workspace, + documentationManager: DocCDocumentationManager, + catalogURL: URL?, + for symbolUSR: String, + fetchSymbolGraph: @Sendable (SymbolLocation) async throws -> String? + ) async throws -> String? { + guard let catalogURL else { + return nil + } + let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL) + guard let index = workspace.index(checkedFor: .deletedFiles), + let symbolInformation = try await index.doccSymbolInformation( + ofUSR: symbolUSR, + fetchSymbolGraph: fetchSymbolGraph + ), + let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation) + else { + return nil + } + return try? documentManager.latestSnapshotOrDisk( + DocumentURI(markupExtensionFileURL), + language: .markdown + )?.text } } diff --git a/Sources/DocumentationLanguageService/DocumentationLanguageService.swift b/Sources/DocumentationLanguageService/DocumentationLanguageService.swift index f5acc05a3..eb0843a4d 100644 --- a/Sources/DocumentationLanguageService/DocumentationLanguageService.swift +++ b/Sources/DocumentationLanguageService/DocumentationLanguageService.swift @@ -136,8 +136,15 @@ package actor DocumentationLanguageService: LanguageService, Sendable { package func symbolGraph( forOnDiskContentsOf symbolDocumentUri: DocumentURI, at location: SymbolLocation - ) async throws -> String? { - return nil + ) async throws -> String { + throw ResponseError.internalError("Not applicable") + } + + package func symbolGraph( + for snapshot: SourceKitLSP.DocumentSnapshot, + at position: LanguageServerProtocol.Position + ) async throws -> (symbolGraph: String, usr: String, overrideDocComments: [String]) { + throw ResponseError.internalError("Not applicable") } package func openGeneratedInterface( diff --git a/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift b/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift index 83c5b9241..ca95726d0 100644 --- a/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift +++ b/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift @@ -26,7 +26,7 @@ extension LanguageServiceRegistry { registry.register(ClangLanguageService.self, for: [.c, .cpp, .objective_c, .objective_cpp]) registry.register(SwiftLanguageService.self, for: [.swift]) #if canImport(DocumentationLanguageService) - registry.register(DocumentationLanguageService.self, for: [.markdown, .tutorial]) + registry.register(DocumentationLanguageService.self, for: [.markdown, .tutorial, .swift]) #endif return registry }() diff --git a/Sources/SourceKitLSP/LanguageService.swift b/Sources/SourceKitLSP/LanguageService.swift index 054e072d8..86a5dd06e 100644 --- a/Sources/SourceKitLSP/LanguageService.swift +++ b/Sources/SourceKitLSP/LanguageService.swift @@ -179,12 +179,19 @@ package protocol LanguageService: AnyObject, Sendable { func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse func symbolInfo(_ request: SymbolInfoRequest) async throws -> [SymbolDetails] + /// Retrieve the symbol graph for the given position in the given snapshot, including the USR of the symbol at the + /// given position and the doc comments of the symbol at that position. + func symbolGraph( + for snapshot: DocumentSnapshot, + at position: Position + ) async throws -> (symbolGraph: String, usr: String, overrideDocComments: [String]) + /// Return the symbol graph at the given location for the contents of the document as they are on-disk (opposed to the /// in-memory modified version of the document). func symbolGraph( forOnDiskContentsOf symbolDocumentUri: DocumentURI, at location: SymbolLocation - ) async throws -> String? + ) async throws -> String /// Request a generated interface of a module to display in the IDE. /// diff --git a/Sources/SwiftLanguageService/DoccDocumentation.swift b/Sources/SwiftLanguageService/DoccDocumentation.swift index 41406856a..b50c6881a 100644 --- a/Sources/SwiftLanguageService/DoccDocumentation.swift +++ b/Sources/SwiftLanguageService/DoccDocumentation.swift @@ -26,201 +26,6 @@ import DocCDocumentation extension SwiftLanguageService { package func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse { - #if canImport(DocCDocumentation) - guard let sourceKitLSPServer else { - throw ResponseError.internalError("SourceKit-LSP is shutting down") - } - guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: req.textDocument.uri) else { - throw ResponseError.workspaceNotOpen(req.textDocument.uri) - } - let documentationManager = workspace.doccDocumentationManager - guard let position = req.position else { - throw ResponseError.invalidParams("A position must be provided for Swift files") - } - let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) - var moduleName: String? = nil - var catalogURL: URL? = nil - if let target = await workspace.buildServerManager.canonicalTarget(for: req.textDocument.uri) { - moduleName = await workspace.buildServerManager.moduleName(for: target) - catalogURL = await workspace.buildServerManager.doccCatalog(for: target) - } - - // Search for the nearest documentable symbol at this location - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - guard - let nearestDocumentableSymbol = DocumentableSymbol.findNearestSymbol( - syntaxTree: syntaxTree, - position: snapshot.absolutePosition(of: position) - ) - else { - throw ResponseError.requestFailed(doccDocumentationError: .noDocumentableSymbols) - } - // Retrieve the symbol graph as well as information about the symbol - let symbolPosition = await adjustPositionToStartOfIdentifier( - snapshot.position(of: nearestDocumentableSymbol.position), - in: snapshot - ) - let (cursorInfo, _, symbolGraph) = try await cursorInfo( - req.textDocument.uri, - Range(symbolPosition), - includeSymbolGraph: true, - fallbackSettingsAfterTimeout: false - ) - guard let symbolGraph, - let cursorInfo = cursorInfo.first, - let symbolUSR = cursorInfo.symbolInfo.usr - else { - throw ResponseError.internalError("Unable to retrieve symbol graph for the document") - } - // Locate the documentation extension and include it in the request if one exists - let markupExtensionFile = await orLog("Finding markup extension file for symbol \(symbolUSR)") { - try await findMarkupExtensionFile( - workspace: workspace, - documentationManager: documentationManager, - catalogURL: catalogURL, - for: symbolUSR, - fetchSymbolGraph: { symbolLocation in - try await withSnapshotFromDiskOpenedInSourcekitd( - uri: symbolLocation.documentUri, - fallbackSettingsAfterTimeout: false - ) { (snapshot, compileCommand) in - let (_, _, symbolGraph) = try await self.cursorInfo( - snapshot, - compileCommand: compileCommand, - Range(snapshot.position(of: symbolLocation)), - includeSymbolGraph: true - ) - return symbolGraph - } - } - ) - } - return try await documentationManager.renderDocCDocumentation( - symbolUSR: symbolUSR, - symbolGraph: symbolGraph, - overrideDocComments: nearestDocumentableSymbol.documentationComments, - markupFile: markupExtensionFile, - moduleName: moduleName, - catalogURL: catalogURL - ) - #else - throw ResponseError.requestFailed("Documentation preview is not available in this build of SourceKit-LSP") - #endif - } - - #if canImport(DocCDocumentation) - private func findMarkupExtensionFile( - workspace: Workspace, - documentationManager: DocCDocumentationManager, - catalogURL: URL?, - for symbolUSR: String, - fetchSymbolGraph: @Sendable (SymbolLocation) async throws -> String? - ) async throws -> String? { - guard let catalogURL else { - return nil - } - let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL) - guard let index = workspace.index(checkedFor: .deletedFiles), - let symbolInformation = try await index.doccSymbolInformation( - ofUSR: symbolUSR, - fetchSymbolGraph: fetchSymbolGraph - ), - let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation) - else { - return nil - } - return try? documentManager.latestSnapshotOrDisk( - DocumentURI(markupExtensionFileURL), - language: .markdown - )?.text - } - #endif -} - -private struct DocumentableSymbol { - let position: AbsolutePosition - let documentationComments: [String] - - init(node: any SyntaxProtocol, position: AbsolutePosition) { - self.position = position - self.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(whereSeparator: \.isNewline) - .map { String($0).trimmingCharacters(in: .whitespaces) } - default: - return [] - } - } - } - - 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 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.subscriptKeyword, - position: subscriptDecl.subscriptKeyword.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 - } - } - - static func findNearestSymbol(syntaxTree: SourceFileSyntax, position: AbsolutePosition) -> DocumentableSymbol? { - 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 symbol = token.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { - return symbol - } - // Walk forward through the tokens until we find a documentable symbol - var previousToken: TokenSyntax? = token - while let nextToken = previousToken?.nextToken(viewMode: .sourceAccurate) { - if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { - return symbol - } - previousToken = nextToken - } - // Walk backwards through the tokens until we find a documentable symbol - previousToken = token - while let nextToken = previousToken?.previousToken(viewMode: .sourceAccurate) { - if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { - return symbol - } - previousToken = nextToken - } - // We couldn't find anything - return nil + throw ResponseError.requestNotImplemented(DoccDocumentationRequest.self) } } diff --git a/Sources/SwiftLanguageService/SymbolGraph.swift b/Sources/SwiftLanguageService/SymbolGraph.swift index e486a1979..ad922f349 100644 --- a/Sources/SwiftLanguageService/SymbolGraph.swift +++ b/Sources/SwiftLanguageService/SymbolGraph.swift @@ -10,25 +10,154 @@ // //===----------------------------------------------------------------------===// +import Foundation package import IndexStoreDB package import LanguageServerProtocol -import SourceKitLSP +package import SourceKitLSP +import SwiftExtensions +import SwiftSyntax extension SwiftLanguageService { package func symbolGraph( forOnDiskContentsOf symbolDocumentUri: DocumentURI, at location: SymbolLocation - ) async throws -> String? { + ) async throws -> String { return try await withSnapshotFromDiskOpenedInSourcekitd( uri: symbolDocumentUri, fallbackSettingsAfterTimeout: false ) { snapshot, compileCommand in - try await cursorInfo( + let symbolGraph = try await cursorInfo( snapshot, compileCommand: compileCommand, Range(snapshot.position(of: location)), includeSymbolGraph: true ).symbolGraph + guard let symbolGraph else { + throw ResponseError.internalError("Unable to retrieve symbol graph") + } + return symbolGraph } } + + package func symbolGraph( + for snapshot: DocumentSnapshot, + at position: Position + ) async throws -> (symbolGraph: String, usr: String, overrideDocComments: [String]) { + // Search for the nearest documentable symbol at this location + let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) + guard + let nearestDocumentableSymbol = DocumentableSymbol.findNearestSymbol( + syntaxTree: syntaxTree, + position: snapshot.absolutePosition(of: position) + ) + else { + throw ResponseError.requestFailed("No documentable symbols were found in this Swift file") + } + // Retrieve the symbol graph as well as information about the symbol + let symbolPosition = await adjustPositionToStartOfIdentifier( + snapshot.position(of: nearestDocumentableSymbol.position), + in: snapshot + ) + let (cursorInfo, _, symbolGraph) = try await cursorInfo( + snapshot.uri, + Range(symbolPosition), + includeSymbolGraph: true, + fallbackSettingsAfterTimeout: false + ) + guard let symbolGraph, + let cursorInfo = cursorInfo.first, + let symbolUSR = cursorInfo.symbolInfo.usr + else { + throw ResponseError.internalError("Unable to retrieve symbol graph for the document") + } + return (symbolGraph, symbolUSR, nearestDocumentableSymbol.documentationComments) + } +} + +private struct DocumentableSymbol { + let position: AbsolutePosition + let documentationComments: [String] + + init(node: any SyntaxProtocol, position: AbsolutePosition) { + self.position = position + self.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(whereSeparator: \.isNewline) + .map { String($0).trimmingCharacters(in: .whitespaces) } + default: + return [] + } + } + } + + 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 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.subscriptKeyword, + position: subscriptDecl.subscriptKeyword.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 + } + } + + static func findNearestSymbol(syntaxTree: SourceFileSyntax, position: AbsolutePosition) -> DocumentableSymbol? { + 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 symbol = token.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { + return symbol + } + // Walk forward through the tokens until we find a documentable symbol + var previousToken: TokenSyntax? = token + while let nextToken = previousToken?.nextToken(viewMode: .sourceAccurate) { + if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { + return symbol + } + previousToken = nextToken + } + // Walk backwards through the tokens until we find a documentable symbol + previousToken = token + while let nextToken = previousToken?.previousToken(viewMode: .sourceAccurate) { + if let symbol = nextToken.ancestorOrSelf(mapping: { DocumentableSymbol(node: $0) }) { + return symbol + } + previousToken = nextToken + } + // We couldn't find anything + return nil + } }