Skip to content

Delete swipe: move by real visual lines (up swipe selects exactly one line above) - #702

Open
Zokii0 wants to merge 1 commit into
LeanBitLab:mainfrom
Zokii0:delete-swipe-visual-lines
Open

Zokii0 wants to merge 1 commit into
LeanBitLab:mainfrom
Zokii0:delete-swipe-visual-lines

Conversation

@Zokii0

@Zokii0 Zokii0 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Related: LeanType issue #467. Based on current main (same source as beta-429-9).

Problem

Swiping up on the delete key selects "lines" using getLineBoundariesBackwards(), which
does not know the real layout of the text field: it splits on \n and cuts longer
segments into chunks of roughly 40-50 characters. These guessed boundaries rarely match the
real soft-wrapped lines, so after an upward step the selection starts at an arbitrary word
somewhere in the previous line.

An earlier attempt (beta-429-7) asked the editor to do the movement with Shift+DPAD_UP on a
reversed selection (anchor = right end, extent = left end). The recording attached to the issue
shows it broken: the selection would not go further than the second line above, and it flickered
in that line when swiping up again. That approach was reverted in beta-429-8.

Likely cause (found by reading the code, not confirmed on a device): an editor reports a reversed
selection to the IME as selStart = anchor, selEnd = extent, so newSelStart > newSelEnd.
InputLogic.onUpdateSelection() passes these values to
resetCachesUponCursorMoveAndReturnSuccess(), which stores them in mExpectedSelStart/End
without normalising. The delete swipe then sees deleteSwipeInitialEnd != expectedSelectionEnd
at the next move event, re-initialises its state from the wrong values and the selection jumps back,
which looks like a stall plus flicker. This change never creates a reversed selection.

Approach

Let the editor find the position one visual line above, but with plain events and without
changing the selection logic:

  1. For an upward step, put a collapsed caret at the current left end of the swipe,
    send a plain DPAD_UP key event (the same events the toolbar arrow keys already send) and read
    the new caret position back with getExtractedText (synchronous).
  2. The result is remembered in deleteSwipeLineBoundaries (index 0 = where the swipe started,
    index n = left end after n upward steps, same column as the arrow key gives).
  3. Moving down does not ask the editor again: it just goes back to the stored entries, so
    up and down always return to exactly the same positions and never go below the original
    start.
  4. The user-visible selection is restored afterwards and the existing code applies the horizontal
    part (deleteSwipeCharOffset / deleteSwipeWordIndex) relative to the entry for the current line,
    so up + left (diagonal) simply adds characters/words left of the exact position above.
  5. All the temporary carets are inside one beginBatchEdit()/endBatchEdit(), so the editor
    reports only the final selection to the IME. The temporary selections are always collapsed or in
    normal order (start <= end), never reversed.

Everything else already in main keeps working unchanged: right-swipe deselection after a vertical
swipe (deleteSwipeSelectionAnchor() is still boundaries[0], the swipe start), the word-mode
forward boundaries, and the thumb-drift filter in PointerTracker.kt for a right swipe after a left swipe
(PointerTracker.kt is not modified).

Changes

  • KeyboardActionListenerImpl.kt
    • New state: deleteSwipeApproxLineBoundaries, deleteSwipeUseEditorLines, deleteSwipeEditorTopReached
      (initialised when a swipe starts, cleared in onUpWithDeletePointerActive() and resetMetaState()).
    • onMoveDeletePointer() extends the boundaries for upward steps before the existing char/word code runs.
    • New extendDeleteSwipeLinesByEditor() and editorPositionOneLineUp().
    • New companion helper extendLineBoundaries(boundaries, targetIndex, oneLineUp) (pure, unit tested) and
      LineExtendResult { REACHED, TOP, FAILED }.
  • RichInputConnection.kt: new getEditorSelection() (synchronous getExtractedText, asks only for a
    window of 10240 chars so large fields are not copied completely).
  • WordDeleteSwipeTest.kt: 5 new tests for extendLineBoundaries with a fake editor (extends one line at a
    time, does not call the editor if the entry exists, stops at the top of the text, reports failure,
    keeps entries found before a failure).

Fallbacks

  • Only used when the keyboard's editor info has the multi-line flag. Single-line fields keep the old
    approximation (an up arrow in a single-line field could move focus).
  • If the editor gives no extracted text, or the caret is not collapsed after the arrow key, the old approximated
    boundaries (getLineBoundariesBackwards) are used for the rest of that swipe.
  • If the editor reports no further line above (top of the text), the selection simply stops there.

Not verified

Not compiled and not run (no Android toolchain available to me). Please test on a device in:
a native EditText, a web textarea (Chrome/Brave), and a field with very long soft-wrapped lines with no spaces
(like the one in the beta-429-7 recording), each in char mode and word-by-word mode:

  • up, up, down, down (must return to the same positions, never below the original cursor);
  • up then right (deselect) and up + left (diagonal);
  • left then right with a slight upward drift (the drift filter must still apply);
  • release deletes exactly the highlighted text;
  • single-line field (old behavior), and the top of the text.

@Zokii0

Zokii0 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

@LeanBitLab Can you test this, or make test apk just for me to test it? You don't have to merge it for next version till it's tested, i want to polish this delete gesture as my last request (it shouldn't be broken like it was back then at version 429-7 , but will test it just in case). This should also solve all those previous issues i found while using your 2D solution. Sorry for troubling you with this same issue and thank you

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