Skip to content

fix(editor): sync TalkBack accessibility focus with the text cursor#1509

Open
makhlwf wants to merge 5 commits into
appdevforall:stagefrom
makhlwf:adfa-editor-a11y-cursor-sync
Open

fix(editor): sync TalkBack accessibility focus with the text cursor#1509
makhlwf wants to merge 5 commits into
appdevforall:stagefrom
makhlwf:adfa-editor-a11y-cursor-sync

Conversation

@makhlwf

@makhlwf makhlwf commented Jul 11, 2026

Copy link
Copy Markdown

Closes #1497

Problem

The code editor is very hard to use with TalkBack. Double-tapping a word/line fails to sync the text cursor with the accessibility focus — the caret disappears or lands in the wrong place, and typing is inserted on the wrong line (e.g. line 8 instead of line 3). Users are forced into the screen reader's native edit box as a workaround.

Root cause: the editor wraps Rosemoe's CodeEditor, a custom-drawn View. Its accessibility node reports the whole document as a single generic editable node (class name CodeEditor) and advertises no text-movement granularities, ACTION_SET_SELECTION, or granularity actions. So TalkBack has no way to move the real text caret in step with the reading position; a double-tap sends ACTION_CLICK to the node centre.

Fix

Make IDEEditor behave like a standard EditText for screen readers:

  • Reports as android.widget.EditText and advertises CHARACTER / WORD / LINE movement granularities plus ACTION_SET_SELECTION and the next/previous-at-granularity actions on the node.
  • Handles those actions by converting absolute character indices ↔ (line, column) via the editor's Indexer and moving the real caret (setSelection/setSelectionRegion), then emits TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY so the spoken segment and the caret stay in sync.
  • Double-tap placement: tracks the last touch-exploration hover point (dispatchHoverEvent) and, on ACTION_CLICK, places the caret at that on-screen point via getPointPositionOnScreen. Gated on touch-exploration being enabled, so normal (sighted) use is unaffected.

