From 04dabe60af0ecdae5011cc8de0723c666f70836a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:09:23 +0200 Subject: [PATCH 1/5] feat(core): Add smart delete functionality Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../app/myzel394/numberhub/core/base/Token.kt | 4 + .../ui/common/textfield/SmartDeleteHandler.kt | 88 +++++++++++++++++++ .../textfield/TextFieldValueExtensions.kt | 9 ++ 3 files changed, 101 insertions(+) create mode 100644 core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/SmartDeleteHandler.kt diff --git a/core/base/src/main/java/app/myzel394/numberhub/core/base/Token.kt b/core/base/src/main/java/app/myzel394/numberhub/core/base/Token.kt index 1cda3b6d..5232f5f4 100644 --- a/core/base/src/main/java/app/myzel394/numberhub/core/base/Token.kt +++ b/core/base/src/main/java/app/myzel394/numberhub/core/base/Token.kt @@ -78,6 +78,10 @@ object Token { power, factorial, modulo, percent, sqrt, ) } + + val allWithoutBrackets by lazy { + all.filter { it !in listOf(leftBracket, rightBracket) } + } } object Func { 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 new file mode 100644 index 00000000..6876d07b --- /dev/null +++ b/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/SmartDeleteHandler.kt @@ -0,0 +1,88 @@ +package app.myzel394.numberhub.core.ui.common.textfield + +import androidx.compose.ui.text.TextRange +import app.myzel394.numberhub.core.base.Token + +// Smartly delete tokens +data class SmartDeleteHandler( + private val value: String, + private val selection: TextRange, +) { + fun calculateDeleteRange(): TextRange { + if (isSelectionARange()) { + return selection + } + + 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, + ); + + if (rightBracketPosition == null) { + val rightBracketRelativeToLeftPosition = findRightBracket(leftBracketPosition + 1) + + return if (rightBracketRelativeToLeftPosition == null) { + // 1+2(+5|+6 + TextRange(leftBracketPosition + 1, position) + } else { + // 1+2(6)+5|+6 + TextRange(takeNextIfIsOperator(rightBracketRelativeToLeftPosition + 1), position) + } + } + + return TextRange(leftBracketPosition + 1, findClosingParen(leftBracketPosition)); + } + + private fun takeNextIfIsOperator(position: Int): Int { + if (position + 1 < value.length && Token.Operator.allWithoutBrackets.contains(value[position].toString())) { + return position + 1 + } + + return position + } + + private fun isSelectionARange(): Boolean = selection.start != selection.end + + private fun findLeftBracket(startPosition: Int): Int? { + for (index in startPosition.coerceAtMost(value.length - 1) downTo 0) { + if (value[index] == Token.Operator.leftBracket[0]) { + return index + } + } + + return null + } + + private fun findRightBracket(startPosition: Int): Int? { + for (index in startPosition.coerceAtMost(value.length - 1) until value.length) { + if (value[index] == Token.Operator.rightBracket[0]) { + return index + } + } + + return null + } + + // Based of https://stackoverflow.com/a/12752226/9878135 + fun findClosingParen(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-- + } + } + return closePos + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/TextFieldValueExtensions.kt b/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/TextFieldValueExtensions.kt index c0cd0749..22403391 100644 --- a/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/TextFieldValueExtensions.kt +++ b/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/textfield/TextFieldValueExtensions.kt @@ -127,6 +127,15 @@ fun TextFieldValue.deleteTokens(): TextFieldValue { ) } +fun TextFieldValue.smartDeleteTokens(): TextFieldValue { + val deleteRange = SmartDeleteHandler(text, selection).calculateDeleteRange() + + return this.copy( + text = text.removeRange(deleteRange.start, deleteRange.end), + selection = TextRange(deleteRange.start), + ) +} + fun TextFieldValue.placeCursorAtTheEnd(): TextFieldValue = copy(selection = TextRange(text.length)) /** From 358f5b4582796aac9c807514d9dc7b1c332ddd59 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:09:49 +0200 Subject: [PATCH 2/5] feat(calculator): Add smart delete functionality to calculator Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../feature/calculator/CalculatorScreen.kt | 4 ++++ .../feature/calculator/CalculatorViewModel.kt | 10 ++++++++ .../components/CalculatorKeyboard.kt | 23 +++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorScreen.kt index 2824d554..41430fa8 100644 --- a/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorScreen.kt +++ b/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorScreen.kt @@ -95,6 +95,7 @@ internal fun CalculatorRoute( onAddTokenClick = viewModel::addTokens, onBracketsClick = viewModel::addBracket, onDeleteClick = viewModel::deleteTokens, + onSmartDeleteClick = viewModel::smartDeleteTokens, onClearClick = viewModel::clearInput, onEqualClick = viewModel::equal, onRadianModeClick = viewModel::updateRadianMode, @@ -114,6 +115,7 @@ internal fun Ready( onAddTokenClick: (String) -> Unit, onBracketsClick: () -> Unit, onDeleteClick: () -> Unit, + onSmartDeleteClick: () -> Unit, onClearClick: () -> Unit, onEqualClick: () -> Unit, onRadianModeClick: (Boolean) -> Unit, @@ -250,6 +252,7 @@ internal fun Ready( onAddTokenClick = onAddTokenClick, onBracketsClick = onBracketsClick, onDeleteClick = onDeleteClick, + onSmartDeleteClick = onSmartDeleteClick, onClearClick = onClearClick, onEqualClick = { focusManager.clearFocus() @@ -349,6 +352,7 @@ private fun PreviewCalculatorScreen() { onAddTokenClick = {}, onBracketsClick = {}, onDeleteClick = {}, + onSmartDeleteClick = {}, onClearClick = {}, onEqualClick = {}, onRadianModeClick = {}, diff --git a/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorViewModel.kt b/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorViewModel.kt index 2644cdd8..104e5cea 100644 --- a/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorViewModel.kt +++ b/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/CalculatorViewModel.kt @@ -28,6 +28,7 @@ import app.myzel394.numberhub.core.ui.common.textfield.addTokens import app.myzel394.numberhub.core.ui.common.textfield.deleteTokens import app.myzel394.numberhub.core.ui.common.textfield.getTextField import app.myzel394.numberhub.core.ui.common.textfield.placeCursorAtTheEnd +import app.myzel394.numberhub.core.ui.common.textfield.smartDeleteTokens import app.myzel394.numberhub.data.common.format import app.myzel394.numberhub.data.common.isExpression import app.myzel394.numberhub.data.common.isGreaterThan @@ -120,6 +121,15 @@ internal class CalculatorViewModel @Inject constructor( updateInput(newValue) } + fun smartDeleteTokens() { + val newValue = if (equalClicked.value) { + TextFieldValue() + } else { + input.value.smartDeleteTokens() + } + updateInput(newValue) + } + fun clearInput() = updateInput(TextFieldValue()) fun updateInput(value: TextFieldValue) { diff --git a/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/components/CalculatorKeyboard.kt b/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/components/CalculatorKeyboard.kt index 674b23c3..3130e044 100644 --- a/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/components/CalculatorKeyboard.kt +++ b/feature/calculator/src/main/java/app/myzel394/numberhub/feature/calculator/components/CalculatorKeyboard.kt @@ -112,6 +112,7 @@ internal fun CalculatorKeyboard( onAddTokenClick: (String) -> Unit, onBracketsClick: () -> Unit, onDeleteClick: () -> Unit, + onSmartDeleteClick: () -> Unit, onClearClick: () -> Unit, onEqualClick: () -> Unit, radianMode: Boolean, @@ -130,6 +131,7 @@ internal fun CalculatorKeyboard( onAddTokenClick = onAddTokenClick, onBracketsClick = onBracketsClick, onDeleteClick = onDeleteClick, + onSmartDeleteClick = onSmartDeleteClick, onClearClick = onClearClick, onEqualClick = onEqualClick, radianMode = radianMode, @@ -146,6 +148,7 @@ internal fun CalculatorKeyboard( onAddTokenClick = onAddTokenClick, onBracketsClick = onBracketsClick, onDeleteClick = onDeleteClick, + onSmartDeleteClick = onSmartDeleteClick, onClearClick = onClearClick, onEqualClick = onEqualClick, radianMode = radianMode, @@ -167,6 +170,7 @@ private fun PortraitKeyboard( onAddTokenClick: (String) -> Unit, onBracketsClick: () -> Unit, onDeleteClick: () -> Unit, + onSmartDeleteClick: () -> Unit, onClearClick: () -> Unit, onEqualClick: () -> Unit, radianMode: Boolean, @@ -318,7 +322,13 @@ private fun PortraitKeyboard( KeyboardButtonLight(mainButtonModifier, IconPack.Key0, Token.Digit._0, KeyboardButtonToken.CONTENT_HEIGHT_TALL) { onAddTokenClick(Token.Digit._0) } KeyboardButtonLight(mainButtonModifier, fractionalIcon, stringResource(fractionalIconDescription), KeyboardButtonToken.CONTENT_HEIGHT_TALL) { onAddTokenClick(Token.Digit.dot) } } - KeyboardButtonLight(mainButtonModifier, IconPack.Backspace, stringResource(R.string.delete_label), KeyboardButtonToken.CONTENT_HEIGHT_TALL, onClearClick) { onDeleteClick() } + KeyboardButtonLight( + mainButtonModifier, + IconPack.Backspace, + stringResource(R.string.delete_label), + KeyboardButtonToken.CONTENT_HEIGHT_TALL, + onSmartDeleteClick, + ) { onDeleteClick() } KeyboardButtonFilled(mainButtonModifier, IconPack.Equal, stringResource(R.string.keyboard_equal), KeyboardButtonToken.CONTENT_HEIGHT_TALL) { onEqualClick() } } @@ -400,6 +410,7 @@ private fun LandscapeKeyboard( onAddTokenClick: (String) -> Unit, onBracketsClick: () -> Unit, onDeleteClick: () -> Unit, + onSmartDeleteClick: () -> Unit, onClearClick: () -> Unit, onEqualClick: () -> Unit, radianMode: Boolean, @@ -473,7 +484,13 @@ private fun LandscapeKeyboard( KeyboardButtonLight(buttonModifier, IconPack.Key0, Token.Digit._0, KeyboardButtonToken.CONTENT_HEIGHT_SHORT) { onAddTokenClick(Token.Digit._0) } KeyboardButtonLight(buttonModifier, fractionalIcon, stringResource(fractionalIconDescription), KeyboardButtonToken.CONTENT_HEIGHT_SHORT) { onAddTokenClick(Token.Digit.dot) } } - KeyboardButtonLight(buttonModifier, IconPack.Backspace, stringResource(R.string.delete_label), KeyboardButtonToken.CONTENT_HEIGHT_SHORT, onClearClick) { onDeleteClick() } + KeyboardButtonLight( + buttonModifier, + IconPack.Backspace, + stringResource(R.string.delete_label), + KeyboardButtonToken.CONTENT_HEIGHT_SHORT, + onSmartDeleteClick, + ) { onDeleteClick() } KeyboardButtonFilled(buttonModifier, IconPack.Plus, stringResource(R.string.keyboard_plus), KeyboardButtonToken.CONTENT_HEIGHT_SHORT) { onAddTokenClick(Token.Operator.plus) } KeyboardButtonFilled(buttonModifier, IconPack.Equal, stringResource(R.string.keyboard_equal), KeyboardButtonToken.CONTENT_HEIGHT_SHORT) { onEqualClick() } } @@ -545,6 +562,7 @@ private fun PreviewPortraitKeyboard() { onAddTokenClick = {}, onBracketsClick = {}, onDeleteClick = {}, + onSmartDeleteClick = {}, onClearClick = {}, onEqualClick = {}, radianMode = true, @@ -567,6 +585,7 @@ private fun PreviewLandscapeKeyboard() { onAddTokenClick = {}, onBracketsClick = {}, onDeleteClick = {}, + onSmartDeleteClick = {}, onClearClick = {}, onEqualClick = {}, radianMode = true, From 6cb9532e6e24d57d7c96bea8b1c007373cdab2c5 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:17:28 +0200 Subject: [PATCH 3/5] test(core): Add tests for SmartDeleteHandler Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../ui/common/textfield/SmartDeleteHandler.kt | 26 ++- .../core/ui/SmartDeleteHandlerTest.kt | 161 ++++++++++++++++++ 2 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 core/ui/src/test/java/app/myzel394/numberhub/core/ui/SmartDeleteHandlerTest.kt 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 6876d07b..e067b459 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 @@ -3,12 +3,24 @@ package app.myzel394.numberhub.core.ui.common.textfield import androidx.compose.ui.text.TextRange import app.myzel394.numberhub.core.base.Token -// Smartly delete tokens +/** Smartly delete tokens + * @param value the current value of the text field + * @param selection the current selection of the text field - Assumed to be a valid selection + */ data class SmartDeleteHandler( private val value: String, private val selection: TextRange, ) { + /** + * Calculate the range to delete based on the current selection. + * + * @return the range to delete - [Inclusive, Exclusive] + */ fun calculateDeleteRange(): TextRange { + if (value == "") { + return TextRange(0, 0) + } + if (isSelectionARange()) { return selection } @@ -30,14 +42,20 @@ data class SmartDeleteHandler( return if (rightBracketRelativeToLeftPosition == null) { // 1+2(+5|+6 - TextRange(leftBracketPosition + 1, position) + TextRange((leftBracketPosition + 1).coerceAtMost(position), position) } else { // 1+2(6)+5|+6 - TextRange(takeNextIfIsOperator(rightBracketRelativeToLeftPosition + 1), position) + TextRange( + takeNextIfIsOperator(rightBracketRelativeToLeftPosition + 1).coerceAtMost( + position, + ), + position, + ) } } - return TextRange(leftBracketPosition + 1, findClosingParen(leftBracketPosition)); + val end = findClosingParen(leftBracketPosition) + return TextRange((leftBracketPosition + 1).coerceAtMost(end), end); } private fun takeNextIfIsOperator(position: Int): Int { 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 new file mode 100644 index 00000000..12a60880 --- /dev/null +++ b/core/ui/src/test/java/app/myzel394/numberhub/core/ui/SmartDeleteHandlerTest.kt @@ -0,0 +1,161 @@ +package app.myzel394.numberhub.core.ui + +import androidx.compose.ui.text.TextRange +import app.myzel394.numberhub.core.ui.common.textfield.SmartDeleteHandler +import org.junit.Test + +class SmartDeleteHandlerTest { + @Test + fun `test simple delete`() { + assertDeleteRange( + "1+2", + TextRange(2, 2), + TextRange(0, 2), + ) + } + + @Test + fun `test brackets, position in between`() { + assertDeleteRange( + "1+(2+3)", + TextRange(4, 4), + TextRange(3, 6), + ) + } + + @Test + fun `test brackets, position at the end`() { + assertDeleteRange( + "1+(2+3)", + TextRange(6, 6), + TextRange(3, 6), + ) + } + + @Test + fun `test brackets, position at the start`() { + assertDeleteRange( + "1+(2+3)", + TextRange(3, 3), + TextRange(3, 6), + ) + } + + @Test + fun `test brackets, but position outside`() { + assertDeleteRange( + "1+(2+3)", + TextRange(1, 1), + TextRange(0, 1), + ) + } + + @Test + fun `test brackets, but cursor outside right of it`() { + assertDeleteRange( + "1+(2+3)54", + TextRange(8, 8), + TextRange(7, 8), + ) + } + + @Test + fun `test brackets, but cursor outside right of it, should go to the operator`() { + assertDeleteRange( + "1+(2+3)×54", + TextRange(9, 9), + TextRange(8, 9), + ) + } + + @Test + fun `test nested brackets, inside the nested one`() { + assertDeleteRange( + "1+(2+(3+4))", + TextRange(8, 8), + TextRange(6, 9), + ) + } + + @Test + fun `test nested brackets, inside the outside one`() { + assertDeleteRange( + "1+(2+(3+4))", + TextRange(4, 4), + TextRange(3, 10), + ) + } + + @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), + ) + } + + @Test + fun `test empty string`() { + assertDeleteRange( + "", + TextRange(0, 0), + TextRange(0, 0), + ) + } + + @Test + fun `test single character with cursor ar 0,0`() { + assertDeleteRange( + "1", + TextRange(0, 0), + TextRange(0, 0), + ) + } + + @Test + fun `test single bracket with cursor at 0,0`() { + assertDeleteRange( + "(", + TextRange(0, 0), + TextRange(0, 0), + ) + } + + @Test + fun `test nested brackets with operators`() { + assertDeleteRange( + "1+(2*(3+4))", + TextRange(8, 8), + TextRange(6, 9), + ) + } + + @Test + fun `test selection range spanning multiple tokens`() { + assertDeleteRange( + "1+(2+3)*4", + TextRange(2, 6), + TextRange(2, 6), + ) + } + + + private fun assertDeleteRange(input: String, selection: TextRange, expected: TextRange) { + val smartDeleteHandler = + SmartDeleteHandler(input, selection) + val actual = smartDeleteHandler.calculateDeleteRange() + assert(expected == actual) { + "Expected: $expected, actual: $actual" + } + } +} \ No newline at end of file From 6d535a9adf058966be5731def083a0c1fd59ec56 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 26 Jul 2024 21:13:33 +0200 Subject: [PATCH 4/5] chore: Update dependencies Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 345b32ee..fad8f577 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ versionName = "0.2.1" androidxBrowserBrowser = "1.8.0" androidGradlePlugin = "8.3.2" -androidxActivityActivityCompose = "1.9.0" +androidxActivityActivityCompose = "1.9.1" androidxAppCompatAppCompat = "1.7.0" androidxCompose = "1.6.8" androidxComposeCompiler = "1.5.9" @@ -15,7 +15,7 @@ androidxDatastoreDatastorePreferences = "1.1.1" androidxEspresso = "3.6.1" androidxHiltHiltNavigationCompose = "1.2.0" androidxMacroBenchmark = "1.2.4" -androidxLifecycleLifecycleRuntimeCompose = "2.8.3" +androidxLifecycleLifecycleRuntimeCompose = "2.8.4" androidxNavigationNavigationCompose = "2.7.7" androidxProfileinstallerProfileinstaller = "1.3.1" androidxRoom = "2.6.1" 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 5/5] fix: Improve smart delete behavior Signed-off-by: Myzel394 <50424412+Myzel394@users.noreply.github.com> --- .../ui/common/textfield/SmartDeleteHandler.kt | 119 +++++++++++++----- .../core/ui/SmartDeleteHandlerTest.kt | 80 ++++++++++-- 2 files changed, 158 insertions(+), 41 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, - ); - - if (rightBracketPosition == null) { - val rightBracketRelativeToLeftPosition = findRightBracket(leftBracketPosition + 1) - - return if (rightBracketRelativeToLeftPosition == null) { - // 1+2(+5|+6 - TextRange((leftBracketPosition + 1).coerceAtMost(position), position) + when (position) { + 0 -> return TextRange(0, 0) + 1 -> return TextRange(0, 1) + } + + val bracketPos = findPreviousBracket(position.coerceAtMost(value.length - 1) - 1) + + 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,25 +82,25 @@ class SmartDeleteHandlerTest { assertDeleteRange( "1+(2+(3+4))", TextRange(4, 4), - TextRange(3, 10), + TextRange(3, 4), ) } @Test - fun `test nested empty brackets`() { + fun `test nested brackets, inside the outside one, at edge`() { assertDeleteRange( - "1+(2+())", - TextRange(4, 4), - TextRange(3, 7), + "1+(2+(3+4))", + TextRange(3, 3), + TextRange(3, 10), ) } @Test - fun `test pure empty nested brackets`() { + fun `test nested empty 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 =