Skip to content

Commit 1246f05

Browse files
committed
prevent recomputations
1 parent b6bd365 commit 1246f05

4 files changed

Lines changed: 110 additions & 10 deletions

File tree

Package.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ let package = Package(
2424
// Targets are the basic building blocks of a package, defining a module or a test suite.
2525
// Targets can depend on other targets in this package and products from dependencies.
2626
.target(
27-
name: "TextDiff"),
27+
name: "TextDiff",
28+
swiftSettings: [
29+
.define("TESTING", .when(configuration: .debug))
30+
]
31+
),
2832
.testTarget(
2933
name: "TextDiffTests",
3034
dependencies: [

Sources/TextDiff/AppKit/DiffTextViewRepresentable.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ struct DiffTextViewRepresentable: NSViewRepresentable {
2020
}
2121

2222
func updateNSView(_ view: NSTextDiffView, context: Context) {
23-
view.style = style
24-
view.mode = mode
25-
view.original = original
26-
view.updated = updated
23+
view.setContent(
24+
original: original,
25+
updated: updated,
26+
style: style,
27+
mode: mode
28+
)
2729
}
2830
}

Sources/TextDiff/AppKit/NSTextDiffView.swift

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,32 @@ public final class NSTextDiffView: NSView {
99
/// Setting this value updates rendered diff output when content changes.
1010
public var original: String {
1111
didSet {
12-
updateSegmentsIfNeeded()
12+
guard !isBatchUpdating else {
13+
return
14+
}
15+
_ = updateSegmentsIfNeeded()
1316
}
1417
}
1518

1619
/// The source text after edits.
1720
/// Setting this value updates rendered diff output when content changes.
1821
public var updated: String {
1922
didSet {
20-
updateSegmentsIfNeeded()
23+
guard !isBatchUpdating else {
24+
return
25+
}
26+
_ = updateSegmentsIfNeeded()
2127
}
2228
}
2329

2430
/// Visual style used to render additions, deletions, and unchanged text.
2531
/// Setting this value redraws the view without recomputing diff segments.
2632
public var style: TextDiffStyle {
2733
didSet {
34+
guard !isBatchUpdating else {
35+
pendingStyleInvalidation = true
36+
return
37+
}
2838
invalidateCachedLayout()
2939
}
3040
}
@@ -33,7 +43,10 @@ public final class NSTextDiffView: NSView {
3343
/// Setting this value updates rendered diff output when mode changes.
3444
public var mode: TextDiffComparisonMode {
3545
didSet {
36-
updateSegmentsIfNeeded()
46+
guard !isBatchUpdating else {
47+
return
48+
}
49+
_ = updateSegmentsIfNeeded()
3750
}
3851
}
3952

@@ -43,6 +56,8 @@ public final class NSTextDiffView: NSView {
4356
private var lastOriginal: String
4457
private var lastUpdated: String
4558
private var lastModeKey: Int
59+
private var isBatchUpdating = false
60+
private var pendingStyleInvalidation = false
4661

4762
private var cachedWidth: CGFloat = -1
4863
private var cachedLayout: DiffLayout?
@@ -83,6 +98,7 @@ public final class NSTextDiffView: NSView {
8398
super.init(frame: .zero)
8499
}
85100

101+
#if TESTING
86102
init(
87103
original: String,
88104
updated: String,
@@ -101,6 +117,7 @@ public final class NSTextDiffView: NSView {
101117
self.segments = diffProvider(original, updated, mode)
102118
super.init(frame: .zero)
103119
}
120+
#endif
104121

105122
@available(*, unavailable, message: "Use init(original:updated:style:mode:)")
106123
required init?(coder: NSCoder) {
@@ -133,17 +150,44 @@ public final class NSTextDiffView: NSView {
133150
}
134151
}
135152

136-
private func updateSegmentsIfNeeded() {
153+
/// Atomically updates view inputs and recomputes diff segments at most once.
154+
public func setContent(
155+
original: String,
156+
updated: String,
157+
style: TextDiffStyle,
158+
mode: TextDiffComparisonMode
159+
) {
160+
isBatchUpdating = true
161+
defer {
162+
isBatchUpdating = false
163+
let needsStyleInvalidation = pendingStyleInvalidation
164+
pendingStyleInvalidation = false
165+
166+
let didRecompute = updateSegmentsIfNeeded()
167+
if needsStyleInvalidation, !didRecompute {
168+
invalidateCachedLayout()
169+
}
170+
}
171+
172+
self.style = style
173+
self.mode = mode
174+
self.original = original
175+
self.updated = updated
176+
}
177+
178+
@discardableResult
179+
private func updateSegmentsIfNeeded() -> Bool {
137180
let newModeKey = Self.modeKey(for: mode)
138181
guard original != lastOriginal || updated != lastUpdated || newModeKey != lastModeKey else {
139-
return
182+
return false
140183
}
141184

142185
lastOriginal = original
143186
lastUpdated = updated
144187
lastModeKey = newModeKey
145188
segments = diffProvider(original, updated, mode)
146189
invalidateCachedLayout()
190+
return true
147191
}
148192

149193
private func layoutForCurrentWidth() -> DiffLayout {

Tests/TextDiffTests/NSTextDiffViewTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,53 @@ func nsTextDiffViewStyleChangeDoesNotRecomputeDiff() {
9191

9292
#expect(callCount == 1)
9393
}
94+
95+
@Test
96+
@MainActor
97+
func nsTextDiffViewSetContentBatchesRecompute() {
98+
var callCount = 0
99+
let view = NSTextDiffView(
100+
original: "old",
101+
updated: "new",
102+
mode: .token
103+
) { _, _, _ in
104+
callCount += 1
105+
return [DiffSegment(kind: .equal, tokenKind: .word, text: "\(callCount)")]
106+
}
107+
108+
var style = TextDiffStyle.default
109+
style.deletionStrikethrough = true
110+
view.setContent(
111+
original: "old-2",
112+
updated: "new-2",
113+
style: style,
114+
mode: .character
115+
)
116+
117+
#expect(callCount == 2)
118+
}
119+
120+
@Test
121+
@MainActor
122+
func nsTextDiffViewSetContentStyleOnlyDoesNotRecomputeDiff() {
123+
var callCount = 0
124+
let view = NSTextDiffView(
125+
original: "old",
126+
updated: "new",
127+
mode: .token
128+
) { _, _, _ in
129+
callCount += 1
130+
return [DiffSegment(kind: .equal, tokenKind: .word, text: "\(callCount)")]
131+
}
132+
133+
var style = TextDiffStyle.default
134+
style.deletionStrikethrough = true
135+
view.setContent(
136+
original: "old",
137+
updated: "new",
138+
style: style,
139+
mode: .token
140+
)
141+
142+
#expect(callCount == 1)
143+
}

0 commit comments

Comments
 (0)