Text-segment iteration lives in the new EditorAccessibilitySegments (character and word mirror the framework's AccessibilityIterators; line walks logical \n-separated lines).

Testing

  • New EditorAccessibilitySegmentsTest (17 tests, Robolectric + Truth) covering following/preceding for character, word and line, plus empty-text / boundary / unsupported-granularity guards. Green via :editor:testV8DebugUnitTest.
  • :editor module compiles clean (no new warnings).

Not covered here

On-device TalkBack behaviour (double-tap placement + granular navigation on real hardware) was not verified in CI — the wiring follows the documented Android EditText accessibility contract and is unit-tested, but a manual TalkBack pass is recommended before release.

The editor wraps Rosemoe's CodeEditor, a custom-drawn View. Its
accessibility node reported the whole document as a single generic
editable node (class name CodeEditor) and advertised no text-movement
granularities, ACTION_SET_SELECTION or granularity actions. TalkBack
therefore had no way to move the real text caret with the reading
position: double-tapping a word/line placed the caret at the node's
centre and subsequent typing landed on the wrong line.

Make IDEEditor behave like a standard EditText for screen readers:

- Report as android.widget.EditText and advertise CHARACTER/WORD/LINE
  movement granularities plus ACTION_SET_SELECTION and the
  next/previous-at-granularity actions.
- Handle those actions by converting absolute char indices to
  (line, column) via the editor's Indexer and moving the real caret,
  then emit TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY so the
  spoken segment and caret stay in sync.
- Track the last touch-exploration hover point and, on ACTION_CLICK,
  place the caret there so a double-tap lands on the explored word/line
  (only active while touch exploration is on, so sighted use is
  unaffected).

Text-segment iteration lives in EditorAccessibilitySegments (character
and word mirror the framework's AccessibilityIterators; line walks
logical '\n'-separated lines) and is covered by unit tests.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@makhlwf

makhlwf commented Jul 11, 2026

Copy link
Copy Markdown
Author

Can someone please build it for me so I can test it? I don't think there is a GitHub action to build it. I followed the documentation, but I need a device to be able to build it, and I don't think I can get one.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

IDEEditor now exposes EditText-style accessibility semantics, supports TalkBack selection and movement actions, synchronizes caret placement from touch exploration, and announces text and selection changes using character, word, line, and paragraph segment calculations.

Changes

Accessibility cursor support

Layer / File(s) Summary
Accessibility segment engine
editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt, editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt
Adds character, word, line, and paragraph segment navigation with boundary, empty-input, unsupported-granularity, and movement tests.
Accessibility node semantics
editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
Advertises EditText semantics and exposes supported movement granularities and selection actions for active editable content.
Accessibility cursor actions and announcements
editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
Handles hover, click, selection, and granularity actions, maps indices to editor positions, emits traversed-text events, and announces text and selection changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TalkBack
  participant IDEEditor
  participant EditorAccessibilitySegments
  participant ContentIndexer
  TalkBack->>IDEEditor: Request movement action
  IDEEditor->>EditorAccessibilitySegments: Calculate next or previous segment
  EditorAccessibilitySegments-->>IDEEditor: Return segment indices
  IDEEditor->>ContentIndexer: Convert indices to line and column
  ContentIndexer-->>IDEEditor: Return editor positions
  IDEEditor-->>TalkBack: Update selection and announce traversed text
Loading

Poem

A bunny taps the editor bright,
And hops through words from left to right.
Lines and paragraphs join the race,
The caret finds its proper place.
“Thump!” says the screen reader light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: syncing TalkBack accessibility focus with the text cursor.
Description check ✅ Passed The description matches the changeset and explains the accessibility fixes and testing.
Linked Issues check ✅ Passed The implementation addresses #1497 by syncing accessibility actions and hover-based caret placement with the editor cursor.
Out of Scope Changes check ✅ Passed The added segment utilities and tests are directly supporting the accessibility fix and stay within scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt (2)

701-726: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

sendTextTraversedEvent copies the entire document on every granularity move.

event.text.add(text.toString()) (line 724) makes a full string copy of the document each time, on top of the copies already made inside EditorAccessibilitySegments. This only runs while an accessibility service is enabled, so impact is limited to that population, but for large files it compounds the per-navigation cost flagged in EditorAccessibilitySegments.kt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt` around
lines 701 - 726, The sendTextTraversedEvent method copies the entire document
for every accessibility navigation event. Remove the
event.text.add(text.toString()) operation and preserve the event metadata and
dispatch flow without creating a full-document string copy.

612-615: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer narrower exception types over broad catch (e: Exception).

Based on learnings, this project prefers catching only the specific exception types expected from the call (e.g. IndexOutOfBoundsException/IllegalArgumentException from setSelection/setSelectionRegion/getCharPosition) rather than a catch-all, to fail fast during development and surface unexpected errors instead of silently swallowing them.

Also applies to: 695-698

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt` around
lines 612 - 615, Replace the broad catch blocks around the accessibility
cursor-placement logic in IDEEditor with only the expected
IndexOutOfBoundsException and IllegalArgumentException types from setSelection,
setSelectionRegion, or getCharPosition; preserve the existing logging and
false-return behavior for those cases while allowing unexpected exceptions to
propagate.

Source: Learnings

editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt (1)

73-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a mid-word offset case for WORD granularity.

All current word tests use offsets that already sit at word/whitespace boundaries. Adding a case like following("hello", word, 2) would have caught the mid-word truncation bug flagged in EditorAccessibilitySegments.kt (word skip-loop doesn't snap back to the enclosing word's start when the offset is already inside a word).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt`
around lines 73 - 105, The WORD navigation tests lack coverage for offsets
inside an existing word. Add a mid-word case in the `word following` tests, such
as calling `following("hello", word, 2)` and asserting the complete word range
`[0, 5]`, so `EditorAccessibilitySegments` preserves the enclosing word instead
of truncating it.
editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt (1)

92-92: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Fresh BreakIterator + full text.toString() copy on every character/word move.

Each call to characterFollowing/characterPreceding/wordFollowing/wordPreceding allocates a new BreakIterator and copies the entire document via text.toString(). Since these are invoked on every single TalkBack character/word navigation action, this is an O(n) allocation per keystroke-equivalent gesture — noticeable on large source files. Consider caching the last (text, BreakIterator) pair (invalidate on text-identity change) or backing the iterator with a CharacterIterator adapter over the CharSequence instead of copying to String.

Also applies to: 106-106, 124-124, 139-139

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt`
at line 92, Update the character and word navigation methods around the
BreakIterator usages to avoid creating a new iterator and copying the entire
CharSequence on every call. Cache and reuse the BreakIterator with the last text
identity, invalidating it when the source text changes, or use a
CharacterIterator adapter backed directly by the CharSequence; apply the same
optimization to characterFollowing, characterPreceding, wordFollowing, and
wordPreceding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt`:
- Around line 550-564: Update IDEEditor’s getAccessibilityClassName() to return
the EditText class name only when !isReleased, isEnabled, and isEditable,
matching createAccessibilityNodeInfo(). For all other states, return the
superclass accessibility class name so read-only or released editors do not
expose edit-box semantics in accessibility events.

In
`@editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt`:
- Around line 121-148: Update wordFollowing and wordPreceding to first snap
mid-word offsets to the enclosing word’s start or end boundary, so navigation
never returns a truncated range such as [2, 5] or [0, 2] for “hello”. Preserve
existing scanning for whitespace and punctuation, and add a regression test in
EditorAccessibilitySegmentsTest.kt covering mid-word offsets.

---

Nitpick comments:
In `@editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt`:
- Around line 701-726: The sendTextTraversedEvent method copies the entire
document for every accessibility navigation event. Remove the
event.text.add(text.toString()) operation and preserve the event metadata and
dispatch flow without creating a full-document string copy.
- Around line 612-615: Replace the broad catch blocks around the accessibility
cursor-placement logic in IDEEditor with only the expected
IndexOutOfBoundsException and IllegalArgumentException types from setSelection,
setSelectionRegion, or getCharPosition; preserve the existing logging and
false-return behavior for those cases while allowing unexpected exceptions to
propagate.

In
`@editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt`:
- Line 92: Update the character and word navigation methods around the
BreakIterator usages to avoid creating a new iterator and copying the entire
CharSequence on every call. Cache and reuse the BreakIterator with the last text
identity, invalidating it when the source text changes, or use a
CharacterIterator adapter backed directly by the CharSequence; apply the same
optimization to characterFollowing, characterPreceding, wordFollowing, and
wordPreceding.

In
`@editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt`:
- Around line 73-105: The WORD navigation tests lack coverage for offsets inside
an existing word. Add a mid-word case in the `word following` tests, such as
calling `following("hello", word, 2)` and asserting the complete word range `[0,
5]`, so `EditorAccessibilitySegments` preserves the enclosing word instead of
truncating it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b2210ce8-0e06-4975-846d-db11ef1fa261

📥 Commits

Reviewing files that changed from the base of the PR and between 232cec2 and 76246e8.

📒 Files selected for processing (3)
  • editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
  • editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt
  • editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt

Comment thread editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt Outdated
- getAccessibilityClassName() now mirrors the !isReleased && isEnabled &&
  isEditable guard used by createAccessibilityNodeInfo(), so a read-only or
  released editor is no longer announced as an "Edit box".
- Word granularity navigation snaps a mid-word offset to the enclosing word's
  start/end, so navigation traverses the whole word (e.g. "hello"@2 -> [0,5])
  instead of a truncated range. Adds regression tests for mid-word offsets.
- Narrow the accessibility catch blocks to IndexOutOfBoundsException /
  IllegalArgumentException instead of a broad Exception, per project convention.
@makhlwf

makhlwf commented Jul 11, 2026

Copy link
Copy Markdown
Author

Thanks for the review @coderabbitai. Addressed in 7ca9295f9. Summary of each point:

Major (both fixed)

  • getAccessibilityClassName() now mirrors the !isReleased && isEnabled && isEditable guard.
  • ✅ Word navigation snaps mid-word offsets to the enclosing word + regression tests added.

Nitpicks

  • Narrower exception types — narrowed the two accessibility catch blocks (handleAccessibilityClick, applySelectionFromIndices) to IndexOutOfBoundsException / IllegalArgumentException.
  • Mid-word WORD test — added following("hello", word, 2) / preceding(...) asserting [0, 5].
  • ⏭️ sendTextTraversedEvent copies the whole documentkeeping as-is by design. For TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, the screen reader substrings [fromIndex, toIndex] out of event.getText(), so the indices must address the full document text — the same thing TextView does, and the same full-text payload the base CodeEditor.createAccessibilityNodeInfo() already sends via info.setText(...). Dropping it would break announcement of the traversed segment. It only runs while an accessibility service is enabled and once per navigation gesture (not per keystroke).
  • ⏭️ Fresh BreakIterator + toString() per movedeferring. Same gating (a11y-only, per-gesture). A cached-iterator-with-text-identity-invalidation adds a correctness-sensitive cache to a rarely-hot path; I'd rather not introduce it without on-device profiling showing it matters. Happy to file a follow-up if you'd like it tracked.

:editor:testV8DebugUnitTest is green (19 tests). Note: on-device TalkBack behaviour still needs a manual pass, as called out in the PR description.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@makhlwf Thanks for the detailed follow-up and for pushing the fixes in 7ca9295f9.

  • The getAccessibilityClassName() guard alignment and the word-navigation snapping fix (with regression tests) both address the major concerns well.
  • Narrowing the catch blocks to IndexOutOfBoundsException/IllegalArgumentException and adding the mid-word WORD test cover the nitpicks cleanly.

On the two deferred items, your reasoning holds up:

  • For sendTextTraversedEvent, since TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY consumers substring event.getText() using [fromIndex, toIndex], the indices do need to address the full document text — that's consistent with how TextView and the base CodeEditor.createAccessibilityNodeInfo() already behave, and it's gated to accessibility-enabled sessions, so I agree keeping it as-is is fine.
  • Deferring the BreakIterator/toString() caching without on-device profiling data is a reasonable call given it's a rarely-hot, per-gesture path rather than per-keystroke. Happy to have this tracked as a follow-up if you'd like — just let me know and I can open an issue referencing this PR and comment.

Nice work getting :editor:testV8DebugUnitTest green across 19 tests. Once the on-device TalkBack pass is done, this should be in good shape.

🐰✨

@makhlwf
makhlwf force-pushed the adfa-editor-a11y-cursor-sync branch from 7ca9295 to e88685a Compare July 11, 2026 22:02
… events

Two TalkBack gaps surfaced in on-device testing:

1. Line and paragraph navigation didn't work. PARAGRAPH granularity was
   never advertised, and the editor never emitted
   TYPE_VIEW_TEXT_SELECTION_CHANGED, so the screen reader could not track
   the caret across the larger granularities. Advertise + implement
   PARAGRAPH (non-empty logical lines, blank lines skipped) and fire a
   selection-changed event on every selection change.

2. Inserted/deleted characters were not announced. Emit
   TYPE_VIEW_TEXT_CHANGED on content changes with fromIndex, added/removed
   counts and beforeText reconstructed from the change, so TalkBack speaks
   typed and deleted text like a standard EditText.

Both event senders are no-ops when no accessibility service is enabled.
Adds unit tests for paragraph navigation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt`:
- Around line 210-234: Update paragraphFollowing and paragraphPreceding in
editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt
to mirror lineFollowing/linePreceding for mid-paragraph offsets, advancing to
the next paragraph or truncating to the preceding paragraph as appropriate. Add
regression tests in
editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt
covering paragraphFollowing("abc\ndef\nghi", 5) → [8,11] and
paragraphPreceding("abc\ndef\nghi", 5) → [4,7].
- Around line 210-234: Update paragraphPreceding so that after locating the
preceding paragraph’s start, it scans end forward to the next newline or text
length instead of returning the raw offset. Preserve the existing handling for
trailing newlines, out-of-range offsets, and missing preceding paragraphs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0b81e6ae-8aad-420e-b085-6121ce76b375

📥 Commits

Reviewing files that changed from the base of the PR and between e88685a and c3d5313.

📒 Files selected for processing (3)
  • editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
  • editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt
  • editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt

paragraphPreceding returned the raw offset as its end bound, so a
mid-paragraph offset yielded a truncated range (e.g. "abc\ndef\nghi"@5
gave [4,5] instead of [4,7]). paragraphFollowing also snapped to the
current line instead of advancing. Rework both to mirror
lineFollowing/linePreceding (advance to the next line forward; return
the whole enclosing line backward) while still skipping blank lines, and
add mid-paragraph regression tests.
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

I'm starting to look at this.

@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

Hi @makhlwf — first, thank you. This is thoughtful, careful work on something that genuinely matters, and it's clear you put real care into it: the code is clean, the EditorAccessibilitySegments logic is well-factored and thoroughly unit-tested, and the whole change is safe (no crashes, correctly gated on touch-exploration so sighted use is untouched). Tackling screen-reader support for a custom-drawn code editor is one of the harder accessibility problems there is, and taking it on for the blind and low-vision developers who asked for it is genuinely appreciated. 🙏

You asked if someone could build and test this on a device, since you couldn't — so I did. I rebased your branch onto current stage, built the debug APK, and ran it on a physical arm device (Samsung Galaxy S22+, Android 16) with TalkBack enabled. I want to share the results honestly, because I think they point somewhere useful.

What checked out:

  • Builds cleanly and the module compiles against current stage.
  • All 26 unit tests pass.
  • No crashes, no exceptions from the accessibility paths.
  • The editor's accessibility node now correctly reports as android.widget.EditText — so the wiring you added is definitely live.

What happened with TalkBack actually running, though, is where I think we hit a wall:

  1. On focus, TalkBack reads the entire file aloud — there's no way to land on a single line.
  2. Swiping doesn't move within the editor — it jumps to the toolbar buttons instead of moving line-by-line through the code.
  3. Double-tap places the caret far from the target — aiming at line 4 landed the caret around line 10; typing then goes to the wrong place.

I dug into why, and I don't think it's a bug in your handlers — I think it's the underlying model. The editor exposes its whole document to accessibility as a single, flat node (that comes from Rosemoe's base CodeEditor, which sets the entire text on one node). Your PR relabels that node as EditText and adds selection/granularity/click handlers on top of it. That contract works beautifully for a small EditText, but for a scrolling code document it runs into limits:

  • Because the whole file is one node, TalkBack reads all of it on focus, and there are no per-line elements to swipe between — so linear swipe escapes to the neighboring nodes (the toolbar).
  • The move-by-granularity actions you implemented are only reachable through TalkBack's reading controls rather than a normal swipe, and in practice they aren't engaging.
  • Caret-on-double-tap relies on the last hover coordinate, but TalkBack's "double-tap anywhere to activate" gesture doesn't deliver a fresh hover at the target line, so the stored point maps to the wrong row.

