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>
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
nav, completions, interactive spans)
* src/nsterm.h (EmacsAccessibilityElement): New base class for virtual
accessibility elements.
(EmacsAccessibilityBuffer): AXTextArea/AXTextField per Emacs window with
cached text, visible runs, and interactive span support.
(EmacsAccessibilityModeLine): AXStaticText exposing mode line content.
(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.h (EmacsAccessibilityElement): New base class.
(EmacsAccessibilityBuffer): AXTextArea/AXTextField per window.
(EmacsAccessibilityModeLine): AXStaticText for mode line.
(EmacsAccessibilityInteractiveSpan): Focusable spans.
(ns_ax_visible_run): Visible buffer range to AX index mapping.
* 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.
(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_scan_interactive_spans): Forward scan for interactive properties.
(ns_ax_find_completion_overlay_range): Fast 3-probe + Foverlays_in bulk
fallback (no per-character scan).
(postAccessibilityUpdates): Re-entrance-guarded dispatcher with NSTRACE.
(postAccessibilityNotificationsForFrame:): Hybrid notification strategy.
(invalidateTextCache, ensureTextCache): @synchronized for thread safety
of visibleRuns/cachedText accessed from AX server thread.
(ns_ax_scan_interactive_spans): Forward scan for interactive props.
(postAccessibilityUpdates): Re-entrance-guarded dispatcher.
(rebuildAccessibilityTree): Build virtual AX element tree.
(postAccessibilityNotificationsForFrame:): Hybrid notification
strategy with record_unwind_current_buffer.
(invalidateTextCache, ensureTextCache): @synchronized for
thread safety of visibleRuns/cachedText.
(accessibilityIndexForCharpos:, charposForAccessibilityIndex:):
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.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
index 7c1ee4c..6455547 100644
--- a/src/nsterm.h
@@ -168,7 +193,7 @@ index 7c1ee4c..6455547 100644
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..248511b 100644
index 932d209..fb5af6a 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -566,7 +591,8 @@ index 932d209..248511b 100644
- if (font_panel_result)
- [font_panel_result release];
- font_panel_result = nil;
+ struct buffer *oldb = current_buffer;
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ if (b != current_buffer)
+ set_buffer_internal_1 (b);
@@ -576,9 +602,7 @@ index 932d209..248511b 100644
+ NSString *nsstr = [NSString stringWithLispString:lstr];
+ NSUInteger len = [nsstr length];
+
+ if (b != oldb)
+ set_buffer_internal_1 (oldb);
+
+ unbind_to (count, Qnil);
+ return len;
}
-#endif
@@ -1127,7 +1151,8 @@ index 932d209..248511b 100644
+ return nil;
+
+ NSString *text = nil;
+ struct buffer *oldb = current_buffer;
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ if (b != current_buffer)
+ set_buffer_internal_1 (b);
+
@@ -1162,8 +1187,7 @@ index 932d209..248511b 100644
+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)];
+ }
+
+ if (b != oldb)
+ set_buffer_internal_1 (oldb);
+ unbind_to (count, Qnil);
+
+ if (text)
+ {
@@ -1276,6 +1300,11 @@ index 932d209..248511b 100644
+- (void)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];
+ if (!w || !WINDOW_LEAF_P (w))
+ return;
@@ -2175,7 +2204,8 @@ index 932d209..248511b 100644
+ ptrdiff_t currentOverlayStart = 0;
+ ptrdiff_t currentOverlayEnd = 0;
+
+ struct buffer *oldb2 = current_buffer;
+ specpdl_ref count2 = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ if (b != current_buffer)
+ set_buffer_internal_1 (b);
+
@@ -2198,28 +2228,24 @@ index 932d209..248511b 100644
+ {
+ ptrdiff_t begv2 = BUF_BEGV (b);
+ ptrdiff_t zv2 = BUF_ZV (b);
+ ptrdiff_t s2 = point;
+ ptrdiff_t e2 = point;
+
+ while (s2 > begv2)
+ {
+ Lisp_Object prev = Fget_char_property (
+ make_fixnum (s2 - 1), Qmouse_face, Qnil);
+ if (!NILP (Fequal (prev, mf)))
+ s2--;
+ else
+ break;
+ }
+ /* Find mouse-face span boundaries using property
+ change functions — O(log n) instead of O(n). */
+ Lisp_Object prev_change
+ = Fprevious_single_char_property_change (
+ make_fixnum (point + 1), Qmouse_face,
+ Qnil, make_fixnum (begv2));
+ ptrdiff_t s2
+ = FIXNUMP (prev_change) ? XFIXNUM (prev_change)
+ : begv2;
+
+ while (e2 < zv2)
+ {
+ Lisp_Object cur = Fget_char_property (
+ make_fixnum (e2), Qmouse_face, Qnil);
+ if (!NILP (Fequal (cur, mf)))
+ e2++;
+ else
+ break;
+ }
+ Lisp_Object next_change
+ = Fnext_single_char_property_change (
+ make_fixnum (point), Qmouse_face,
+ Qnil, make_fixnum (zv2));
+ ptrdiff_t e2
+ = FIXNUMP (next_change) ? XFIXNUM (next_change)
+ : zv2;
+
+ if (e2 > s2)
+ {
@@ -2279,8 +2305,7 @@ index 932d209..248511b 100644
+ }
+ }
+
+ if (b != oldb2)
+ set_buffer_internal_1 (oldb2);
+ unbind_to (count2, Qnil);
+
+ /* Final fallback: read the current line at point. */
+ if (!announceText)