diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 02e0f05..8f38a6f 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -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 -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)