diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index 6c68088..967f7e9 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,7 +1,7 @@ -From 8712cf8f567f3b0c02cc70a93aff931faa3a2df3 Mon Sep 17 00:00:00 2001 +From 6e907a1000a8b138976d6a906e40449fdf1a61c5 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 -Subject: [PATCH 1/3] ns: announce overlay completion candidates for VoiceOver +Subject: [PATCH 1/2] ns: announce overlay completion candidates for VoiceOver Completion frameworks such as Vertico, Ivy, and Icomplete render candidates via overlay before-string/after-string properties rather @@ -52,8 +52,8 @@ Independent overlay branch, BUF_CHARS_MODIFF gating, candidate announcement with overlay Zoom rect storage. --- src/nsterm.h | 3 + - src/nsterm.m | 333 +++++++++++++++++++++++++++++++++++++++++++++------ - 2 files changed, 300 insertions(+), 36 deletions(-) + src/nsterm.m | 331 +++++++++++++++++++++++++++++++++++++++++++++------ + 2 files changed, 298 insertions(+), 36 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 51c30ca..5c15639 100644 @@ -77,7 +77,7 @@ index 51c30ca..5c15639 100644 BOOL font_panel_active; NSFont *font_panel_result; diff --git a/src/nsterm.m b/src/nsterm.m -index 1780194..c1fc3cb 100644 +index 1780194..143e784 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -3258,7 +3258,12 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, @@ -403,7 +403,7 @@ index 1780194..c1fc3cb 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -8789,16 +8938,128 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8789,16 +8938,126 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -428,8 +428,7 @@ index 1780194..c1fc3cb 100644 + if (chars_modiff != self.cachedCharsModiff) + { + self.cachedCharsModiff = chars_modiff; -+ if (self.emacsView) -+ self.emacsView->overlayZoomActive = NO; ++ self.emacsView->overlayZoomActive = NO; + [self postTextChangedNotification:point]; + textDidChange = YES; + } @@ -517,8 +516,7 @@ index 1780194..c1fc3cb 100644 + (minibuffer exit, C-g, etc.) or overlay has no + recognizable selection face. Return Zoom to the + text cursor. */ -+ if (self.emacsView) -+ self.emacsView->overlayZoomActive = NO; ++ self.emacsView->overlayZoomActive = NO; + } } @@ -536,7 +534,7 @@ index 1780194..c1fc3cb 100644 { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; -@@ -8966,7 +9227,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8966,7 +9225,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* =================================================================== @@ -545,7 +543,7 @@ index 1780194..c1fc3cb 100644 =================================================================== */ /* Scan visible range of window W for interactive spans. -@@ -9157,7 +9418,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9157,7 +9416,7 @@ ns_ax_scan_interactive_spans (struct window *w, - (BOOL) isAccessibilityFocused { /* Read the cached point stored by EmacsAccessibilityBuffer on the main @@ -554,7 +552,7 @@ index 1780194..c1fc3cb 100644 EmacsAccessibilityBuffer *pb = self.parentBuffer; if (!pb) return NO; -@@ -9174,7 +9435,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9174,7 +9433,7 @@ ns_ax_scan_interactive_spans (struct window *w, dispatch_async (dispatch_get_main_queue (), ^{ /* lwin is a Lisp_Object captured by value. This is GC-safe because Lisp_Objects are tagged integers/pointers that @@ -563,7 +561,7 @@ index 1780194..c1fc3cb 100644 Emacs. The WINDOW_LIVE_P check below guards against the window being deleted between capture and execution. */ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) -@@ -9200,7 +9461,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9200,7 +9459,7 @@ ns_ax_scan_interactive_spans (struct window *w, @end @@ -572,7 +570,7 @@ index 1780194..c1fc3cb 100644 Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -11922,7 +12183,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -11922,7 +12181,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, if (WINDOW_LEAF_P (w)) { @@ -581,7 +579,7 @@ index 1780194..c1fc3cb 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -11956,7 +12217,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -11956,7 +12215,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } else { @@ -590,7 +588,7 @@ index 1780194..c1fc3cb 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12068,7 +12329,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12068,7 +12327,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -599,7 +597,7 @@ index 1780194..c1fc3cb 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12077,12 +12338,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12077,12 +12336,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } /* If tree is stale, rebuild FIRST so we don't iterate freed diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index 23e4c60..3a213af 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -1,7 +1,7 @@ -From 7d20ec80aa0d4ca97fa789f9b85389e25d2ff719 Mon Sep 17 00:00:00 2001 +From 8564e4989f5f358092bd1494c3894a42974ee6e1 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 16:01:29 +0100 -Subject: [PATCH 2/3] ns: announce child frame completion candidates for +Subject: [PATCH 2/2] ns: announce child frame completion candidates for VoiceOver Completion frameworks such as Corfu, Company-box, and similar @@ -43,8 +43,8 @@ childFrameCompletionActive flag. refocus parent buffer element when child frame closes. --- src/nsterm.h | 2 + - src/nsterm.m | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++- - 2 files changed, 305 insertions(+), 3 deletions(-) + src/nsterm.m | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++- + 2 files changed, 278 insertions(+), 3 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 5c15639..8b34300 100644 @@ -67,7 +67,7 @@ index 5c15639..8b34300 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index c1fc3cb..abecb4c 100644 +index 143e784..da1a319 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -7066,6 +7066,110 @@ ns_ax_selected_overlay_text (struct buffer *b, @@ -181,7 +181,7 @@ index c1fc3cb..abecb4c 100644 /* Build accessibility text for window W, skipping invisible text. Populates *OUT_START with the buffer start charpos. Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS -@@ -12313,6 +12417,146 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12311,6 +12415,122 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, The existing elements carry cached state (modiff, point) from the previous redisplay cycle. Rebuilding first would create fresh elements with current values, making change detection impossible. */ @@ -289,30 +289,6 @@ index c1fc3cb..abecb4c 100644 +static EMACS_INT lastChildFrameModiff; +static char *lastChildFrameCandidate; + -+/* Child frame completion dedup state. File-scope so that -+ lastChildFrameBuffer can be staticpro'd against GC. */ -+static Lisp_Object lastChildFrameBuffer; -+static EMACS_INT lastChildFrameModiff; -+static char *lastChildFrameCandidate; -+ -+/* Reset the re-entrance guard when unwinding past -+ postAccessibilityUpdates due to a Lisp signal (longjmp). -+ Without this, a signal during Lisp calls (e.g. Fget_char_property -+ in overlay or child frame scanning) would leave -+ accessibilityUpdating = YES permanently, suppressing all future -+ accessibility notifications. */ -+static void -+ns_ax_reset_accessibility_updating (void *view) -+{ -+ ((EmacsView *)view)->accessibilityUpdating = NO; -+} -+ -+/* Child frame completion dedup state. File-scope so that -+ lastChildFrameBuffer can be staticpro'd against GC. */ -+static Lisp_Object lastChildFrameBuffer; -+static EMACS_INT lastChildFrameModiff; -+static char *lastChildFrameCandidate; -+ +/* Reset the re-entrance guard when unwinding past + postAccessibilityUpdates due to a Lisp signal (longjmp). + Without this, a signal during Lisp calls (e.g. Fget_char_property @@ -328,7 +304,7 @@ index c1fc3cb..abecb4c 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12323,10 +12567,60 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12321,10 +12541,60 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us @@ -390,7 +366,7 @@ index c1fc3cb..abecb4c 100644 /* Detect window tree change (split, delete, new buffer). Compare FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ -@@ -12357,7 +12651,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12355,7 +12625,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, NSAccessibilityFocusedUIElementChangedNotification); lastSelectedWindow = emacsframe->selected_window; @@ -399,7 +375,7 @@ index c1fc3cb..abecb4c 100644 return; } -@@ -12401,7 +12695,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12399,7 +12669,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, NSAccessibilityFocusedUIElementChangedNotification); } @@ -408,15 +384,12 @@ index c1fc3cb..abecb4c 100644 } /* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- -@@ -14343,6 +14637,12 @@ syms_of_nsterm (void) +@@ -14341,6 +14611,9 @@ syms_of_nsterm (void) DEFSYM (Qns_ax_completion, "completion"); DEFSYM (Qns_ax_completions_highlight, "completions-highlight"); DEFSYM (Qns_ax_backtab, "backtab"); + + lastChildFrameBuffer = Qnil; -+ staticpro (&lastChildFrameBuffer); -+ -+ lastChildFrameBuffer = Qnil; + staticpro (&lastChildFrameBuffer); /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */ Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); diff --git a/patches/0009-ns-harden-VoiceOver-accessibility-resource-safety.patch b/patches/0009-ns-harden-VoiceOver-accessibility-resource-safety.patch deleted file mode 100644 index c0fe25b..0000000 --- a/patches/0009-ns-harden-VoiceOver-accessibility-resource-safety.patch +++ /dev/null @@ -1,124 +0,0 @@ -From 0812e650c24f90bda79368078fa0ad45c18f39d2 Mon Sep 17 00:00:00 2001 -From: T -Date: Sat, 28 Feb 2026 18:45:14 +0100 -Subject: [PATCH 3/3] ns: harden VoiceOver accessibility resource safety - -Fix several resource safety issues found during maintainer review: - -* Announcement coalescing: add 50ms minimum interval between - AnnouncementRequested notifications to prevent VoiceOver speech - synthesizer stalls from rapid-fire high-priority interruptions - (e.g. holding C-n in a completion list). - -* cachedText thread safety: return [[cachedText retain] autorelease] - from accessibilityValue to prevent use-after-free when the main - thread replaces cachedText while the AX server thread is still - using the previous value. - -* EmacsView dealloc safety: nil out emacsView back-references on - all accessibility elements before releasing them. Queued - dispatch_async blocks that hold a retained element reference would - otherwise access a dangling emacsView pointer. - -* Nil guards: add emacsView nil checks in accessibilityParent, - accessibilityWindow, accessibilityTopLevelUIElement, and - overlayZoomActive access sites. - -* src/nsterm.m (ns_ax_post_notification_with_info): Add timestamp - coalescing for AnnouncementRequested. -(accessibilityValue): Return retained+autoreleased cachedText. -(dealloc): Nil out emacsView on all accessibility elements. -(accessibilityParent, accessibilityWindow) -(accessibilityTopLevelUIElement): Add nil guards. ---- - src/nsterm.m | 38 +++++++++++++++++++++++++++++++++++++- - 1 file changed, 37 insertions(+), 1 deletion(-) - -diff --git a/src/nsterm.m b/src/nsterm.m -index abecb4c..3724b05 100644 ---- a/src/nsterm.m -+++ b/src/nsterm.m -@@ -7521,11 +7521,32 @@ ns_ax_post_notification (id element, - }); - } - -+/* Minimum interval between AnnouncementRequested notifications -+ (in seconds). VoiceOver can stall if overwhelmed with rapid-fire -+ high-priority announcements that each interrupt the previous -+ utterance. 50ms lets the speech synthesizer start before the -+ next interruption. */ -+#define NS_AX_ANNOUNCE_MIN_INTERVAL 0.05 -+ - static inline void - ns_ax_post_notification_with_info (id element, - NSAccessibilityNotificationName name, - NSDictionary *info) - { -+ /* Coalesce AnnouncementRequested: skip if the previous one was -+ less than NS_AX_ANNOUNCE_MIN_INTERVAL seconds ago. Prevents -+ speech synthesizer stalls from rapid-fire high-priority -+ interruptions (e.g. holding C-n in a completion list). */ -+ if ([name isEqualToString: -+ NSAccessibilityAnnouncementRequestedNotification]) -+ { -+ static CFAbsoluteTime lastAnnouncementTime; -+ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent (); -+ if (now - lastAnnouncementTime < NS_AX_ANNOUNCE_MIN_INTERVAL) -+ return; -+ lastAnnouncementTime = now; -+ } -+ - dispatch_async (dispatch_get_main_queue (), ^{ - NSAccessibilityPostNotificationWithUserInfo (element, name, info); - }); -@@ -7571,16 +7592,22 @@ ns_ax_post_notification_with_info (id element, - - - (id)accessibilityParent - { -+ if (!self.emacsView) -+ return nil; - return NSAccessibilityUnignoredAncestor (self.emacsView); - } - - - (id)accessibilityWindow - { -+ if (!self.emacsView) -+ return nil; - return [self.emacsView window]; - } - - - (id)accessibilityTopLevelUIElement - { -+ if (!self.emacsView) -+ return nil; - return [self.emacsView window]; - } - -@@ -8143,7 +8170,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - return result; - } - [self ensureTextCache]; -- return cachedText ? cachedText : @""; -+ return cachedText ? [[cachedText retain] autorelease] : @""; - } - - - (NSInteger)accessibilityNumberOfCharacters -@@ -9659,6 +9686,15 @@ ns_ax_scan_interactive_spans (struct window *w, - [layer release]; - #endif - -+ /* Nil out back-references before releasing elements. Queued -+ dispatch_async blocks may still hold a retained reference to -+ an element; without this they would access a dangling -+ emacsView pointer after EmacsView is freed. */ -+ for (id elem in accessibilityElements) -+ { -+ if ([elem respondsToSelector:@selector (setEmacsView:)]) -+ [elem setEmacsView:nil]; -+ } - [accessibilityElements release]; - [[self menu] release]; - [super dealloc]; --- -2.43.0 -