Skip to content

Commit 6ca467e

Browse files
committed
Merge branch 'fix/invalidation' into feat/override-layout-behavior
2 parents 726b1ec + 949639b commit 6ca467e

File tree

9 files changed

+48
-38
lines changed

9 files changed

+48
-38
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Edits.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ extension TextLayoutManager: NSTextStorageDelegate {
4545
let insertedStringRange = NSRange(location: editedRange.location, length: editedRange.length - delta)
4646
removeLayoutLinesIn(range: insertedStringRange)
4747
insertNewLines(for: editedRange)
48-
invalidateLayoutForRange(editedRange)
48+
49+
setNeedsLayout()
4950
}
5051

5152
/// Removes all lines in the range, as if they were deleted. This is a setup for inserting the lines back in on an
@@ -65,10 +66,10 @@ extension TextLayoutManager: NSTextStorageDelegate {
6566
lineStorage.delete(lineAt: nextLine.range.location)
6667
let delta = -intersection.length + nextLine.range.length
6768
if delta != 0 {
68-
lineStorage.update(atIndex: linePosition.range.location, delta: delta, deltaHeight: 0)
69+
lineStorage.update(atOffset: linePosition.range.location, delta: delta, deltaHeight: 0)
6970
}
7071
} else {
71-
lineStorage.update(atIndex: linePosition.range.location, delta: -intersection.length, deltaHeight: 0)
72+
lineStorage.update(atOffset: linePosition.range.location, delta: -intersection.length, deltaHeight: 0)
7273
}
7374
}
7475
}
@@ -100,7 +101,7 @@ extension TextLayoutManager: NSTextStorageDelegate {
100101
if location == lineStorage.length {
101102
// Insert a new line at the end of the document, need to insert a new line 'cause there's nothing to
102103
// split. Also, append the new text to the last line.
103-
lineStorage.update(atIndex: location, delta: insertedString.length, deltaHeight: 0.0)
104+
lineStorage.update(atOffset: location, delta: insertedString.length, deltaHeight: 0.0)
104105
lineStorage.insert(
105106
line: TextLine(),
106107
atOffset: location + insertedString.length,
@@ -114,7 +115,7 @@ extension TextLayoutManager: NSTextStorageDelegate {
114115
let splitLength = linePosition.range.max - location
115116
let lineDelta = insertedString.length - splitLength // The difference in the line being edited
116117
if lineDelta != 0 {
117-
lineStorage.update(atIndex: location, delta: lineDelta, deltaHeight: 0.0)
118+
lineStorage.update(atOffset: location, delta: lineDelta, deltaHeight: 0.0)
118119
}
119120

120121
lineStorage.insert(
@@ -125,7 +126,7 @@ extension TextLayoutManager: NSTextStorageDelegate {
125126
)
126127
}
127128
} else {
128-
lineStorage.update(atIndex: location, delta: insertedString.length, deltaHeight: 0.0)
129+
lineStorage.update(atOffset: location, delta: insertedString.length, deltaHeight: 0.0)
129130
}
130131
}
131132
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Invalidation.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ extension TextLayoutManager {
1414
for linePosition in lineStorage.linesStartingAt(rect.minY, until: rect.maxY) {
1515
linePosition.data.setNeedsLayout()
1616
}
17-
layoutLines()
17+
18+
layoutView?.needsLayout = true
1819
}
1920

2021
/// Invalidates layout for the given range of text.
@@ -24,11 +25,12 @@ extension TextLayoutManager {
2425
linePosition.data.setNeedsLayout()
2526
}
2627

27-
layoutLines()
28+
layoutView?.needsLayout = true
2829
}
2930

3031
public func setNeedsLayout() {
3132
needsLayout = true
3233
visibleLineIds.removeAll(keepingCapacity: true)
34+
layoutView?.needsLayout = true
3335
}
3436
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
//
2-
// File.swift
2+
// TextLayoutManager+ensureLayout.swift
33
// CodeEditTextView
44
//
5-
// Created by Khan Winter on 4/10/25.
5+
// Created by Khan Winter on 4/7/25.
66
//
77

88
import AppKit
99

1010
extension TextLayoutManager {
11+
/// Contains all data required to perform layout on a text line.
12+
private struct LineLayoutData {
13+
let minY: CGFloat
14+
let maxY: CGFloat
15+
let maxWidth: CGFloat
16+
}
17+
1118
/// Asserts that the caller is not in an active layout pass.
1219
/// See docs on ``isInLayout`` for more details.
1320
private func assertNotInLayout() {
@@ -60,7 +67,7 @@ extension TextLayoutManager {
6067
)
6168
if lineSize.height != linePosition.height {
6269
lineStorage.update(
63-
atIndex: linePosition.range.location,
70+
atOffset: linePosition.range.location,
6471
delta: 0,
6572
deltaHeight: lineSize.height - linePosition.height
6673
)
@@ -200,7 +207,7 @@ extension TextLayoutManager {
200207
/// Invalidates and prepares a line position for display.
201208
/// - Parameter position: The line position to prepare.
202209
/// - Returns: The height of the newly laid out line and all it's fragments.
203-
package func preparePositionForDisplay(_ position: TextLineStorage<TextLine>.TextLinePosition) -> CGFloat {
210+
func preparePositionForDisplay(_ position: TextLineStorage<TextLine>.TextLinePosition) -> CGFloat {
204211
guard let textStorage else { return 0 }
205212
let displayData = TextLine.DisplayData(
206213
maxWidth: maxLineLayoutWidth,

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,7 @@ extension TextLayoutManager {
121121
return nil
122122
}
123123
if linePosition.data.lineFragments.isEmpty {
124-
let newHeight = preparePositionForDisplay(linePosition)
125-
if linePosition.height != newHeight {
126-
delegate?.layoutManagerHeightDidUpdate(newHeight: lineStorage.height)
127-
}
124+
ensureLayoutUntil(offset)
128125
}
129126

130127
guard let fragmentPosition = linePosition.data.typesetter.lineFragments.getLine(
@@ -293,7 +290,7 @@ extension TextLayoutManager {
293290
let height = preparePositionForDisplay(linePosition)
294291
if height != linePosition.height {
295292
lineStorage.update(
296-
atIndex: linePosition.range.location,
293+
atOffset: linePosition.range.location,
297294
delta: 0,
298295
deltaHeight: height - linePosition.height
299296
)

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,6 @@ public class TextLayoutManager: NSObject {
101101
(delegate?.textViewportSize().width ?? .greatestFiniteMagnitude) - edgeInsets.horizontal
102102
}
103103

104-
/// Contains all data required to perform layout on a text line.
105-
struct LineLayoutData {
106-
let minY: CGFloat
107-
let maxY: CGFloat
108-
let maxWidth: CGFloat
109-
}
110-
111104
// MARK: - Init
112105

113106
/// Initialize a text layout manager and prepare it for use.

Sources/CodeEditTextView/TextLineStorage/TextLineStorage.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,26 +190,29 @@ public final class TextLineStorage<Data: Identifiable> {
190190
/// - Complexity `O(m log n)` where `m` is the number of lines that need to be deleted as a result of this update.
191191
/// and `n` is the number of lines stored in the tree.
192192
/// - Parameters:
193-
/// - index: The index where the edit began
193+
/// - offset: The offset where the edit began
194194
/// - delta: The change in length of the document. Negative for deletes, positive for insertions.
195195
/// - deltaHeight: The change in height of the document.
196-
public func update(atIndex index: Int, delta: Int, deltaHeight: CGFloat) {
197-
assert(index >= 0 && index <= self.length, "Invalid index, expected between 0 and \(self.length). Got \(index)")
196+
public func update(atOffset offset: Int, delta: Int, deltaHeight: CGFloat) {
197+
assert(
198+
offset >= 0 && offset <= self.length,
199+
"Invalid index, expected between 0 and \(self.length). Got \(offset)"
200+
)
198201
assert(delta != 0 || deltaHeight != 0, "Delta must be non-0")
199202
let position: NodePosition?
200-
if index == self.length { // Updates at the end of the document are valid
203+
if offset == self.length { // Updates at the end of the document are valid
201204
position = lastNode
202205
} else {
203-
position = search(for: index)
206+
position = search(for: offset)
204207
}
205208
guard let position else {
206-
assertionFailure("No line found at index \(index)")
209+
assertionFailure("No line found at index \(offset)")
207210
return
208211
}
209212
if delta < 0 {
210213
assert(
211-
index - position.textPos > delta,
212-
"Delta too large. Deleting \(-delta) from line at position \(index) extends beyond the line's range."
214+
offset - position.textPos > delta,
215+
"Delta too large. Deleting \(-delta) from line at position \(offset) extends beyond the line's range."
213216
)
214217
}
215218
length += delta

Sources/CodeEditTextView/TextView/TextView+Layout.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import Foundation
99

1010
extension TextView {
1111
override public func layout() {
12-
layoutManager.layoutLines()
1312
super.layout()
13+
layoutManager.layoutLines()
1414
}
1515

1616
open override class var isCompatibleWithResponsiveScrolling: Bool {

Sources/CodeEditTextView/TextView/TextView+SetText.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ extension TextView {
2727
textStorage.addAttributes(typingAttributes, range: documentRange)
2828
layoutManager.textStorage = textStorage
2929
layoutManager.reset()
30+
storageDelegate.addDelegate(layoutManager)
3031

3132
selectionManager.textStorage = textStorage
3233
selectionManager.setSelectedRanges(selectionManager.textSelections.map { $0.range })
34+
NotificationCenter.default.post(
35+
Notification(
36+
name: TextSelectionManager.selectionChangedNotification,
37+
object: selectionManager
38+
)
39+
)
3340

3441
_undoManager?.clearStack()
3542

Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ final class TextLayoutLineStorageTests: XCTestCase {
8989

9090
// Single Element
9191
tree.insert(line: TextLine(), atOffset: 0, length: 1, height: 1.0)
92-
tree.update(atIndex: 0, delta: 20, deltaHeight: 5.0)
92+
tree.update(atOffset: 0, delta: 20, deltaHeight: 5.0)
9393
XCTAssertEqual(tree.length, 21, "Tree length incorrect")
9494
XCTAssertEqual(tree.count, 1, "Tree count incorrect")
9595
XCTAssertEqual(tree.height, 6, "Tree height incorrect")
@@ -98,7 +98,7 @@ final class TextLayoutLineStorageTests: XCTestCase {
9898

9999
// Update First
100100
tree = createBalancedTree()
101-
tree.update(atIndex: 0, delta: 12, deltaHeight: -0.5)
101+
tree.update(atOffset: 0, delta: 12, deltaHeight: -0.5)
102102
XCTAssertEqual(tree.height, 14.5, "Tree height incorrect")
103103
XCTAssertEqual(tree.count, 15, "Tree count changed")
104104
XCTAssertEqual(tree.length, 132, "Tree length incorrect")
@@ -107,7 +107,7 @@ final class TextLayoutLineStorageTests: XCTestCase {
107107

108108
// Update Last
109109
tree = createBalancedTree()
110-
tree.update(atIndex: tree.length - 1, delta: -14, deltaHeight: 1.75)
110+
tree.update(atOffset: tree.length - 1, delta: -14, deltaHeight: 1.75)
111111
XCTAssertEqual(tree.height, 16.75, "Tree height incorrect")
112112
XCTAssertEqual(tree.count, 15, "Tree count changed")
113113
XCTAssertEqual(tree.length, 106, "Tree length incorrect")
@@ -116,7 +116,7 @@ final class TextLayoutLineStorageTests: XCTestCase {
116116

117117
// Update middle
118118
tree = createBalancedTree()
119-
tree.update(atIndex: 45, delta: -9, deltaHeight: 1.0)
119+
tree.update(atOffset: 45, delta: -9, deltaHeight: 1.0)
120120
XCTAssertEqual(tree.height, 16.0, "Tree height incorrect")
121121
XCTAssertEqual(tree.count, 15, "Tree count changed")
122122
XCTAssertEqual(tree.length, 111, "Tree length incorrect")
@@ -131,7 +131,7 @@ final class TextLayoutLineStorageTests: XCTestCase {
131131
let originalHeight = tree.height
132132
let originalCount = tree.count
133133
let originalLength = tree.length
134-
tree.update(atIndex: Int.random(in: 0..<tree.length), delta: delta, deltaHeight: deltaHeight)
134+
tree.update(atOffset: Int.random(in: 0..<tree.length), delta: delta, deltaHeight: deltaHeight)
135135
XCTAssert(originalCount == tree.count, "Tree count should not change on update")
136136
XCTAssert(originalHeight + deltaHeight == tree.height, "Tree height incorrect")
137137
XCTAssert(originalLength + delta == tree.length, "Tree length incorrect")

0 commit comments

Comments
 (0)