Skip to content

fix(input): correct the accelerated backspace emoji measurement - #134

Merged
AsafMah merged 4 commits into
devfrom
asafmah-fix-backspace-paragraph-delete
Aug 20, 2026
Merged

fix(input): correct the accelerated backspace emoji measurement#134
AsafMah merged 4 commits into
devfrom
asafmah-fix-backspace-paragraph-delete

Conversation

@AsafMah

@AsafMah AsafMah commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Fixes #133

What this is now

This PR was originally a paragraph-backspace fix. Device testing did not confirm the bug, so the unverified behaviour change has been split out to the parked draft #140, and what remains here is only what's independently correct.

What device testing showed

On the reporter's Samsung SM-S936B, driving the real on-screen backspace key against the unfixed build:

App Flow Result on unfixed build
Samsung Notes type → Enter → backspace merged correctly
Samsung Notes caret moved in by tap, then backspace merged correctly
MS Word type → Enter → backspace merged correctly
MS Word caret to paragraph start via MOVE_HOME merged correctly
MS Word empty paragraphs (Enter ×3) removed correctly
MS Word held backspace across the boundary deleted through correctly

Both apps honour deleteSurroundingText across a paragraph boundary, contradicting the premise. The reporter's settings are all defaults (gesture_method=fallback), so no exotic fork backspace path was armed either. See #140 for the full write-up and what would un-park it.

What's left here

A real bug, independently verifiable. The accelerated (held-backspace) second deletion measured its length from codePointBeforeCursor — the code point read before the first deletion — instead of codePointBeforeCursorToDeleteAgain:

final int codePointBeforeCursorToDeleteAgain = mConnection.getCodePointBeforeCursor();
if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
    final int lengthToDeleteAgain = codePointBeforeCursor > 0xFE00        // <-- wrong variable
            || StringUtils.mightBeEmoji(codePointBeforeCursor)            // <-- wrong variable

When the second character back is a multi-code-point emoji this picks length 1 and splits it; in the opposite case it over-deletes. Only reachable once mDeleteCount > DELETE_ACCELERATE_AT, i.e. while holding backspace, which is probably why it went unnoticed. Present in upstream v4.1.2 too — reported as LeanBitLab/LeanType#423.

Two latent test-harness bugs.

  • deleteSurroundingText didn't clamp to the available text and drove selectionStart negative on an empty field.
  • setText accepted a requireIdle parameter and then called handleMessages() without it, so reset() couldn't tolerate a leftover delayed message. Harmless until adding tests reshuffled JUnit's hash-based method ordering, at which point it failed an unrelated test during setup rather than in its own logic.

Test infrastructure kept. The fake InputConnection can simulate a block-based editor: paragraph-scoped reads, and cross-paragraph deletes that report success while doing nothing (deliberately mirroring the "returns true but no-ops" behaviour that makes this class of bug invisible). The two tests asserting the parked behaviour are @Ignored with the device findings recorded inline, so the hypothesis and the evidence against it stay together rather than the hypothesis quietly disappearing.

Verification

324 tests, 4 failed — the documented Windows-only ParserTest baseline, confirmed with tools/check_test_results.py from #139 (which also checks the results aren't stale and that its own enumeration agrees with the declared totals).

AsafMah and others added 2 commits August 20, 2026 05:57
Backspace appeared dead at the start of a paragraph in rich-text editors
(MS Word, Samsung/Google Notes, Docs). Those editors model paragraphs as
separate blocks and silently ignore a deleteSurroundingText that would
merge two of them, while honouring a real KEYCODE_DEL - which is what
other keyboards send.

HeliBoard already had a KEYCODE_DEL fallback (8f80c2a) but scoped it to
browsers only, so plain multi-line fields never reached it. Upstream
v4.1.2 carries the same logic, so this is not fixed by merging upstream.

Changes in handleBackspaceEvent:
- Route deletion of a '\n' through a key event instead of
  deleteSurroundingText. Equivalent in a plain EditText.
- Broaden the NOT_A_CODE fallback to any editor that hides context, but
  only when the expected cursor position is > 0, so a genuine document
  start stays a no-op and we never dispatch key events at apps that react
  to them (recipient chips, search boxes, web views).
