patches: fix all safe pre-submission issues

- Blocker #1: add BOOL emacsMovedCursor = YES in patch 0005 so series
  compiles standalone; patch 0008 replaces it with !voiceoverSetPoint
- Blocker #3: change all Daneel authorship to Martin Sukany
- Zoom gate: remove ns_accessibility_enabled guards from Zoom code
  paths (0005 no longer adds them; 0008 retains the clarifying comment)
- eassert: remove redundant BUFFER_LIVE_P eassert with contradictory
  comment in patch 0008
- macos.texi: integrate orphan 'Block-style cursors' paragraph as
  @item in the Known Limitations list
- cindex: restore @cindex Zoom, cursor tracking (macOS) removed in 0008
- ChangeLog 0002: list only functions actually added in that patch
- ChangeLog 0008: accurate description (remove wrong BUF_CHARS_MODIFF
  claim for ensureTextCache; ns_ax_buffer_text block_input was in 0001)
- git apply --check: all 9 patches apply cleanly on fresh base

Remaining open issue: BUF_MODIFF regression in patch 0007 (ensureTextCache
O(N) rebuild per font-lock pass) requires design decision before
submission.
This commit is contained in:
2026-03-03 17:50:18 +01:00
parent 3a3fbbac19
commit 70f0cb9a86
9 changed files with 270 additions and 247 deletions

View File

@@ -1,7 +1,7 @@
From 0470786c91eb4a464d8580387b83a4a8d4e4f8eb Mon Sep 17 00:00:00 2001 From fcc1826baee5b424d5fdc176239c5675aee6159b Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 22:39:35 +0100 Date: Sat, 28 Feb 2026 22:39:35 +0100
Subject: [PATCH] ns: integrate with macOS Zoom for cursor tracking Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking
Inform macOS Zoom of the text cursor position so the zoomed viewport Inform macOS Zoom of the text cursor position so the zoomed viewport
follows keyboard focus in Emacs. Also track completion candidates so follows keyboard focus in Emacs. Also track completion candidates so
@@ -86,7 +86,7 @@ index 932d209f56..88c9251c18 100644
#endif #endif
static EmacsMenu *dockMenu; static EmacsMenu *dockMenu;
@@ -1081,6 +1086,284 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) @@ -1081,6 +1086,281 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
} }
@@ -103,9 +103,9 @@ index 932d209f56..88c9251c18 100644
+ +
+/* Cached wrapper around ns_zoom_enabled_p (). +/* Cached wrapper around ns_zoom_enabled_p ().
+ ns_zoom_enabled_p () performs a synchronous Mach IPC roundtrip to the + ns_zoom_enabled_p () performs a synchronous Mach IPC roundtrip to the
+ macOS Accessibility server (~50-200 uss per call). With call sites + macOS Accessibility server (~50-200 µs per call). With call sites
+ in ns_draw_window_cursor, ns_update_end, and ns_zoom_track_completion, + in ns_draw_window_cursor, ns_update_end, and ns_zoom_track_completion,
+ the overhead accumulates to ~150-600 uss per redisplay cycle. Zoom + the overhead accumulates to ~150-600 µs per redisplay cycle. Zoom
+ state changes only on explicit user action in System Settings, so a + state changes only on explicit user action in System Settings, so a
+ 1-second TTL is safe and indistinguishable from querying every frame. + 1-second TTL is safe and indistinguishable from querying every frame.
+ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */ + Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */
@@ -130,9 +130,6 @@ index 932d209f56..88c9251c18 100644
+ Defined here so the Zoom patch compiles independently of the + Defined here so the Zoom patch compiles independently of the
+ VoiceOver patches. */ + VoiceOver patches. */
+static bool +static bool
+/* Note: ns_ax_face_is_selected in the VoiceOver series has identical
+ logic. The duplication is intentional: both series are
+ independent and must compile standalone. */
+ns_zoom_face_is_selected (Lisp_Object face) +ns_zoom_face_is_selected (Lisp_Object face)
+{ +{
+ if (SYMBOLP (face)) + if (SYMBOLP (face))

View File

@@ -1,7 +1,7 @@
From 6c075e29fccc7dc6e9df693c4123ce0001a2dbfc Mon Sep 17 00:00:00 2001 From 488b91178be9a2dfd022533fce6b4adcd5c2ead0 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction Subject: [PATCH 2/9] ns: add accessibility base classes and text extraction
Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa) Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa)
port. No existing code paths are modified. port. No existing code paths are modified.
@@ -33,7 +33,7 @@ set non-nil automatically when an AT is detected at startup.
2 files changed, 585 insertions(+) 2 files changed, 585 insertions(+)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index ea6e7ba4f5..5746e9e9bd 100644 index ea6e7ba4f5..f245675513 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -453,6 +453,124 @@ enum ns_return_frame_mode @@ -453,6 +453,124 @@ enum ns_return_frame_mode
@@ -44,7 +44,7 @@ index ea6e7ba4f5..5746e9e9bd 100644
+ +
+ Accessibility virtual elements (macOS / Cocoa only) + Accessibility virtual elements (macOS / Cocoa only)
+ +
+ ========================================================================= */ + ========================================================================== */
+ +
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+@class EmacsView; +@class EmacsView;
@@ -52,11 +52,11 @@ index ea6e7ba4f5..5746e9e9bd 100644
+/* Base class for virtual accessibility elements attached to EmacsView. */ +/* Base class for virtual accessibility elements attached to EmacsView. */
+@interface EmacsAccessibilityElement : NSAccessibilityElement +@interface EmacsAccessibilityElement : NSAccessibilityElement
+@property (nonatomic, unsafe_unretained) EmacsView *emacsView; +@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
+/* Lisp window object --- safe across GC cycles. +/* Lisp window object safe across GC cycles.
+ GC safety: these Lisp_Objects are NOT visible to GC via staticpro + GC safety: these Lisp_Objects are NOT visible to GC via staticpro
+ or the specpdl stack. This is safe because: + or the specpdl stack. This is safe because:
+ (1) Emacs GC runs only on the main thread, at well-defined safe + (1) Emacs GC runs only on the main thread, at well-defined safe
+ points during Lisp evaluation --- never during redisplay. + points during Lisp evaluation never during redisplay.
+ (2) Accessibility elements are owned by EmacsView which belongs to + (2) Accessibility elements are owned by EmacsView which belongs to
+ an active frame; windows referenced here are always reachable + an active frame; windows referenced here are always reachable
+ from the frame's window tree until rebuildAccessibilityTree + from the frame's window tree until rebuildAccessibilityTree
@@ -81,7 +81,7 @@ index ea6e7ba4f5..5746e9e9bd 100644
+ NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */ + NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */
+} ns_ax_visible_run; +} ns_ax_visible_run;
+ +
+/* Virtual AXTextArea element --- one per visible Emacs window (buffer). */ +/* Virtual AXTextArea element one per visible Emacs window (buffer). */
+@interface EmacsAccessibilityBuffer +@interface EmacsAccessibilityBuffer
+ : EmacsAccessibilityElement <NSAccessibility> + : EmacsAccessibilityElement <NSAccessibility>
+{ +{
@@ -119,12 +119,12 @@ index ea6e7ba4f5..5746e9e9bd 100644
+- (void)invalidateInteractiveSpans; +- (void)invalidateInteractiveSpans;
+@end +@end
+ +
+/* Virtual AXStaticText element --- one per mode line. */ +/* Virtual AXStaticText element one per mode line. */
+@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement +@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement
+@end +@end
+ +
+/* Span types for interactive AX child elements. */ +/* Span types for interactive AX child elements. */
+typedef NS_ENUM (NSInteger, EmacsAXSpanType) +typedef NS_ENUM(NSInteger, EmacsAXSpanType)
+{ +{
+ EmacsAXSpanTypeNone = -1, + EmacsAXSpanTypeNone = -1,
+ EmacsAXSpanTypeButton = 0, + EmacsAXSpanTypeButton = 0,
@@ -200,7 +200,7 @@ index 88c9251c18..9d36de66f9 100644
#include "systime.h" #include "systime.h"
#include "character.h" #include "character.h"
#include "xwidget.h" #include "xwidget.h"
@@ -7201,6 +7202,436 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg @@ -7201,6 +7202,432 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
} }
#endif #endif
@@ -208,7 +208,7 @@ index 88c9251c18..9d36de66f9 100644
+ +
+ Accessibility virtual elements (macOS / Cocoa only) + Accessibility virtual elements (macOS / Cocoa only)
+ +
+ ========================================================================= */ + ========================================================================== */
+ +
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+ +
@@ -249,12 +249,8 @@ index 88c9251c18..9d36de66f9 100644
+ +
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ /* block_input must precede record_unwind_protect_void (unblock_input):
+ if anything between SPECPDL_INDEX and block_input were to throw,
+ the unwind handler would call unblock_input without a matching
+ block_input, corrupting the input-blocking reference count. */
+ block_input ();
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ block_input ();
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
@@ -295,7 +291,7 @@ index 88c9251c18..9d36de66f9 100644
+ +
+ /* Extract this visible run's text. Use + /* Extract this visible run's text. Use
+ Fbuffer_substring_no_properties which correctly handles the + Fbuffer_substring_no_properties which correctly handles the
+ buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would + buffer gap raw BUF_BYTE_ADDRESS reads across the gap would
+ include garbage bytes when the run spans the gap position. */ + include garbage bytes when the run spans the gap position. */
+ Lisp_Object lstr = Fbuffer_substring_no_properties ( + Lisp_Object lstr = Fbuffer_substring_no_properties (
+ make_fixnum (pos), make_fixnum (run_end)); + make_fixnum (pos), make_fixnum (run_end));
@@ -376,7 +372,7 @@ index 88c9251c18..9d36de66f9 100644
+ return NSZeroRect; + return NSZeroRect;
+ +
+ /* charpos_start and charpos_len are already in buffer charpos + /* charpos_start and charpos_len are already in buffer charpos
+ space --- the caller maps AX string indices through + space the caller maps AX string indices through
+ charposForAccessibilityIndex which handles invisible text. */ + charposForAccessibilityIndex which handles invisible text. */
+ ptrdiff_t cp_start = charpos_start; + ptrdiff_t cp_start = charpos_start;
+ ptrdiff_t cp_end = cp_start + charpos_len; + ptrdiff_t cp_end = cp_start + charpos_len;
@@ -518,7 +514,7 @@ index 88c9251c18..9d36de66f9 100644
+} +}
+ +
+/* Build label for span [START, END) in BUF_OBJ. +/* Build label for span [START, END) in BUF_OBJ.
+ Priority: completion--string -> buffer text -> help-echo. */ + Priority: completion--string buffer text help-echo. */
+static NSString * +static NSString *
+ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end, +ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end,
+ Lisp_Object buf_obj) + Lisp_Object buf_obj)

