Skip to content

Commit 21b7e34

Browse files
committed
Assert When Layout Is Recursively Called
1 parent 49aee7f commit 21b7e34

File tree

3 files changed

+30
-1
lines changed

3 files changed

+30
-1
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ extension TextLayoutManager {
191191
for fragmentPosition in line.data.lineFragments.linesInRange(relativeRange) {
192192
guard let intersectingRange = fragmentPosition.range.intersection(relativeRange) else { continue }
193193
let fragmentRect = fragmentPosition.data.rectFor(range: intersectingRange)
194+
guard fragmentRect.width > 0 else { continue }
194195
rects.append(
195196
CGRect(
196197
x: fragmentRect.minX + edgeInsets.left,

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,33 @@ public class TextLayoutManager: NSObject {
199199
/// ``TextLayoutManager/estimateLineHeight()`` is called.
200200
private var _estimateLineHeight: CGFloat?
201201

202+
/// Asserts that the caller is not in an active layout pass.
203+
/// See docs on ``isInLayout`` for more details.
204+
private func assertNotInLayout() {
205+
#if DEBUG // This is redundant, but it keeps the flag debug-only too which helps prevent misuse.
206+
assert(!isInLayout, "layoutLines called while already in a layout pass. This is a programmer error.")
207+
#endif
208+
}
209+
202210
// MARK: - Layout
203211

204212
/// Lays out all visible lines
205213
func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
214+
assertNotInLayout()
206215
guard let visibleRect = rect ?? delegate?.visibleRect,
207216
!isInTransaction,
208217
let textStorage else {
209218
return
210219
}
220+
221+
// The macOS may call `layout` on the textView while we're laying out fragment views. This ensures the view
222+
// tree modifications caused by this method are atomic, so macOS won't call `layout` while we're already doing
223+
// that
224+
CATransaction.begin()
225+
#if DEBUG
226+
isInLayout = true
227+
#endif
228+
211229
let minY = max(visibleRect.minY - verticalLayoutPadding, 0)
212230
let maxY = max(visibleRect.maxY + verticalLayoutPadding, 0)
213231
let originalHeight = lineStorage.height
@@ -253,6 +271,11 @@ public class TextLayoutManager: NSObject {
253271
newVisibleLines.insert(linePosition.data.id)
254272
}
255273

274+
#if DEBUG
275+
isInLayout = false
276+
#endif
277+
CATransaction.commit()
278+
256279
// Enqueue any lines not used in this layout pass.
257280
viewReuseQueue.enqueueViews(notInSet: usedFragmentIDs)
258281

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public class TextSelectionManager: NSObject {
8686
/// Set the selected ranges to new ranges. Overrides any existing selections.
8787
/// - Parameter range: The selected ranges to set.
8888
public func setSelectedRanges(_ ranges: [NSRange]) {
89+
let oldRanges = textSelections.map(\.range)
90+
8991
textSelections.forEach { $0.view?.removeFromSuperview() }
9092
// Remove duplicates, invalid ranges, update suggested X position.
9193
textSelections = Set(ranges)
@@ -99,8 +101,11 @@ public class TextSelectionManager: NSObject {
99101
return selection
100102
}
101103
updateSelectionViews()
102-
NotificationCenter.default.post(Notification(name: Self.selectionChangedNotification, object: self))
103104
delegate?.setNeedsDisplay()
105+
106+
if oldRanges != textSelections.map(\.range) {
107+
NotificationCenter.default.post(Notification(name: Self.selectionChangedNotification, object: self))
108+
}
104109
}
105110

106111
/// Append a new selected range to the existing ones.

0 commit comments

Comments
 (0)