Skip to content

Commit c7ecbde

Browse files
committed
Add TextLayoutManager Tests
1 parent f1276bd commit c7ecbde

File tree

2 files changed

+104
-2
lines changed

2 files changed

+104
-2
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Edits.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ extension TextLayoutManager: NSTextStorageDelegate {
7272
}
7373
}
7474
}
75-
75+
7676
/// Inserts any newly inserted lines into the line layout storage. Exits early if the range is empty.
7777
/// - Parameter range: The range of the string that was inserted into the text storage.
7878
private func insertNewLines(for range: NSRange) {
@@ -97,7 +97,7 @@ extension TextLayoutManager: NSTextStorageDelegate {
9797
/// - location: The location the string is being inserted into.
9898
private func applyLineInsert(_ insertedString: NSString, at location: Int) {
9999
if LineEnding(line: insertedString as String) != nil {
100-
if location == textStorage?.length ?? 0 {
100+
if location == lineStorage.length {
101101
// Insert a new line at the end of the document, need to insert a new line 'cause there's nothing to
102102
// split. Also, append the new text to the last line.
103103
lineStorage.update(atIndex: location, delta: insertedString.length, deltaHeight: 0.0)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import Testing
2+
import AppKit
3+
@testable import CodeEditTextView
4+
5+
extension TextLineStorage {
6+
/// Validate that the internal tree is intact and correct.
7+
///
8+
/// Ensures that:
9+
/// - All lines can be queried by their index starting from `0`.
10+
/// - All lines can be found by iterating `y` positions.
11+
func validateInternalState() {
12+
func validateLines(_ lines: [TextLineStorage<Data>.TextLinePosition]) {
13+
var _lastLine: TextLineStorage<Data>.TextLinePosition?
14+
for line in lines {
15+
guard let lastLine = _lastLine else {
16+
#expect(line.index == 0)
17+
_lastLine = line
18+
return
19+
}
20+
21+
#expect(line.index == lastLine.index + 1)
22+
#expect(line.yPos >= lastLine.yPos + lastLine.height)
23+
#expect(line.range.location == lastLine.range.max + 1)
24+
_lastLine = line
25+
}
26+
}
27+
28+
let linesUsingIndex = (0..<count).compactMap({ getLine(atIndex: $0) })
29+
validateLines(linesUsingIndex)
30+
31+
let linesUsingYValue = Array(linesStartingAt(0, until: height))
32+
validateLines(linesUsingYValue)
33+
}
34+
}
35+
36+
@Suite
37+
@MainActor
38+
struct TextLayoutManagerTests {
39+
let textView: TextView
40+
let textStorage: NSTextStorage
41+
let layoutManager: TextLayoutManager
42+
43+
init() throws {
44+
textView = TextView(string: "A\nB\nC\nD")
45+
textStorage = textView.textStorage
46+
layoutManager = try #require(textView.layoutManager)
47+
}
48+
49+
@Test(
50+
arguments: [
51+
("\nE", NSRange(location: 6, length: 0), 5),
52+
("0\n", NSRange(location: 0, length: 0), 5), // at beginning
53+
("A\nBC\nD", NSRange(location: 3, length: 0), 6), // in middle
54+
("A\r\nB\nC\rD", NSRange(location: 0, length: 0), 7) // Insert mixed line breaks
55+
]
56+
)
57+
func insertText(_ testItem: (String, NSRange, Int)) throws { // swiftlint:disable:this large_tuple
58+
let (insertText, insertRange, lineCount) = testItem
59+
60+
textStorage.replaceCharacters(in: insertRange, with: insertText)
61+
62+
#expect(layoutManager.lineCount == lineCount)
63+
#expect(layoutManager.lineStorage.length == textStorage.length)
64+
layoutManager.lineStorage.validateInternalState()
65+
}
66+
67+
@Test(
68+
arguments: [
69+
(NSRange(location: 5, length: 2), 3), // At end
70+
(NSRange(location: 0, length: 2), 3), // At beginning
71+
(NSRange(location: 2, length: 3), 3) // In middle
72+
]
73+
)
74+
func deleteText(_ testItem: (NSRange, Int)) throws {
75+
let (deleteRange, lineCount) = testItem
76+
77+
textStorage.deleteCharacters(in: deleteRange)
78+
79+
#expect(layoutManager.lineCount == lineCount)
80+
#expect(layoutManager.lineStorage.length == textStorage.length)
81+
layoutManager.lineStorage.validateInternalState()
82+
}
83+
84+
@Test(
85+
arguments: [
86+
("\nD\nE\nF", NSRange(location: 5, length: 2), 6), // At end
87+
("A\nY\nZ", NSRange(location: 0, length: 1), 6), // At beginning
88+
("1\n2\n", NSRange(location: 2, length: 4), 4), // In middle
89+
("A\nB\nC\nD\nE\nF\nG", NSRange(location: 0, length: 7), 7), // Entire string
90+
("A\r\nB\nC\r", NSRange(location: 0, length: 6), 4) // Mixed line breaks
91+
]
92+
)
93+
func replaceText(_ testItem: (String, NSRange, Int)) throws { // swiftlint:disable:this large_tuple
94+
let (replaceText, replaceRange, lineCount) = testItem
95+
96+
textStorage.replaceCharacters(in: replaceRange, with: replaceText)
97+
98+
#expect(layoutManager.lineCount == lineCount)
99+
#expect(layoutManager.lineStorage.length == textStorage.length)
100+
layoutManager.lineStorage.validateInternalState()
101+
}
102+
}

0 commit comments

Comments
 (0)