Fix three VoiceOver bugs: crash, cursor sync, word announcement

Bug 1 (crash): postEchoAreaAnnouncementIfNeeded called Fbuffer_string()
without block_input, allowing timer events to interleave with Lisp
calls and corrupt buffer state.  Added block_input/unblock_input via
record_unwind_protect_void.  Also removed unpaired unblock_input()
in postCompletionAnnouncementForBuffer (patch 0003) that became a
double-unblock after patch 0008 added block_input + unwind protect.

Bug 2 (cursor sync): VoiceOver browse cursor did not follow Emacs
keyboard cursor because SelectedTextChanged with discontiguous
direction alone is not sufficient for VoiceOver to re-anchor.  Added
NSAccessibilityFocusedUIElementChangedNotification post when Emacs
moves the cursor across a line boundary, forcing VoiceOver to re-query
accessibilitySelectedTextRange.

Bug 3 (word off-by-one): Evil w (next-word) read the previous word
instead of the destination word.  VO auto-speech from
SelectedTextChanged with direction=next+granularity=word reads the
traversed word, not the arrived-at word.  Added explicit word
announcement (like char moves) that reads the word AT the new cursor
position using whitespace-delimited word boundary scan.
This commit is contained in:
2026-03-02 10:42:54 +01:00
parent 7ab55a7fb3
commit cd288e8c76
2 changed files with 77 additions and 7 deletions

View File

@@ -435,9 +435,9 @@ index 8d44b5f..29b646d 100644
specpdl_ref count2 = SPECPDL_INDEX ();
+ /* Register unblock_input as an unwind action so that if any Lisp
+ call below signals (triggering a longjmp through unbind_to),
+ block_input is always paired with an unblock_input. The explicit
+ unblock_input() at the end of the function is still needed for
+ the normal (non-signal) path. */
+ block_input is always paired with an unblock_input. The
+ unbind_to call at the end of the function unwinds this.
+ record_unwind_protect_void plus unbind_to is idempotent. */
+ record_unwind_protect_void (unblock_input);
record_unwind_current_buffer ();
if (b != current_buffer)
@@ -518,7 +518,7 @@ index 8d44b5f..29b646d 100644
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
if (point > oldPoint)
direction = ns_ax_text_selection_direction_next;
@@ -9488,6 +9670,26 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
@@ -9488,6 +9670,41 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
granularity = ns_ax_text_selection_granularity_line;
}
@@ -541,6 +541,21 @@ index 8d44b5f..29b646d 100644
+ handles them correctly. */
+ if (emacsMovedCursor && !isCtrlNP)
+ direction = ns_ax_text_selection_direction_discontiguous;
+
+ /* Post FocusedUIElementChanged when Emacs moved the cursor
+ across a line boundary so VoiceOver re-anchors its browse
+ cursor at the new accessibilitySelectedTextRange.
+ SelectedTextChanged with discontiguous direction alone is
+ not sufficient; VoiceOver requires this notification to
+ re-query the focused element and update its internal
+ browse position. */
+ if (emacsMovedCursor
+ && granularity
+ == ns_ax_text_selection_granularity_line
+ && [self isAccessibilityFocused])
+ ns_ax_post_notification (
+ self,
+ NSAccessibilityFocusedUIElementChangedNotification);
+
/* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused])
@@ -592,7 +607,7 @@ index 8d44b5f..29b646d 100644
scrollbarsNeedingUpdate = 0;
fs_state = FULLSCREEN_NONE;
fs_before_fs = next_maximized = -1;
@@ -12688,6 +12909,152 @@ - (id)accessibilityFocusedUIElement
@@ -12688,6 +12909,154 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */
@@ -631,7 +646,9 @@ index 8d44b5f..29b646d 100644
+ set_buffer_internal_1 is preferred over set_buffer_internal in
+ a redisplay context: it skips point-motion hooks that could
+ trigger further redisplay or modify buffer state unexpectedly. */
+ block_input ();
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_protect_void (unblock_input);
+ record_unwind_current_buffer ();
+ set_buffer_internal_1 (eb);
+ Lisp_Object ls = Fbuffer_string ();