The direction that I think actually gets there is exposing a virtual view hierarchy via AccessibilityNodeProvider / ExploreByTouchHelper — one virtual node per line (or visible row). With that in place, TalkBack reads line-by-line, swipes line-to-line, and a double-tap resolves to the line under the finger. It's a bigger change than this PR, and honestly the best home for it might be Rosemoe's upstream editor rather than our fork — it'd help every project that uses it, and it's the layer that owns the single-node behavior. Might be worth opening a conversation there first.

The good news: your EditorAccessibilitySegments (character/word/line/paragraph segmentation) is exactly the kind of pure, well-tested logic a provider-based implementation would reuse — so that work isn't lost.

One gentle process suggestion for next time, and it would have helped here: try to work in tiny, incremental baby steps spread across several small PRs, rather than one large change. This PR is 700+ lines, which makes it hard to tease apart — when something doesn't behave as expected on-device, it's tough to isolate which piece is responsible, and it's a lot to review and land as a unit. If this had arrived as a sequence of small PRs — say, (1) just the pure EditorAccessibilitySegments + tests, (2) the node/EditText semantics, (3) granularity handling, (4) double-tap placement — each one could be verified on a device on its own, and we'd have caught the single-node limitation at step 2 with far less work invested. Smaller PRs are easier to review, easier to test, easier to correct course on, and much less discouraging when one step needs rethinking. It's the way I'd recommend approaching the virtual-hierarchy work too. 🙂

