Skip to content

Commit 3ce2bc9

Browse files
authored
SE-0356: Implement snippet slicing (#338)
Add an optional `slice` argument to the `@Snippet` directive, which will only render a code listing comprised of the line range of the slice. Remove snippet groups, as they are no longer used in the current design. Replace logic that filters snippets and fake modules that hold snippets to a more generic "is virtual" logic. A virtual symbol, topic, or documentation node should not have its own page of documentation. Testing: Moved snippet tests to their own test bundle as it was causing a lot of thrash with navigator tests. rdar://95220570
1 parent aeb7408 commit 3ce2bc9

File tree

23 files changed

+280
-160
lines changed

23 files changed

+280
-160
lines changed

Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/SwiftDocC/Converter/DocumentationContextConverter.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,8 @@ public class DocumentationContextConverter {
7373
/// - source: The source file for the documentation node.
7474
/// - Returns: The render node representation of the documentation node.
7575
public func renderNode(for node: DocumentationNode, at source: URL?) throws -> RenderNode? {
76-
switch node.kind {
77-
case .module:
78-
guard !context.onlyHasSnippetRelatedChildren(for: node.reference) else {
79-
return nil
80-
}
81-
case .snippet, .snippetGroup:
76+
guard !node.isVirtual else {
8277
return nil
83-
default: break
8478
}
8579

8680
var translator = RenderNodeTranslator(

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,18 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
143143
///
144144
/// This property is initialized during the registration of a documentation bundle.
145145
public private(set) var rootModules: [ResolvedTopicReference]!
146-
146+
147147
/// The topic reference of the root module, if it's the only registered module.
148148
var soleRootModuleReference: ResolvedTopicReference? {
149-
rootModules.count == 1 ? rootModules.first : nil
149+
guard rootModules.count > 1 else {
150+
return rootModules.first
151+
}
152+
// There are multiple "root modules" but some may be "virtual".
153+
// Removing those may leave only one root module left.
154+
let nonVirtualModules = rootModules.filter {
155+
topicGraph.nodes[$0]?.isVirtual ?? false
156+
}
157+
return nonVirtualModules.count == 1 ? nonVirtualModules.first : nil
150158
}
151159

152160
/// Map of document URLs to topic references.
@@ -993,7 +1001,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
9931001
} else {
9941002
source = .external
9951003
}
996-
let graphNode = TopicGraph.Node(reference: reference, kind: documentation.kind, source: source, title: symbol.defaultSymbol!.names.title)
1004+
let graphNode = TopicGraph.Node(reference: reference, kind: documentation.kind, source: source, title: symbol.defaultSymbol!.names.title, isVirtual: module.isVirtual)
9971005

9981006
return ((reference, symbol.uniqueIdentifier, graphNode, documentation), [])
9991007
}
@@ -1185,7 +1193,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
11851193
}
11861194
}
11871195
}
1188-
1196+
11891197
if let rootURL = symbolGraphLoader.mainModuleURL(forModule: moduleName), let rootModule = unifiedSymbolGraph.moduleData[rootURL] {
11901198
addPreparedSymbolToContext(
11911199
preparedSymbolData(.init(fromSingleSymbol: moduleSymbol, module: rootModule, isMainGraph: true), reference: moduleReference, module: rootModule, moduleReference: moduleReference, bundle: bundle, fileURL: fileURL)
@@ -1937,15 +1945,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
19371945
topicGraphGlobalAnalysis()
19381946

19391947
preResolveModuleNames()
1940-
1941-
// Symbol Graph modules whose children are all snippets should not
1942-
// be presented as a top-level preview page.
1943-
// First, a package might have a similar but different name to its
1944-
// primary library. Second, snippets are not automatically curated as
1945-
// other symbols might be.
1946-
rootModules.removeAll {
1947-
onlyHasSnippetRelatedChildren(for: $0)
1948-
}
19491948
}
19501949

19511950
/// Given a list of topics that have been automatically curated, checks if a topic has been additionally manually curated
@@ -2330,17 +2329,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
23302329

23312330
topicGraph.traverseBreadthFirst(from: node, observe)
23322331
}
2333-
2334-
/// Returns whether a documentation node only has snippet or snippet group children.
2335-
func onlyHasSnippetRelatedChildren(for reference: ResolvedTopicReference) -> Bool {
2336-
let children = children(of: reference)
2337-
guard !children.isEmpty else {
2338-
return false
2339-
}
2340-
return children
2341-
.compactMap { $0.kind }
2342-
.allSatisfy({ $0 == .snippet || $0 == .snippetGroup })
2343-
}
23442332

