From b6a576a3129ffd324a6b26ebe136f9235d56aeff Mon Sep 17 00:00:00 2001 From: Daneel Date: Mon, 2 Mar 2026 21:30:42 +0100 Subject: [PATCH] patches: fix discontiguous moves reading only first word For discontiguous moves (teleports, org-agenda items separated by blank lines, multi-line jumps), AXSelectedTextChanged was sent with AXTextSelectionDirection=discontiguous. VoiceOver interprets an explicit discontiguous direction as 're-anchor only' and reads only the word at the cursor, ignoring VoiceOver's own line-browse mode. The pre-review code (51f5944) omitted direction/granularity for all moves and let VoiceOver determine what to read from its navigation state. This correctly reads the full line when the VoiceOver rotor is in line mode, which is the typical setting for text navigation. Fix: omit AXTextSelectionDirection and AXTextSelectionGranularity from AXSelectedTextChanged when direction=discontiguous. Include them only for sequential moves (direction=next/previous), where the explicit hint ensures VoiceOver reads the correct unit without an extra state query. This fixes: - org-agenda / org-super-agenda j/k: items separated by blank lines cause singleLineMove=NO (non-adjacent AX indices), so direction was discontiguous -> only first word read. - Any other navigation that crosses blank or invisible lines. Sequential moves (C-n/C-p, single adjacent j/k) still include direction + granularity=line for reliable full-line reads. --- ...-with-macOS-Zoom-for-cursor-tracking.patch | 2 +- ...lity-base-classes-and-text-extractio.patch | 2 +- ...fer-accessibility-element-core-proto.patch | 2 +- ...tification-dispatch-and-mode-line-el.patch | 2 +- ...ive-span-elements-for-Tab-navigation.patch | 2 +- ...essibility-with-EmacsView-and-redisp.patch | 2 +- ...r-accessibility-section-to-macOS-app.patch | 2 +- ...lay-completion-candidates-for-VoiceO.patch | 2 +- ...d-frame-completion-candidates-for-Vo.patch | 82 +++++++++++-------- 9 files changed, 56 insertions(+), 42 deletions(-) diff --git a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch index 3680f62..be5bfe9 100644 --- a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch +++ b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch @@ -1,4 +1,4 @@ -From 0ef896396709b9b0a832d07476dfc7ded2eca9fc Mon Sep 17 00:00:00 2001 +From e27ae42313eb5cb5cab14de348f83db216a17a53 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 22:39:35 +0100 Subject: [PATCH 0/8] ns: integrate with macOS Zoom for cursor tracking diff --git a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch index 294ee64..bf127a2 100644 --- a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch +++ b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch @@ -1,4 +1,4 @@ -From 195c2d4ccc1d607984468fa7f9a8e41005463f04 Mon Sep 17 00:00:00 2001 +From 9650910e7ac6e423ea9beaa75033d12693b93c89 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction diff --git a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch index 6cdd3c5..57200b2 100644 --- a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch +++ b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch @@ -1,4 +1,4 @@ -From 96ab29ec0bd830c6ba81632c8ccfedb4fdec85d2 Mon Sep 17 00:00:00 2001 +From 9ac52a3f3e57628bd06516bc439b8ec388207098 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 2/8] ns: implement buffer accessibility element (core diff --git a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch index 36746f8..e004b27 100644 --- a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch +++ b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch @@ -1,4 +1,4 @@ -From 23f2a62922e7fe9a4075b5e2074bde292434f882 Mon Sep 17 00:00:00 2001 +From 361aecfc858d712943921435454d9a7235d145ed Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line diff --git a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch index 0981609..5d3c78a 100644 --- a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch +++ b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch @@ -1,4 +1,4 @@ -From 04edba6e94622176c9869ba2564cd4fda4d2edd8 Mon Sep 17 00:00:00 2001 +From a0ea23e5b05e7ab6048d6dd3e9e05aae00dc6939 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation diff --git a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch index bfb6c9e..307626f 100644 --- a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch +++ b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch @@ -1,4 +1,4 @@ -From c3afdca23634547cb961efd8368e09393ade2690 Mon Sep 17 00:00:00 2001 +From ca511140b95caf299ab1b24b7a22de03a2e5b543 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay diff --git a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch index 6b0370a..704314c 100644 --- a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch +++ b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch @@ -1,4 +1,4 @@ -From 312ef33147100f5486aa42b70d44ee9e143fd6ce Mon Sep 17 00:00:00 2001 +From 2cfc623598b666515fe3cf05ee8c578601e0e587 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index 8509c1a..b199aed 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,4 +1,4 @@ -From a264f3ec7acb0f736e79bd692bd79c11b0f16c2e Mon Sep 17 00:00:00 2001 +From 239d804cf216a05a2b62aeeda7ab2cc5795c158b Mon Sep 17 00:00:00 2001 From: Daneel Date: Mon, 2 Mar 2026 18:39:46 +0100 Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index 59e2adf..8a4144b 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -1,4 +1,4 @@ -From 089ff332d52b9595e774b654a9259c65450cdaa2 Mon Sep 17 00:00:00 2001 +From 235fb607dfe06a242044218a2ed0ea82fed4f82f Mon Sep 17 00:00:00 2001 From: Daneel Date: Mon, 2 Mar 2026 18:49:13 +0100 Subject: [PATCH 8/8] ns: announce child frame completion candidates for @@ -33,8 +33,8 @@ area announcements. doc/emacs/macos.texi | 14 +- etc/NEWS | 18 +- src/nsterm.h | 20 ++ - src/nsterm.m | 493 ++++++++++++++++++++++++++++++++++++++----- - 4 files changed, 475 insertions(+), 70 deletions(-) + src/nsterm.m | 504 +++++++++++++++++++++++++++++++++++++------ + 4 files changed, 482 insertions(+), 74 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi index 8d4a7825d8..03a657f970 100644 @@ -149,7 +149,7 @@ index 21a93bc799..bdd40b8eb7 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index 8f744d1bf3..20a50281db 100644 +index 8f744d1bf3..1f3b2ad78a 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -1126,24 +1126,19 @@ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */ @@ -339,29 +339,43 @@ index 8f744d1bf3..20a50281db 100644 specpdl_ref count = SPECPDL_INDEX (); record_unwind_current_buffer (); /* Ensure block_input is always matched by unblock_input even if -@@ -9060,15 +9166,23 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point +@@ -9053,22 +9159,33 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point + && granularity + == ns_ax_text_selection_granularity_character); + +- /* Always post SelectedTextChanged to interrupt VoiceOver reading +- and update cursor tracking / braille displays. */ ++ /* Post SelectedTextChanged to interrupt VoiceOver reading and ++ update cursor tracking / braille displays. ++ For sequential moves (direction = next/previous): include ++ direction + granularity so VoiceOver reads the destination line ++ or word without additional state queries. ++ For discontiguous jumps (teleports, multi-line leaps): omit ++ direction and granularity and let VoiceOver determine what to read ++ from its own navigation state. This matches the pre-review ++ behaviour and ensures VoiceOver reads the full destination line ++ even when the jump skips blank or invisible lines (e.g. org-agenda ++ items separated by blank lines, where adjacency detection cannot ++ classify the move as singleLineMove). */ + NSMutableDictionary *moveInfo = [NSMutableDictionary dictionary]; + moveInfo[@"AXTextStateChangeType"] = @(ns_ax_text_state_change_selection_move); - moveInfo[@"AXTextSelectionDirection"] = @(direction); +- moveInfo[@"AXTextSelectionDirection"] = @(direction); moveInfo[@"AXTextChangeElement"] = self; - /* Omit granularity for character moves so VoiceOver does not - derive its own speech (it would read the wrong character - for block-cursor mode). Include it for word/line/ - selection so VoiceOver reads the appropriate text. */ - if (!isCharMove) -+ /* Include granularity for sequential moves so VoiceOver reads the -+ appropriate unit. Omit for character moves (announced explicitly -+ below) and for discontiguous jumps (destination line announced -+ explicitly; omitting granularity lets VoiceOver use its default -+ behaviour and re-anchor its browse cursor). */ -+ if (!isCharMove -+ && direction != ns_ax_text_selection_direction_discontiguous) - moveInfo[@"AXTextSelectionGranularity"] = @(granularity); +- moveInfo[@"AXTextSelectionGranularity"] = @(granularity); ++ BOOL isDiscontiguous ++ = (direction == ns_ax_text_selection_direction_discontiguous); ++ if (!isDiscontiguous && !isCharMove) ++ { ++ moveInfo[@"AXTextSelectionDirection"] = @(direction); ++ moveInfo[@"AXTextSelectionGranularity"] = @(granularity); ++ } -+ /* Post SelectedTextChanged from the parent EmacsView (an NSView) -+ rather than from self (a custom NSObject element). VoiceOver -+ processes text-change notifications more reliably from view-based -+ elements. Include UIElementsKey so VoiceOver knows which child -+ element's selectedTextRange to re-query. */ + moveInfo[NSAccessibilityUIElementsKey] = @[self]; ns_ax_post_notification_with_info ( - self, @@ -369,7 +383,7 @@ index 8f744d1bf3..20a50281db 100644 NSAccessibilitySelectedTextChangedNotification, moveInfo); -@@ -9166,12 +9280,17 @@ user expectation ("w" jumps to next word and reads it). */ +@@ -9166,12 +9283,17 @@ user expectation ("w" jumps to next word and reads it). */ } } @@ -392,7 +406,7 @@ index 8f744d1bf3..20a50281db 100644 if (cachedText && granularity == ns_ax_text_selection_granularity_line) { -@@ -9236,6 +9355,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b +@@ -9236,6 +9358,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b block_input (); specpdl_ref count2 = SPECPDL_INDEX (); @@ -404,7 +418,7 @@ index 8f744d1bf3..20a50281db 100644 record_unwind_protect_void (unblock_input); record_unwind_current_buffer (); if (b != current_buffer) -@@ -9412,12 +9536,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9412,12 +9539,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f if (!b) return; @@ -434,7 +448,7 @@ index 8f744d1bf3..20a50281db 100644 if (modiff != self.cachedModiff) { self.cachedModiff = modiff; -@@ -9431,6 +9572,7 @@ Text property changes (e.g. face updates from +@@ -9431,6 +9575,7 @@ Text property changes (e.g. face updates from { self.cachedCharsModiff = chars_modiff; [self postTextChangedNotification:point]; @@ -442,7 +456,7 @@ index 8f744d1bf3..20a50281db 100644 } } -@@ -9453,8 +9595,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9453,8 +9598,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property displayed in the minibuffer. In normal editing buffers, font-lock and other modes change BUF_OVERLAY_MODIFF on every redisplay, triggering O(overlays) work per keystroke. @@ -460,7 +474,7 @@ index 8f744d1bf3..20a50281db 100644 goto skip_overlay_scan; int selected_line = -1; -@@ -9500,7 +9649,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9500,7 +9652,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property self.cachedPoint = point; self.cachedMarkActive = markActive; @@ -480,7 +494,7 @@ index 8f744d1bf3..20a50281db 100644 NSInteger direction = ns_ax_text_selection_direction_discontiguous; if (point > oldPoint) direction = ns_ax_text_selection_direction_next; -@@ -9512,6 +9672,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9512,6 +9675,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property /* --- Granularity detection --- */ NSInteger granularity = ns_ax_text_selection_granularity_unknown; @@ -488,7 +502,7 @@ index 8f744d1bf3..20a50281db 100644 [self ensureTextCache]; if (cachedText && oldPoint > 0) { -@@ -9526,7 +9687,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9526,7 +9690,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property NSRange newLine = [cachedText lineRangeForRange: NSMakeRange (newIdx, 0)]; if (oldLine.location != newLine.location) @@ -508,7 +522,7 @@ index 8f744d1bf3..20a50281db 100644 else { NSUInteger dist = (newIdx > oldIdx -@@ -9548,34 +9720,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9548,34 +9723,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property granularity = ns_ax_text_selection_granularity_line; } @@ -556,7 +570,7 @@ index 8f744d1bf3..20a50281db 100644 { NSWindow *win = [self.emacsView window]; if (win) -@@ -9734,6 +9895,13 @@ - (NSRect)accessibilityFrame +@@ -9734,6 +9898,13 @@ - (NSRect)accessibilityFrame if (vis_start >= vis_end) return @[]; @@ -570,7 +584,7 @@ index 8f744d1bf3..20a50281db 100644 block_input (); specpdl_ref blk_count = SPECPDL_INDEX (); record_unwind_protect_void (unblock_input); -@@ -9858,6 +10026,7 @@ than O(chars). Fall back to pos+1 as safety net. */ +@@ -9858,6 +10029,7 @@ than O(chars). Fall back to pos+1 as safety net. */ pos = span_end; } @@ -578,7 +592,7 @@ index 8f744d1bf3..20a50281db 100644 return [[spans copy] autorelease]; } -@@ -10039,6 +10208,10 @@ - (void)dealloc +@@ -10039,6 +10211,10 @@ - (void)dealloc #endif [accessibilityElements release]; @@ -589,7 +603,7 @@ index 8f744d1bf3..20a50281db 100644 [[self menu] release]; [super dealloc]; } -@@ -11488,6 +11661,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f +@@ -11488,6 +11664,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f windowClosing = NO; processingCompose = NO; @@ -599,7 +613,7 @@ index 8f744d1bf3..20a50281db 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; fs_before_fs = next_maximized = -1; -@@ -12796,6 +12972,154 @@ - (id)accessibilityFocusedUIElement +@@ -12796,6 +12975,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. */ @@ -754,7 +768,7 @@ index 8f744d1bf3..20a50281db 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12806,11 +13130,64 @@ - (void)postAccessibilityUpdates +@@ -12806,11 +13133,64 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us