Skip to content

Commit f019ab8

Browse files
Highlight declaration differences in overloaded symbol groups (#928)
rdar://116409531
1 parent d6e1217 commit f019ab8

File tree

6 files changed

+3297
-39
lines changed

6 files changed

+3297
-39
lines changed

Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift

Lines changed: 349 additions & 33 deletions
Large diffs are not rendered by default.

Sources/SwiftDocC/Model/Rendering/Symbol/DeclarationRenderSection+SymbolGraph.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,19 @@ extension DeclarationRenderSection.Token {
1616
/// - Parameters:
1717
/// - fragment: The symbol-graph declaration fragment to render.
1818
/// - identifier: An optional reference to a symbol.
19-
init(fragment: SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment, identifier: String?) {
19+
init(
20+
fragment: SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment,
21+
identifier: String?,
22+
highlight: Bool = false
23+
) {
2024
self.text = fragment.spelling
2125
self.kind = Kind(rawValue: fragment.kind.rawValue) ?? .text
2226
self.identifier = identifier
2327
self.preciseIdentifier = fragment.preciseIdentifier
28+
if highlight {
29+
self.highlight = .changed
30+
} else {
31+
self.highlight = nil
32+
}
2433
}
2534
}

Sources/SwiftDocC/Model/Rendering/Symbol/DeclarationsRenderSection.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public struct DeclarationRenderSection: Codable, Equatable {
7777
/// For example, `123` is represented as a single token of kind "number".
7878
public struct Token: Codable, Hashable, Equatable {
7979
/// The token text content.
80-
public let text: String
80+
public var text: String
8181
/// The token programming kind.
8282
public let kind: Kind
8383

@@ -114,24 +114,40 @@ public struct DeclarationRenderSection: Codable, Equatable {
114114

115115
/// If the token is a known symbol, its precise identifier as vended in the symbol graph.
116116
public let preciseIdentifier: String?
117-
117+
118+
/// The kind of highlight the token should be rendered with.
119+
public var highlight: Highlight?
120+
121+
/// The kinds of highlights that can be applied to a token.
122+
public enum Highlight: String, Codable, RawRepresentable {
123+
/// A highlight representing generalized change, not specifically added or removed.
124+
case changed
125+
}
126+
118127
/// Creates a new declaration token with optional identifier and precise identifier.
119128
/// - Parameters:
120129
/// - text: The text content of the token.
121130
/// - kind: The kind of the token.
122131
/// - identifier: If the token refers to a known symbol, its identifier.
123132
/// - preciseIdentifier: If the refers to a symbol, its precise identifier.
124-
public init(text: String, kind: Kind, identifier: String? = nil, preciseIdentifier: String? = nil) {
133+
public init(
134+
text: String,
135+
kind: Kind,
136+
identifier: String? = nil,
137+
preciseIdentifier: String? = nil,
138+
highlight: Highlight? = nil
139+
) {
125140
self.text = text
126141
self.kind = kind
127142
self.identifier = identifier
128143
self.preciseIdentifier = preciseIdentifier
144+
self.highlight = highlight
129145
}
130146

131147
// MARK: - Codable
132148

133149
private enum CodingKeys: CodingKey {
134-
case text, kind, identifier, preciseIdentifier, otherDeclarations
150+
case text, kind, identifier, preciseIdentifier, highlight, otherDeclarations
135151
}
136152

137153
public func encode(to encoder: Encoder) throws {
@@ -141,6 +157,7 @@ public struct DeclarationRenderSection: Codable, Equatable {
141157
try container.encode(kind, forKey: .kind)
142158
try container.encodeIfPresent(identifier, forKey: .identifier)
143159
try container.encodeIfPresent(preciseIdentifier, forKey: .preciseIdentifier)
160+
try container.encodeIfPresent(highlight, forKey: .highlight)
144161
}
145162

146163
public init(from decoder: Decoder) throws {
@@ -150,6 +167,7 @@ public struct DeclarationRenderSection: Codable, Equatable {
150167
kind = try container.decode(Kind.self, forKey: .kind)
151168
preciseIdentifier = try container.decodeIfPresent(String.self, forKey: .preciseIdentifier)
152169
identifier = try container.decodeIfPresent(String.self, forKey: .identifier)
170+
highlight = try container.decodeIfPresent(Highlight.self, forKey: .highlight)
153171

154172
if let reference = identifier {
155173
decoder.registerReferences([reference])

Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2616,6 +2616,12 @@
26162616
},
26172617
"preciseIdentifier": {
26182618
"type": "string"
2619+
},
2620+
"highlight": {
2621+
"type": "string",
2622+
"enum": [
2623+
"changed"
2624+
]
26192625
}
26202626
}
26212627
},

Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift

Lines changed: 228 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import Foundation
1212
import XCTest
1313
@testable import SwiftDocC
14+
import SwiftDocCTestUtilities
1415

1516
class DeclarationsRenderSectionTests: XCTestCase {
1617
func testDecodingTokens() throws {
@@ -76,7 +77,7 @@ class DeclarationsRenderSectionTests: XCTestCase {
7677
)
7778
}
7879
}
79-
80+
8081
func testDoNotEmitOtherDeclarationsIfEmpty() throws {
8182

8283
let encoder = RenderJSONEncoder.makeEncoder(prettyPrint: true)
@@ -151,4 +152,230 @@ class DeclarationsRenderSectionTests: XCTestCase {
151152
XCTAssertEqual(declarationsSection.declarations.count, 2)
152153
XCTAssert(declarationsSection.declarations.allSatisfy({ $0.platforms == [.iOS, .macOS] }))
153154
}
155+
156+
func testHighlightDiff() throws {
157+
enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled)
158+
159+
let symbolGraphFile = Bundle.module.url(
160+
forResource: "FancyOverloads",
161+
withExtension: "symbols.json",
162+
subdirectory: "Test Resources"
163+
)!
164+
165+
let tempURL = try createTempFolder(content: [
166+
Folder(name: "unit-test.docc", content: [
167+
InfoPlist(displayName: "FancyOverloads", identifier: "com.test.example"),
168+
CopyOfFile(original: symbolGraphFile),
169+
])
170+
])
171+
172+
let (_, bundle, context) = try loadBundle(from: tempURL)
173+
174+
// Make sure that type decorators like arrays, dictionaries, and optionals are correctly highlighted.
175+
do {
176+
// func overload1(param: Int) {} // <- overload group
177+
// func overload1(param: Int?) {}
178+
// func overload1(param: [Int]) {}
179+
// func overload1(param: [Int]?) {}
180+
// func overload1(param: Set<Int>) {}
181+
// func overload1(param: [Int: Int]) {}
182+
let reference = ResolvedTopicReference(
183+
bundleIdentifier: bundle.identifier,
184+
path: "/documentation/FancyOverloads/overload1(param:)-8nk5z",
185+
sourceLanguage: .swift
186+
)
187+
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
188+
var translator = RenderNodeTranslator(
189+
context: context,
190+
bundle: bundle,
191+
identifier: reference,
192+
source: nil
193+
)
194+
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
195+
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
196+
XCTAssertEqual(declarationsSection.declarations.count, 1)
197+
let declarations = try XCTUnwrap(declarationsSection.declarations.first)
198+
199+
XCTAssertEqual(
200+
declarationAndHighlights(for: declarations.tokens),
201+
[
202+
"func overload1(param: Int)",
203+
" ",
204+
]
205+
)
206+
207+
XCTAssertEqual(
208+
declarations.otherDeclarations?.declarations.flatMap({ declarationAndHighlights(for: $0.tokens) }),
209+
[
210+
"func overload1(param: Int?)",
211+
" ~ ",
212+
213+
"func overload1(param: Set<Int>)",
214+
" ~~~~ ~ ",
215+
216+
"func overload1(param: [Int : Int])",
217+
" ~ ~~~~~~ ",
218+
219+
"func overload1(param: [Int])",
220+
" ~ ~ ",
221+
222+
"func overload1(param: [Int]?)",
223+
" ~ ~~ ",
224+
]
225+
)
226+
}
227+
228+
// Verify the behavior of the highlighter in the face of tuples and closures, which can
229+
// confuse the differencing code with excess parentheses and commas.
230+
do {
231+
// func overload2(p1: Int, p2: Int) {}
232+
// func overload2(p1: (Int, Int), p2: Int) {}
233+
// func overload2(p1: Int, p2: (Int, Int)) {}
234+
// func overload2(p1: (Int) -> (), p2: Int) {}
235+
// func overload2(p1: (Int) -> Int, p2: Int) {}
236+
// func overload2(p1: (Int) -> Int?, p2: Int) {}
237+
// func overload2(p1: ((Int) -> Int)?, p2: Int) {} // <- overload group
238+
let reference = ResolvedTopicReference(
239+
bundleIdentifier: bundle.identifier,
240+
path: "/documentation/FancyOverloads/overload2(p1:p2:)-4p1sq",
241+
sourceLanguage: .swift
242+
)
243+
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
244+
var translator = RenderNodeTranslator(
245+
context: context,
246+
bundle: bundle,
247+
identifier: reference,
248+
source: nil
249+
)
250+
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
251+
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
252+
XCTAssertEqual(declarationsSection.declarations.count, 1)
253+
let declarations = try XCTUnwrap(declarationsSection.declarations.first)
254+
255+
XCTAssertEqual(
256+
declarationAndHighlights(for: declarations.tokens),
257+
[
258+
"func overload2(p1: ((Int) -> Int)?, p2: Int)",
259+
" ~~ ~~~~~~~~~~ "
260+
]
261+
)
262+
263+
XCTAssertEqual(
264+
declarations.otherDeclarations?.declarations.flatMap({ declarationAndHighlights(for: $0.tokens) }),
265+
[
266+
"func overload2(p1: (Int) -> (), p2: Int)",
267+
" ~ ~~~~~~~ ",
268+
269+
"func overload2(p1: (Int) -> Int, p2: Int)",
270+
" ~ ~~~~~~~~ ",
271+
272+
"func overload2(p1: (Int) -> Int?, p2: Int)",
273+
" ~ ~~~~~~~~~ ",
274+
275+
// FIXME: adjust the token processing so that the comma inside the tuple isn't treated as common?
276+
// (it breaks the declaration pretty-printer in Swift-DocC-Render and causes it to skip pretty-printing)
277+
"func overload2(p1: (Int, Int), p2: Int)",
278+
" ~ ~~~~~ ",
279+
280+
// FIXME: adjust the token processing so that the common parenthesis is always the final one
281+
"func overload2(p1: Int, p2: (Int, Int))",
282+
" ~ ~~~~~ ~",
283+
284+
"func overload2(p1: Int, p2: Int)",
285+
" ",
286+
]
287+
)
288+
}
289+
290+
// Verify that the presence of type parameters doesn't cause the opening parenthesis of an
291+
// argument list to also be highlighted, since it is combined into the same token as the
292+
// closing angle bracket in the symbol graph. Also ensure that the leading space of the
293+
// rendered where clause is not highlighted.
294+
do {
295+
// func overload3(_ p: [Int: Int]) {} // <- overload group
296+
// func overload3<T: Hashable>(_ p: [T: T]) {}
297+
// func overload3<K: Hashable, V>(_ p: [K: V]) {}
298+
let reference = ResolvedTopicReference(
299+
bundleIdentifier: bundle.identifier,
300+
path: "/documentation/FancyOverloads/overload3(_:)-xql2",
301+
sourceLanguage: .swift
302+
)
303+
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
304+
var translator = RenderNodeTranslator(
305+
context: context,
306+
bundle: bundle,
307+
identifier: reference,
308+
source: nil
309+
)
310+
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
311+
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
312+
XCTAssertEqual(declarationsSection.declarations.count, 1)
313+
let declarations = try XCTUnwrap(declarationsSection.declarations.first)
314+
315+
XCTAssertEqual(
316+
declarationAndHighlights(for: declarations.tokens),
317+
[
318+
"func overload3(_ p: [Int : Int])",
319+
" ~~~ ~~~ ",
320+
]
321+
)
322+
323+
XCTAssertEqual(
324+
declarations.otherDeclarations?.declarations.flatMap({ declarationAndHighlights(for: $0.tokens) }),
325+
[
326+
"func overload3<K, V>(_ p: [K : V]) where K : Hashable",
327+
" ~~~~~~ ~ ~ ~~~~~~~~~~~~~~~~~~",
328+
329+
"func overload3<T>(_ p: [T : T]) where T : Hashable",
330+
" ~~~ ~ ~ ~~~~~~~~~~~~~~~~~~",
331+
]
332+
)
333+
}
334+
}
335+
336+
func testDontHighlightWhenOverloadsAreDisabled() throws {
337+
let symbolGraphFile = Bundle.module.url(
338+
forResource: "FancyOverloads",
339+
withExtension: "symbols.json",
340+
subdirectory: "Test Resources"
341+
)!
342+
343+
let tempURL = try createTempFolder(content: [
344+
Folder(name: "unit-test.docc", content: [
345+
InfoPlist(displayName: "FancyOverloads", identifier: "com.test.example"),
346+
CopyOfFile(original: symbolGraphFile),
347+
])
348+
])
349+
350+
let (_, bundle, context) = try loadBundle(from: tempURL)
351+
352+
for hash in ["7eht8", "8p1lo", "858ja"] {
353+
let reference = ResolvedTopicReference(
354+
bundleIdentifier: bundle.identifier,
355+
path: "/documentation/FancyOverloads/overload3(_:)-\(hash)",
356+
sourceLanguage: .swift
357+
)
358+
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
359+
var translator = RenderNodeTranslator(
360+
context: context,
361+
bundle: bundle,
362+
identifier: reference,
363+
source: nil
364+
)
365+
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
366+
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
367+
XCTAssertEqual(declarationsSection.declarations.count, 1)
368+
let declarations = try XCTUnwrap(declarationsSection.declarations.first)
369+
370+
XCTAssert(declarations.tokens.allSatisfy({ $0.highlight == nil }))
371+
}
372+
}
373+
}
374+
375+
/// Render a list of declaration tokens as a plain-text decoration and as a plain-text rendering of which characters are highlighted.
376+
func declarationAndHighlights(for tokens: [DeclarationRenderSection.Token]) -> [String] {
377+
[
378+
tokens.map({ $0.text }).joined(),
379+
tokens.map({ String(repeating: $0.highlight == .changed ? "~" : " ", count: $0.text.count) }).joined()
380+
]
154381
}

0 commit comments

Comments
 (0)