23452333
/**
23462334
Attempt to resolve a ``TopicReference``.
@@ -2447,9 +2435,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
24472435
/// The references of all the pages in the topic graph.
24482436
public var knownPages: [ResolvedTopicReference] {
24492437
return topicGraph.nodes.values
2450-
.filter { $0.kind.isPage &&
2451-
!externallyResolvedSymbols.contains($0.reference) &&
2452-
!onlyHasSnippetRelatedChildren(for: $0.reference) }
2438+
.filter { !$0.isVirtual && $0.kind.isPage &&
2439+
!externallyResolvedSymbols.contains($0.reference) }
24532440
.map { $0.reference }
24542441
}
24552442

Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ extension AutomaticCuration {
196196
case .`property`: return "Instance Properties"
197197
case .`protocol`: return "Protocols"
198198
case .snippet: return "Snippets"
199-
case .snippetGroup: return "Snippet Groups"
200199
case .`struct`: return "Structures"
201200
case .`subscript`: return "Subscripts"
202201
case .`typeMethod`: return "Type Methods"

Sources/SwiftDocC/Infrastructure/Topic Graph/TopicGraph.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,16 @@ struct TopicGraph {
8484
/// If true, the hierarchy path is resolvable.
8585
let isResolvable: Bool
8686

87-
init(reference: ResolvedTopicReference, kind: DocumentationNode.Kind, source: ContentLocation, title: String, isResolvable: Bool = true) {
87+
/// If true, the topic should not be rendered and exists solely to mark relationships.
88+
let isVirtual: Bool
89+
90+
init(reference: ResolvedTopicReference, kind: DocumentationNode.Kind, source: ContentLocation, title: String, isResolvable: Bool = true, isVirtual: Bool = false) {
8891
self.reference = reference
8992
self.kind = kind
9093
self.source = source
9194
self.title = title
9295
self.isResolvable = isResolvable
96+
self.isVirtual = isVirtual
9397
}
9498

9599
func withReference(_ reference: ResolvedTopicReference) -> Node {

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public struct DocumentationNode {
5858

5959
/// The unified symbol data that backs this node, if it's backed by a symbol; otherwise `nil`.
6060
public var unifiedSymbol: UnifiedSymbolGraph.Symbol?
61+
62+
/// If true, the node was created implicitly and should not generally be rendered as a page of documentation.
63+
public var isVirtual: Bool
6164

6265
/// A discrete unit of documentation
6366
struct DocumentationChunk {
@@ -122,7 +125,7 @@ public struct DocumentationNode {
122125
/// - markup: The markup that makes up the content for the node.
123126
/// - semantic: The parsed documentation structure that's described by the documentation content.
124127
/// - platformNames: The names of the platforms for which the node is available.
125-
public init(reference: ResolvedTopicReference, kind: Kind, sourceLanguage: SourceLanguage, availableSourceLanguages: Set<SourceLanguage>? = nil, name: Name, markup: Markup, semantic: Semantic?, platformNames: Set<String>? = nil) {
128+
public init(reference: ResolvedTopicReference, kind: Kind, sourceLanguage: SourceLanguage, availableSourceLanguages: Set<SourceLanguage>? = nil, name: Name, markup: Markup, semantic: Semantic?, platformNames: Set<String>? = nil, isVirtual: Bool = false) {
126129
self.reference = reference
127130
self.kind = kind
128131
self.sourceLanguage = sourceLanguage
@@ -133,6 +136,7 @@ public struct DocumentationNode {
133136
self.symbol = nil
134137
self.platformNames = platformNames
135138
self.docChunks = [DocumentationChunk(source: .sourceCode(location: nil), markup: markup)]
139+
self.isVirtual = isVirtual
136140
updateAnchorSections()
137141
}
138142

@@ -171,6 +175,7 @@ public struct DocumentationNode {
171175
self.name = .symbol(declaration: .init([.plain(defaultSymbol.names.title)]))
172176
self.symbol = defaultSymbol
173177
self.unifiedSymbol = unifiedSymbol
178+
self.isVirtual = moduleData.isVirtual
174179

175180
self.markup = Document()
176181
self.docChunks = []
@@ -455,7 +460,6 @@ public struct DocumentationNode {
455460
case .`property`: return .instanceProperty
456461
case .`protocol`: return .protocol
457462
case .snippet: return .snippet
458-
case .snippetGroup: return .snippetGroup
459463
case .`struct`: return .structure
460464
case .`subscript`: return .instanceSubscript
461465
case .`typeMethod`: return .typeMethod
@@ -564,6 +568,8 @@ public struct DocumentationNode {
564568
redirectsVariants: .init(swiftVariant: article?.redirects)
565569
)
566570

571+
self.isVirtual = symbol.isVirtual
572+
567573
updateAnchorSections()
568574
}
569575

@@ -596,6 +602,7 @@ public struct DocumentationNode {
596602
self.availableSourceLanguages = [reference.sourceLanguage]
597603
self.docChunks = [DocumentationChunk(source: .documentationExtension, markup: articleMarkup)]
598604
self.markup = articleMarkup
605+
self.isVirtual = false
599606

600607
updateAnchorSections()
601608
}

Sources/SwiftDocC/Model/Kind.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ extension DocumentationNode {
2525
.volume,
2626
.chapter,
2727
.onPageLandmark,
28-
.snippet,
29-
.snippetGroup:
28+
.snippet:
3029
return false
3130
default:
3231
return true
@@ -156,9 +155,6 @@ extension DocumentationNode.Kind {
156155
public static let object = DocumentationNode.Kind(name: "Object", id: "org.swift.docc.kind.dictionary", isSymbol: true)
157156
/// A snippet.
158157
public static let snippet = DocumentationNode.Kind(name: "Snippet", id: "org.swift.docc.kind.snippet", isSymbol: true)
159-
/// A group of snippets.
160-
public static let snippetGroup = DocumentationNode.Kind(name: "Snippet Group", id: "org.swift.docc.kind.snippetGroup", isSymbol: true)
161-
162158

163159
/// The list of all known kinds of documentation nodes.
164160
/// - Note: The `unknown` value is not included.

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -210,36 +210,27 @@ struct RenderContentCompiler: MarkupVisitor {
210210
mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> [RenderContent] {
211211
switch blockDirective.name {
212212
case Snippet.directiveName:
213-
guard let snippetURL = blockDirective.arguments()[Snippet.Semantics.Path.argumentName],
213+
let arguments = blockDirective.arguments()
214+
guard let snippetURL = arguments[Snippet.Semantics.Path.argumentName],
214215
let snippetReference = resolveSymbolReference(destination: snippetURL.value),
215216
let snippetEntity = try? context.entity(with: snippetReference),
216217
let snippetSymbol = snippetEntity.symbol,
217218
let snippetMixin = snippetSymbol.mixins[SymbolGraph.Symbol.Snippet.mixinKey] as? SymbolGraph.Symbol.Snippet else {
218219
return []
219220
}
220-
221-
let docCommentContent = snippetEntity.markup.children.flatMap { self.visit($0) }
222-
223-
let codeContent = snippetMixin.chunks.flatMap { chunk -> [RenderContent] in
224-
guard !chunk.code.isEmpty else {
225-
return []
226-
}
227-
228-
var elements = [RenderContent]()
229-
230-
if let chunkName = chunk.name {
231-
elements.append(RenderBlockContent.paragraph(inlineContent: [
232-
RenderInlineContent.strong(inlineContent: [
233-
RenderInlineContent.text(chunkName)
234-
])
235-
]))
236-
}
237-
238-
elements.append(RenderBlockContent.codeListing(syntax: chunk.language, code: [chunk.code], metadata: nil))
239-
return elements
221+
222+
if let requestedSlice = arguments[Snippet.Semantics.Slice.argumentName]?.value,
223+
let requestedLineRange = snippetMixin.slices[requestedSlice] {
224+
// Render only the slice.
225+
let lineRange = requestedLineRange.lowerBound..<min(requestedLineRange.upperBound, snippetMixin.lines.count)
226+
let lines = snippetMixin.lines[lineRange]
227+
return [RenderBlockContent.codeListing(syntax: snippetMixin.language, code: Array(lines), metadata: nil)]
228+
} else {
229+
// Render the whole snippet with its explanation content.
230+
let docCommentContent = snippetEntity.markup.children.flatMap { self.visit($0) }
231+
let code = RenderBlockContent.codeListing(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil)
232+
return docCommentContent + [code]
240233
}
241-
242-
return docCommentContent + codeContent
243234
default:
244235
return []
245236
}

0 commit comments

Comments
 (0)