Skip to content

Commit aaedffc

Browse files
handle Markdown and Tutorial files in textDocument/doccDocumentation
1 parent 03da4a4 commit aaedffc

18 files changed

+1470
-162
lines changed

Package.swift

+6
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ var targets: [Target] = [
478478
"ToolchainRegistry",
479479
"TSCExtensions",
480480
.product(name: "SwiftDocC", package: "swift-docc"),
481+
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
482+
.product(name: "Markdown", package: "swift-markdown"),
481483
.product(name: "IndexStoreDB", package: "indexstore-db"),
482484
.product(name: "Crypto", package: "swift-crypto"),
483485
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
@@ -760,6 +762,8 @@ var dependencies: [Package.Dependency] {
760762
return [
761763
.package(path: "../indexstore-db"),
762764
.package(path: "../swift-docc"),
765+
.package(path: "../swift-docc-symbolkit"),
766+
.package(path: "../swift-markdown"),
763767
.package(path: "../swift-tools-support-core"),
764768
.package(path: "../swift-argument-parser"),
765769
.package(path: "../swift-syntax"),
@@ -771,6 +775,8 @@ var dependencies: [Package.Dependency] {
771775
return [
772776
.package(url: "https://github.com/swiftlang/indexstore-db.git", branch: relatedDependenciesBranch),
773777
.package(url: "https://github.com/swiftlang/swift-docc.git", branch: relatedDependenciesBranch),
778+
.package(url: "https://github.com/swiftlang/swift-docc-symbolkit.git", branch: relatedDependenciesBranch),
779+
.package(url: "https://github.com/swiftlang/swift-markdown.git", branch: relatedDependenciesBranch),
774780
.package(url: "https://github.com/apple/swift-tools-support-core.git", branch: relatedDependenciesBranch),
775781
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"),
776782
.package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch),

Sources/SemanticIndex/CheckedIndex.swift

+12
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,18 @@ package final class CheckedIndex {
139139
}
140140
}
141141

142+
@discardableResult package func forEachCanonicalSymbolOccurrence(
143+
byName name: String,
144+
body: (SymbolOccurrence) -> Bool
145+
) -> Bool {
146+
index.forEachCanonicalSymbolOccurrence(byName: name) { occurrence in
147+
guard self.checker.isUpToDate(occurrence.location) else {
148+
return true // continue
149+
}
150+
return body(occurrence)
151+
}
152+
}
153+
142154
package func symbols(inFilePath path: String) -> [Symbol] {
143155
guard self.hasUpToDateUnit(for: DocumentURI(filePath: path, isDirectory: false)) else {
144156
return []

Sources/SourceKitLSP/Clang/ClangLanguageService.swift

+6
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,12 @@ extension ClangLanguageService {
500500
return try await forwardRequestToClangd(req)
501501
}
502502

503+
#if canImport(SwiftDocC)
504+
func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse {
505+
throw ResponseError.requestFailed(doccDocumentationError: .noDocumentation)
506+
}
507+
#endif
508+
503509
func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] {
504510
return try await forwardRequestToClangd(req)
505511
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#if canImport(SwiftDocC)
2+
import Foundation
3+
import IndexStoreDB
4+
import LanguageServerProtocol
5+
import SemanticIndex
6+
@preconcurrency import SwiftDocC
7+
import SwiftExtensions
8+
9+
extension CheckedIndex {
10+
func doccSymbolLink(forUSR usr: String) -> DocCSymbolLink? {
11+
guard let topLevelSymbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
12+
return nil
13+
}
14+
let module = topLevelSymbolOccurrence.location.moduleName
15+
var components = [topLevelSymbolOccurrence.symbol.name]
16+
// Find any child symbols
17+
var symbolOccurrence: SymbolOccurrence? = topLevelSymbolOccurrence
18+
while let currentSymbolOccurrence = symbolOccurrence, components.count > 0 {
19+
let parentRelation = currentSymbolOccurrence.relations.first { $0.roles.contains(.childOf) }
20+
guard let parentRelation else {
21+
break
22+
}
23+
if parentRelation.symbol.kind == .extension {
24+
symbolOccurrence = occurrences(relatedToUSR: parentRelation.symbol.usr, roles: .extendedBy).first
25+
} else {
26+
symbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: parentRelation.symbol.usr)
27+
}
28+
if let symbolOccurrence {
29+
components.insert(symbolOccurrence.symbol.name, at: 0)
30+
}
31+
}
32+
return DocCSymbolLink(string: module)?.appending(components: components)
33+
}
34+
35+
/// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given `DocCSymbolLink`.
36+
///
37+
/// If the `DocCSymbolLink` has an ambiguous definition, the most important role of this function is to deterministically return
38+
/// the same result every time.
39+
func primaryDefinitionOrDeclarationOccurrence(
40+
ofDocCSymbolLink symbolLink: DocCSymbolLink
41+
) -> SymbolOccurrence? {
42+
var components = symbolLink.components
43+
guard components.count > 0 else {
44+
return nil
45+
}
46+
// Do a lookup to find the top level symbol
47+
let topLevelSymbolName = components.removeLast().name
48+
var topLevelSymbolOccurrences = [SymbolOccurrence]()
49+
forEachCanonicalSymbolOccurrence(byName: topLevelSymbolName) { symbolOccurrence in
50+
guard symbolOccurrence.location.moduleName == symbolLink.moduleName else {
51+
return true
52+
}
53+
topLevelSymbolOccurrences.append(symbolOccurrence)
54+
return true
55+
}
56+
guard let topLevelSymbolOccurrence = topLevelSymbolOccurrences.first else {
57+
return nil
58+
}
59+
// Find any child symbols
60+
var symbolOccurrence: SymbolOccurrence? = topLevelSymbolOccurrence
61+
while let currentSymbolOccurrence = symbolOccurrence, components.count > 0 {
62+
let nextComponent = components.removeLast()
63+
let parentRelation = currentSymbolOccurrence.relations.first {
64+
$0.roles.contains(.childOf) && $0.symbol.name == nextComponent.name
65+
}
66+
guard let parentRelation else {
67+
break
68+
}
69+
if parentRelation.symbol.kind == .extension {
70+
symbolOccurrence = occurrences(relatedToUSR: parentRelation.symbol.usr, roles: .extendedBy).first
71+
} else {
72+
symbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: parentRelation.symbol.usr)
73+
}
74+
}
75+
guard symbolOccurrence != nil else {
76+
return nil
77+
}
78+
return topLevelSymbolOccurrence
79+
}
80+
}
81+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if canImport(SwiftDocC)
14+
import Foundation
15+
import IndexStoreDB
16+
import LanguageServerProtocol
17+
import SemanticIndex
18+
@preconcurrency import SwiftDocC
19+
import SwiftExtensions
20+
21+
struct DocCBuildInformation {
22+
let catalogURL: URL?
23+
let moduleName: String?
24+
let catalogIndex: DocCCatalogIndex?
25+
26+
init(catalogURL: URL? = nil, moduleName: String? = nil, catalogIndex: DocCCatalogIndex? = nil) {
27+
self.catalogURL = catalogURL
28+
self.moduleName = moduleName
29+
self.catalogIndex = catalogIndex
30+
}
31+
}
32+
33+
extension Workspace {
34+
private var documentationManager: DocumentationManager {
35+
get throws {
36+
guard let sourceKitLSPServer else {
37+
throw ResponseError.unknown("Connection to the editor closed")
38+
}
39+
return sourceKitLSPServer.documentationManager
40+
}
41+
}
42+
43+
func doccBuildInformation(for document: DocumentURI) async -> DocCBuildInformation {
44+
let target = await buildSystemManager.canonicalTarget(for: document)
45+
guard let target else {
46+
return DocCBuildInformation()
47+
}
48+
let sourceFiles = (try? await buildSystemManager.sourceFiles(in: [target]).flatMap(\.sources)) ?? []
49+
var moduleName: String? = nil
50+
let catalogURL: URL? = sourceFiles.compactMap(\.uri.fileURL?.doccCatalogURL).first
51+
for sourceFile in sourceFiles {
52+
let language = await buildSystemManager.defaultLanguage(for: sourceFile.uri, in: target)
53+
guard language == .swift else {
54+
continue
55+
}
56+
moduleName = await buildSystemManager.moduleName(for: sourceFile.uri, in: target)
57+
if moduleName != nil {
58+
break
59+
}
60+
}
61+
var catalogIndex: DocCCatalogIndex? = nil
62+
if let catalogURL {
63+
catalogIndex = try? await documentationManager.catalogIndex(for: catalogURL, moduleName: moduleName)
64+
}
65+
return DocCBuildInformation(catalogURL: catalogURL, moduleName: moduleName, catalogIndex: catalogIndex)
66+
}
67+
}
68+
69+
extension URL {
70+
var doccCatalogURL: URL? {
71+
var pathComponents = self.pathComponents
72+
var result = self
73+
while let lastPathComponent = pathComponents.last {
74+
if lastPathComponent.hasSuffix(".docc") {
75+
return result
76+
}
77+
pathComponents.removeLast()
78+
result.deleteLastPathComponent()
79+
}
80+
return nil
81+
}
82+
}
83+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if canImport(SwiftDocC)
14+
import Foundation
15+
@preconcurrency import SwiftDocC
16+
import SwiftExtensions
17+
18+
final actor DocCCatalogIndexManager {
19+
private let server: DocCServer
20+
private var catalogToIndexMap = [URL: Result<DocCCatalogIndex, DocCIndexError>]()
21+
22+
init(server: DocCServer) {
23+
self.server = server
24+
}
25+
26+
func invalidate(catalogURLs: some Collection<URL>) {
27+
guard catalogURLs.count > 0 else {
28+
return
29+
}
30+
for catalogURL in catalogURLs {
31+
catalogToIndexMap.removeValue(forKey: catalogURL)
32+
}
33+
}
34+
35+
func index(for catalogURL: URL, moduleName: String?) async throws(DocCIndexError) -> DocCCatalogIndex {
36+
if let existingCatalog = catalogToIndexMap[catalogURL] {
37+
return try existingCatalog.get()
38+
}
39+
let catalogIndexResult: Result<DocCCatalogIndex, DocCIndexError>
40+
do {
41+
let convertResponse = try await server.convert(
42+
externalIDsToConvert: [],
43+
documentPathsToConvert: [],
44+
includeRenderReferenceStore: true,
45+
documentationBundleLocation: catalogURL,
46+
documentationBundleDisplayName: moduleName ?? "unknown",
47+
documentationBundleIdentifier: "unknown",
48+
symbolGraphs: [],
49+
emitSymbolSourceFileURIs: true,
50+
markupFiles: [],
51+
tutorialFiles: [],
52+
convertRequestIdentifier: UUID().uuidString
53+
)
54+
catalogIndexResult = Result { convertResponse }
55+
.flatMap { convertResponse in
56+
guard let renderReferenceStoreData = convertResponse.renderReferenceStore else {
57+
return .failure(.unexpectedlyNilRenderReferenceStore)
58+
}
59+
return .success(renderReferenceStoreData)
60+
}
61+
.flatMap { renderReferenceStoreData in
62+
Result { try JSONDecoder().decode(RenderReferenceStore.self, from: renderReferenceStoreData) }
63+
.flatMapError { .failure(.decodingFailure($0)) }
64+
}
65+
.map { DocCCatalogIndex(from: $0) }
66+
} catch {
67+
catalogIndexResult = .failure(.serverError(error))
68+
}
69+
catalogToIndexMap[catalogURL] = catalogIndexResult
70+
return try catalogIndexResult.get()
71+
}
72+
}
73+
74+
/// Represents a potential error that the ``DocCCatalogIndexManager`` could encounter while indexing
75+
enum DocCIndexError: LocalizedError {
76+
case decodingFailure(Error)
77+
case serverError(DocCServerError)
78+
case unexpectedlyNilRenderReferenceStore
79+
80+
var errorDescription: String? {
81+
switch self {
82+
case .decodingFailure(let decodingError):
83+
return "Failed to decode a received message: \(decodingError.localizedDescription)"
84+
case .serverError(let serverError):
85+
return "DocC server failed to convert the catalog: \(serverError.localizedDescription)"
86+
case .unexpectedlyNilRenderReferenceStore:
87+
return "Did not receive a RenderReferenceStore from the DocC server"
88+
}
89+
}
90+
}
91+
92+
struct DocCCatalogIndex: Sendable {
93+
private let assetReferenceToDataAsset: [String: DataAsset]
94+
private let fuzzyAssetReferenceToDataAsset: [String: DataAsset]
95+
private let documentationExtensionToSourceURL: [DocCSymbolLink: URL]
96+
let articlePathToSourceURLAndReference: [String: (URL, TopicRenderReference)]
97+
let tutorialPathToSourceURLAndReference: [String: (URL, TopicRenderReference)]
98+
let tutorialOverviewPathToSourceURLAndReference: [String: (URL, TopicRenderReference)]
99+
100+
func asset(for assetReference: AssetReference) -> DataAsset? {
101+
assetReferenceToDataAsset[assetReference.assetName] ?? fuzzyAssetReferenceToDataAsset[assetReference.assetName]
102+
}
103+
104+
func documentationExtension(for symbolLink: DocCSymbolLink) -> URL? {
105+
documentationExtensionToSourceURL[symbolLink]
106+
}
107+
108+
init(from renderReferenceStore: RenderReferenceStore) {
109+
// Assets
110+
var assetReferenceToDataAsset = [String: DataAsset]()
111+
var fuzzyAssetReferenceToDataAsset = [String: DataAsset]()
112+
for (reference, asset) in renderReferenceStore.assets {
113+
var asset = asset
114+
asset.variants = asset.variants.compactMapValues { $0.withScheme("doc-asset") }
115+
assetReferenceToDataAsset[reference.assetName] = asset
116+
if let indexOfExtensionDelimiter = reference.assetName.lastIndex(of: ".") {
117+
let assetNameWithoutExtension = reference.assetName.prefix(upTo: indexOfExtensionDelimiter)
118+
fuzzyAssetReferenceToDataAsset[String(assetNameWithoutExtension)] = asset
119+
}
120+
}
121+
self.assetReferenceToDataAsset = assetReferenceToDataAsset
122+
self.fuzzyAssetReferenceToDataAsset = fuzzyAssetReferenceToDataAsset
123+
// Markdown and Tutorial content
124+
var documentationExtensionToSourceURL = [DocCSymbolLink: URL]()
125+
var articlePathToSourceURLAndReference = [String: (URL, TopicRenderReference)]()
126+
var tutorialPathToSourceURLAndReference = [String: (URL, TopicRenderReference)]()
127+
var tutorialOverviewPathToSourceURLAndReference = [String: (URL, TopicRenderReference)]()
128+
for (renderReferenceKey, topicContentValue) in renderReferenceStore.topics {
129+
guard let topicRenderReference = topicContentValue.renderReference as? TopicRenderReference,
130+
let topicContentSource = topicContentValue.source
131+
else {
132+
continue
133+
}
134+
135+
if topicContentValue.isDocumentationExtensionContent {
136+
guard
137+
let absoluteSymbolLink = AbsoluteSymbolLink(string: topicContentValue.renderReference.identifier.identifier)
138+
else {
139+
continue
140+
}
141+
let doccSymbolLink = DocCSymbolLink(absoluteSymbolLink: absoluteSymbolLink)
142+
documentationExtensionToSourceURL[doccSymbolLink] = topicContentValue.source
143+
} else if topicRenderReference.kind == .article {
144+
articlePathToSourceURLAndReference[renderReferenceKey.url.lastPathComponent] = (
145+
topicContentSource, topicRenderReference
146+
)
147+
} else if topicRenderReference.kind == .tutorial {
148+
tutorialPathToSourceURLAndReference[renderReferenceKey.url.lastPathComponent] = (
149+
topicContentSource, topicRenderReference
150+
)
151+
} else if topicRenderReference.kind == .overview {
152+
tutorialOverviewPathToSourceURLAndReference[renderReferenceKey.url.lastPathComponent] = (
153+
topicContentSource, topicRenderReference
154+
)
155+
}
156+
}
157+
self.documentationExtensionToSourceURL = documentationExtensionToSourceURL
158+
self.articlePathToSourceURLAndReference = articlePathToSourceURLAndReference
159+
self.tutorialPathToSourceURLAndReference = tutorialPathToSourceURLAndReference
160+
self.tutorialOverviewPathToSourceURLAndReference = tutorialOverviewPathToSourceURLAndReference
161+
}
162+
}
163+
164+
fileprivate extension URL {
165+
func withScheme(_ scheme: String) -> URL {
166+
var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
167+
components?.scheme = scheme
168+
return components?.url ?? self
169+
}
170+
}
171+
#endif

0 commit comments

Comments
 (0)