From 7045f438b57d3752e778b15788a33d041cc7d479 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:25:13 +0200 Subject: [PATCH] fix: Improve smart delete behavior Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../ui/common/textfield/SmartDeleteHandler.kt | 115 ++++++++++++++---- .../core/ui/SmartDeleteHandlerTest.kt | 86 ++++++++++--- 2 files changed, 159 insertions(+), 42 deletions(-) diff --git a/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/SmartDeleteHandler.kt b/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/SmartDeleteHandler.kt index e067b459..ed0df756 100644 --- a/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/SmartDeleteHandler.kt +++ b/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/SmartDeleteHandler.kt @@ -27,35 +27,47 @@ data class SmartDeleteHandler( val position = selection.start - val rightBracketPosition = - findRightBracket(position); - val leftBracketPosition = - findLeftBracket(position) - ?: return TextRange( - rightBracketPosition?.let { takeNextIfIsOperator(it) } - ?.let { if (it > position) 0 else it } ?: 0, - position, - ); + when (position) { + 0 -> return TextRange(0, 0) + 1 -> return TextRange(0, 1) + } - if (rightBracketPosition == null) { - val rightBracketRelativeToLeftPosition = findRightBracket(leftBracketPosition + 1) + val bracketPos = findPreviousBracket(position.coerceAtMost(value.length - 1) - 1) - return if (rightBracketRelativeToLeftPosition == null) { - // 1+2(+5|+6 - TextRange((leftBracketPosition + 1).coerceAtMost(position), position) + if (bracketPos == null) { + return TextRange(0, position) + } + + val isAtLeftEdge = + position - 1 == bracketPos && value[bracketPos] == Token.Operator.leftBracket[0] + val isAtRightEdge = + position - 1 == bracketPos && value[bracketPos] == Token.Operator.rightBracket[0] + + if (!isAtLeftEdge && !isAtRightEdge) { + return TextRange(bracketPos + 1, position) + } + + if (isAtRightEdge) { + val leftBracketPos = findClosingParenBackwards(bracketPos) + + return if (leftBracketPos != null) { + TextRange(leftBracketPos + 1, position) } else { - // 1+2(6)+5|+6 - TextRange( - takeNextIfIsOperator(rightBracketRelativeToLeftPosition + 1).coerceAtMost( - position, - ), - position, - ) + // Weird case, should not happen + TextRange(0, position + 1) } } - val end = findClosingParen(leftBracketPosition) - return TextRange((leftBracketPosition + 1).coerceAtMost(end), end); + val rightBracketPos = findClosingParen(bracketPos) + + if (rightBracketPos != null) { + return TextRange(bracketPos + 1, rightBracketPos) + } + + // Find previous bracket and return range from there to cursor position + val previousBracketPos = findPreviousBracket(bracketPos - 1)?.let { it + 1 } ?: 0 + + return TextRange(previousBracketPos, position) } private fun takeNextIfIsOperator(position: Int): Int { @@ -68,6 +80,16 @@ data class SmartDeleteHandler( private fun isSelectionARange(): Boolean = selection.start != selection.end + private fun findPreviousBracket(startPosition: Int): Int? { + for (index in startPosition.coerceAtMost(value.length - 1) downTo 0) { + if (value[index] == Token.Operator.rightBracket[0] || value[index] == Token.Operator.leftBracket[0]) { + return index + } + } + + return null + } + private fun findLeftBracket(startPosition: Int): Int? { for (index in startPosition.coerceAtMost(value.length - 1) downTo 0) { if (value[index] == Token.Operator.leftBracket[0]) { @@ -79,7 +101,7 @@ data class SmartDeleteHandler( } private fun findRightBracket(startPosition: Int): Int? { - for (index in startPosition.coerceAtMost(value.length - 1) until value.length) { + for (index in startPosition.coerceAtMost(value.length - 1) downTo 0) { if (value[index] == Token.Operator.rightBracket[0]) { return index } @@ -88,19 +110,60 @@ data class SmartDeleteHandler( return null } + private fun isAtEdge(position: Int): Boolean { + if (position == 0) { + return false + } + + val previousCharacter = value[position.coerceAtMost(value.length - 1) - 1] + + return previousCharacter == Token.Operator.leftBracket[0] || previousCharacter == Token.Operator.rightBracket[0] + } + // Based of https://stackoverflow.com/a/12752226/9878135 - fun findClosingParen(openPos: Int): Int { + fun findClosingParen(openPos: Int): Int? { var closePos = openPos var counter = 1 while (counter > 0) { - val c = value[++closePos] + val nextPos = ++closePos + + if (nextPos >= value.length) { + return null + } + + val c = value[nextPos] if (c == Token.Operator.leftBracket[0]) { counter++ } else if (c == Token.Operator.rightBracket[0]) { counter-- } } + + if (closePos == openPos) { + return null + } + + return closePos + } + + fun findClosingParenBackwards(openPos: Int): Int? { + var closePos = openPos + var counter = 1 + + while (counter > 0) { + val c = value[--closePos] + if (c == Token.Operator.leftBracket[0]) { + counter-- + } else if (c == Token.Operator.rightBracket[0]) { + counter++ + } + } + + if (closePos == openPos) { + return null + } + return closePos } } \ No newline at end of file diff --git a/core/ui/src/test/java/app/myzel394/numberhub/core/ui/SmartDeleteHandlerTest.kt b/core/ui/src/test/java/app/myzel394/numberhub/core/ui/SmartDeleteHandlerTest.kt index 12a60880..9fbd2b14 100644 --- a/core/ui/src/test/java/app/myzel394/numberhub/core/ui/SmartDeleteHandlerTest.kt +++ b/core/ui/src/test/java/app/myzel394/numberhub/core/ui/SmartDeleteHandlerTest.kt @@ -19,7 +19,7 @@ class SmartDeleteHandlerTest { assertDeleteRange( "1+(2+3)", TextRange(4, 4), - TextRange(3, 6), + TextRange(3, 4), ) } @@ -64,7 +64,7 @@ class SmartDeleteHandlerTest { assertDeleteRange( "1+(2+3)×54", TextRange(9, 9), - TextRange(8, 9), + TextRange(7, 9), ) } @@ -73,7 +73,7 @@ class SmartDeleteHandlerTest { assertDeleteRange( "1+(2+(3+4))", TextRange(8, 8), - TextRange(6, 9), + TextRange(6, 8), ) } @@ -82,6 +82,15 @@ class SmartDeleteHandlerTest { assertDeleteRange( "1+(2+(3+4))", TextRange(4, 4), + TextRange(3, 4), + ) + } + + @Test + fun `test nested brackets, inside the outside one, at edge`() { + assertDeleteRange( + "1+(2+(3+4))", + TextRange(3, 3), TextRange(3, 10), ) } @@ -89,18 +98,9 @@ class SmartDeleteHandlerTest { @Test fun `test nested empty brackets`() { assertDeleteRange( - "1+(2+())", - TextRange(4, 4), - TextRange(3, 7), - ) - } - - @Test - fun `test pure empty nested brackets`() { - assertDeleteRange( - "1+((()))", - TextRange(5, 5), - TextRange(5, 5), + "1+(2+(6))", + TextRange(3, 3), + TextRange(3, 8), ) } @@ -122,6 +122,24 @@ class SmartDeleteHandlerTest { ) } + @Test + fun `test right bracket cursor after it`() { + assertDeleteRange( + "5+(2+6)+4", + TextRange(8, 8), + TextRange(7, 8), + ) + } + + @Test + fun `test right bracket cursor at edge`() { + assertDeleteRange( + "5+(2+6)+4", + TextRange(7, 7), + TextRange(3, 7), + ) + } + @Test fun `test single bracket with cursor at 0,0`() { assertDeleteRange( @@ -135,7 +153,7 @@ class SmartDeleteHandlerTest { fun `test nested brackets with operators`() { assertDeleteRange( "1+(2*(3+4))", - TextRange(8, 8), + TextRange(6, 6), TextRange(6, 9), ) } @@ -149,6 +167,42 @@ class SmartDeleteHandlerTest { ) } + @Test + fun `test empty brackets deletes rest`() { + assertDeleteRange( + "5+(", + TextRange(3, 3), + TextRange(0, 3), + ) + } + + @Test + fun `left bracket, no right bracket`() { + assertDeleteRange( + "1+(5+6+", + TextRange(8, 8), + TextRange(3, 8), + ) + } + + @Test + fun `no bracket`() { + assertDeleteRange( + "1+5+6", + TextRange(5, 5), + TextRange(0, 5), + ) + } + + @Test + fun `closed bracket before, but left bracket after`() { + assertDeleteRange( + "1+(5+6)+(5+", + TextRange(11, 11), + TextRange(9, 11), + ) + } + private fun assertDeleteRange(input: String, selection: TextRange, expected: TextRange) { val smartDeleteHandler =