Skip to content

Commit 3da95ef

Browse files
use LinkCompletionTools to parse SwiftDocC symbol links
1 parent 1e4afaf commit 3da95ef

9 files changed

+299
-230
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ var targets: [Target] = [
227227
"SwiftExtensions",
228228
.product(name: "IndexStoreDB", package: "indexstore-db"),
229229
.product(name: "SwiftDocC", package: "swift-docc"),
230+
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
230231
],
231232
exclude: ["CMakeLists.txt"],
232233
swiftSettings: globalSwiftSettings

Sources/DocCDocumentation/DocCCatalogIndexManager.swift

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
package import Foundation
14-
@preconcurrency import SwiftDocC
14+
@_spi(LinkCompletion) @preconcurrency import SwiftDocC
1515

1616
final actor DocCCatalogIndexManager {
1717
private let server: DocCServer
@@ -74,63 +74,74 @@ package enum DocCIndexError: LocalizedError {
7474
}
7575

7676
package struct DocCCatalogIndex: Sendable {
77-
private let assetReferenceToDataAsset: [String: DataAsset]
78-
private let documentationExtensionToSourceURL: [DocCSymbolLink: URL]
79-
let articlePathToSourceURLAndReference: [String: (URL, TopicRenderReference)]
80-
let tutorialPathToSourceURLAndReference: [String: (URL, TopicRenderReference)]
81-
let tutorialOverviewPathToSourceURLAndReference: [String: (URL, TopicRenderReference)]
82-
83-
func asset(for assetReference: AssetReference) -> DataAsset? {
84-
assetReferenceToDataAsset[assetReference.assetName]
85-
}
77+
/// A map from an asset name to its DataAsset contents.
78+
let assets: [String: DataAsset]
79+
80+
/// An array of DocCSymbolLink and their associated document URLs.
81+
let documentationExtensions: [(link: DocCSymbolLink, documentURL: URL?)]
82+
83+
/// A map from article name to its TopicRenderReference.
84+
let articles: [String: TopicRenderReference]
85+
86+
/// A map from tutorial name to its TopicRenderReference.
87+
let tutorials: [String: TopicRenderReference]
8688

87-
package func documentationExtension(for symbolLink: DocCSymbolLink) -> URL? {
88-
return documentationExtensionToSourceURL[symbolLink]
89+
// A map from tutorial overview name to its TopicRenderReference.
90+
let tutorialOverviews: [String: TopicRenderReference]
91+
92+
/// Retrieves the documentation extension URL for the given symbol if one exists.
93+
///
94+
/// - Parameter symbolInformation: The `DocCSymbolInformation` representing the symbol to search for.
95+
package func documentationExtension(for symbolInformation: DocCSymbolInformation) -> URL? {
96+
documentationExtensions.filter { symbolInformation.matches($0.link) }.first?.documentURL
8997
}
9098

9199
init(from renderReferenceStore: RenderReferenceStore) {
92100
// Assets
93-
var assetReferenceToDataAsset: [String: DataAsset] = [:]
101+
var assets: [String: DataAsset] = [:]
94102
for (reference, asset) in renderReferenceStore.assets {
95103
var asset = asset
96104
asset.variants = asset.variants.compactMapValues { $0.withScheme("doc-asset") }
97-
assetReferenceToDataAsset[reference.assetName] = asset
105+
assets[reference.assetName] = asset
98106
}
99-
self.assetReferenceToDataAsset = assetReferenceToDataAsset
107+
self.assets = assets
100108
// Markdown and Tutorial content
101-
var documentationExtensionToSourceURL: [DocCSymbolLink: URL] = [:]
102-
var articlePathToSourceURLAndReference = [String: (URL, TopicRenderReference)]()
103-
var tutorialPathToSourceURLAndReference = [String: (URL, TopicRenderReference)]()
104-
var tutorialOverviewPathToSourceURLAndReference = [String: (URL, TopicRenderReference)]()
109+
var documentationExtensionToSourceURL: [(link: DocCSymbolLink, documentURL: URL?)] = []
110+
var articles: [String: TopicRenderReference] = [:]
111+
var tutorials: [String: TopicRenderReference] = [:]
112+
var tutorialOverviews: [String: TopicRenderReference] = [:]
105113
for (renderReferenceKey, topicContentValue) in renderReferenceStore.topics {
106-
guard let topicRenderReference = topicContentValue.renderReference as? TopicRenderReference,
107-
let topicContentSource = topicContentValue.source
108-
else {
114+
guard let topicRenderReference = topicContentValue.renderReference as? TopicRenderReference else {
109115
continue
110116
}
117+
// Article and Tutorial URLs in SwiftDocC are always of the form `doc://<BundleID>/<Type>/<ModuleName>/<Filename>`.
118+
// Therefore, we only really need to store the filename in these cases which will always be the last path component.
111119
let lastPathComponent = renderReferenceKey.url.lastPathComponent
112120

113121
switch topicRenderReference.kind {
114122
case .article:
115-
articlePathToSourceURLAndReference[lastPathComponent] = (topicContentSource, topicRenderReference)
123+
articles[lastPathComponent] = topicRenderReference
116124
case .tutorial:
117-
tutorialPathToSourceURLAndReference[lastPathComponent] = (topicContentSource, topicRenderReference)
125+
tutorials[lastPathComponent] = topicRenderReference
118126
case .overview:
119-
tutorialOverviewPathToSourceURLAndReference[lastPathComponent] = (topicContentSource, topicRenderReference)
127+
tutorialOverviews[lastPathComponent] = topicRenderReference
120128
default:
121-
guard topicContentValue.isDocumentationExtensionContent,
122-
let absoluteSymbolLink = AbsoluteSymbolLink(string: topicContentValue.renderReference.identifier.identifier)
123-
else {
129+
guard topicContentValue.isDocumentationExtensionContent else {
130+
continue
131+
}
132+
// Documentation extensions are always of the form `doc://<BundleID>/documentation/<SymbolPath>`.
133+
// We want to parse the `SymbolPath` in this case and store it in the index for lookups later.
134+
let linkString = renderReferenceKey.url.pathComponents[2...].joined(separator: "/")
135+
guard let doccSymbolLink = DocCSymbolLink(linkString: linkString) else {
124136
continue
125137
}
126-
let doccSymbolLink = DocCSymbolLink(absoluteSymbolLink: absoluteSymbolLink)
127-
documentationExtensionToSourceURL[doccSymbolLink] = topicContentValue.source
138+
documentationExtensionToSourceURL.append((link: doccSymbolLink, documentURL: topicContentValue.source))
128139
}
129140
}
130-
self.documentationExtensionToSourceURL = documentationExtensionToSourceURL
131-
self.articlePathToSourceURLAndReference = articlePathToSourceURLAndReference
132-
self.tutorialPathToSourceURLAndReference = tutorialPathToSourceURLAndReference
133-
self.tutorialOverviewPathToSourceURLAndReference = tutorialOverviewPathToSourceURLAndReference
141+
self.documentationExtensions = documentationExtensionToSourceURL
142+
self.articles = articles
143+
self.tutorials = tutorials
144+
self.tutorialOverviews = tutorialOverviews
134145
}
135146
}
136147

Sources/DocCDocumentation/DocCDocumentationManager.swift

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
import BuildServerProtocol
1414
package import BuildSystemIntegration
1515
package import Foundation
16-
package import IndexStoreDB
1716
package import LanguageServerProtocol
1817
import SKLogging
19-
package import SemanticIndex
2018
import SwiftDocC
2119

2220
package struct DocCDocumentationManager: Sendable {
@@ -53,90 +51,6 @@ package struct DocCDocumentationManager: Sendable {
5351
try await catalogIndexManager.index(for: catalogURL)
5452
}
5553

56-
package func symbolLink(string: String) -> DocCSymbolLink? {
57-
DocCSymbolLink(string: string)
58-
}
59-
60-
private func parentSymbol(of symbol: SymbolOccurrence, in index: CheckedIndex) -> SymbolOccurrence? {
61-
let allParentRelations = symbol.relations
62-
.filter { $0.roles.contains(.childOf) }
63-
.sorted()
64-
if allParentRelations.count > 1 {
65-
logger.debug("Symbol \(symbol.symbol.usr) has multiple parent symbols")
66-
}
67-
guard let parentRelation = allParentRelations.first else {
68-
return nil
69-
}
70-
if parentRelation.symbol.kind == .extension {
71-
let allSymbolOccurrences = index.occurrences(relatedToUSR: parentRelation.symbol.usr, roles: .extendedBy)
72-
.sorted()
73-
if allSymbolOccurrences.count > 1 {
74-
logger.debug("Extension \(parentRelation.symbol.usr) extends multiple symbols")
75-
}
76-
return allSymbolOccurrences.first
77-
}
78-
return index.primaryDefinitionOrDeclarationOccurrence(ofUSR: parentRelation.symbol.usr)
79-
}
80-
81-
package func symbolLink(forUSR usr: String, in index: CheckedIndex) -> DocCSymbolLink? {
82-
guard let topLevelSymbolOccurrence = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
83-
return nil
84-
}
85-
let module = topLevelSymbolOccurrence.location.moduleName
86-
var components = [topLevelSymbolOccurrence.symbol.name]
87-
// Find any parent symbols
88-
var symbolOccurrence: SymbolOccurrence = topLevelSymbolOccurrence
89-
while let parentSymbolOccurrence = parentSymbol(of: symbolOccurrence, in: index) {
90-
components.insert(parentSymbolOccurrence.symbol.name, at: 0)
91-
symbolOccurrence = parentSymbolOccurrence
92-
}
93-
return DocCSymbolLink(string: module)?.appending(components: components)
94-
}
95-
96-
/// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given `DocCSymbolLink`.
97-
///
98-
/// If the `DocCSymbolLink` has an ambiguous definition, the most important role of this function is to deterministically return
99-
/// the same result every time.
100-
package func primaryDefinitionOrDeclarationOccurrence(
101-
ofDocCSymbolLink symbolLink: DocCSymbolLink,
102-
in index: CheckedIndex
103-
) -> SymbolOccurrence? {
104-
var components = symbolLink.components
105-
guard components.count > 0 else {
106-
return nil
107-
}
108-
// Do a lookup to find the top level symbol
109-
let topLevelSymbolName = components.removeLast().name
110-
var topLevelSymbolOccurrences: [SymbolOccurrence] = []
111-
index.forEachCanonicalSymbolOccurrence(byName: topLevelSymbolName) { symbolOccurrence in
112-
guard symbolOccurrence.location.moduleName == symbolLink.moduleName else {
113-
return true // continue
114-
}
115-
topLevelSymbolOccurrences.append(symbolOccurrence)
116-
return true // continue
117-
}
118-
// Search each potential symbol's parents to find an exact match
119-
let symbolOccurences = topLevelSymbolOccurrences.filter { topLevelSymbolOccurrence in
120-
var components = components
121-
var symbolOccurrence = topLevelSymbolOccurrence
122-
while let parentSymbolOccurrence = parentSymbol(of: symbolOccurrence, in: index), !components.isEmpty {
123-
let nextComponent = components.removeLast()
124-
guard parentSymbolOccurrence.symbol.name == nextComponent.name else {
125-
return false
126-
}
127-
symbolOccurrence = parentSymbolOccurrence
128-
}
129-
guard components.isEmpty else {
130-
return false
131-
}
132-
return true
133-
}.sorted()
134-
if symbolOccurences.count > 1 {
135-
logger.debug("Multiple symbols found for DocC symbol link '\(symbolLink.absoluteString)'")
136-
}
137-
return symbolOccurences.first
138-
}
139-
14054
/// Generates the SwiftDocC RenderNode for a given symbol, tutorial, or markdown file.
14155
///
14256
/// - Parameters:

Sources/DocCDocumentation/DocCReferenceResolutionService.swift

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Foundation
1414
import IndexStoreDB
1515
import LanguageServerProtocol
1616
import SemanticIndex
17-
@preconcurrency import SwiftDocC
17+
@_spi(Linkcompletion) @preconcurrency import SwiftDocC
1818
import SwiftExtensions
1919

2020
final class DocCReferenceResolutionService: DocumentationService, Sendable {
@@ -85,7 +85,7 @@ final class DocCReferenceResolutionService: DocumentationService, Sendable {
8585
guard let catalog = context.catalogIndex else {
8686
throw .indexNotAvailable
8787
}
88-
guard let dataAsset = catalog.asset(for: assetReference) else {
88+
guard let dataAsset = catalog.assets[assetReference.assetName] else {
8989
throw .assetNotFound
9090
}
9191
return .asset(dataAsset)
@@ -95,9 +95,9 @@ final class DocCReferenceResolutionService: DocumentationService, Sendable {
9595
let resolvedReference: TopicRenderReference? =
9696
switch relevantPathComponents.first {
9797
case NodeURLGenerator.Path.documentationFolderName:
98-
context.catalogIndex?.articlePathToSourceURLAndReference[topicURL.lastPathComponent]?.1
98+
context.catalogIndex?.articles[topicURL.lastPathComponent]
9999
case NodeURLGenerator.Path.tutorialsFolderName:
100-
context.catalogIndex?.tutorialPathToSourceURLAndReference[topicURL.lastPathComponent]?.1
100+
context.catalogIndex?.tutorials[topicURL.lastPathComponent]
101101
default:
102102
nil
103103
}
@@ -106,15 +106,15 @@ final class DocCReferenceResolutionService: DocumentationService, Sendable {
106106
}
107107
// Otherwise this must be a link to a symbol
108108
let urlString = topicURL.absoluteString
109-
guard let absoluteSymbolLink = AbsoluteSymbolLink(string: urlString) else {
109+
guard let doccSymbolLink = DocCSymbolLink(linkString: urlString) else {
110110
throw .invalidURLInRequest
111111
}
112112
// Don't bother checking to see if the symbol actually exists in the index. This can be time consuming and
113113
// it would be better to report errors/warnings for unresolved symbols directly within the document, anyway.
114114
return .resolvedInformation(
115115
OutOfProcessReferenceResolver.ResolvedInformation(
116116
symbolURL: topicURL,
117-
symbolName: absoluteSymbolLink.symbolName
117+
symbolName: doccSymbolLink.symbolName
118118
)
119119
)
120120
}
@@ -159,18 +159,6 @@ struct DocCReferenceResolutionContext {
159159
let catalogIndex: DocCCatalogIndex?
160160
}
161161

162-
fileprivate extension AbsoluteSymbolLink {
163-
var symbolName: String {
164-
guard !representsModule else {
165-
return module
166-
}
167-
guard let lastComponent = basePathComponents.last else {
168-
return topLevelSymbol.name
169-
}
170-
return lastComponent.name
171-
}
172-
}
173-
174162
fileprivate extension OutOfProcessReferenceResolver.ResolvedInformation {
175163
init(symbolURL: URL, symbolName: String) {
176164
self = OutOfProcessReferenceResolver.ResolvedInformation(

0 commit comments

Comments
 (0)