I know this isn't the outcome you were hoping for after the effort you put in, and I'm sorry about that — but please don't let it discourage you. This was a real, useful contribution: it surfaced the actual problem clearly and moved our understanding forward, and I'd genuinely welcome more from you. I can't promise you a test device, but I'm glad to build and run your future iterations on a real device with TalkBack and report back what I see — exactly like I did here — so the lack of hardware doesn't have to block you. Happy to pair on the provider-based approach or look at an upstream Rosemoe issue together if you'd like to keep going. Thank you again. 🙏

(One small logistics note: the branch currently has a merge conflict with stage from some editor changes that landed after you branched — I resolved it locally to test, and I'm glad to share that resolution whenever it's useful.)

@makhlwf

makhlwf commented Jul 15, 2026

Copy link
Copy Markdown
Author

Hi @hal-eisen-adfa, thank you for building this and testing it on a real device with TalkBack. I really appreciate the detailed notes.

I want to clarify the intended scope and testing flow, because I think a few of the observations may be measuring a different interaction model than the one this PR is trying to support.

The granularity is meant to be controlled from TalkBack only. Most of the changes I made are deliberately on the TalkBack/accessibility side, because I do not want to change behavior for normal users or affect interactions people are already used to. So the scope here is accessibility only, specifically screen-reader behavior.

