From be668bc58799e1ef5f2f6a7652d6c6a2cdb190dd Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:07:05 -0500 Subject: [PATCH 1/2] Fix Text Disappearing --- .../TextLineStorage/TextLineStorage.swift | 13 +- .../TextView/TextView+ReplaceCharacters.swift | 6 + .../TextLayoutLineStorageTests.swift | 128 ++++++++++++++++-- 3 files changed, 136 insertions(+), 11 deletions(-) diff --git a/Sources/CodeEditTextView/TextLineStorage/TextLineStorage.swift b/Sources/CodeEditTextView/TextLineStorage/TextLineStorage.swift index 72b94effd..f944fae30 100644 --- a/Sources/CodeEditTextView/TextLineStorage/TextLineStorage.swift +++ b/Sources/CodeEditTextView/TextLineStorage/TextLineStorage.swift @@ -58,6 +58,13 @@ public final class TextLineStorage { public init() { } + init(root: Node, count: Int, length: Int, height: CGFloat) { + self.root = root + self.count = count + self.length = length + self.height = height + } + // MARK: - Public Methods /// Inserts a new line for the given range. @@ -408,9 +415,9 @@ private extension TextLineStorage { } else { transplant(nodeY, with: nodeY.right) - nodeY.right?.leftSubtreeCount = nodeY.leftSubtreeCount - nodeY.right?.leftSubtreeHeight = nodeY.leftSubtreeHeight - nodeY.right?.leftSubtreeOffset = nodeY.leftSubtreeOffset + nodeY.right?.leftSubtreeCount += nodeY.leftSubtreeCount + nodeY.right?.leftSubtreeHeight += nodeY.leftSubtreeHeight + nodeY.right?.leftSubtreeOffset += nodeY.leftSubtreeOffset nodeY.right = nodeZ.right nodeY.right?.parent = nodeY diff --git a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift index 6acd0d040..93fc2e715 100644 --- a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift +++ b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift @@ -47,6 +47,12 @@ extension TextView { } textStorage.endEditing() + + // Cause a layout pass now that we've finished editing, if there were any edits. + if !ranges.isEmpty { + layout() + } + if !skipUpdateSelection { selectionManager.notifyAfterEdit() } diff --git a/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift b/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift index 30b9376de..1acec9a87 100644 --- a/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift +++ b/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift @@ -7,7 +7,12 @@ fileprivate extension CGFloat { } } +extension UUID: @retroactive Identifiable { + public var id: UUID { self } +} + final class TextLayoutLineStorageTests: XCTestCase { + /// Creates a balanced height=3 tree useful for testing and debugging. /// - Returns: A new tree. fileprivate func createBalancedTree() -> TextLineStorage { @@ -20,16 +25,16 @@ final class TextLayoutLineStorageTests: XCTestCase { return tree } + struct ChildData { + let length: Int + let count: Int + let height: CGFloat + } + /// Recursively checks that the given tree has the correct metadata everywhere. /// - Parameter tree: The tree to check. - fileprivate func assertTreeMetadataCorrect(_ tree: TextLineStorage) throws { - struct ChildData { - let length: Int - let count: Int - let height: CGFloat - } - - func checkChildren(_ node: TextLineStorage.Node?) -> ChildData { + fileprivate func assertTreeMetadataCorrect(_ tree: TextLineStorage) throws { + func checkChildren(_ node: TextLineStorage.Node?) -> ChildData { guard let node else { return ChildData(length: 0, count: 0, height: 0.0) } let leftSubtreeData = checkChildren(node.left) let rightSubtreeData = checkChildren(node.right) @@ -272,4 +277,111 @@ final class TextLayoutLineStorageTests: XCTestCase { } } } + + func test_transplantWithExistingLeftNodes() throws { // swiftlint:disable:this function_body_length + typealias Storage = TextLineStorage + typealias Node = TextLineStorage.Node + // Test that when transplanting a node with no left nodes, with a node with left nodes, that + // the resulting tree has valid 'left_' metadata + // 1 + // / \ + // 7 2 + // / + // 3 ← this will be moved, this test ensures 4 retains it's left subtree count + // \ + // 4 + // | | + // 5 6 + + let node5 = Node( + length: 5, + data: UUID(), + leftSubtreeOffset: 0, + leftSubtreeHeight: 0, + leftSubtreeCount: 0, + height: 1, + left: nil, + right: nil, + parent: nil, + color: .black + ) + + let node6 = Node( + length: 6, + data: UUID(), + leftSubtreeOffset: 0, + leftSubtreeHeight: 0, + leftSubtreeCount: 0, + height: 1, + left: nil, + right: nil, + parent: nil, + color: .black + ) + + let node4 = Node( + length: 4, + data: UUID(), + leftSubtreeOffset: 5, + leftSubtreeHeight: 1, + leftSubtreeCount: 1, // node5 is on the left + height: 1, + left: node5, + right: node6, + parent: nil, + color: .black + ) + node5.parent = node4 + node6.parent = node4 + + let node3 = Node( + length: 3, + data: UUID(), + leftSubtreeOffset: 0, + leftSubtreeHeight: 0, + leftSubtreeCount: 0, + height: 1, + left: nil, + right: node4, + parent: nil, + color: .black + ) + node4.parent = node3 + + let node2 = Node( + length: 2, + data: UUID(), + leftSubtreeOffset: 18, + leftSubtreeHeight: 4, + leftSubtreeCount: 4, // node3 is on the left + height: 1, + left: node3, + right: nil, + parent: nil, + color: .black + ) + node3.parent = node2 + + let node7 = Node(length: 7, data: UUID(), height: 1) + + let node1 = Node( + length: 1, + data: UUID(), + leftSubtreeOffset: 7, + leftSubtreeHeight: 1, + leftSubtreeCount: 1, + height: 1, + left: node7, + right: node2, + parent: nil, + color: .black + ) + node2.parent = node1 + + let storage = Storage(root: node1, count: 7, length: 28, height: 7) + + storage.delete(lineAt: 7) // Delete the root + + try assertTreeMetadataCorrect(storage) + } } From a26562c02c7897589db4a6c2dadecca5dbb00861 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:12:36 -0500 Subject: [PATCH 2/2] Linter --- Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift b/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift index 1acec9a87..5316f1638 100644 --- a/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift +++ b/Tests/CodeEditTextViewTests/TextLayoutLineStorageTests.swift @@ -11,7 +11,7 @@ extension UUID: @retroactive Identifiable { public var id: UUID { self } } -final class TextLayoutLineStorageTests: XCTestCase { +final class TextLayoutLineStorageTests: XCTestCase { // swiftlint:disable:this type_body_length /// Creates a balanced height=3 tree useful for testing and debugging. /// - Returns: A new tree.