Skip to content

Commit 7376fb8

Browse files
makhlwfdara-abijo-adfa
authored andcommitted
fix(editor): address review feedback on a11y cursor sync
- 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.
1 parent feb5bd5 commit 7376fb8

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

‎editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,14 @@ constructor(
651651
private val isTouchExplorationEnabled: Boolean
652652
get() = accessibilityManager?.isTouchExplorationEnabled == true
653653

654-
override fun getAccessibilityClassName(): CharSequence = EditText::class.java.name
654+
override fun getAccessibilityClassName(): CharSequence =
655+
// Only advertise EditText semantics in the same states createAccessibilityNodeInfo()
656+
// does, so a read-only or released editor is not announced as an "Edit box".
657+
if (!isReleased && isEnabled && isEditable) {
658+
EditText::class.java.name
659+
} else {
660+
super.getAccessibilityClassName()
661+
}
655662

656663
override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo? {
657664
val info = super.createAccessibilityNodeInfo() ?: return null
@@ -713,7 +720,10 @@ constructor(
713720
if (line < 0 || column < 0) return false
714721
setSelection(line, column)
715722
true
716-
} catch (e: Exception) {
723+
} catch (e: IndexOutOfBoundsException) {
724+
log.error("Error placing cursor from accessibility click", e)
725+
false
726+
} catch (e: IllegalArgumentException) {
717727
log.error("Error placing cursor from accessibility click", e)
718728
false
719729
}
@@ -796,7 +806,10 @@ constructor(
796806
setSelectionRegion(startPos.line, startPos.column, endPos.line, endPos.column)
797807
}
798808
true
799-
} catch (e: Exception) {
809+
} catch (e: IndexOutOfBoundsException) {
810+
log.error("Error applying accessibility selection [$start, $end]", e)
811+
false
812+
} catch (e: IllegalArgumentException) {
800813
log.error("Error applying accessibility selection [$start, $end]", e)
801814
false
802815
}

‎editor/src/main/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegments.kt‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ object EditorAccessibilitySegments {
123123
if (length <= 0 || offset >= length) return null
124124
val iterator = BreakIterator.getWordInstance().apply { setText(text.toString()) }
125125
var start = if (offset < 0) 0 else offset
126+
// If the offset lands inside a word, snap back to that word's start so the whole
127+
// enclosing word is traversed instead of a truncated tail (e.g. "hello"@2 -> [0,5]).
128+
if (isLetterOrDigit(text, start)) {
129+
while (start > 0 && isLetterOrDigit(text, start - 1)) start--
130+
}
126131
// Skip past any non-word characters (whitespace, punctuation) to the start of a word.
127132
while (!isLetterOrDigit(text, start) && !isWordStart(text, start)) {
128133
start = iterator.following(start)
@@ -138,6 +143,11 @@ object EditorAccessibilitySegments {
138143
if (length <= 0 || offset <= 0) return null
139144
val iterator = BreakIterator.getWordInstance().apply { setText(text.toString()) }
140145
var end = if (offset > length) length else offset
146+
// If the offset lands inside a word, snap forward to that word's end so the whole
147+
// enclosing word is traversed instead of a truncated head (e.g. "hello"@2 -> [0,5]).
148+
if (isLetterOrDigit(text, end - 1) && isLetterOrDigit(text, end)) {
149+
while (end < length && isLetterOrDigit(text, end)) end++
150+
}
141151
while (!isLetterOrDigit(text, end - 1) && !isWordEnd(text, end)) {
142152
end = iterator.preceding(end)
143153
if (end == DONE) return null

‎editor/src/test/java/com/itsaky/androidide/editor/utils/EditorAccessibilitySegmentsTest.kt‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ class EditorAccessibilitySegmentsTest {
8282
assertThat(following("foo bar", word, 7)).isNull()
8383
}
8484

85+
@Test
86+
fun `word following from inside a word snaps to the whole enclosing word`() {
87+
// Offset 2 is inside "hello"; the whole word must be traversed, not a truncated tail.
88+
assertThat(following("hello", word, 2)).isEqualTo(listOf(0, 5))
89+
}
90+
91+
@Test
92+
fun `word preceding from inside a word snaps to the whole enclosing word`() {
93+
assertThat(preceding("hello", word, 2)).isEqualTo(listOf(0, 5))
94+
}
95+
8596
@Test
8697
fun `word preceding returns previous word`() {
8798
val text = "foo bar"

0 commit comments

Comments
 (0)