From d61c26472b46afe5b284a4e3397f9f7721409881 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Sun, 24 May 2026 19:15:59 +0300 Subject: [PATCH 1/2] Fix two-thumb backspace fragment modes Preserve committed gesture fragment metadata so Delete last fragment can pop repeated fragments after delayed autospace commits. Also rename the setting label and make whole-word live composing deletion respect its sub-option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../keyboard/latin/inputlogic/InputLogic.java | 138 ++++++++++++++---- app/src/main/res/values/strings.xml | 2 +- .../keyboard/latin/InputLogicTest.kt | 85 ++++++++++- .../settings/SettingsContainerTest.kt | 7 + dev-log.md | 37 +++++ 5 files changed, 237 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 14a93db93..e42efb302 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -134,7 +134,11 @@ public final class InputLogic { // character at a time. The list is kept in sync with {@code mWordComposer.getTypedWord()}: // entries past the current length are filtered at read time, and the list is cleared // outright whenever the composing word is committed or reset. - private final java.util.ArrayList mGestureFragmentBoundaries = new java.util.ArrayList<>(); + private final ArrayList mGestureFragmentBoundaries = new ArrayList<>(); + /** Fragment lengths within the most recent gesture-driven commit. The last entry includes + * the trailing autospace if one was inserted. Used by "Delete last fragment" after the + * composing word has already been auto-committed. */ + private final ArrayList mLastGestureCommittedFragmentLengths = new ArrayList<>(); // ---- Unified combining-mode state machine ---------------------------------------------- // After every composing-word-extending event (tap of a letter OR gesture completion), @@ -882,8 +886,7 @@ public void onCancelBatchInput(final LatinIME.UIHandler handler) { // composing word is committed or reset; stale entries beyond the current length are // filtered in {@link #tryFragmentBackspace} as a defensive measure. - /** Record a fragment boundary at the current composing-word length. No-op when not in manual+fragment mode. */ - private void recordFragmentBoundaryIfTracking(final SettingsValues sv) { + private boolean shouldTrackFragmentBoundaries(final SettingsValues sv) { // Two paths into fragment tracking: // * Legacy manual-spacing mode (#1.1) — gated on both prefs. // * Multi-part word composition (#1.6) — combining mode is the trigger; fragment @@ -893,9 +896,20 @@ private void recordFragmentBoundaryIfTracking(final SettingsValues sv) { final boolean multipartTracking = sv.mMultipartAutoExtendInCombining && sv.mCombiningGraceMs > 0 && sv.mGestureFragmentBackspace; - if (!legacyTracking && !multipartTracking) return; + return legacyTracking || multipartTracking; + } + + /** Record a fragment boundary at the current composing-word length. No-op when not in fragment mode. */ + private void recordFragmentBoundaryIfTracking(final SettingsValues sv) { + if (!shouldTrackFragmentBoundaries(sv)) return; if (!mWordComposer.isComposingWord()) return; - final int len = mWordComposer.getTypedWord().length(); + recordFragmentBoundaryIfTracking(sv, mWordComposer.getTypedWord().length()); + } + + /** Record a fragment boundary at a known composing-word length. */ + private void recordFragmentBoundaryIfTracking(final SettingsValues sv, final int len) { + if (!shouldTrackFragmentBoundaries(sv)) return; + if (len <= 0) return; // Don't record duplicates (e.g. the same fragment appended twice in quick succession). if (!mGestureFragmentBoundaries.isEmpty() && mGestureFragmentBoundaries.get(mGestureFragmentBoundaries.size() - 1) == len) { @@ -909,6 +923,13 @@ private void clearFragmentBoundaries() { if (!mGestureFragmentBoundaries.isEmpty()) mGestureFragmentBoundaries.clear(); } + private void clearCommittedGestureBackspaceState() { + mLastGestureCommittedLength = 0; + if (!mLastGestureCommittedFragmentLengths.isEmpty()) { + mLastGestureCommittedFragmentLengths.clear(); + } + } + // ---- Unified combining-mode helpers -------------------------------------------------- /** @@ -925,7 +946,7 @@ private void enterCombiningMode(final SettingsValues settingsValues, final boole // next-word predictions" one-shot — the user moved on to typing the next word. mAutospaceAlternativesPending = false; mAutoCommitRevertLength = 0; - mLastGestureCommittedLength = 0; + clearCommittedGestureBackspaceState(); mAutospaceJustWritten = false; final int baseGraceMs = settingsValues.mCombiningGraceMs; if (baseGraceMs <= 0) return; @@ -955,7 +976,7 @@ private void enterJoinNextMode(final SettingsValues settingsValues) { OneShotSpaceAction.armJoinNext(); mAutospaceAlternativesPending = false; mAutoCommitRevertLength = 0; - mLastGestureCommittedLength = 0; + clearCommittedGestureBackspaceState(); mAutospaceJustWritten = false; if (Constants.CODE_SPACE == mConnection.getCodePointBeforeCursor() && !mWordComposer.isComposingWord()) { @@ -1018,7 +1039,7 @@ void cancelCombiningMode() { // represent the user explicitly leaving the just-auto-committed context. mAutospaceAlternativesPending = false; mAutoCommitRevertLength = 0; - mLastGestureCommittedLength = 0; + clearCommittedGestureBackspaceState(); mAutospaceJustWritten = false; mCombiningWordHasGestureFragment = false; if (mInCombiningMode) { @@ -1097,6 +1118,8 @@ private void onCombiningGraceExpired() { // commit itself doesn't move the cursor. We need the typed-word length plus whatever // chars the autospace path writes to know how much to undo on a suggestion-pick. final String typedWordAtCommit = mWordComposer.getTypedWord(); + final ArrayList fragmentLengthsAtCommit = + getFragmentLengthsForCommit(typedWordAtCommit.length()); final int cursorBefore = mConnection.getExpectedSelectionEnd(); mConnection.beginBatchEdit(); if (sv.mCombiningAutocorrectOnAutospace) { @@ -1184,6 +1207,21 @@ private void onCombiningGraceExpired() { // 0 for tap-only commits, where char-by-char delete is the right behavior. if (wordHadGestureFragment) { mLastGestureCommittedLength = writtenChars; + mLastGestureCommittedFragmentLengths.clear(); + if (!fragmentLengthsAtCommit.isEmpty()) { + final int committedDelta = committedLen - typedWordAtCommit.length(); + final int lastIndex = fragmentLengthsAtCommit.size() - 1; + final int adjustedLastFragmentLen = + fragmentLengthsAtCommit.get(lastIndex) + committedDelta + autospaceChars; + if (adjustedLastFragmentLen > 0) { + fragmentLengthsAtCommit.set(lastIndex, adjustedLastFragmentLen); + mLastGestureCommittedFragmentLengths.addAll(fragmentLengthsAtCommit); + } else if (writtenChars > 0) { + mLastGestureCommittedFragmentLengths.add(writtenChars); + } + } else if (writtenChars > 0) { + mLastGestureCommittedFragmentLengths.add(writtenChars); + } } // "keep_alternatives" — fall through, do nothing. } @@ -1204,6 +1242,7 @@ private boolean tryFragmentBackspace(final SettingsValues sv) { && sv.mCombiningGraceMs > 0 && sv.mGestureFragmentBackspace; if (!legacyTracking && !multipartTracking) return false; + if (sv.mCombiningBackspaceDeletesGestureWord) return false; if (mGestureFragmentBoundaries.isEmpty()) return false; if (!mWordComposer.isComposingWord()) { clearFragmentBoundaries(); @@ -1214,16 +1253,24 @@ private boolean tryFragmentBackspace(final SettingsValues sv) { final int currentLen = mWordComposer.getTypedWord().length(); // Filter out stale boundaries past the current length. while (!mGestureFragmentBoundaries.isEmpty() - && mGestureFragmentBoundaries.get(mGestureFragmentBoundaries.size() - 1) >= currentLen) { + && mGestureFragmentBoundaries.get(mGestureFragmentBoundaries.size() - 1) > currentLen) { mGestureFragmentBoundaries.remove(mGestureFragmentBoundaries.size() - 1); } if (mGestureFragmentBoundaries.isEmpty()) return false; - // Pop one boundary and shrink to the previous one (or 0 if no more). - mGestureFragmentBoundaries.remove(mGestureFragmentBoundaries.size() - 1); - final int newLen = mGestureFragmentBoundaries.isEmpty() - ? 0 - : mGestureFragmentBoundaries.get(mGestureFragmentBoundaries.size() - 1); + final int lastBoundary = mGestureFragmentBoundaries.get(mGestureFragmentBoundaries.size() - 1); + final int newLen; + if (lastBoundary == currentLen) { + // The last marker is the end of the current fragment. Pop it and shrink to the + // previous marker, or to 0 for a single-fragment word. + mGestureFragmentBoundaries.remove(mGestureFragmentBoundaries.size() - 1); + newLen = mGestureFragmentBoundaries.isEmpty() + ? 0 + : mGestureFragmentBoundaries.get(mGestureFragmentBoundaries.size() - 1); + } else { + // Defensive fallback for words whose current fragment end was not recorded. + newLen = lastBoundary; + } final String oldWord = mWordComposer.getTypedWord(); final String newWord = newLen <= 0 @@ -1250,6 +1297,22 @@ private boolean tryFragmentBackspace(final SettingsValues sv) { return true; } + private ArrayList getFragmentLengthsForCommit(final int currentLen) { + final ArrayList fragmentLengths = new ArrayList<>(); + if (currentLen <= 0) return fragmentLengths; + int previousBoundary = 0; + for (int i = 0; i < mGestureFragmentBoundaries.size(); ++i) { + final int boundary = mGestureFragmentBoundaries.get(i); + if (boundary <= previousBoundary || boundary > currentLen) continue; + fragmentLengths.add(boundary - previousBoundary); + previousBoundary = boundary; + } + if (previousBoundary < currentLen) { + fragmentLengths.add(currentLen - previousBoundary); + } + return fragmentLengths; + } + // TODO: on the long term, this method should become private, but it will be // difficult. // Especially, how do we deal with InputMethodService.onDisplayCompletions? @@ -2236,6 +2299,8 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu // clears it. If non-zero (the previous commit was a gesture), this backspace MIGHT // delete the whole word — see further down, after the autocorrect-revert branch. final int gestureCommittedLen = mLastGestureCommittedLength; + final ArrayList gestureCommittedFragmentLengths = + new ArrayList<>(mLastGestureCommittedFragmentLengths); // Combining mode: a backspace always cancels the pending commit. The user is // explicitly retracting input; we don't want the timer to fire mid-correction. cancelCombiningMode(); @@ -2298,15 +2363,13 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); } else if ((inputTransaction.getSettingsValues().mGestureManualSpacing || inputTransaction.getSettingsValues().mCombiningGraceMs > 0) - && inputTransaction.getSettingsValues().mCombiningBackspaceDeletesGestureWord) { + && inputTransaction.getSettingsValues().mCombiningBackspaceDeletesGestureWord + && inputTransaction.getSettingsValues().mCombiningBackspaceDeletesComposingText) { final int wordLength = mWordComposer.getTypedWord().length(); - if (inputTransaction.getSettingsValues().mCombiningBackspaceDeletesComposingText) { - mConnection.beginBatchEdit(); - mConnection.deleteTextBeforeCursor(wordLength); - mConnection.endBatchEdit(); - } else { - mConnection.finishComposingText(); - } + mConnection.beginBatchEdit(); + mConnection.deleteTextBeforeCursor(wordLength); + mConnection.finishComposingText(); + mConnection.endBatchEdit(); mWordComposer.reset(); deletedWholeComposingWord = true; StatsUtils.onBackspaceWordDelete(wordLength); @@ -2348,6 +2411,21 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu // words don't qualify — gestureCommittedLen is 0 for those. We delete via // deleteTextBeforeCursor instead of the usual single-char path so the user // doesn't have to mash backspace to undo a swiped word. + final int gestureCommittedFragmentLen = gestureCommittedFragmentLengths.isEmpty() + ? 0 + : gestureCommittedFragmentLengths.remove(gestureCommittedFragmentLengths.size() - 1); + if (gestureCommittedFragmentLen > 0 + && inputTransaction.getSettingsValues().mGestureFragmentBackspace + && !inputTransaction.getSettingsValues().mCombiningBackspaceDeletesGestureWord) { + mConnection.beginBatchEdit(); + mConnection.deleteTextBeforeCursor(gestureCommittedFragmentLen); + mConnection.endBatchEdit(); + mLastGestureCommittedFragmentLengths.clear(); + mLastGestureCommittedFragmentLengths.addAll(gestureCommittedFragmentLengths); + StatsUtils.onBackspaceWordDelete(gestureCommittedFragmentLen); + inputTransaction.setRequiresUpdateSuggestions(); + return; + } if (gestureCommittedLen > 0 && inputTransaction.getSettingsValues().mCombiningBackspaceDeletesGestureWord) { mConnection.beginBatchEdit(); @@ -3678,17 +3756,19 @@ public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, } else { setSuggestedWords(SuggestedWords.getEmptyInstance()); } - // PREF_GESTURE_FRAGMENT_BACKSPACE: record this gesture as a fragment boundary - // so backspace can pop the whole word at once. When extending an existing - // composing word, both the prior fragments AND this new one are already in the - // boundaries list (prior fragments were recorded at their own append time); - // recordFragmentBoundaryIfTracking adds the NEW boundary at the end. No-op if - // PREF_GESTURE_FRAGMENT_BACKSPACE isn't on. + // PREF_GESTURE_FRAGMENT_BACKSPACE: make sure the previous word length is present + // (fresh gesture words did not have an earlier extension point), then record this + // gesture's end so backspace can pop exactly one fragment. + if (!usedMergedTrail) { + recordFragmentBoundaryIfTracking(settingsValues, prevTypedWord.length()); + } recordFragmentBoundaryIfTracking(settingsValues); } else { // Non-extend path replaces the whole composing word each gesture; any prior - // fragment boundaries become meaningless. + // fragment boundaries become meaningless, then the new gesture becomes the only + // fragment currently available to pop. clearFragmentBoundaries(); + recordFragmentBoundaryIfTracking(settingsValues); } // Tap-promotion-extend is a one-shot decision per gesture; clear the flag so the // next gesture re-evaluates the timing. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98816d057..6d2ba92d2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -294,7 +294,7 @@ Backspace behavior Choose what backspace removes while using manual spacing or delayed autospace. Delete one character - Delete last word part + Delete last fragment Delete whole word Explicit "join next" modifier Force the next input to extend the current word regardless of timing. diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index 7ab90f319..bc5fa4fd7 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -390,7 +390,7 @@ class InputLogicTest { assertEquals("", composingText) } - @Test fun wholeWordBackspaceCanKeepManualSpacingComposingTextInEditor() { + @Test fun wholeWordBackspaceWithLiveComposingDeleteOffFallsBackToOneCharacter() { reset() latinIME.prefs().edit { putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, true) @@ -405,7 +405,88 @@ class InputLogicTest { functionalKeyPress(KeyCode.DELETE) - assertEquals("hello", textBeforeCursor) + assertEquals("hell", textBeforeCursor) + assertEquals("hell", composingText) + } + + @Test fun fragmentBackspaceDeletesOnlySwipeFragment() { + reset() + latinIME.prefs().edit { + putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, true) + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, true) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, false) + } + + gestureInput("fire") + assertEquals("fire", textBeforeCursor) + assertEquals("fire", composingText) + + functionalKeyPress(KeyCode.DELETE) + + assertEquals("", textBeforeCursor) + assertEquals("", composingText) + } + + @Test fun fragmentBackspaceDeletesLastSwipeFragmentInMultipartWord() { + reset() + latinIME.prefs().edit { + putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, true) + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, true) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, false) + } + + gestureInput("fire") + gestureInput("truck") + assertEquals("firetruck", textBeforeCursor) + assertEquals("firetruck", composingText) + + functionalKeyPress(KeyCode.DELETE) + + assertEquals("fire", textBeforeCursor) + assertEquals("fire", composingText) + } + + @Test fun fragmentBackspaceDeletesLastSwipeFragmentAfterAutospaceCommit() { + reset() + latinIME.prefs().edit { + putInt(Settings.PREF_COMBINING_GRACE_MS, 1000) + putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, false) + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, true) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, false) + } + + gestureInput("fire") + gestureInput("truck") + expireCombiningGrace() + assertEquals("firetruck ", textBeforeCursor) + assertEquals("", composingText) + + functionalKeyPress(KeyCode.DELETE) + + assertEquals("fire", textBeforeCursor) + assertEquals("", composingText) + + functionalKeyPress(KeyCode.DELETE) + + assertEquals("", textBeforeCursor) + assertEquals("", composingText) + } + + @Test fun wholeWordBackspaceWithLiveComposingDeleteOnClearsComposingSpan() { + reset() + latinIME.prefs().edit { + putBoolean(Settings.PREF_GESTURE_MANUAL_SPACING, true) + putBoolean(Settings.PREF_GESTURE_FRAGMENT_BACKSPACE, false) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD, true) + putBoolean(Settings.PREF_COMBINING_BACKSPACE_DELETES_COMPOSING_TEXT, true) + } + + chainInput("hello") + functionalKeyPress(KeyCode.DELETE) + chainInput("hi") + functionalKeyPress(KeyCode.DELETE) + + assertEquals("", textBeforeCursor) assertEquals("", composingText) } diff --git a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt index 77ff16ce3..afc560f28 100644 --- a/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt +++ b/app/src/test/java/helium314/keyboard/settings/SettingsContainerTest.kt @@ -2,6 +2,7 @@ package helium314.keyboard.settings import android.content.Context import androidx.test.core.app.ApplicationProvider +import helium314.keyboard.latin.R import helium314.keyboard.latin.settings.Settings import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -73,4 +74,10 @@ class SettingsContainerTest { assertEquals(Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS, container[Settings.PREF_GESTURE_DEBUG_ACCUMULATE_FRAGMENTS]?.key) } + + @Test + fun twoThumbFragmentBackspaceLabelMatchesBehavior() { + val context = ApplicationProvider.getApplicationContext() + assertEquals("Delete last fragment", context.getString(R.string.two_thumb_backspace_fragment)) + } } diff --git a/dev-log.md b/dev-log.md index 3cb159928..93c19ab26 100644 --- a/dev-log.md +++ b/dev-log.md @@ -560,3 +560,40 @@ After testing, the user confirmed the tap-during-swipe fragment behavior was cau ### Open Questions / Next Steps - Commit and push the PR update. + +--- + +## 2026-05-24 — Fix two-thumb backspace modes + +### Context +The user reported that the two-thumb backspace selector had misleading wording and broken behavior: **Delete last word part** should be **Delete last fragment**, fragment mode behaved like normal character delete, and whole-word mode mishandled live composing text depending on the **Delete live composing text** sub-option. + +### Actions Taken +- Renamed the fragment-mode label to **Delete last fragment**. +- Updated fragment boundary tracking in `InputLogic.java` so the first swipe fragment is recorded and extended swipe fragments record both the previous boundary and the new fragment end. +- Fixed fragment backspace so an end boundary equal to the current composing length is treated as the current fragment marker, not stale state. +- Changed whole-word backspace with **Delete live composing text** disabled to fall back to normal one-character live composing deletion instead of consuming backspace as a no-op. +- Cleared the editor composing span after whole-word live composing deletion to avoid stale composing state. +- Preserved the committed fragment stack after delayed autospace commits so repeated **Delete last fragment** presses pop one committed fragment at a time instead of reverting to character deletion after the first pop. +- Added focused JVM regression tests for live fragment backspace, repeated committed fragment backspace, whole-word live composing deletion on/off, and the renamed label. +- Ran focused backspace/settings regression tests. `:app:installStandardDebug` built the standard APK but could not install because Gradle reported no connected devices. + +### Decisions Made +- Kept default gesture/batch rejection behavior outside fragment mode unchanged; the fix is scoped to the two-thumb backspace selector and live composing option. +- Added a single-fragment regression test because **Delete last fragment** should also delete a one-swipe composing word, not only the last piece of a multi-fragment word. + +### Manual Tests — Two-thumb Backspace Modes + +| # | Steps | Expected Result | +|---|---|---| +| 1 | Open **Settings → Two-thumb typing → Backspace behavior**. | The fragment option is labeled **Delete last fragment**. | +| 2 | Enable manual spacing, choose **Delete last fragment**, swipe one word, then press Backspace. | The whole swiped fragment is removed in one press. | +| 3 | With **Delete last fragment**, swipe a word fragment and then swipe another fragment to extend it, then press Backspace. | Only the latest fragment is removed; the previous fragment remains composing. | +| 4 | With **Delete last fragment**, wait for delayed autospace to commit the combined word, then press Backspace twice. | The first press removes the autospace and latest fragment; the second press removes the previous fragment. | +| 5 | Choose **Delete whole word**, turn **Delete live composing text** off, type letters, then press Backspace. | One character is deleted and the remaining text stays live/composing. | +| 6 | Choose **Delete whole word**, turn **Delete live composing text** on, type letters, then press Backspace. | The whole live composing word is removed in one press, and the next typed word deletes normally. | + +### Open Questions / Next Steps +- Full `InputLogicTest` class execution still fails on existing unrelated tests (`tapOnlyCombiningWordDoesNotShowAutospaceIndicatorWhenGestureGateEnabled`, `insertLetterIntoWordHangulFails`, `revert autocorrect on delete`); the focused backspace/settings tests pass. +- Install still needs to be retried when a device is connected. +- Open the bugfix PR from `fix-backspace-delete-options` into `main`. From eea68f027ff644c1a02872f19ed4639909f2818d Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Mon, 25 May 2026 08:43:59 +0300 Subject: [PATCH 2/2] Update backspace install validation log Record the successful wireless ADB install after re-pairing the device. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dev-log.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-log.md b/dev-log.md index 93c19ab26..686c24afb 100644 --- a/dev-log.md +++ b/dev-log.md @@ -576,7 +576,8 @@ The user reported that the two-thumb backspace selector had misleading wording a - Cleared the editor composing span after whole-word live composing deletion to avoid stale composing state. - Preserved the committed fragment stack after delayed autospace commits so repeated **Delete last fragment** presses pop one committed fragment at a time instead of reverting to character deletion after the first pop. - Added focused JVM regression tests for live fragment backspace, repeated committed fragment backspace, whole-word live composing deletion on/off, and the renamed label. -- Ran focused backspace/settings regression tests. `:app:installStandardDebug` built the standard APK but could not install because Gradle reported no connected devices. +- Ran focused backspace/settings regression tests. +- Re-paired wireless ADB to `SM-S936B` and installed `:app:installStandardDebug`. ### Decisions Made - Kept default gesture/batch rejection behavior outside fragment mode unchanged; the fix is scoped to the two-thumb backspace selector and live composing option. @@ -595,5 +596,4 @@ The user reported that the two-thumb backspace selector had misleading wording a ### Open Questions / Next Steps - Full `InputLogicTest` class execution still fails on existing unrelated tests (`tapOnlyCombiningWordDoesNotShowAutospaceIndicatorWhenGestureGateEnabled`, `insertLetterIntoWordHangulFails`, `revert autocorrect on delete`); the focused backspace/settings tests pass. -- Install still needs to be retried when a device is connected. -- Open the bugfix PR from `fix-backspace-delete-options` into `main`. +- PR opened at https://github.com/AsafMah/LeanType/pull/8.