Skip to content

Commit 33f5bdc

Browse files
committed
Implement Compact DiffFormat
1 parent d5702d9 commit 33f5bdc

2 files changed

Lines changed: 128 additions & 6 deletions

File tree

Sources/CustomDump/Diff.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
111111
elementIndent: Int,
112112
elementSeparator: String,
113113
collapseUnchanged: Bool,
114+
hideUnchanged: Bool = false,
114115
filter isIncluded: (Mirror.Child) -> Bool = { _ in true },
115116
areEquivalent: (Mirror.Child, Mirror.Child) -> Bool = { $0.label == $1.label },
116117
areInIncreasingOrder: ((Mirror.Child, Mirror.Child) -> Bool)? = nil,
@@ -220,6 +221,11 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
220221

221222
func flushUnchanged() {
222223
guard collapseUnchanged else { return }
224+
// When the caller opted into silent-discard mode, drop without printing.
225+
if hideUnchanged {
226+
unchangedBuffer.removeAll()
227+
return
228+
}
223229
if areInIncreasingOrder == nil && unchangedBuffer.count == 1 {
224230
let child = unchangedBuffer[0]
225231
print(
@@ -390,7 +396,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
390396
suffix: ")",
391397
elementIndent: 2,
392398
elementSeparator: ",",
393-
collapseUnchanged: false,
399+
collapseUnchanged: format.hideUnchangedChildren,
400+
hideUnchanged: format.hideUnchangedChildren,
394401
filter: macroPropertyFilter(for: lhs)
395402
)
396403
tracker.visitedItems.insert(lhsItem)
@@ -426,7 +433,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
426433
suffix: ")",
427434
elementIndent: 2,
428435
elementSeparator: ",",
429-
collapseUnchanged: false,
436+
collapseUnchanged: format.hideUnchangedChildren,
437+
hideUnchanged: format.hideUnchangedChildren,
430438
filter: macroPropertyFilter(for: lhs)
431439
)
432440
} else {
@@ -599,7 +607,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
599607
suffix: ")",
600608
elementIndent: 2,
601609
elementSeparator: ",",
602-
collapseUnchanged: false,
610+
collapseUnchanged: format.hideUnchangedChildren,
611+
hideUnchanged: format.hideUnchangedChildren,
603612
map: { child, _ in
604613
if child.label?.first == "." {
605614
child.label = nil
@@ -671,7 +680,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
671680
suffix: ")",
672681
elementIndent: 2,
673682
elementSeparator: ",",
674-
collapseUnchanged: false,
683+
collapseUnchanged: format.hideUnchangedChildren,
684+
hideUnchanged: format.hideUnchangedChildren,
675685
filter: macroPropertyFilter(for: lhs)
676686
)
677687

@@ -683,7 +693,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
683693
suffix: ")",
684694
elementIndent: 2,
685695
elementSeparator: ",",
686-
collapseUnchanged: false,
696+
collapseUnchanged: format.hideUnchangedChildren,
697+
hideUnchanged: format.hideUnchangedChildren,
687698
map: { child, _ in
688699
if child.label?.first == "." {
689700
child.label = nil
@@ -766,14 +777,21 @@ public struct DiffFormat: Sendable {
766777
/// something "unchanged."
767778
public var both: String
768779

780+
/// When `true`, unchanged children of struct, class, tuple, enum, and `_CustomDiffObject` types
781+
/// are silently omitted from the diff output, while the parent container name is still shown.
782+
/// Collection, dictionary, set, and multi-line string diffs are unaffected.
783+
public var hideUnchangedChildren: Bool
784+
769785
public init(
770786
first: String,
771787
second: String,
772-
both: String
788+
both: String,
789+
hideUnchangedChildren: Bool = false
773790
) {
774791
self.first = first
775792
self.second = second
776793
self.both = both
794+
self.hideUnchangedChildren = hideUnchangedChildren
777795
}
778796

779797
/// The default format for ``diff(_:_:format:)`` output, appropriate for where monospaced fonts
@@ -790,6 +808,10 @@ public struct DiffFormat: Sendable {
790808
/// figure space (" ") for unchanged. These three characters are more likely to render with equal
791809
/// widths in proportional fonts.
792810
public static let proportional = Self(first: "\u{2212}", second: "+", both: "\u{2007}")
811+
812+
/// A compact diff format that hides unchanged properties, showing only changed ones with parent
813+
/// context. Useful for deep or wide types to reduce noise when only a few fields change.
814+
public static let compact = Self(first: "-", second: "+", both: " ", hideUnchangedChildren: true)
793815
}
794816

795817
private struct Line: CustomDumpStringConvertible, Identifiable {

Tests/CustomDumpTests/DiffTests.swift

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,106 @@ final class DiffTests: XCTestCase {
728728
)
729729
}
730730

731+
func testHideUnchangedChildren() {
732+
// Test 1: Struct — unchanged property hidden (last element changes)
733+
expectNoDifference(
734+
diff(User(id: 42, name: "Blob"), User(id: 42, name: "Blob, Jr."), format: .compact),
735+
"""
736+
User(
737+
- name: "Blob"
738+
+ name: "Blob, Jr."
739+
)
740+
"""
741+
)
742+
743+
// Test 2: Struct — changed property in the middle (trailing comma expected)
744+
struct Info: Equatable {
745+
var age: Int
746+
var name: String
747+
var email: String
748+
}
749+
expectNoDifference(
750+
diff(
751+
Info(age: 30, name: "Blob", email: "blob@example.com"),
752+
Info(age: 30, name: "Blob, Jr.", email: "blob@example.com"),
753+
format: .compact
754+
),
755+
"""
756+
DiffTests.Info(
757+
- name: "Blob",
758+
+ name: "Blob, Jr.",
759+
)
760+
"""
761+
)
762+
763+
// Test 3: Enum — unchanged associated value hidden
764+
expectNoDifference(
765+
diff(Enum.fizz(42, buzz: "Blob"), Enum.fizz(42, buzz: "Glob"), format: .compact),
766+
"""
767+
Enum.fizz(
768+
- buzz: "Blob"
769+
+ buzz: "Glob"
770+
)
771+
"""
772+
)
773+
774+
// Test 4: Tuple — unchanged elements hidden
775+
expectNoDifference(
776+
diff((42, "Blob"), (42, "Blob, Jr."), format: .compact),
777+
"""
778+
(
779+
- "Blob"
780+
+ "Blob, Jr."
781+
)
782+
"""
783+
)
784+
785+
// Test 5: Nested struct — unchanged at both levels hidden
786+
expectNoDifference(
787+
diff(
788+
Pair(driver: User(id: 1, name: "Blob"), passenger: User(id: 2, name: "Blob, Jr.")),
789+
Pair(driver: User(id: 1, name: "Blob"), passenger: User(id: 2, name: "Blob, Sr.")),
790+
format: .compact
791+
),
792+
"""
793+
Pair(
794+
passenger: User(
795+
- name: "Blob, Jr."
796+
+ name: "Blob, Sr."
797+
)
798+
)
799+
"""
800+
)
801+
802+
// Test 6: Collection is NOT silently dropped — still shows `… (N unchanged)`
803+
expectNoDifference(
804+
diff([1, 2, 3, 4, 5], [1, 2, 99, 4, 5], format: .compact),
805+
"""
806+
[
807+
… (2 unchanged),
808+
- [2]: 3,
809+
+ [2]: 99,
810+
… (2 unchanged)
811+
]
812+
"""
813+
)
814+
815+
// Test 7: Default format still shows all unchanged properties (regression)
816+
expectNoDifference(
817+
diff(User(id: 42, name: "Blob"), User(id: 42, name: "Blob, Jr.")),
818+
"""
819+
User(
820+
id: 42,
821+
- name: "Blob"
822+
+ name: "Blob, Jr."
823+
)
824+
"""
825+
)
826+
827+
// Test 8: Equal values return `nil` with `.compact`
828+
XCTAssertEqual(diff(User(id: 1, name: "Blob"), User(id: 1, name: "Blob"), format: .compact), nil)
829+
}
830+
731831
#if !os(WASI)
732832
func testNestedCustomMirror() {
733833
#if compiler(>=5.4)

0 commit comments

Comments
 (0)