Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin.inputlogic;

import java.util.ArrayList;
import java.util.List;

/**
* One revertible input-unit stack for backspace (issue #31).
*
* <p>Consolidates the length bookkeeping that drives fragment- and whole-word-backspace, which
* was previously three separate fields scattered across {@link InputLogic}:
* <ul>
* <li><b>Composing side</b> — the cumulative fragment boundaries of the <em>active</em>
* composing word (one strictly-increasing entry per recorded gesture/tap fragment).</li>
* <li><b>Committed side</b> — the total length and per-fragment lengths of the <em>last
* committed</em> gesture word, so a backspace right after commit can pop the last fragment
* or delete the whole word.</li>
* </ul>
*
* <p>This class owns only the unit-length bookkeeping. The editor side effects (composing-text
* updates, {@code deleteTextBeforeCursor}, stats) and the policy decisions (when to record/pop)
* stay in {@link InputLogic}. Behaviour is identical to the pre-extraction code; the value is a
* single, separately unit-testable home for the corruption-prone boundary math.
*/
final class BackspaceUnitStack {

/** Composing side: cumulative fragment boundaries (strictly increasing word lengths). */
private final ArrayList<Integer> mComposingBoundaries = new ArrayList<>();

/** Committed side: total length of the last committed gesture word (0 = none / tap-only). */
private int mCommittedLength;
/** Committed side: per-fragment lengths of the last committed gesture word. */
private final ArrayList<Integer> mCommittedFragmentLengths = new ArrayList<>();

// ===== composing side =====

boolean hasComposingBoundaries() {
return !mComposingBoundaries.isEmpty();
}

/**
* Record a fragment boundary at the given composing-word length. No-op for a non-positive
* length or a duplicate of the current top (the same fragment appended twice in quick
* succession).
*/
void recordComposingBoundary(final int len) {
if (len <= 0) return;
if (!mComposingBoundaries.isEmpty()
&& mComposingBoundaries.get(mComposingBoundaries.size() - 1) == len) {
return;
}
mComposingBoundaries.add(len);
}

/** Drop all composing boundaries. Call after committing / resetting the composing word. */
void clearComposing() {
if (!mComposingBoundaries.isEmpty()) mComposingBoundaries.clear();
}

/**
* Pop the most-recent composing fragment, given the current composing-word length.
*
* <p>Stale boundaries past {@code currentLen} are trimmed first. Returns the new word length
* the composing word should shrink to: the previous boundary (or {@code 0} for a
* single-fragment word) when the top marker is the current fragment end, or the top boundary
* itself as a defensive fallback when the current fragment end was never recorded. Returns
* {@code -1} when there is no fragment to pop (caller should fall through to char-delete).
*/
int popComposingFragment(final int currentLen) {
if (mComposingBoundaries.isEmpty()) return -1;
while (!mComposingBoundaries.isEmpty()
&& mComposingBoundaries.get(mComposingBoundaries.size() - 1) > currentLen) {
mComposingBoundaries.remove(mComposingBoundaries.size() - 1);
}
if (mComposingBoundaries.isEmpty()) return -1;
final int lastBoundary = mComposingBoundaries.get(mComposingBoundaries.size() - 1);
if (lastBoundary == currentLen) {
// Top marker is the end of the current fragment: pop it and shrink to the previous
// marker, or to 0 for a single-fragment word.
mComposingBoundaries.remove(mComposingBoundaries.size() - 1);
return mComposingBoundaries.isEmpty()
? 0
: mComposingBoundaries.get(mComposingBoundaries.size() - 1);
}
// Defensive fallback for words whose current fragment end was not recorded.
return lastBoundary;
}

/**
* Build the per-fragment lengths (deltas) for a composing word of {@code currentLen} that is
* about to be committed: a delta per in-range boundary, plus a trailing fragment for any tail
* past the last boundary.
*/
ArrayList<Integer> fragmentLengthsForCommit(final int currentLen) {
final ArrayList<Integer> fragmentLengths = new ArrayList<>();
if (currentLen <= 0) return fragmentLengths;
int previousBoundary = 0;
for (int i = 0; i < mComposingBoundaries.size(); ++i) {
final int boundary = mComposingBoundaries.get(i);
if (boundary <= previousBoundary || boundary > currentLen) continue;
fragmentLengths.add(boundary - previousBoundary);
previousBoundary = boundary;
}
if (previousBoundary < currentLen) {
fragmentLengths.add(currentLen - previousBoundary);
}
return fragmentLengths;
}

// ===== committed side =====

int committedLength() {
return mCommittedLength;
}

/** A defensive copy of the committed fragment lengths (snapshot before a pop). */
ArrayList<Integer> copyCommittedFragmentLengths() {
return new ArrayList<>(mCommittedFragmentLengths);
}

/** Replace the committed gesture word: its total length and per-fragment lengths. */
void setCommitted(final int length, final List<Integer> fragmentLengths) {
mCommittedLength = length;
mCommittedFragmentLengths.clear();
mCommittedFragmentLengths.addAll(fragmentLengths);
}

/** Replace just the committed fragment lengths (after popping one fragment off the top). */
void setCommittedFragmentLengths(final List<Integer> fragmentLengths) {
mCommittedFragmentLengths.clear();
mCommittedFragmentLengths.addAll(fragmentLengths);
}

/** Reset all committed-gesture state (total length + fragment lengths). */
void clearCommitted() {
mCommittedLength = 0;
if (!mCommittedFragmentLengths.isEmpty()) mCommittedFragmentLengths.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,11 @@ public final class InputLogic {
* because PHANTOM would make the next letter ALSO write a space, double-spacing. */
private boolean mAutospaceJustWritten;

// Two-thumb typing (#1.1, sub-option PREF_GESTURE_FRAGMENT_BACKSPACE): char-offsets that
// mark the END of each "fragment" appended to the current composing word under manual
// spacing. A fragment is one gesture's output OR one tap's letter. With the pref on,
// backspace pops the most recent fragment in one keystroke instead of deleting one
// 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 ArrayList<Integer> 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<Integer> mLastGestureCommittedFragmentLengths = new ArrayList<>();
// Backspace input-unit stack (#31): the single home for the fragment- / whole-word-backspace
// length bookkeeping. Owns both the active composing word's fragment boundaries (one entry
// per recorded gesture/tap fragment, kept in sync with mWordComposer.getTypedWord()) and the
// last committed gesture word's total + per-fragment lengths. See BackspaceUnitStack.
private final BackspaceUnitStack mBackspaceUnits = new BackspaceUnitStack();

// ---- Unified combining-mode state machine ----------------------------------------------
// After every composing-word-extending event (tap of a letter OR gesture completion),
Expand Down Expand Up @@ -956,25 +949,16 @@ private void recordFragmentBoundaryIfTracking(final SettingsValues sv) {
/** 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) {
return;
}
mGestureFragmentBoundaries.add(len);
mBackspaceUnits.recordComposingBoundary(len);
}

/** Clear all recorded fragment boundaries. Call after committing / resetting the composing word. */
private void clearFragmentBoundaries() {
if (!mGestureFragmentBoundaries.isEmpty()) mGestureFragmentBoundaries.clear();
mBackspaceUnits.clearComposing();
}

private void clearCommittedGestureBackspaceState() {
mLastGestureCommittedLength = 0;
if (!mLastGestureCommittedFragmentLengths.isEmpty()) {
mLastGestureCommittedFragmentLengths.clear();
}
mBackspaceUnits.clearCommitted();
}

// ---- Unified combining-mode helpers --------------------------------------------------
Expand Down Expand Up @@ -1177,13 +1161,6 @@ public boolean isInCombiningMode() {
* Cleared on any new input that arms combining mode, on cancel, on the next space tap,
* and after a suggestion-pick. */
private int mAutoCommitRevertLength;
/** Length of the most recent gesture-driven commit (typed word + autospace). Set in
* {@link #onCombiningGraceExpired} when {@code mWordComposer.isBatchMode()} was true at
* commit time. Consumed by the first backspace tap when
* {@code PREF_COMBINING_BACKSPACE_DELETES_GESTURE_WORD} is on, deleting the whole word
* + space in one keystroke (unless an autocorrect-revert applies first — that always
* wins). Cleared on any new input that arms combining mode. */
private int mLastGestureCommittedLength;
/** Set in {@link #onPickSuggestionManually} when the picker reverted an auto-committed
* word; consumed at the bottom of the same method to insert a visible trailing space so
* the cursor lands at "the |" instead of "the|". */
Expand Down Expand Up @@ -1300,22 +1277,22 @@ private void onCombiningGraceExpired() {
// arm "first backspace deletes the whole word" — see handleBackspaceEvent. Stays
// 0 for tap-only commits, where char-by-char delete is the right behavior.
if (wordHadGestureFragment) {
mLastGestureCommittedLength = writtenChars;
mLastGestureCommittedFragmentLengths.clear();
final ArrayList<Integer> committedFragments = new ArrayList<>();
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);
committedFragments.addAll(fragmentLengthsAtCommit);
} else if (writtenChars > 0) {
mLastGestureCommittedFragmentLengths.add(writtenChars);
committedFragments.add(writtenChars);
}
} else if (writtenChars > 0) {
mLastGestureCommittedFragmentLengths.add(writtenChars);
committedFragments.add(writtenChars);
}
mBackspaceUnits.setCommitted(writtenChars, committedFragments);
}
// "keep_alternatives" — fall through, do nothing.
}
Expand All @@ -1337,34 +1314,16 @@ private boolean tryFragmentBackspace(final SettingsValues sv) {
&& sv.mGestureFragmentBackspace;
if (!legacyTracking && !multipartTracking) return false;
if (sv.mCombiningBackspaceDeletesGestureWord) return false;
if (mGestureFragmentBoundaries.isEmpty()) return false;
if (!mBackspaceUnits.hasComposingBoundaries()) return false;
if (!mWordComposer.isComposingWord()) {
clearFragmentBoundaries();
return false;
}
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) return false;

final int currentLen = mWordComposer.getTypedWord().length();
// Filter out stale boundaries past the current length.
while (!mGestureFragmentBoundaries.isEmpty()
&& mGestureFragmentBoundaries.get(mGestureFragmentBoundaries.size() - 1) > currentLen) {
mGestureFragmentBoundaries.remove(mGestureFragmentBoundaries.size() - 1);
}
if (mGestureFragmentBoundaries.isEmpty()) return false;

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 int newLen = mBackspaceUnits.popComposingFragment(currentLen);
if (newLen < 0) return false;

final String oldWord = mWordComposer.getTypedWord();
final String newWord = newLen <= 0
Expand Down Expand Up @@ -1392,19 +1351,7 @@ private boolean tryFragmentBackspace(final SettingsValues sv) {
}

private ArrayList<Integer> getFragmentLengthsForCommit(final int currentLen) {
final ArrayList<Integer> 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;
return mBackspaceUnits.fragmentLengthsForCommit(currentLen);
}

// TODO: on the long term, this method should become private, but it will be
Expand Down Expand Up @@ -2408,9 +2355,9 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu
// Combining mode: snapshot the gesture-word-length flag BEFORE cancelCombiningMode
// 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 int gestureCommittedLen = mBackspaceUnits.committedLength();
final ArrayList<Integer> gestureCommittedFragmentLengths =
new ArrayList<>(mLastGestureCommittedFragmentLengths);
mBackspaceUnits.copyCommittedFragmentLengths();
// 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();
Expand Down Expand Up @@ -2578,8 +2525,7 @@ private void handleBackspaceEvent(final Event event, final InputTransaction inpu
mConnection.beginBatchEdit();
mConnection.deleteTextBeforeCursor(gestureCommittedFragmentLen);
mConnection.endBatchEdit();
mLastGestureCommittedFragmentLengths.clear();
mLastGestureCommittedFragmentLengths.addAll(gestureCommittedFragmentLengths);
mBackspaceUnits.setCommittedFragmentLengths(gestureCommittedFragmentLengths);
StatsUtils.onBackspaceWordDelete(gestureCommittedFragmentLen);
inputTransaction.setRequiresUpdateSuggestions();
return;
Expand Down
Loading
Loading