View File

@@ -1,7 +1,7 @@
From 705bba0809db081f538c124aa900bc911de9b0ff Mon Sep 17 00:00:00 2001 From ba093f98d3bb1281278170f01f0792c2b24cf94f Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 2/8] ns: implement buffer accessibility element (core Subject: [PATCH 3/9] ns: implement buffer accessibility element (core
protocol) protocol)
Implement the NSAccessibility text protocol for Emacs buffer windows. Implement the NSAccessibility text protocol for Emacs buffer windows.
@@ -17,10 +17,13 @@ loop is safe: it runs only on actual character modifications.
(accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs (accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs
(ax_length == length); fall back to sequence walk for multi-byte runs. (ax_length == length); fall back to sequence walk for multi-byte runs.
(charposForAccessibilityIndex:): Symmetric O(1) fast path. (charposForAccessibilityIndex:): Symmetric O(1) fast path.
(accessibilitySelectedTextRange, accessibilityLineForIndex:) (accessibilityRole, accessibilityLabel, accessibilityValue)
(accessibilityIndexForLine:, accessibilityRangeForIndex:) (accessibilityNumberOfCharacters, accessibilitySelectedText)
(accessibilityStringForRange:, accessibilityFrameForRange:) (accessibilitySelectedTextRange, accessibilityInsertionPointLineNumber)
(accessibilityRangeForPosition:, setAccessibilitySelectedTextRange:) (accessibilityRangeForLine:, accessibilityRangeForIndex:)
(accessibilityStyleRangeForIndex:, accessibilityFrameForRange:)
(accessibilityRangeForPosition:, accessibilityVisibleCharacterRange)
(accessibilityFrame, setAccessibilitySelectedTextRange:)
(setAccessibilityFocused:): Implement NSAccessibility protocol methods. (setAccessibilityFocused:): Implement NSAccessibility protocol methods.
--- ---
src/nsterm.m | 1115 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 1115 ++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -352,7 +355,7 @@ index 9d36de66f9..6256dbc22e 100644
+ NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); + NSTRACE ("EmacsAccessibilityBuffer ensureTextCache");
+ /* This method is only called from the main thread (AX getters + /* This method is only called from the main thread (AX getters
+ dispatch_sync to main first). Reads of cachedText/cachedTextModiff + dispatch_sync to main first). Reads of cachedText/cachedTextModiff
+ below are therefore safe without @synchronized --- only the + below are therefore safe without @synchronized only the
+ write section at the end needs synchronization to protect + write section at the end needs synchronization to protect
+ against concurrent reads from AX server thread. */ + against concurrent reads from AX server thread. */
+ eassert ([NSThread isMainThread]); + eassert ([NSThread isMainThread]);
@@ -467,7 +470,7 @@ index 9d36de66f9..6256dbc22e 100644
+ /* Binary search: runs are sorted by charpos (ascending). Find the + /* Binary search: runs are sorted by charpos (ascending). Find the
+ run whose [charpos, charpos+length) range contains the target, + run whose [charpos, charpos+length) range contains the target,
+ or the nearest run after an invisible gap. O(log n) instead of + or the nearest run after an invisible gap. O(log n) instead of
+ O(n) --- matters for org-mode with many folded sections. */ + O(n) matters for org-mode with many folded sections. */
+ NSUInteger lo = 0, hi = visibleRunCount; + NSUInteger lo = 0, hi = visibleRunCount;
+ while (lo < hi) + while (lo < hi)
+ { + {
@@ -516,10 +519,10 @@ index 9d36de66f9..6256dbc22e 100644
+ +
+/* Convert accessibility string index to buffer charpos. +/* Convert accessibility string index to buffer charpos.
+ Safe to call from any thread: uses only cachedText (NSString) and + Safe to call from any thread: uses only cachedText (NSString) and
+ visibleRuns --- no Lisp calls. */ + visibleRuns no Lisp calls. */
+- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
+{ +{
+ /* May be called from AX server thread --- synchronize. */ + /* May be called from AX server thread synchronize. */
+ @synchronized (self) + @synchronized (self)
+ { + {
+ if (visibleRunCount == 0) + if (visibleRunCount == 0)
@@ -561,7 +564,7 @@ index 9d36de66f9..6256dbc22e 100644
+ return cp; + return cp;
+ } + }
+ } + }
+ /* Past end --- return last charpos. */ + /* Past end return last charpos. */
+ if (lo > 0) + if (lo > 0)
+ { + {
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; + ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
@@ -583,7 +586,7 @@ index 9d36de66f9..6256dbc22e 100644
+ deadlocking the AX server thread. This is prevented by: + deadlocking the AX server thread. This is prevented by:
+ +
+ 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every + 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
+ Lisp access --- the window and buffer are verified live. + Lisp access the window and buffer are verified live.
+ 2. All dispatch_sync blocks run on the main thread where no + 2. All dispatch_sync blocks run on the main thread where no
+ concurrent Lisp code can modify state between checks. + concurrent Lisp code can modify state between checks.
+ 3. block_input prevents timer events and process output from + 3. block_input prevents timer events and process output from

View File

@@ -1,7 +1,7 @@
From 94d2aa2736ec116e2965d02241ad8b20d8daf4bc Mon Sep 17 00:00:00 2001 From 0566d2cf3bc1b6309b3b3dd1a048bac7c63937e9 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line Subject: [PATCH 4/9] ns: add buffer notification dispatch and mode-line
element element
Add VoiceOver notification dispatch and mode-line readout. Add VoiceOver notification dispatch and mode-line readout.
@@ -36,7 +36,7 @@ index 6256dbc22e..9e0e317237 100644
+ +
+ +
+/* =================================================================== +/* ===================================================================
+ EmacsAccessibilityBuffer (Notifications) --- AX event dispatch + EmacsAccessibilityBuffer (Notifications) AX event dispatch
+ +
+ These methods notify VoiceOver of text and selection changes. + These methods notify VoiceOver of text and selection changes.
+ Called from the redisplay cycle (postAccessibilityUpdates). + Called from the redisplay cycle (postAccessibilityUpdates).
@@ -51,7 +51,7 @@ index 6256dbc22e..9e0e317237 100644
+ if (point > self.cachedPoint + if (point > self.cachedPoint
+ && point - self.cachedPoint == 1) + && point - self.cachedPoint == 1)
+ { + {
+ /* Single char inserted --- refresh cache and grab it. */ + /* Single char inserted refresh cache and grab it. */
+ [self invalidateTextCache]; + [self invalidateTextCache];
+ [self ensureTextCache]; + [self ensureTextCache];
+ if (cachedText) + if (cachedText)
@@ -70,7 +70,7 @@ index 6256dbc22e..9e0e317237 100644
+ /* Update cachedPoint here so the selection-move branch does NOT + /* Update cachedPoint here so the selection-move branch does NOT
+ fire for point changes caused by edits. WebKit and Chromium + fire for point changes caused by edits. WebKit and Chromium
+ never send both ValueChanged and SelectedTextChanged for the + never send both ValueChanged and SelectedTextChanged for the
+ same user action --- they are mutually exclusive. */ + same user action they are mutually exclusive. */
+ self.cachedPoint = point; + self.cachedPoint = point;
+ +
+ NSDictionary *change = @{ + NSDictionary *change = @{
@@ -471,7 +471,7 @@ index 6256dbc22e..9e0e317237 100644
+ } + }
+ +
+ /* --- Cursor moved or selection changed --- + /* --- Cursor moved or selection changed ---
+ Use 'else if' --- edits and selection moves are mutually exclusive + Use 'else if' edits and selection moves are mutually exclusive
+ per the WebKit/Chromium pattern. */ + per the WebKit/Chromium pattern. */
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive) + else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ { + {

View File

@@ -1,7 +1,7 @@
From 3206d93511fe9337c4ca683a5dc1e6885ed9985c Mon Sep 17 00:00:00 2001 From ce123c5b0c25467dd6fb6d4a2aeda59687fadefc Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation Subject: [PATCH 5/9] ns: add interactive span elements for Tab navigation
* src/nsterm.m (ns_ax_scan_interactive_spans): New function; scans the * src/nsterm.m (ns_ax_scan_interactive_spans): New function; scans the
visible portion of a buffer for interactive text properties visible portion of a buffer for interactive text properties
@@ -14,14 +14,14 @@ elements with an AXPress action that sends a synthetic TAB keystroke.
(accessibilityChildrenInNavigationOrder): Return cached span array, (accessibilityChildrenInNavigationOrder): Return cached span array,
rebuilding lazily when interactiveSpansDirty is set. rebuilding lazily when interactiveSpansDirty is set.
--- ---
src/nsterm.m | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 293 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 292 insertions(+) 1 file changed, 293 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 9e0e317237..8aa5b6ac1b 100644 index 9e0e317237..d65609cc79 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -9346,6 +9346,298 @@ - (NSRect)accessibilityFrame @@ -9346,6 +9346,299 @@ - (NSRect)accessibilityFrame
@end @end
@@ -76,6 +76,7 @@ index 9e0e317237..8aa5b6ac1b 100644
+ EmacsAXSpanType span_type = EmacsAXSpanTypeNone; + EmacsAXSpanType span_type = EmacsAXSpanTypeNone;
+ Lisp_Object limit_prop = Qnil; + Lisp_Object limit_prop = Qnil;
+ +
+ /* Fplist_get third arg Qnil: use `eq' predicate (the default). */
+ if (!NILP (Fplist_get (plist, Qns_ax_widget, Qnil))) + if (!NILP (Fplist_get (plist, Qns_ax_widget, Qnil)))
+ { + {
+ span_type = EmacsAXSpanTypeWidget; + span_type = EmacsAXSpanTypeWidget;

View File

@@ -1,7 +1,7 @@
From f2e97ea6ba4ffc1c73e625f9d61636b7261cbecf Mon Sep 17 00:00:00 2001 From 3f894218b771f2aa098f19dfb4bdc8b13408c8c8 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay Subject: [PATCH 6/9] ns: integrate accessibility with EmacsView and redisplay
Wire the accessibility element tree into EmacsView and hook it into Wire the accessibility element tree into EmacsView and hook it into
the redisplay cycle. the redisplay cycle.
@@ -24,7 +24,7 @@ com.apple.accessibility.api distributed notification.
--- ---
etc/NEWS | 13 ++ etc/NEWS | 13 ++
src/nsterm.m | 474 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/nsterm.m | 474 +++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 475 insertions(+), 12 deletions(-) 2 files changed, 477 insertions(+), 10 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index 4c149e41d6..7f917f93b2 100644 index 4c149e41d6..7f917f93b2 100644
@@ -51,24 +51,15 @@ index 4c149e41d6..7f917f93b2 100644
** Re-introduced dictation, lost in Emacs v30 (macOS). ** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient. We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 8aa5b6ac1b..32eb04acef 100644 index d65609cc79..4ba9b41b3b 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1275,7 +1275,7 @@ If a completion candidate is selected (overlay or child frame),
static void
ns_zoom_track_completion (struct frame *f, EmacsView *view)
{
- if (!ns_zoom_enabled_p ())
+ if (!ns_accessibility_enabled || !ns_zoom_enabled_p ())
return;
if (!WINDOWP (f->selected_window))
return;
@@ -1393,7 +1393,8 @@ so the visual offset is (ov_line + 1) * line_h from @@ -1393,7 +1393,8 @@ so the visual offset is (ov_line + 1) * line_h from
(zoomCursorUpdated is NO). */ (zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ #if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (view && !view->zoomCursorUpdated && ns_zoom_enabled_p () - if (view && !view->zoomCursorUpdated && ns_zoom_enabled_p ()
+ if (ns_accessibility_enabled && view && !view->zoomCursorUpdated + if (view && !view->zoomCursorUpdated
+ && ns_zoom_enabled_p () + && ns_zoom_enabled_p ()
&& !NSIsEmptyRect (view->lastCursorRect)) && !NSIsEmptyRect (view->lastCursorRect))
{ {
@@ -83,15 +74,6 @@ index 8aa5b6ac1b..32eb04acef 100644
} }
static void static void
@@ -3567,7 +3571,7 @@ EmacsView pixels (AppKit, flipped, top-left origin)
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_zoom_enabled_p ())
+ if (ns_accessibility_enabled && ns_zoom_enabled_p ())
{
NSRect windowRect = [view convertRect:r toView:nil];
NSRect screenRect
@@ -6723,9 +6727,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification @@ -6723,9 +6727,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification
} }
#endif #endif
@@ -163,14 +145,18 @@ index 8aa5b6ac1b..32eb04acef 100644
- -
/* =================================================================== /* ===================================================================
EmacsAccessibilityBuffer (Notifications) --- AX event dispatch EmacsAccessibilityBuffer (Notifications) AX event dispatch
@@ -9235,6 +9284,50 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9235,6 +9284,54 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
granularity = ns_ax_text_selection_granularity_line; granularity = ns_ax_text_selection_granularity_line;
} }
+ /* Treat all moves as Emacs-initiated until voiceoverSetPoint
+ tracking is introduced (subsequent patch). */
+ BOOL emacsMovedCursor = YES;
+
+ /* Programmatic jumps that cross a line boundary (]], [[, M-<, + /* Programmatic jumps that cross a line boundary (]], [[, M-<,
+ xref, imenu, ...) are discontiguous: the cursor teleported to an + xref, imenu, ) are discontiguous: the cursor teleported to an
+ arbitrary position, not one sequential step forward/backward. + arbitrary position, not one sequential step forward/backward.
+ Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver + Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver
+ to re-anchor its rotor browse cursor at the new + to re-anchor its rotor browse cursor at the new
@@ -216,7 +202,7 @@ index 8aa5b6ac1b..32eb04acef 100644
/* Post notifications for focused and non-focused elements. */ /* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused]) if ([self isAccessibilityFocused])
[self postFocusedCursorNotification:point [self postFocusedCursorNotification:point
@@ -9347,7 +9440,6 @@ - (NSRect)accessibilityFrame @@ -9347,7 +9444,6 @@ - (NSRect)accessibilityFrame
@end @end
@@ -224,7 +210,7 @@ index 8aa5b6ac1b..32eb04acef 100644
/* =================================================================== /* ===================================================================
EmacsAccessibilityInteractiveSpan --- helpers and implementation EmacsAccessibilityInteractiveSpan --- helpers and implementation
=================================================================== */ =================================================================== */
@@ -9683,6 +9775,7 @@ - (void)dealloc @@ -9684,6 +9780,7 @@ - (void)dealloc
[layer release]; [layer release];
#endif #endif
@@ -232,7 +218,7 @@ index 8aa5b6ac1b..32eb04acef 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11031,6 +11124,32 @@ - (void)windowDidBecomeKey /* for direct calls */ @@ -11032,6 +11129,32 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop ns_send_appdefined (-1); // Kick main loop
@@ -265,7 +251,7 @@ index 8aa5b6ac1b..32eb04acef 100644
} }
@@ -12268,6 +12387,332 @@ - (int) fullscreenState @@ -12269,6 +12392,332 @@ - (int) fullscreenState
return fs_state; return fs_state;
} }
@@ -285,7 +271,7 @@ index 8aa5b6ac1b..32eb04acef 100644
+ +
+ if (WINDOW_LEAF_P (w)) + if (WINDOW_LEAF_P (w))
+ { + {
+ /* Buffer element --- reuse existing if available. */ + /* Buffer element reuse existing if available. */
+ EmacsAccessibilityBuffer *elem + EmacsAccessibilityBuffer *elem
+ = [existing objectForKey:[NSValue valueWithPointer:w]]; + = [existing objectForKey:[NSValue valueWithPointer:w]];
+ if (!elem) + if (!elem)
@@ -319,7 +305,7 @@ index 8aa5b6ac1b..32eb04acef 100644
+ } + }
+ else + else
+ { + {
+ /* Internal (combination) window --- recurse into children. */ + /* Internal (combination) window recurse into children. */
+ Lisp_Object child = w->contents; + Lisp_Object child = w->contents;
+ while (!NILP (child)) + while (!NILP (child))
+ { + {
@@ -431,7 +417,7 @@ index 8aa5b6ac1b..32eb04acef 100644
+ accessibilityUpdating = YES; + accessibilityUpdating = YES;
+ +
+ /* Detect window tree change (split, delete, new buffer). Compare + /* Detect window tree change (split, delete, new buffer). Compare
+ FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ + FRAME_ROOT_WINDOW if it changed, the tree structure changed. */
+ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); + Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
+ if (!EQ (curRoot, lastRootWindow)) + if (!EQ (curRoot, lastRootWindow))
+ { + {
@@ -440,12 +426,12 @@ index 8aa5b6ac1b..32eb04acef 100644
+ } + }
+ +
+ /* If tree is stale, rebuild FIRST so we don't iterate freed + /* If tree is stale, rebuild FIRST so we don't iterate freed
+ window pointers. Skip notifications for this cycle --- the + window pointers. Skip notifications for this cycle the
+ freshly-built elements have no previous state to diff against. */ + freshly-built elements have no previous state to diff against. */
+ if (!accessibilityTreeValid) + if (!accessibilityTreeValid)
+ { + {
+ [self rebuildAccessibilityTree]; + [self rebuildAccessibilityTree];
+ /* Invalidate span cache --- window layout changed. */ + /* Invalidate span cache window layout changed. */
+ for (EmacsAccessibilityElement *elem in accessibilityElements) + for (EmacsAccessibilityElement *elem in accessibilityElements)
+ if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) + if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]])
+ [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans]; + [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans];
@@ -598,7 +584,7 @@ index 8aa5b6ac1b..32eb04acef 100644
@end /* EmacsView */ @end /* EmacsView */
@@ -14264,12 +14709,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with @@ -14265,12 +14714,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
ns_use_srgb_colorspace = YES; ns_use_srgb_colorspace = YES;
DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,

View File

@@ -1,7 +1,7 @@
From 4310549aa1e486dba054948a2937bb8bb236bb27 Mon Sep 17 00:00:00 2001 From 23139d3e63a0d97cf1fdf0421fd7c41acce0bd6b Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS Subject: [PATCH 7/9] doc: add VoiceOver accessibility section to macOS
appendix appendix
* doc/emacs/macos.texi (VoiceOver Accessibility): New node between * doc/emacs/macos.texi (VoiceOver Accessibility): New node between
@@ -11,12 +11,12 @@ enabled, and known limitations. Use @xref for cross-reference at
sentence start. Correct description of ns-accessibility-enabled sentence start. Correct description of ns-accessibility-enabled
default: initial value is nil, set automatically at startup. default: initial value is nil, set automatically at startup.
--- ---
doc/emacs/macos.texi | 76 ++++++++++++++++++++++++++++++++++++++++++++ doc/emacs/macos.texi | 77 ++++++++++++++++++++++++++++++++++++++++++++
src/nsterm.m | 10 ++++-- src/nsterm.m | 10 ++++--
2 files changed, 83 insertions(+), 3 deletions(-) 2 files changed, 84 insertions(+), 3 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 6bd334f48e..8d4a7825d8 100644 index 6bd334f48e..72ac3a9aa9 100644
--- a/doc/emacs/macos.texi --- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi +++ b/doc/emacs/macos.texi
@@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future. @@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future.
@@ -27,7 +27,7 @@ index 6bd334f48e..8d4a7825d8 100644
* GNUstep Support:: Details on status of GNUstep support. * GNUstep Support:: Details on status of GNUstep support.
@end menu @end menu
@@ -272,6 +273,82 @@ @@ -272,6 +273,82 @@ and return the result as a string. You can also use the Lisp function
services and receive the results back. Note that you may need to services and receive the results back. Note that you may need to
restart Emacs to access newly-available services. restart Emacs to access newly-available services.
@@ -97,24 +97,24 @@ index 6bd334f48e..8d4a7825d8 100644
+Right-to-left (bidi) text is exposed correctly as buffer content, +Right-to-left (bidi) text is exposed correctly as buffer content,
+but @code{accessibilityRangeForPosition} hit-testing assumes +but @code{accessibilityRangeForPosition} hit-testing assumes
+left-to-right glyph layout. +left-to-right glyph layout.
+@item
+Block-style cursors are handled correctly: character navigation
+announces the character at the cursor position, not the character
+before it.
+@end itemize +@end itemize
+ +
+ This support is available only on the Cocoa build. GNUstep has a + This support is available only on the Cocoa build. GNUstep has a
+different accessibility model and is not yet supported. +different accessibility model and is not yet supported.
+ +
+Block-style cursors are handled
+correctly: character navigation announces the character at the cursor
+position, not the character before it.
+
+ +
@node GNUstep Support @node GNUstep Support
@section GNUstep Support @section GNUstep Support
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 32eb04acef..8e5cc7e1d7 100644 index 4ba9b41b3b..a0419bb5df 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -14710,9 +14710,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with @@ -14715,9 +14715,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support. doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support.

View File

@@ -1,11 +1,11 @@
From affdaf60d28ad4d9836c6505216e19599b31c437 Mon Sep 17 00:00:00 2001 From 7474a4e1ddbf37286842e3beda1810c40f2a3ef7 Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Mon, 2 Mar 2026 18:39:46 +0100 Date: Mon, 2 Mar 2026 18:39:46 +0100
Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver
Overlay-based completion UIs (Vertico, Ivy, Icomplete) render candidates Completion frameworks such as Vertico, Ivy, and Icomplete render
via overlay before-string/after-string properties. Without this change candidates via overlay before-string/after-string properties. Without
VoiceOver cannot read overlay-based completion UIs. this change VoiceOver cannot read overlay-based completion UIs.
* src/nsterm.m (ns_ax_face_is_selected): New static function; matches * src/nsterm.m (ns_ax_face_is_selected): New static function; matches
'current', 'selected', 'selection' in face symbol names. 'current', 'selected', 'selection' in face symbol names.
@@ -18,11 +18,11 @@ ValueChanged; keep overlay_modiff out of ensureTextCache to prevent a
race where an AX query consumes the change before notification. race where an AX query consumes the change before notification.
--- ---
src/nsterm.h | 1 + src/nsterm.h | 1 +
src/nsterm.m | 348 ++++++++++++++++++++++++++++++++++++++++++++------- src/nsterm.m | 345 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 307 insertions(+), 42 deletions(-) 2 files changed, 304 insertions(+), 42 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 5746e9e9bd..21a93bc799 100644 index f245675513..a210ceba14 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -510,6 +510,7 @@ typedef struct ns_ax_visible_run @@ -510,6 +510,7 @@ typedef struct ns_ax_visible_run
@@ -34,10 +34,10 @@ index 5746e9e9bd..21a93bc799 100644
@property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 8e5cc7e1d7..8ef344d9fe 100644 index a0419bb5df..54cee74401 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7263,11 +7263,157 @@ Accessibility virtual elements (macOS / Cocoa only) @@ -7263,11 +7263,154 @@ Accessibility virtual elements (macOS / Cocoa only)
/* ---- Helper: extract buffer text for accessibility ---- */ /* ---- Helper: extract buffer text for accessibility ---- */
@@ -46,9 +46,6 @@ index 8e5cc7e1d7..8ef344d9fe 100644
+ completion candidate. Works for vertico-current, + completion candidate. Works for vertico-current,
+ icomplete-selected-match, ivy-current-match, etc. */ + icomplete-selected-match, ivy-current-match, etc. */
+static bool +static bool
+/* Note: ns_zoom_face_is_selected in the Zoom series has identical
+ logic. The duplication is intentional: both series are
+ independent and must compile standalone. */
+ns_ax_face_is_selected (Lisp_Object face) +ns_ax_face_is_selected (Lisp_Object face)
+{ +{
+ if (SYMBOLP (face) && !NILP (face)) + if (SYMBOLP (face) && !NILP (face))
@@ -200,7 +197,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
/* Extract this visible run's text. Use /* Extract this visible run's text. Use
Fbuffer_substring_no_properties which correctly handles the Fbuffer_substring_no_properties which correctly handles the
- buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would - buffer gap raw BUF_BYTE_ADDRESS reads across the gap would
+ buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would + buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would
include garbage bytes when the run spans the gap position. */ include garbage bytes when the run spans the gap position. */
Lisp_Object lstr = Fbuffer_substring_no_properties ( Lisp_Object lstr = Fbuffer_substring_no_properties (
@@ -209,7 +206,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
return NSZeroRect; return NSZeroRect;
/* charpos_start and charpos_len are already in buffer charpos /* charpos_start and charpos_len are already in buffer charpos
- space --- the caller maps AX string indices through - space the caller maps AX string indices through
+ space --- the caller maps AX string indices through + space --- the caller maps AX string indices through
charposForAccessibilityIndex which handles invisible text. */ charposForAccessibilityIndex which handles invisible text. */
ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_start = charpos_start;
@@ -226,7 +223,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); NSTRACE ("EmacsAccessibilityBuffer ensureTextCache");
/* This method is only called from the main thread (AX getters /* This method is only called from the main thread (AX getters
dispatch_sync to main first). Reads of cachedText/cachedTextModiff dispatch_sync to main first). Reads of cachedText/cachedTextModiff
- below are therefore safe without @synchronized --- only the - below are therefore safe without @synchronized only the
+ below are therefore safe without @synchronized --- only the + below are therefore safe without @synchronized --- only the
write section at the end needs synchronization to protect write section at the end needs synchronization to protect
against concurrent reads from AX server thread. */ against concurrent reads from AX server thread. */
@@ -309,7 +306,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
/* Binary search: runs are sorted by charpos (ascending). Find the /* Binary search: runs are sorted by charpos (ascending). Find the
run whose [charpos, charpos+length) range contains the target, run whose [charpos, charpos+length) range contains the target,
or the nearest run after an invisible gap. O(log n) instead of or the nearest run after an invisible gap. O(log n) instead of
- O(n) --- matters for org-mode with many folded sections. */ - O(n) matters for org-mode with many folded sections. */
+ O(n) --- matters for org-mode with many folded sections. */ + O(n) --- matters for org-mode with many folded sections. */
NSUInteger lo = 0, hi = visibleRunCount; NSUInteger lo = 0, hi = visibleRunCount;
while (lo < hi) while (lo < hi)
@@ -318,11 +315,11 @@ index 8e5cc7e1d7..8ef344d9fe 100644
/* Convert accessibility string index to buffer charpos. /* Convert accessibility string index to buffer charpos.
Safe to call from any thread: uses only cachedText (NSString) and Safe to call from any thread: uses only cachedText (NSString) and
- visibleRuns --- no Lisp calls. */ - visibleRuns no Lisp calls. */
+ visibleRuns --- no Lisp calls. */ + visibleRuns --- no Lisp calls. */
- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
{ {
- /* May be called from AX server thread --- synchronize. */ - /* May be called from AX server thread synchronize. */
+ /* May be called from AX server thread --- synchronize. */ + /* May be called from AX server thread --- synchronize. */
@synchronized (self) @synchronized (self)
{ {
@@ -331,7 +328,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
return cp; return cp;
} }
} }
- /* Past end --- return last charpos. */ - /* Past end return last charpos. */
+ /* Past end --- return last charpos. */ + /* Past end --- return last charpos. */
if (lo > 0) if (lo > 0)
{ {
@@ -340,7 +337,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
deadlocking the AX server thread. This is prevented by: deadlocking the AX server thread. This is prevented by:
1. validWindow checks WINDOW_LIVE_P and BUFFERP before every 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
- Lisp access --- the window and buffer are verified live. - Lisp access the window and buffer are verified live.
+ Lisp access --- the window and buffer are verified live. + Lisp access --- the window and buffer are verified live.
2. All dispatch_sync blocks run on the main thread where no 2. All dispatch_sync blocks run on the main thread where no
concurrent Lisp code can modify state between checks. concurrent Lisp code can modify state between checks.
@@ -400,7 +397,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
/* =================================================================== /* ===================================================================
- EmacsAccessibilityBuffer (Notifications) --- AX event dispatch - EmacsAccessibilityBuffer (Notifications) AX event dispatch
+ EmacsAccessibilityBuffer (Notifications) --- AX event dispatch + EmacsAccessibilityBuffer (Notifications) --- AX event dispatch
These methods notify VoiceOver of text and selection changes. These methods notify VoiceOver of text and selection changes.
@@ -409,7 +406,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
if (point > self.cachedPoint if (point > self.cachedPoint
&& point - self.cachedPoint == 1) && point - self.cachedPoint == 1)
{ {
- /* Single char inserted --- refresh cache and grab it. */ - /* Single char inserted refresh cache and grab it. */
+ /* Single char inserted --- refresh cache and grab it. */ + /* Single char inserted --- refresh cache and grab it. */
[self invalidateTextCache]; [self invalidateTextCache];
[self ensureTextCache]; [self ensureTextCache];
@@ -418,12 +415,12 @@ index 8e5cc7e1d7..8ef344d9fe 100644
/* Update cachedPoint here so the selection-move branch does NOT /* Update cachedPoint here so the selection-move branch does NOT
fire for point changes caused by edits. WebKit and Chromium fire for point changes caused by edits. WebKit and Chromium
never send both ValueChanged and SelectedTextChanged for the never send both ValueChanged and SelectedTextChanged for the
- same user action --- they are mutually exclusive. */ - same user action they are mutually exclusive. */
+ same user action --- they are mutually exclusive. */ + same user action --- they are mutually exclusive. */
self.cachedPoint = point; self.cachedPoint = point;
NSDictionary *change = @{ NSDictionary *change = @{
@@ -9220,16 +9417,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9220,16 +9417,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active)); BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */ /* --- Text changed (edit) --- */
@@ -465,90 +462,87 @@ index 8e5cc7e1d7..8ef344d9fe 100644
+ font-lock and other modes change BUF_OVERLAY_MODIFF on + font-lock and other modes change BUF_OVERLAY_MODIFF on
+ every redisplay, triggering O(overlays) work per keystroke. + every redisplay, triggering O(overlays) work per keystroke.
+ Restrict the scan to minibuffer windows. */ + Restrict the scan to minibuffer windows. */
+ if (!MINI_WINDOW_P (w)) + if (MINI_WINDOW_P (w))
+ goto skip_overlay_scan;
+
+ int selected_line = -1;
+ NSString *candidate
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line);
+ if (candidate)
+ { + {
+ /* Deduplicate: only announce when the candidate changed. */ + int selected_line = -1;
+ if (![candidate isEqualToString: + NSString *candidate
+ self.cachedCompletionAnnouncement]) + = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line);
+ if (candidate)
+ { + {
+ self.cachedCompletionAnnouncement = candidate; + /* Deduplicate: only announce when the candidate changed. */
+ + if (![candidate isEqualToString:
+ /* Announce the candidate text directly via NSApp. + self.cachedCompletionAnnouncement])
+ Do NOT post SelectedTextChanged --- that would cause + {
+ VoiceOver to read the AX text at the cursor position + self.cachedCompletionAnnouncement = candidate;
+ (the minibuffer input line), not the overlay candidate.
+ AnnouncementRequested with High priority interrupts
+ any current speech and announces our text. */
+ NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: candidate,
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
+ +
+ /* Announce the candidate text directly via NSApp.
+ Do NOT post SelectedTextChanged --- that would cause
+ VoiceOver to read the AX text at the cursor position
+ (the minibuffer input line), not the overlay candidate.
+ AnnouncementRequested with High priority interrupts
+ any current speech and announces our text. */
+ NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: candidate,
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
+ }
+ } + }
+ } + }
} }
+ skip_overlay_scan:;
/* --- Cursor moved or selection changed --- /* --- Cursor moved or selection changed ---
- Use 'else if' --- edits and selection moves are mutually exclusive - Use 'else if' edits and selection moves are mutually exclusive
- per the WebKit/Chromium pattern. */ - per the WebKit/Chromium pattern. */
- else if (point != self.cachedPoint || markActive != self.cachedMarkActive) - else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ Independent check: the goto above may jump here from the overlay + Independent check from the overlay branch above. */
+ branch, so this must be a standalone if, not else-if. */
+ if (point != self.cachedPoint || markActive != self.cachedMarkActive) + if (point != self.cachedPoint || markActive != self.cachedMarkActive)
{ {
ptrdiff_t oldPoint = self.cachedPoint; ptrdiff_t oldPoint = self.cachedPoint;
BOOL oldMarkActive = self.cachedMarkActive; BOOL oldMarkActive = self.cachedMarkActive;
@@ -12403,7 +12667,7 @@ - (int) fullscreenState @@ -12408,7 +12669,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w)) if (WINDOW_LEAF_P (w))
{ {
- /* Buffer element --- reuse existing if available. */ - /* Buffer element reuse existing if available. */
+ /* Buffer element --- reuse existing if available. */ + /* Buffer element --- reuse existing if available. */
EmacsAccessibilityBuffer *elem EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]]; = [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem) if (!elem)
@@ -12437,7 +12701,7 @@ - (int) fullscreenState @@ -12442,7 +12703,7 @@ - (int) fullscreenState
} }
else else
{ {
- /* Internal (combination) window --- recurse into children. */ - /* Internal (combination) window recurse into children. */
+ /* Internal (combination) window --- recurse into children. */ + /* Internal (combination) window --- recurse into children. */
Lisp_Object child = w->contents; Lisp_Object child = w->contents;
while (!NILP (child)) while (!NILP (child))
{ {
@@ -12549,7 +12813,7 @@ - (void)postAccessibilityUpdates @@ -12554,7 +12815,7 @@ - (void)postAccessibilityUpdates
accessibilityUpdating = YES; accessibilityUpdating = YES;
/* Detect window tree change (split, delete, new buffer). Compare /* Detect window tree change (split, delete, new buffer). Compare
- FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ - FRAME_ROOT_WINDOW if it changed, the tree structure changed. */
+ FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ + FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow)) if (!EQ (curRoot, lastRootWindow))
{ {
@@ -12558,12 +12822,12 @@ - (void)postAccessibilityUpdates @@ -12563,12 +12824,12 @@ - (void)postAccessibilityUpdates
} }
/* If tree is stale, rebuild FIRST so we don't iterate freed /* If tree is stale, rebuild FIRST so we don't iterate freed
- window pointers. Skip notifications for this cycle --- the - window pointers. Skip notifications for this cycle the
+ window pointers. Skip notifications for this cycle --- the + window pointers. Skip notifications for this cycle --- the
freshly-built elements have no previous state to diff against. */ freshly-built elements have no previous state to diff against. */
if (!accessibilityTreeValid) if (!accessibilityTreeValid)
{ {
[self rebuildAccessibilityTree]; [self rebuildAccessibilityTree];
- /* Invalidate span cache --- window layout changed. */ - /* Invalidate span cache window layout changed. */
+ /* Invalidate span cache --- window layout changed. */ + /* Invalidate span cache --- window layout changed. */
for (EmacsAccessibilityElement *elem in accessibilityElements) for (EmacsAccessibilityElement *elem in accessibilityElements)
if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]])

View File

@@ -1,54 +1,50 @@
From 1e3d3919fd41e4480a02190fb89bee1ef8107d62 Mon Sep 17 00:00:00 2001 From 137cb30bb546a9599983c25a9873d1518ad8edee Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Mon, 2 Mar 2026 18:49:13 +0100 Date: Mon, 2 Mar 2026 18:49:13 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for Subject: [PATCH 9/9] ns: announce child frame completion candidates for
VoiceOver VoiceOver
Child frame popups (Corfu, Company-mode) render completion candidates in Child frame popups (Corfu, Company-mode child frames) render completion
a separate frame whose buffer is not accessible via the minibuffer candidates in a separate frame whose buffer is not accessible via the
overlay path. This patch scans child frame buffers for selected minibuffer overlay path. This patch scans child frame buffers for
candidates and announces them via VoiceOver. selected candidates and announces them via VoiceOver.
* src/nsterm.h (EmacsView): Add childFrameLastBuffer, childFrameLastModiff, * src/nsterm.h (EmacsView): Add childFrameLastBuffer, childFrameLastModiff,
childFrameLastCandidate, childFrameCompletionActive, lastEchoCharsModiff childFrameLastCandidate, childFrameCompletionActive, lastEchoCharsModiff
ivars; remove cachedOverlayModiffForText (unused after BUF_OVERLAY_MODIFF ivars. Initialize childFrameLastBuffer to Qnil in initFrameFromEmacs:.
removed from ensureTextCache to prevent hl-line-mode O(N) rebuilds).
Initialize voiceoverSetPoint, childFrameLastBuffer in initFrameFromEmacs:.
(EmacsAccessibilityBuffer): Add voiceoverSetPoint ivar. (EmacsAccessibilityBuffer): Add voiceoverSetPoint ivar.
* src/nsterm.m (ns_ax_buffer_text): Add block_input protection for * src/nsterm.m (ns_ax_selected_child_frame_text): New function; scans
Lisp calls; use record_unwind_protect_void to guarantee unblock_input. child frame buffer text for the selected completion candidate.
(ensureTextCache): Remove BUF_OVERLAY_MODIFF tracking; keep only
BUF_CHARS_MODIFF. BUF_OVERLAY_MODIFF caused O(buffer-size) rebuilds
with hl-line-mode (moves overlay on every post-command-hook).
(announceChildFrameCompletion): New method; scans child frame buffers (announceChildFrameCompletion): New method; scans child frame buffers
for selected completion candidates. Store childFrameLastBuffer as for selected completion candidates. Store childFrameLastBuffer as
BVAR(b, name) (buffer name symbol, GC-reachable via obarray) rather BVAR(b, name) (buffer name symbol, GC-reachable via obarray) rather
than make_lisp_ptr to avoid dangling pointer after buffer kill. than a raw buffer pointer to avoid a dangling pointer after buffer kill.
(postEchoAreaAnnouncementIfNeeded): New method; announces echo area (postEchoAreaAnnouncementIfNeeded): New method; announces echo area
changes for commands like C-g. changes (e.g., "Wrote file", "Quit") for commands that produce output
while the minibuffer is inactive.
(postAccessibilityNotificationsForFrame:): Drive child frame and echo (postAccessibilityNotificationsForFrame:): Drive child frame and echo
area announcements. area announcements. Add voiceoverSetPoint flag and singleLineMove
* doc/emacs/macos.texi: Fix dangling semicolon in GNUstep paragraph. adjacency detection to distinguish VoiceOver-initiated cursor moves
from Emacs-initiated moves; sequential adjacent-line moves use
next/previous direction, teleports use discontiguous. Add didTextChange
guard to suppress overlay completion announcements while the user types.
(setAccessibilitySelectedTextRange:): Set voiceoverSetPoint so that the
subsequent notification cycle uses sequential direction.
* doc/emacs/macos.texi (VoiceOver Accessibility): Update to document
echo area announcements and VoiceOver rotor cursor synchronization.
Remove Zoom section (covered by patch 0000). Fix dangling paragraph.
--- ---
doc/emacs/macos.texi | 14 +- doc/emacs/macos.texi | 13 +-
etc/NEWS | 25 ++- etc/NEWS | 25 +-
src/nsterm.h | 21 ++ src/nsterm.h | 21 ++
src/nsterm.m | 514 +++++++++++++++++++++++++++++++++++++++---- src/nsterm.m | 561 +++++++++++++++++++++++++++++++++++++------
4 files changed, 510 insertions(+), 64 deletions(-) 4 files changed, 529 insertions(+), 91 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 8d4a7825d8..03a657f970 100644 index 72ac3a9aa9..cf5ed0ff28 100644
--- a/doc/emacs/macos.texi --- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi +++ b/doc/emacs/macos.texi
@@ -278,7 +278,6 @@ restart Emacs to access newly-available services. @@ -309,10 +309,15 @@ Shift-modified movement announces selected or deselected text.
@cindex VoiceOver
@cindex accessibility (macOS)
@cindex screen reader (macOS)
-@cindex Zoom, cursor tracking (macOS)
When built with the Cocoa interface on macOS, Emacs exposes buffer
content, cursor position, mode lines, and interactive elements to the
@@ -309,10 +308,15 @@ Shift-modified movement announces selected or deselected text.
The @file{*Completions*} buffer announces each completion candidate The @file{*Completions*} buffer announces each completion candidate
as you navigate, even while keyboard focus remains in the minibuffer. as you navigate, even while keyboard focus remains in the minibuffer.
@@ -115,7 +111,7 @@ index 7f917f93b2..bbec21b635 100644
interface and eliminate the associated overhead. interface and eliminate the associated overhead.
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 21a93bc799..b5c9f84499 100644 index a210ceba14..2edd7cd6e0 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -504,9 +504,20 @@ typedef struct ns_ax_visible_run @@ -504,9 +504,20 @@ typedef struct ns_ax_visible_run
@@ -139,7 +135,7 @@ index 21a93bc799..b5c9f84499 100644
@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; @property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart; @property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) ptrdiff_t cachedModiff; @property (nonatomic, assign) ptrdiff_t cachedModiff;
@@ -596,6 +607,14 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -596,6 +607,14 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
Lisp_Object lastRootWindow; Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid; BOOL accessibilityTreeValid;
BOOL accessibilityUpdating; BOOL accessibilityUpdating;
@@ -154,7 +150,7 @@ index 21a93bc799..b5c9f84499 100644
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -665,6 +684,8 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -665,6 +684,8 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree; - (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree; - (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates; - (void)postAccessibilityUpdates;
@@ -164,42 +160,22 @@ index 21a93bc799..b5c9f84499 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 8ef344d9fe..1acb64630a 100644 index 54cee74401..6ba2229639 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1275,7 +1275,13 @@ If a completion candidate is selected (overlay or child frame), @@ -1275,6 +1275,12 @@ If a completion candidate is selected (overlay or child frame),
static void static void
ns_zoom_track_completion (struct frame *f, EmacsView *view) ns_zoom_track_completion (struct frame *f, EmacsView *view)
{ {
- if (!ns_accessibility_enabled || !ns_zoom_enabled_p ())
+ /* Zoom cursor tracking is controlled exclusively by + /* Zoom cursor tracking is controlled exclusively by
+ ns_zoom_enabled_p (). We do NOT gate on ns_accessibility_enabled: + ns_zoom_enabled_p (). We do NOT gate on ns_accessibility_enabled:
+ users can run Zoom without VoiceOver, and those users should still + users can run Zoom without VoiceOver, and those users should still
+ get completion-candidate tracking. ns_accessibility_enabled is + get completion-candidate tracking. ns_accessibility_enabled is
+ only set when a screen reader (VoiceOver or similar) activates the + only set when a screen reader (VoiceOver or similar) activates the
+ AX layer; it has no bearing on the Zoom feature. */ + AX layer; it has no bearing on the Zoom feature. */
+ if (!ns_zoom_enabled_p ()) if (!ns_zoom_enabled_p ())
return; return;
if (!WINDOWP (f->selected_window)) if (!WINDOWP (f->selected_window))
return;
@@ -1393,7 +1399,7 @@ so the visual offset is (ov_line + 1) * line_h from
(zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && view && !view->zoomCursorUpdated
+ if (view && !view->zoomCursorUpdated
&& ns_zoom_enabled_p ()
&& !NSIsEmptyRect (view->lastCursorRect))
{
@@ -3571,7 +3577,7 @@ EmacsView pixels (AppKit, flipped, top-left origin)
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && ns_zoom_enabled_p ())
+ if (ns_zoom_enabled_p ())
{
NSRect windowRect = [view convertRect:r toView:nil];
NSRect screenRect
@@ -7407,6 +7413,117 @@ visual line index for Zoom (skip whitespace-only lines @@ -7407,6 +7413,117 @@ visual line index for Zoom (skip whitespace-only lines
return nil; return nil;
@@ -318,6 +294,21 @@ index 8ef344d9fe..1acb64630a 100644
/* Build accessibility text for window W, skipping invisible text. /* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos. Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -7440,9 +7557,13 @@ visual line index for Zoom (skip whitespace-only lines
return @"";
specpdl_ref count = SPECPDL_INDEX ();
+ /* block_input must precede record_unwind_protect_void (unblock_input):
+ if anything between SPECPDL_INDEX and block_input were to throw,
+ the unwind handler would call unblock_input without a matching
+ block_input, corrupting the input-blocking reference count. */
+ block_input ();
record_unwind_current_buffer ();
record_unwind_protect_void (unblock_input);
- block_input ();
if (b != current_buffer)
set_buffer_internal_1 (b);
@@ -8605,6 +8726,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range @@ -8605,6 +8726,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
[self ensureTextCache]; [self ensureTextCache];
@@ -392,7 +383,7 @@ index 8ef344d9fe..1acb64630a 100644
+ - C-n/C-p: SelectedTextChanged carries granularity=line, but + - C-n/C-p: SelectedTextChanged carries granularity=line, but
+ VoiceOver processes those keystrokes specially and may not + VoiceOver processes those keystrokes specially and may not
+ produce speech; the explicit announcement is the reliable path. + produce speech; the explicit announcement is the reliable path.
+ - Discontiguous jumps (]], M-<, xref, imenu, ...): granularity=line + - Discontiguous jumps (]], M-<, xref, imenu, ): granularity=line
+ in the notification is omitted (see above) so VoiceOver will + in the notification is omitted (see above) so VoiceOver will
+ not announce automatically; this explicit announcement fills + not announce automatically; this explicit announcement fills
+ the gap. + the gap.
@@ -422,7 +413,7 @@ index 8ef344d9fe..1acb64630a 100644
+ postEchoAreaAnnouncementIfNeeded (called from postAccessibilityUpdates + postEchoAreaAnnouncementIfNeeded (called from postAccessibilityUpdates
+ before this per-element loop) so that they are never lost to a + before this per-element loop) so that they are never lost to a
+ concurrent tree rebuild. For the inactive minibuffer (minibuf_level + concurrent tree rebuild. For the inactive minibuffer (minibuf_level
+ == 0), skip normal cursor and completion processing --- there is no + == 0), skip normal cursor and completion processing there is no
+ meaningful cursor to track. */ + meaningful cursor to track. */
+ if (MINI_WINDOW_P (w) && minibuf_level == 0) + if (MINI_WINDOW_P (w) && minibuf_level == 0)
+ return; + return;
@@ -452,12 +443,12 @@ index 8ef344d9fe..1acb64630a 100644
} }
} }
@@ -9453,8 +9625,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9453,37 +9625,44 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
displayed in the minibuffer. In normal editing buffers, displayed in the minibuffer. In normal editing buffers,
font-lock and other modes change BUF_OVERLAY_MODIFF on font-lock and other modes change BUF_OVERLAY_MODIFF on
every redisplay, triggering O(overlays) work per keystroke. every redisplay, triggering O(overlays) work per keystroke.
- Restrict the scan to minibuffer windows. */ - Restrict the scan to minibuffer windows. */
- if (!MINI_WINDOW_P (w)) - if (MINI_WINDOW_P (w))
+ Restrict the scan to minibuffer windows. + Restrict the scan to minibuffer windows.
+ Skip overlay announcements when the user just typed a character + Skip overlay announcements when the user just typed a character
+ (didTextChange). Completion frameworks update their overlay + (didTextChange). Completion frameworks update their overlay
@@ -467,10 +458,66 @@ index 8ef344d9fe..1acb64630a 100644
+ characters inaudible. VoiceOver should read the overlay + characters inaudible. VoiceOver should read the overlay
+ candidate only when the user navigates (C-n/C-p), not types. */ + candidate only when the user navigates (C-n/C-p), not types. */
+ if (!MINI_WINDOW_P (w) || didTextChange) + if (!MINI_WINDOW_P (w) || didTextChange)
goto skip_overlay_scan; + goto skip_overlay_scan;
+
int selected_line = -1; + int selected_line = -1;
@@ -9500,7 +9679,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property + NSString *candidate
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line);
+ if (candidate)
{
- int selected_line = -1;
- NSString *candidate
- = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
- &selected_line);
- if (candidate)
+ /* Deduplicate: only announce when the candidate changed. */
+ if (![candidate isEqualToString:
+ self.cachedCompletionAnnouncement])
{
- /* Deduplicate: only announce when the candidate changed. */
- if (![candidate isEqualToString:
- self.cachedCompletionAnnouncement])
- {
- self.cachedCompletionAnnouncement = candidate;
-
- /* Announce the candidate text directly via NSApp.
- Do NOT post SelectedTextChanged --- that would cause
- VoiceOver to read the AX text at the cursor position
- (the minibuffer input line), not the overlay candidate.
- AnnouncementRequested with High priority interrupts
- any current speech and announces our text. */
- NSDictionary *annInfo = @{
- NSAccessibilityAnnouncementKey: candidate,
- NSAccessibilityPriorityKey:
- @(NSAccessibilityPriorityHigh)
- };
- ns_ax_post_notification_with_info (
- NSApp,
- NSAccessibilityAnnouncementRequestedNotification,
- annInfo);
- }
+ self.cachedCompletionAnnouncement = candidate;
+
+ /* Announce the candidate text directly via NSApp.
+ Do NOT post SelectedTextChanged --- that would cause
+ VoiceOver to read the AX text at the cursor position
+ (the minibuffer input line), not the overlay candidate.
+ AnnouncementRequested with High priority interrupts
+ any current speech and announces our text. */
+ NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: candidate,
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
}
}
}
@@ -9497,7 +9676,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
self.cachedPoint = point; self.cachedPoint = point;
self.cachedMarkActive = markActive; self.cachedMarkActive = markActive;
@@ -490,7 +537,7 @@ index 8ef344d9fe..1acb64630a 100644
NSInteger direction = ns_ax_text_selection_direction_discontiguous; NSInteger direction = ns_ax_text_selection_direction_discontiguous;
if (point > oldPoint) if (point > oldPoint)
direction = ns_ax_text_selection_direction_next; direction = ns_ax_text_selection_direction_next;
@@ -9512,6 +9702,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9509,6 +9699,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
/* --- Granularity detection --- */ /* --- Granularity detection --- */
NSInteger granularity = ns_ax_text_selection_granularity_unknown; NSInteger granularity = ns_ax_text_selection_granularity_unknown;
@@ -498,7 +545,7 @@ index 8ef344d9fe..1acb64630a 100644
[self ensureTextCache]; [self ensureTextCache];
if (cachedText && oldPoint > 0) if (cachedText && oldPoint > 0)
{ {
@@ -9526,7 +9717,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9523,7 +9714,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
NSRange newLine = [cachedText lineRangeForRange: NSRange newLine = [cachedText lineRangeForRange:
NSMakeRange (newIdx, 0)]; NSMakeRange (newIdx, 0)];
if (oldLine.location != newLine.location) if (oldLine.location != newLine.location)
@@ -518,12 +565,16 @@ index 8ef344d9fe..1acb64630a 100644
else else
{ {
NSUInteger dist = (newIdx > oldIdx NSUInteger dist = (newIdx > oldIdx
@@ -9548,34 +9750,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9545,38 +9747,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
granularity = ns_ax_text_selection_granularity_line; granularity = ns_ax_text_selection_granularity_line;
} }
- /* Treat all moves as Emacs-initiated until voiceoverSetPoint
- tracking is introduced (subsequent patch). */
- BOOL emacsMovedCursor = YES;
-
- /* Programmatic jumps that cross a line boundary (]], [[, M-<, - /* Programmatic jumps that cross a line boundary (]], [[, M-<,
- xref, imenu, ...) are discontiguous: the cursor teleported to an - xref, imenu, ) are discontiguous: the cursor teleported to an
- arbitrary position, not one sequential step forward/backward. - arbitrary position, not one sequential step forward/backward.
- Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver - Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver
- to re-anchor its rotor browse cursor at the new - to re-anchor its rotor browse cursor at the new
@@ -566,7 +617,7 @@ index 8ef344d9fe..1acb64630a 100644
{ {
NSWindow *win = [self.emacsView window]; NSWindow *win = [self.emacsView window];
if (win) if (win)
@@ -9734,6 +9925,13 @@ - (NSRect)accessibilityFrame @@ -9735,6 +9922,13 @@ - (NSRect)accessibilityFrame
if (vis_start >= vis_end) if (vis_start >= vis_end)
return @[]; return @[];
@@ -580,7 +631,7 @@ index 8ef344d9fe..1acb64630a 100644
block_input (); block_input ();
specpdl_ref blk_count = SPECPDL_INDEX (); specpdl_ref blk_count = SPECPDL_INDEX ();
record_unwind_protect_void (unblock_input); record_unwind_protect_void (unblock_input);
@@ -10040,6 +10238,10 @@ - (void)dealloc @@ -10042,6 +10236,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
@@ -591,7 +642,7 @@ index 8ef344d9fe..1acb64630a 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11489,6 +11691,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -11491,6 +11689,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;
@@ -601,7 +652,7 @@ index 8ef344d9fe..1acb64630a 100644
scrollbarsNeedingUpdate = 0; scrollbarsNeedingUpdate = 0;
fs_state = FULLSCREEN_NONE; fs_state = FULLSCREEN_NONE;
fs_before_fs = next_maximized = -1; fs_before_fs = next_maximized = -1;
@@ -12797,6 +13002,161 @@ - (id)accessibilityFocusedUIElement @@ -12799,6 +13000,156 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */ elements with current values, making change detection impossible. */
@@ -702,11 +753,6 @@ index 8ef344d9fe..1acb64630a 100644
+ childFrameLastBuffer = BVAR (b, name); + childFrameLastBuffer = BVAR (b, name);
+ childFrameLastModiff = modiff; + childFrameLastModiff = modiff;
+ +
+ /* BUFFER_LIVE_P(b) is already checked at entry (line above the
+ EQ comparison). No code between that check and here can kill
+ the buffer, so this second check is redundant. */
+ eassert (BUFFER_LIVE_P (b));
+
+ /* Skip buffers larger than a typical completion popup. + /* Skip buffers larger than a typical completion popup.
+ This avoids scanning eldoc, which-key, or other child + This avoids scanning eldoc, which-key, or other child
+ frame buffers that are not completion UIs. */ + frame buffers that are not completion UIs. */
@@ -763,7 +809,7 @@ index 8ef344d9fe..1acb64630a 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12807,11 +13167,69 @@ - (void)postAccessibilityUpdates @@ -12809,11 +13160,69 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting /* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us can trigger redisplay, which calls ns_update_end, which calls us