On focus reading the whole editor: that is also the normal behavior I expect from a large editable text node. If you focus a text box in a notebook app, or a long paragraph you wrote in a normal editor, TalkBack will often announce the whole text/content for that focused text field. So I do not think reading the full document on initial focus means this PR is failing by itself.

On the line focus + double-tap point: I do not think the intended workflow is "focus a line, then double-tap to place the cursor on that focused line." The workflow requested by the initial users was closer to this:

  1. Use TalkBack's reading controls to choose the needed granularity, for example word granularity.
  2. Focus the code editor text box.
  3. Move through the editor using TalkBack granularity navigation, for example word by word.
  4. Double-tap to set the editor cursor at the currently reached word/segment.

That is the flow I implemented and tested in the code: TalkBack movement granularity actions update the accessibility/editor selection, and activation places the cursor at that current accessibility position. So if the test was mainly normal swipe focus traversal between views, or focusing what TalkBack treats as a line and then double-tapping, that is not quite the behavior this PR is trying to provide.

I agree that a virtual view hierarchy with AccessibilityNodeProvider / ExploreByTouchHelper could be a stronger long-term model for line-by-line exploration, and it may belong upstream in Rosemoe. But I still think this PR is useful as a narrower accessibility-only improvement because it gives TalkBack users granularity-based navigation and cursor placement without changing behavior for non-screen-reader users.

Could you please re-review with the TalkBack granularity flow above if that is not what you tested? If you have specific testing instructions or a different expected TalkBack gesture sequence, please share them and I will look into it. I really want this work to land if the intended TalkBack path behaves correctly, because it would help users start getting used to this editor being more accessible.

@makhlwf

makhlwf commented Jul 15, 2026

Copy link
Copy Markdown
Author

Hi @hal-eisen-adfa, I handled the branch conflict you mentioned.

I merged current upstream/stage into adfa-editor-a11y-cursor-sync, resolved the conflict in editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt, and pushed the result in 7f9a3189f.

The resolution keeps both sides of the editor changes:

  • the TalkBack/accessibility text and selection events from this PR
  • the newer stage editor updates, including refreshModifiedState() and inline-suggestion dismissal

I also reran ./gradlew :editor:testV8DebugUnitTest, and it passed with BUILD SUCCESSFUL.

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.

Accessibility: AccessibilityNodeProvider should accurately sync accessibility focus coordinates

2 participants