diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformer.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformer.kt index faa7304d..844b03e4 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformer.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformer.kt @@ -16,5 +16,5 @@ interface BigTextTransformer { fun replace(range: IntRange, text: String, offsetMapping: BigTextTransformOffsetMapping) -// fun restoreToOriginal(range: IntRange) + fun restoreToOriginal(range: IntRange) } diff --git a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformerImpl.kt b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformerImpl.kt index 978d4194..72a8df08 100644 --- a/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformerImpl.kt +++ b/src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/ux/bigtext/BigTextTransformerImpl.kt @@ -92,19 +92,24 @@ class BigTextTransformerImpl(internal val delegate: BigTextImpl) : BigTextImpl(c return BigTextTransformNodeValue() } - fun insertOriginal(pos: Int, nodeValue: BigTextNodeValue) { - require(pos in 0 .. originalLength) { "Out of bound. pos = $pos, originalLength = $originalLength" } + fun insertOriginal( + pos: Int, + nodeValue: BigTextNodeValue, + bufferOffsetStart: Int = nodeValue.bufferOffsetStart, + bufferOffsetEndExclusive: Int = nodeValue.bufferOffsetEndExclusive, + ) { + require(pos in 0..originalLength) { "Out of bound. pos = $pos, originalLength = $originalLength" } insertChunkAtPosition( position = pos, - chunkedStringLength = nodeValue.bufferLength, + chunkedStringLength = bufferOffsetEndExclusive - bufferOffsetStart, ownership = BufferOwnership.Delegated, buffer = nodeValue.buffer, - range = nodeValue.bufferOffsetStart until nodeValue.bufferOffsetEndExclusive + range = bufferOffsetStart until bufferOffsetEndExclusive ) { bufferIndex = -1 - bufferOffsetStart = nodeValue.bufferOffsetStart - bufferOffsetEndExclusive = nodeValue.bufferOffsetEndExclusive + this.bufferOffsetStart = bufferOffsetStart + this.bufferOffsetEndExclusive = bufferOffsetEndExclusive this.buffer = nodeValue.buffer this.bufferOwnership = BufferOwnership.Delegated @@ -521,6 +526,41 @@ class BigTextTransformerImpl(internal val delegate: BigTextImpl) : BigTextImpl(c override fun replace(range: IntRange, text: String) = transformReplace(range, text) override fun replace(range: IntRange, text: String, offsetMapping: BigTextTransformOffsetMapping) = transformReplace(range, text, offsetMapping) + + override fun restoreToOriginal(range: IntRange) { + val renderPositionAtOriginalStart = findTransformedPositionByOriginalPosition(range.start) + val renderPositionAtOriginalEnd = findTransformedPositionByOriginalPosition(range.endInclusive) + + deleteTransformIf(range) + deleteOriginal(range) + + // insert the original text from `delegate` + val originalNodeStart = delegate.tree.findNodeByCharIndex(range.start) + ?: throw IndexOutOfBoundsException("Original node at position ${range.start} not found") + var nodePositionStart = delegate.tree.findPositionStart(originalNodeStart) + var insertPoint = range.start + var node = originalNodeStart + var insertOffsetStart = node.value.bufferOffsetStart + (range.start - nodePositionStart) + do { + val insertOffsetEndExclusive = if ((nodePositionStart + node.value.bufferLength) > (range.endInclusive + 1)) { + node.value.bufferOffsetStart + (range.endInclusive + 1 - nodePositionStart) + } else { + node.value.bufferOffsetEndExclusive + } + insertOriginal(insertPoint, node.value, insertOffsetStart, insertOffsetEndExclusive) + + if (insertPoint + (insertOffsetEndExclusive - insertOffsetStart) > range.endInclusive) { + break + } + + node = delegate.tree.nextNode(node)!! + nodePositionStart = delegate.tree.findPositionStart(node) + insertPoint += insertOffsetEndExclusive - insertOffsetStart + insertOffsetStart = node.value.bufferOffsetStart + } while (nodePositionStart <= range.endInclusive) + + layout(maxOf(0, renderPositionAtOriginalStart - 1), minOf(length, renderPositionAtOriginalEnd + 1)) + } } fun RedBlackTree.Node.transformedOffset(): Int = diff --git a/src/jvmTest/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/bigtext/transform/BigTextTransformerImplTest.kt b/src/jvmTest/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/bigtext/transform/BigTextTransformerImplTest.kt index 26c3f999..5ecce278 100644 --- a/src/jvmTest/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/bigtext/transform/BigTextTransformerImplTest.kt +++ b/src/jvmTest/kotlin/com/sunnychung/application/multiplatform/hellohttp/test/bigtext/transform/BigTextTransformerImplTest.kt @@ -3,6 +3,7 @@ package com.sunnychung.application.multiplatform.hellohttp.test.bigtext.transfor import com.sunnychung.application.multiplatform.hellohttp.test.bigtext.randomString import com.sunnychung.application.multiplatform.hellohttp.ux.bigtext.BigText import com.sunnychung.application.multiplatform.hellohttp.ux.bigtext.BigTextImpl +import com.sunnychung.application.multiplatform.hellohttp.ux.bigtext.BigTextTransformOffsetMapping import com.sunnychung.application.multiplatform.hellohttp.ux.bigtext.BigTextTransformerImpl import com.sunnychung.application.multiplatform.hellohttp.ux.bigtext.isD import org.junit.jupiter.api.BeforeEach @@ -895,6 +896,67 @@ class BigTextTransformerImplTest { } } + @ParameterizedTest + @ValueSource(ints = [1048576, 64, 16]) + fun restoreToOriginal(chunkSize: Int) { + val initialText = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + val original = BigTextImpl(chunkSize = chunkSize) + original.append(initialText) + val transformed = BigTextTransformerImpl(original) + + transformed.replace(11 .. 18, "ABCD", BigTextTransformOffsetMapping.Incremental) + "12345678901ABCD0123456789012345678901234567890123456789012345678901234567890".let { expected -> + assertEquals(expected, transformed.buildString()) + assertEquals(initialText, original.buildString()) + assertAllSubstring(expected, transformed) + } + + transformed.insertAt(61, "EFGHIJ") + "12345678901ABCD012345678901234567890123456789012345678901EFGHIJ2345678901234567890".let { expected -> + assertEquals(expected, transformed.buildString()) + assertEquals(initialText, original.buildString()) + assertAllSubstring(expected, transformed) + } + + transformed.restoreToOriginal(0 .. initialText.length - 1) + initialText.let { expected -> + assertEquals(expected, transformed.buildString()) + assertEquals(expected, original.buildString()) + assertAllSubstring(expected, transformed) + } + } + + @ParameterizedTest + @ValueSource(ints = [1048576, 64, 16]) + fun restoreToOriginalThenOriginalDelete(chunkSize: Int) { + val initialText = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + val original = BigTextImpl(chunkSize = chunkSize) + original.append(initialText) + val transformed = BigTextTransformerImpl(original) + + transformed.replace(11 .. 18, "ABCD", BigTextTransformOffsetMapping.WholeBlock) + "12345678901ABCD0123456789012345678901234567890123456789012345678901234567890".let { expected -> + assertEquals(expected, transformed.buildString()) + assertEquals(initialText, original.buildString()) + assertAllSubstring(expected, transformed) + } + + transformed.restoreToOriginal(11 .. 18) + initialText.let { expected -> + assertEquals(expected, transformed.buildString()) + assertEquals(expected, original.buildString()) + assertAllSubstring(expected, transformed) + } + + original.delete(18 .. 18) + + "1234567890123456780123456789012345678901234567890123456789012345678901234567890".let { expected -> + assertEquals(expected, original.buildString()) + assertEquals(expected, transformed.buildString()) + assertAllSubstring(expected, transformed) + } + } + @BeforeEach fun beforeEach() { isD = false