Skip to content

Commit 66e1065

Browse files
Fix Text Disappearing (#88)
### Description Fixes two small bugs: - When deleting a line from the line text storage, a replaced node's left subtree metadata would be replaced, instead of added to when removing that node. This caused the left subtree to be invisible in some cases, leading to broken layout. - Force a layout pass after editing, as was the behavior before the recent layout manager changes. Fixes an issue where editing text would not move the cursor correctly. - Adds a test case to the aforementioned line storage bug. ### Related Issues * closes CodeEditApp/CodeEditSourceEditor#196 ### Checklist <!--- Add things that are not yet implemented above --> - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots N/A
1 parent 337b05f commit 66e1065

File tree

3 files changed

+137
-12
lines changed

3 files changed

+137
-12
lines changed

Sources/CodeEditTextView/TextLineStorage/TextLineStorage.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ public final class TextLineStorage<Data: Identifiable> {
5858

5959
public init() { }
6060

61+
init(root: Node<Data>, count: Int, length: Int, height: CGFloat) {
62+
self.root = root
63+
self.count = count
64+
self.length = length
65+
self.height = height
66+
}
67+
6168
// MARK: - Public Methods
6269

6370
/// Inserts a new line for the given range.
@@ -408,9 +415,9 @@ private extension TextLineStorage {
408415
} else {
409416
transplant(nodeY, with: nodeY.right)
410417

411-
nodeY.right?.leftSubtreeCount = nodeY.leftSubtreeCount
412-
nodeY.right?.leftSubtreeHeight = nodeY.leftSubtreeHeight
413-
nodeY.right?.leftSubtreeOffset = nodeY.leftSubtreeOffset
418+
nodeY.right?.leftSubtreeCount += nodeY.leftSubtreeCount
419+
nodeY.right?.leftSubtreeHeight += nodeY.leftSubtreeHeight
420+
nodeY.right?.leftSubtreeOffset += nodeY.leftSubtreeOffset
414421

415422
nodeY.right = nodeZ.right
416423
nodeY.right?.parent = nodeY

Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ extension TextView {
4747
}
4848

4949
textStorage.endEditing()
50+
51+
// Cause a layout pass now that we've finished editing, if there were any edits.
52+
if !ranges.isEmpty {
53+
layout()
54+
}
55+
5056
if !skipUpdateSelection {
5157
selectionManager.notifyAfterEdit()
5258
}

Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ fileprivate extension CGFloat {
77
}
88
}
99

10-
final class TextLayoutLineStorageTests: XCTestCase {
10+
extension UUID: @retroactive Identifiable {
11+
public var id: UUID { self }
12+
}
13+
14+
final class TextLayoutLineStorageTests: XCTestCase { // swiftlint:disable:this type_body_length
15+
1116
/// Creates a balanced height=3 tree useful for testing and debugging.
1217
/// - Returns: A new tree.
1318
fileprivate func createBalancedTree() -> TextLineStorage<TextLine> {
@@ -20,16 +25,16 @@ final class TextLayoutLineStorageTests: XCTestCase {
2025
return tree
2126
}
2227

28+
struct ChildData {
29+
let length: Int
30+
let count: Int
31+
let height: CGFloat
32+
}
33+
2334
/// Recursively checks that the given tree has the correct metadata everywhere.
2435
/// - Parameter tree: The tree to check.
25-
fileprivate func assertTreeMetadataCorrect(_ tree: TextLineStorage<TextLine>) throws {
26-
struct ChildData {
27-
let length: Int
28-
let count: Int
29-
let height: CGFloat
30-
}
31-
32-
func checkChildren(_ node: TextLineStorage<TextLine>.Node<TextLine>?) -> ChildData {
36+
fileprivate func assertTreeMetadataCorrect<T: Identifiable>(_ tree: TextLineStorage<T>) throws {
37+
func checkChildren(_ node: TextLineStorage<T>.Node<T>?) -> ChildData {
3338
guard let node else { return ChildData(length: 0, count: 0, height: 0.0) }
3439
let leftSubtreeData = checkChildren(node.left)
3540
let rightSubtreeData = checkChildren(node.right)
@@ -272,4 +277,111 @@ final class TextLayoutLineStorageTests: XCTestCase {
272277
}
273278
}
274279
}
280+
281+
func test_transplantWithExistingLeftNodes() throws { // swiftlint:disable:this function_body_length
282+
typealias Storage = TextLineStorage<UUID>
283+
typealias Node = TextLineStorage<UUID>.Node
284+
// Test that when transplanting a node with no left nodes, with a node with left nodes, that
285+
// the resulting tree has valid 'left_' metadata
286+
// 1
287+
// / \
288+
// 7 2
289+
// /
290+
// 3 ← this will be moved, this test ensures 4 retains it's left subtree count
291+
// \
292+
// 4
293+
// | |
294+
// 5 6
295+
296+
let node5 = Node(
297+
length: 5,
298+
data: UUID(),
299+
leftSubtreeOffset: 0,
300+
leftSubtreeHeight: 0,
301+
leftSubtreeCount: 0,
302+
height: 1,
303+
left: nil,
304+
right: nil,
305+
parent: nil,
306+
color: .black
307+
)
308+
309+
let node6 = Node(
310+
length: 6,
311+
data: UUID(),
312+
leftSubtreeOffset: 0,
313+
leftSubtreeHeight: 0,
314+
leftSubtreeCount: 0,
315+
height: 1,
316+
left: nil,
317+
right: nil,
318+
parent: nil,
319+
color: .black
320+
)
321+
322+
let node4 = Node(
323+
length: 4,
324+
data: UUID(),
325+
leftSubtreeOffset: 5,
326+
leftSubtreeHeight: 1,
327+
leftSubtreeCount: 1, // node5 is on the left
328+
height: 1,
329+
left: node5,
330+
right: node6,
331+
parent: nil,
332+
color: .black
333+
)
334+
node5.parent = node4
335+
node6.parent = node4
336+
337+
let node3 = Node(
338+
length: 3,
339+
data: UUID(),
340+
leftSubtreeOffset: 0,
341+
leftSubtreeHeight: 0,
342+
leftSubtreeCount: 0,
343+
height: 1,
344+
left: nil,
345+
right: node4,
346+
parent: nil,
347+
color: .black
348+
)
349+
node4.parent = node3
350+
351+
let node2 = Node(
352+
length: 2,
353+
data: UUID(),
354+
leftSubtreeOffset: 18,
355+
leftSubtreeHeight: 4,
356+
leftSubtreeCount: 4, // node3 is on the left
357+
height: 1,
358+
left: node3,
359+
right: nil,
360+
parent: nil,
361+
color: .black
362+
)
363+
node3.parent = node2
364+
365+
let node7 = Node(length: 7, data: UUID(), height: 1)
366+
367+
let node1 = Node(
368+
length: 1,
369+
data: UUID(),
370+
leftSubtreeOffset: 7,
371+
leftSubtreeHeight: 1,
372+
leftSubtreeCount: 1,
373+
height: 1,
374+
left: node7,
375+
right: node2,
376+
parent: nil,
377+
color: .black
378+
)
379+
node2.parent = node1
380+
381+
let storage = Storage(root: node1, count: 7, length: 28, height: 7)
382+
383+
storage.delete(lineAt: 7) // Delete the root
384+
385+
try assertTreeMetadataCorrect(storage)
386+
}
275387
}

0 commit comments

Comments
 (0)