patches: maintainer review R2 fixes — all must-fix items resolved

- unwind-protect in ns_ax_utf16_length_for_buffer_range
- unwind-protect in ns_ax_completion_text_for_span
- unwind-protect in postAccessibilityNotificationsForFrame
- NSTRACE added to all 4 key functions (3 were missing)
- O(n) mouse-face scan → Fprevious/Fnext_single_char_property_change
- etc/NEWS entry added to patch
- Main-thread invariant comment in ensureTextCache
This commit is contained in:
2026-02-27 14:35:04 +01:00
parent eafc80e324
commit 1ecb9908af

View File

@@ -1,39 +1,64 @@
From 900c20da8271f503ed3c224795a001dfca8b92f1 Mon Sep 17 00:00:00 2001 From 2daab9a7e1018505a93bf72591a0d695352648d7 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Fri, 27 Feb 2026 14:29:32 +0100 Date: Fri, 27 Feb 2026 14:34:58 +0100
Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
nav, completions, interactive spans) nav, completions, interactive spans)
* src/nsterm.h (EmacsAccessibilityElement): New base class for virtual * src/nsterm.h (EmacsAccessibilityElement): New base class.
accessibility elements. (EmacsAccessibilityBuffer): AXTextArea/AXTextField per window.
(EmacsAccessibilityBuffer): AXTextArea/AXTextField per Emacs window with (EmacsAccessibilityModeLine): AXStaticText for mode line.
cached text, visible runs, and interactive span support. (EmacsAccessibilityInteractiveSpan): Focusable spans.
(EmacsAccessibilityModeLine): AXStaticText exposing mode line content. (ns_ax_visible_run): Visible buffer range to AX index mapping.
(EmacsAccessibilityInteractiveSpan): Focusable element for buttons,
links, completion candidates, and keymap overlays.
(ns_ax_visible_run): Maps contiguous visible buffer ranges to AX string
indices, skipping invisible text.
* src/nsterm.m (ns_ax_buffer_text): Build accessibility string with * src/nsterm.m (ns_ax_buffer_text): Build AX string with
record_unwind_current_buffer for exception safety. record_unwind_current_buffer for exception safety.
(ns_ax_utf16_length_for_buffer_range): UTF-16 length with
record_unwind_current_buffer.
(ns_ax_completion_text_for_span): Completion text extraction with
record_unwind_current_buffer.
(ns_ax_find_completion_overlay_range): Fast 3-probe + Foverlays_in
bulk fallback.
(ns_ax_event_is_line_nav_key): Detect C-n/C-p/Tab/backtab. (ns_ax_event_is_line_nav_key): Detect C-n/C-p/Tab/backtab.
(ns_ax_scan_interactive_spans): Forward scan for interactive properties. (ns_ax_scan_interactive_spans): Forward scan for interactive props.
(ns_ax_find_completion_overlay_range): Fast 3-probe + Foverlays_in bulk (postAccessibilityUpdates): Re-entrance-guarded dispatcher.
fallback (no per-character scan). (rebuildAccessibilityTree): Build virtual AX element tree.
(postAccessibilityUpdates): Re-entrance-guarded dispatcher with NSTRACE. (postAccessibilityNotificationsForFrame:): Hybrid notification
(postAccessibilityNotificationsForFrame:): Hybrid notification strategy. strategy with record_unwind_current_buffer.
(invalidateTextCache, ensureTextCache): @synchronized for thread safety (invalidateTextCache, ensureTextCache): @synchronized for
of visibleRuns/cachedText accessed from AX server thread. thread safety of visibleRuns/cachedText.
(accessibilityIndexForCharpos:, charposForAccessibilityIndex:): (accessibilityIndexForCharpos:, charposForAccessibilityIndex:):
Thread-safe index mapping with @synchronized. Thread-safe index mapping with @synchronized.
(ns_draw_phys_cursor): UAZoomChangeFocus with MAC_OS_X_VERSION guard. (ns_draw_phys_cursor): UAZoomChangeFocus with version guard.
* etc/NEWS: New feature: VoiceOver accessibility on macOS. * etc/NEWS: Announce VoiceOver accessibility on macOS.
--- ---
etc/NEWS | 11 +
src/nsterm.h | 108 ++ src/nsterm.h | 108 ++
src/nsterm.m | 2727 +++++++++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 2727 +++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 2693 insertions(+), 142 deletions(-) 3 files changed, 2704 insertions(+), 142 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS
index 7367e3c..0e4480a 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -4374,6 +4374,17 @@ allowing Emacs users access to speech recognition utilities.
Note: Accepting this permission allows the use of system APIs, which may
send user data to Apple's speech recognition servers.
+---
+** VoiceOver accessibility support on macOS.
+Emacs now exposes buffer content, cursor position, and interactive
+elements to the macOS accessibility subsystem (VoiceOver). This
+includes AXBoundsForRange for macOS Zoom cursor tracking, line and
+word navigation announcements, Tab-navigable interactive spans
+(buttons, links, completion candidates), and completion announcements
+for the *Completions* buffer. The implementation uses a virtual
+accessibility tree with per-window elements, hybrid SelectedTextChanged
+and AnnouncementRequested notifications, and thread-safe text caching.
+
---
** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..6455547 100644 index 7c1ee4c..6455547 100644
--- a/src/nsterm.h --- a/src/nsterm.h
@@ -168,7 +193,7 @@ index 7c1ee4c..6455547 100644
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..248511b 100644 index 932d209..fb5af6a 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -566,7 +591,8 @@ index 932d209..248511b 100644
- if (font_panel_result) - if (font_panel_result)
- [font_panel_result release]; - [font_panel_result release];
- font_panel_result = nil; - font_panel_result = nil;
+ struct buffer *oldb = current_buffer; + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
@@ -576,9 +602,7 @@ index 932d209..248511b 100644
+ NSString *nsstr = [NSString stringWithLispString:lstr]; + NSString *nsstr = [NSString stringWithLispString:lstr];
+ NSUInteger len = [nsstr length]; + NSUInteger len = [nsstr length];
+ +
+ if (b != oldb) + unbind_to (count, Qnil);
+ set_buffer_internal_1 (oldb);
+
+ return len; + return len;
} }
-#endif -#endif
@@ -1127,7 +1151,8 @@ index 932d209..248511b 100644
+ return nil; + return nil;
+ +
+ NSString *text = nil; + NSString *text = nil;
+ struct buffer *oldb = current_buffer; + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
@@ -1162,8 +1187,7 @@ index 932d209..248511b 100644
+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)]; + text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)];
+ } + }
+ +
+ if (b != oldb) + unbind_to (count, Qnil);
+ set_buffer_internal_1 (oldb);
+ +
+ if (text) + if (text)
+ { + {
@@ -1276,6 +1300,11 @@ index 932d209..248511b 100644
+- (void)ensureTextCache +- (void)ensureTextCache
+{ +{
+ NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); + NSTRACE ("EmacsAccessibilityBuffer ensureTextCache");
+ /* This method is only called from the main thread (AX getters
+ dispatch_sync to main first). Reads of cachedText/cachedTextModiff
+ below are therefore safe without @synchronized — only the
+ write section at the end needs synchronization to protect
+ against concurrent reads from AX server thread. */
+ struct window *w = [self validWindow]; + struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w)) + if (!w || !WINDOW_LEAF_P (w))
+ return; + return;
@@ -2175,7 +2204,8 @@ index 932d209..248511b 100644
+ ptrdiff_t currentOverlayStart = 0; + ptrdiff_t currentOverlayStart = 0;
+ ptrdiff_t currentOverlayEnd = 0; + ptrdiff_t currentOverlayEnd = 0;
+ +
+ struct buffer *oldb2 = current_buffer; + specpdl_ref count2 = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
@@ -2198,28 +2228,24 @@ index 932d209..248511b 100644
+ { + {
+ ptrdiff_t begv2 = BUF_BEGV (b); + ptrdiff_t begv2 = BUF_BEGV (b);
+ ptrdiff_t zv2 = BUF_ZV (b); + ptrdiff_t zv2 = BUF_ZV (b);
+ ptrdiff_t s2 = point;
+ ptrdiff_t e2 = point;
+ +
+ while (s2 > begv2) + /* Find mouse-face span boundaries using property
+ { + change functions — O(log n) instead of O(n). */
+ Lisp_Object prev = Fget_char_property ( + Lisp_Object prev_change
+ make_fixnum (s2 - 1), Qmouse_face, Qnil); + = Fprevious_single_char_property_change (
+ if (!NILP (Fequal (prev, mf))) + make_fixnum (point + 1), Qmouse_face,
+ s2--; + Qnil, make_fixnum (begv2));
+ else + ptrdiff_t s2
+ break; + = FIXNUMP (prev_change) ? XFIXNUM (prev_change)
+ } + : begv2;
+ +
+ while (e2 < zv2) + Lisp_Object next_change
+ { + = Fnext_single_char_property_change (
+ Lisp_Object cur = Fget_char_property ( + make_fixnum (point), Qmouse_face,
+ make_fixnum (e2), Qmouse_face, Qnil); + Qnil, make_fixnum (zv2));
+ if (!NILP (Fequal (cur, mf))) + ptrdiff_t e2
+ e2++; + = FIXNUMP (next_change) ? XFIXNUM (next_change)
+ else + : zv2;
+ break;
+ }
+ +
+ if (e2 > s2) + if (e2 > s2)
+ { + {
@@ -2279,8 +2305,7 @@ index 932d209..248511b 100644
+ } + }
+ } + }
+ +
+ if (b != oldb2) + unbind_to (count2, Qnil);
+ set_buffer_internal_1 (oldb2);
+ +
+ /* Final fallback: read the current line at point. */ + /* Final fallback: read the current line at point. */
+ if (!announceText) + if (!announceText)