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:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user