Skip to content

Commit 7b56c7c

Browse files
committed
Add Invalidated Layout Test, Delegate Editing Test, Remove Layout Manager Assertion
1 parent c45d93e commit 7b56c7c

File tree

4 files changed

+76
-17
lines changed

4 files changed

+76
-17
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,25 +201,13 @@ public class TextLayoutManager: NSObject {
201201

202202
// MARK: - Layout
203203

204-
/// Asserts that the caller is not in an active layout pass.
205-
/// See docs on ``isInLayout`` for more details.
206-
private func assertNotInLayout() {
207-
#if DEBUG // This is redundant, but it keeps the flag debug-only too which helps prevent misuse.
208-
assert(!isInLayout, "layoutLines called while already in a layout pass. This is a programmer error.")
209-
#endif
210-
}
211-
212204
/// Lays out all visible lines
213205
func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
214-
// assertNotInLayout()
215206
guard let visibleRect = rect ?? delegate?.visibleRect,
216207
!isInTransaction,
217208
let textStorage else {
218209
return
219210
}
220-
// #if DEBUG
221-
// isInLayout = true
222-
// #endif
223211
let minY = max(visibleRect.minY - verticalLayoutPadding, 0)
224212
let maxY = max(visibleRect.maxY + verticalLayoutPadding, 0)
225213
let originalHeight = lineStorage.height
@@ -271,10 +259,6 @@ public class TextLayoutManager: NSObject {
271259
// Update the visible lines with the new set.
272260
visibleLineIds = newVisibleLines
273261

274-
// #if DEBUG
275-
// isInLayout = false
276-
// #endif
277-
278262
// These are fine to update outside of `isInLayout` as our internal data structures are finalized at this point
279263
// so laying out again won't break our line storage or visible line.
280264

Tests/CodeEditTextViewTests/EmphasisManagerTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import Foundation
44

55
@Suite()
66
struct EmphasisManagerTests {
7-
@Test() @MainActor
7+
@Test()
8+
@MainActor
89
func testFlashEmphasisLayersNotLeaked() {
910
// Ensure layers are not leaked when switching from flash emphasis to any other emphasis type.
1011
let textView = TextView(string: "Lorem Ipsum")

Tests/CodeEditTextViewTests/TextLayoutManagerTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ struct TextLayoutManagerTests {
4242

4343
init() throws {
4444
textView = TextView(string: "A\nB\nC\nD")
45+
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
4546
textStorage = textView.textStorage
4647
layoutManager = try #require(textView.layoutManager)
4748
}
@@ -99,4 +100,34 @@ struct TextLayoutManagerTests {
99100
#expect(layoutManager.lineStorage.length == textStorage.length)
100101
layoutManager.lineStorage.validateInternalState()
101102
}
103+
104+
/// # 04/09/25
105+
/// This ensures that getting line rect info does not invalidate layout. The issue was previously caused by a call to
106+
/// ``TextLayoutManager/preparePositionForDisplay``.
107+
@Test
108+
func getRectsDoesNotRemoveLayoutInfo() {
109+
layoutManager.layoutLines(in: NSRect(x: 0, y: 0, width: 1000, height: 1000))
110+
let lineFragmentIDs = Set(
111+
layoutManager.lineStorage
112+
.linesInRange(NSRange(location: 0, length: 7))
113+
.flatMap(\.data.lineFragments)
114+
.map(\.data.id)
115+
)
116+
117+
_ = layoutManager.rectsFor(range: NSRange(start: 0, end: 7))
118+
119+
#expect(
120+
layoutManager.lineStorage.linesInRange(NSRange(location: 0, length: 7)).allSatisfy({ position in
121+
!position.data.lineFragments.isEmpty
122+
})
123+
)
124+
let afterLineFragmentIDs = Set(
125+
layoutManager.lineStorage
126+
.linesInRange(NSRange(location: 0, length: 7))
127+
.flatMap(\.data.lineFragments)
128+
.map(\.data.id)
129+
)
130+
#expect(lineFragmentIDs == afterLineFragmentIDs, "Line fragments were invalidated by `rectsFor(range:)` call.")
131+
layoutManager.lineStorage.validateInternalState()
132+
}
102133
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Testing
2+
import AppKit
3+
@testable import CodeEditTextView
4+
5+
@Suite
6+
@MainActor
7+
struct TextViewTests {
8+
class MockDelegate: TextViewDelegate {
9+
var shouldReplaceContents: ((_ textView: TextView, _ range: NSRange, _ string: String) -> Bool)?
10+
11+
func textView(_ textView: TextView, shouldReplaceContentsIn range: NSRange, with string: String) -> Bool {
12+
shouldReplaceContents?(textView, range, string) ?? true
13+
}
14+
}
15+
16+
let textView: TextView
17+
let delegate: MockDelegate
18+
19+
init() {
20+
textView = TextView(string: "Lorem Ipsum")
21+
delegate = MockDelegate()
22+
textView.delegate = delegate
23+
}
24+
25+
@Test
26+
func delegateChangesText() {
27+
var hasReplaced = false
28+
delegate.shouldReplaceContents = { textView, _, _ -> Bool in
29+
if !hasReplaced {
30+
hasReplaced.toggle()
31+
textView.replaceCharacters(in: NSRange(location: 0, length: 0), with: " World ")
32+
}
33+
34+
return true
35+
}
36+
37+
textView.replaceCharacters(in: NSRange(location: 0, length: 0), with: "Hello")
38+
39+
#expect(textView.string == "Hello World Lorem Ipsum")
40+
// available in test module
41+
textView.layoutManager.lineStorage.validateInternalState()
42+
}
43+
}

0 commit comments

Comments
 (0)