Skip to content

Commit 053a7d4

Browse files
committed
Highlight Invalidation Fix
1 parent 9102955 commit 053a7d4

File tree

4 files changed

+67
-20
lines changed

4 files changed

+67
-20
lines changed

Sources/CodeEditSourceEditor/Highlighting/HighlighProviding/HighlightProviderState.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ class HighlightProviderState {
116116

117117
highlightInvalidRanges()
118118
}
119+
120+
/// Accumulates all pending ranges and calls `queryHighlights`.
121+
func highlightInvalidRanges() {
122+
var ranges: [NSRange] = []
123+
while let nextRange = getNextRange() {
124+
ranges.append(nextRange)
125+
pendingSet.insert(range: nextRange)
126+
}
127+
queryHighlights(for: ranges)
128+
}
119129
}
120130

121131
extension HighlightProviderState {
@@ -143,16 +153,6 @@ extension HighlightProviderState {
143153
}
144154

145155
private extension HighlightProviderState {
146-
/// Accumulates all pending ranges and calls `queryHighlights`.
147-
func highlightInvalidRanges() {
148-
var ranges: [NSRange] = []
149-
while let nextRange = getNextRange() {
150-
ranges.append(nextRange)
151-
pendingSet.insert(range: nextRange)
152-
}
153-
queryHighlights(for: ranges)
154-
}
155-
156156
/// Gets the next `NSRange` to highlight based on the invalid set, visible set, and pending set.
157157
/// - Returns: An `NSRange` to highlight if it could be fetched.
158158
func getNextRange() -> NSRange? {

Sources/CodeEditSourceEditor/Highlighting/Highlighter.swift

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ class Highlighter: NSObject {
118118
highlightProviders.forEach { $0.invalidate() }
119119
}
120120

121+
public func invalidate(_ set: IndexSet) {
122+
highlightProviders.forEach { $0.invalidate(set) }
123+
}
124+
121125
/// Sets the language and causes a re-highlight of the entire text.
122126
/// - Parameter language: The language to update to.
123127
public func setLanguage(language: CodeLanguage) {
@@ -154,11 +158,6 @@ extension Highlighter: NSTextStorageDelegate {
154158
// This method is called whenever attributes are updated, so to avoid re-highlighting the entire document
155159
// each time an attribute is applied, we check to make sure this is in response to an edit.
156160
guard editedMask.contains(.editedCharacters), let textView else { return }
157-
if delta > 0 {
158-
visibleRangeProvider.visibleSet.insert(range: editedRange)
159-
}
160-
161-
visibleRangeProvider.updateVisibleSet(textView: textView)
162161

163162
let styleContainerRange: Range<Int>
164163
let newLength: Int
@@ -176,6 +175,12 @@ extension Highlighter: NSTextStorageDelegate {
176175
withCount: newLength
177176
)
178177

178+
if delta > 0 {
179+
visibleRangeProvider.visibleSet.insert(range: editedRange)
180+
}
181+
182+
visibleRangeProvider.updateVisibleSet(textView: textView)
183+
179184
let providerRange = NSRange(location: editedRange.location, length: editedRange.length - delta)
180185
highlightProviders.forEach { $0.storageDidUpdate(range: providerRange, delta: delta) }
181186
}
@@ -196,26 +201,30 @@ extension Highlighter: NSTextStorageDelegate {
196201
extension Highlighter: StyledRangeContainerDelegate {
197202
func styleContainerDidUpdate(in range: NSRange) {
198203
guard let textView, let attributeProvider else { return }
204+
// textView.layoutManager.beginTransaction()
199205
textView.textStorage.beginEditing()
200206

201207
let storage = textView.textStorage
202208

203209
var offset = range.location
204210
for run in styleContainer.runsIn(range: range) {
205-
let range = NSRange(location: offset, length: run.length)
211+
guard let range = NSRange(location: offset, length: run.length).intersection(range) else {
212+
continue
213+
}
206214
storage?.setAttributes(attributeProvider.attributesFor(run.capture), range: range)
207-
offset += run.length
215+
offset += range.length
208216
}
209217

210218
textView.textStorage.endEditing()
211-
textView.layoutManager.invalidateLayoutForRange(range)
219+
// textView.layoutManager.endTransaction()
220+
// textView.layoutManager.invalidateLayoutForRange(range)
212221
}
213222
}
214223

215224
// MARK: - VisibleRangeProviderDelegate
216225

217226
extension Highlighter: VisibleRangeProviderDelegate {
218227
func visibleSetDidUpdate(_ newIndices: IndexSet) {
219-
highlightProviders.forEach { $0.invalidate(newIndices) }
228+
highlightProviders.forEach { $0.highlightInvalidRanges() }
220229
}
221230
}

Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public final class TreeSitterClient: HighlightProviding {
5252
package var pendingEdits: Atomic<[InputEdit]> = Atomic([])
5353

5454
/// Optional flag to force every operation to be done on the caller's thread.
55-
var forceSyncOperation: Bool = false
55+
package var forceSyncOperation: Bool = false
5656

5757
public init() { }
5858

Tests/CodeEditSourceEditorTests/Highlighting/HighlighterTests.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,42 @@ final class HighlighterTests: XCTestCase {
5959
"Highlighter did not query again after cancelling the first request"
6060
)
6161
}
62+
63+
@MainActor
64+
func test_highlightsDoNotInvalidateEntireTextView() {
65+
class SentryStorageDelegate: NSObject, NSTextStorageDelegate {
66+
var editedIndices: IndexSet = IndexSet()
67+
68+
func textStorage(
69+
_ textStorage: NSTextStorage,
70+
didProcessEditing editedMask: NSTextStorageEditActions,
71+
range editedRange: NSRange,
72+
changeInLength delta: Int) {
73+
editedIndices.insert(integersIn: editedRange)
74+
}
75+
}
76+
77+
let highlightProvider = TreeSitterClient()
78+
highlightProvider.forceSyncOperation = true
79+
let attributeProvider = MockAttributeProvider()
80+
let textView = Mock.textView()
81+
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
82+
textView.setText("func helloWorld() {\n\tprint(\"Hello World!\")\n}")
83+
84+
let highlighter = Mock.highlighter(
85+
textView: textView,
86+
highlightProvider: highlightProvider,
87+
attributeProvider: attributeProvider
88+
)
89+
90+
highlighter.invalidate()
91+
92+
let sentryStorage = SentryStorageDelegate()
93+
textView.addStorageDelegate(sentryStorage)
94+
95+
let invalidSet = IndexSet(integersIn: NSRange(location: 0, length: 24))
96+
highlighter.invalidate(invalidSet) // Invalidate first line
97+
98+
XCTAssertEqual(sentryStorage.editedIndices, invalidSet) // Should only cause highlights on the first line
99+
}
62100
}

0 commit comments

Comments
 (0)