- Never chain the accelerated second delete onto a key event: key events
  take an asynchronous route that ignores batch edits, so a following
  deleteSurroundingText could race ahead and act on pre-merge text.
- Fix a pre-existing bug in the accelerated path, which tested the first
  code point instead of codePointBeforeCursorToDeleteAgain and could
  split an emoji. Also present upstream; worth reporting.

Tests: the fake InputConnection can now simulate a block-based editor
(paragraph-scoped reads, cross-paragraph deletes that report success but
do nothing). Also fixes two latent harness bugs: deleteSurroundingText
did not clamp to available text (drove selectionStart negative), and
setText ignored its own requireIdle parameter.

The tap-into-paragraph variant is added as an @ignore'd test: it measures
expectedSelectionStart == 0, making it indistinguishable from an empty
field, so it needs a device trace before it can be fixed safely.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a88bd77f-d993-44c5-9efc-c7124f0d825e
#133)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a88bd77f-d993-44c5-9efc-c7124f0d825e
AsafMah added a commit that referenced this pull request Aug 20, 2026
Section 5 was still headed "THE OPEN TASK - publish signed v0.2.0", which
is no longer true: v0.2.0 was published on 2026-08-20 with all four
signed APKs and is marked latest. The runner outage that blocked it
resolved on its own; Release run 31128748928 succeeded and produced the
draft.

Reframes section 5 from a blocker into the verified release procedure,
since the recipe itself is still what the next release should follow, and
keeps the outage signature so it is recognised rather than re-debugged.

Also refreshes the TL;DR table (v0.2.0 published, current dev head, the
four open PRs including #134, #136 and #137) and replaces section 12's
"publish v0.2.0" item with the work that is actually outstanding: device
verification of #134 and #137, re-pointing LeanType-check-upstream-main
to v4.1.2 to re-check the two guarded upstream defects, reporting the
emoji accelerated-delete bug upstream, and deciding the fate of the
unfinished worktrees whose commits exist on no remote.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a88bd77f-d993-44c5-9efc-c7124f0d825e
Device testing did not confirm the paragraph-merge theory this branch was
originally built around, so the unverified behaviour change is parked on
`feat/backspace-paragraph-keyevent` and only the provable fixes remain here.

On a Samsung SM-S936B, on the unfixed build, both Microsoft Word and
Samsung Notes merged paragraphs correctly in every flow tried: typed
Enter then backspace, caret moved into the paragraph by tap, empty
paragraphs, and a held backspace across the boundary. The premise that
those editors ignore deleteSurroundingText across a block boundary is
therefore not established, and routing '\n' deletion through KEYCODE_DEL
is not justified on this evidence.

What remains is independently correct:

- The accelerated (held-backspace) second deletion measured its length
  from codePointBeforeCursor -- the code point read *before* the first
  deletion -- instead of codePointBeforeCursorToDeleteAgain. When the
  second character back is a multi-code-point emoji that picks the wrong
  length and can split it; in the opposite case it can over-delete.
  Present in upstream v4.1.2 as well, reported as
  LeanBitLab#423.

- Two latent test-harness bugs. deleteSurroundingText did not clamp to
  the available text and drove selectionStart negative on an empty
  field. setText accepted a requireIdle parameter and then called
  handleMessages() without it, so reset() could not tolerate a leftover
  delayed message -- harmless until adding tests reshuffled JUnit's
  hash-based ordering, at which point it failed an unrelated test during
  setup rather than in its own logic.

The fake InputConnection can still simulate a block-based editor
(paragraph-scoped reads, cross-paragraph deletes that report success
while doing nothing). The two tests that assert the parked behaviour are
@ignore'd with the device findings recorded, so the hypothesis and the
evidence against it stay together.

Suite: 324 tests, 4 failed -- the documented Windows-only ParserTest
baseline, confirmed with tools/check_test_results.py (#139).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a88bd77f-d993-44c5-9efc-c7124f0d825e
@AsafMah AsafMah changed the title fix(input): make backspace merge paragraphs in block-based editors fix(input): correct the accelerated backspace emoji measurement Aug 20, 2026
…paragraph-delete

# Conflicts:
#	CHANGELOG.md
@AsafMah
AsafMah merged commit be2aa10 into dev Aug 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant