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 f5bcd1d..9bd2e0f 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,4 +1,4 @@ -From 7474a4e1ddbf37286842e3beda1810c40f2a3ef7 Mon Sep 17 00:00:00 2001 +From 217177caefc709c37ae04732ec595ace903e4cc4 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Mon, 2 Mar 2026 18:39:46 +0100 Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver @@ -11,15 +11,24 @@ this change VoiceOver cannot read overlay-based completion UIs. 'current', 'selected', 'selection' in face symbol names. (ns_ax_selected_overlay_text): New function; scan overlay strings in the window for a line with a selected face; return its text. -(EmacsAccessibilityBuffer(Notifications) -postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF +(ensureTextCache): Switch cache-validity counter from BUF_CHARS_MODIFF +to BUF_MODIFF. Fold/unfold commands (org-mode, outline-mode, +hideshow-mode) change the 'invisible text property via +`put-text-property', which bumps BUF_MODIFF but not BUF_CHARS_MODIFF. +Using BUF_CHARS_MODIFF would serve stale AX text across fold/unfold. +The rebuild is O(visible-buffer-text) but ensureTextCache is called +exclusively from AX getters at human interaction speed, never from the +redisplay notification path; font-lock passes cause zero rebuild cost. +(postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF changes independently of text changes. Use BUF_CHARS_MODIFF to gate -ValueChanged; keep overlay_modiff out of ensureTextCache to prevent a -race where an AX query consumes the change before notification. +ValueChanged. Do not call ensureTextCache from the cursor-moved branch: +the granularity detection uses cachedText directly (falling back to +granularity_unknown when the cache is absent), so font-lock passes +cannot trigger O(buffer-size) rebuilds via the notification path. --- src/nsterm.h | 1 + - src/nsterm.m | 345 ++++++++++++++++++++++++++++++++++++++++++++------- - 2 files changed, 304 insertions(+), 42 deletions(-) + src/nsterm.m | 358 ++++++++++++++++++++++++++++++++++++++++++++------- + 2 files changed, 316 insertions(+), 43 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index f245675513..a210ceba14 100644 @@ -34,7 +43,7 @@ index f245675513..a210ceba14 100644 @property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; diff --git a/src/nsterm.m b/src/nsterm.m -index a0419bb5df..54cee74401 100644 +index a0419bb5df..b9d3a0eb53 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -7263,11 +7263,154 @@ Accessibility virtual elements (macOS / Cocoa only) @@ -228,7 +237,7 @@ index a0419bb5df..54cee74401 100644 write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); -@@ -8005,25 +8149,34 @@ - (void)ensureTextCache +@@ -8005,25 +8149,38 @@ - (void)ensureTextCache if (!b) return; @@ -259,12 +268,16 @@ index a0419bb5df..54cee74401 100644 + as if it were visible, or miss newly revealed content entirely. + + BUF_MODIFF is bumped by all buffer modifications including -+ text-property changes (e.g. font-lock face assignments), causing a -+ full text-cache rebuild on each redisplay cycle. This is acceptable -+ because `ensureTextCache' is only called when VoiceOver queries -+ accessibilityValue or related AX properties --- which happens at -+ human interaction speed, not at redisplay speed. The per-rebuild -+ cost is O(visible-buffer-text). ++ text-property changes (e.g. font-lock face assignments). The ++ per-rebuild cost is O(visible-buffer-text), but `ensureTextCache' ++ is called exclusively from AX getters (accessibilityValue, ++ accessibilitySelectedTextRange, etc.) which run at human interaction ++ speed --- not from the redisplay notification path. Font-lock ++ passes do not call this method, so the rebuild cost per font-lock ++ cycle is zero. The redisplay notification path (postAccessibility- ++ NotificationsForFrame:) uses cachedText directly without calling ++ ensureTextCache; granularity detection falls back gracefully when ++ the cache is absent. + + Do NOT use BUF_OVERLAY_MODIFF alone: org-mode >= 29 (org-fold-core) + uses text properties, not overlays, for folding, so @@ -280,7 +293,7 @@ index a0419bb5df..54cee74401 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -8039,7 +8192,7 @@ included in the cached AX text (it is handled separately via +@@ -8039,7 +8196,7 @@ included in the cached AX text (it is handled separately via { [cachedText release]; cachedText = [text retain]; @@ -289,7 +302,7 @@ index a0419bb5df..54cee74401 100644 cachedTextStart = start; if (visibleRuns) -@@ -8051,9 +8204,9 @@ included in the cached AX text (it is handled separately via +@@ -8051,9 +8208,9 @@ included in the cached AX text (it is handled separately via Walk the cached text once, recording the start offset of each line. Uses NSString lineRangeForRange: --- O(N) in the total text --- but this loop runs only on cache rebuild, which is @@ -302,7 +315,7 @@ index a0419bb5df..54cee74401 100644 enters this code. */ if (lineStartOffsets) xfree (lineStartOffsets); -@@ -8108,7 +8261,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -8108,7 +8265,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos /* Binary search: runs are sorted by charpos (ascending). Find the run whose [charpos, charpos+length) range contains the target, or the nearest run after an invisible gap. O(log n) instead of @@ -311,7 +324,7 @@ index a0419bb5df..54cee74401 100644 NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { -@@ -8157,10 +8310,10 @@ by run length (visible window), not total buffer size. */ +@@ -8157,10 +8314,10 @@ by run length (visible window), not total buffer size. */ /* Convert accessibility string index to buffer charpos. Safe to call from any thread: uses only cachedText (NSString) and @@ -324,7 +337,7 @@ index a0419bb5df..54cee74401 100644 @synchronized (self) { if (visibleRunCount == 0) -@@ -8202,7 +8355,7 @@ the slow path (composed character sequence walk), which is +@@ -8202,7 +8359,7 @@ the slow path (composed character sequence walk), which is return cp; } } @@ -333,7 +346,7 @@ index a0419bb5df..54cee74401 100644 if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -8224,7 +8377,7 @@ the slow path (composed character sequence walk), which is +@@ -8224,7 +8381,7 @@ the slow path (composed character sequence walk), which is deadlocking the AX server thread. This is prevented by: 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every @@ -342,7 +355,7 @@ index a0419bb5df..54cee74401 100644 2. All dispatch_sync blocks run on the main thread where no concurrent Lisp code can modify state between checks. 3. block_input prevents timer events and process output from -@@ -8570,6 +8723,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber +@@ -8570,6 +8727,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber return [self lineForAXIndex:point_idx]; } @@ -393,7 +406,7 @@ index a0419bb5df..54cee74401 100644 - (NSRange)accessibilityRangeForLine:(NSInteger)line { if (![NSThread isMainThread]) -@@ -8792,7 +8989,7 @@ - (NSRect)accessibilityFrame +@@ -8792,7 +8993,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -402,7 +415,7 @@ index a0419bb5df..54cee74401 100644 These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8807,7 +9004,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8807,7 +9008,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point if (point > self.cachedPoint && point - self.cachedPoint == 1) { @@ -411,7 +424,7 @@ index a0419bb5df..54cee74401 100644 [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) -@@ -8826,7 +9023,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8826,7 +9027,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point /* Update cachedPoint here so the selection-move branch does NOT fire for point changes caused by edits. WebKit and Chromium never send both ValueChanged and SelectedTextChanged for the @@ -420,7 +433,7 @@ index a0419bb5df..54cee74401 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -9220,16 +9417,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9220,16 +9421,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -505,7 +518,24 @@ index a0419bb5df..54cee74401 100644 { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; -@@ -12408,7 +12669,7 @@ - (int) fullscreenState +@@ -9247,8 +9512,15 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f + bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP); + + /* --- Granularity detection --- */ ++ /* Use cached text as-is; do NOT call ensureTextCache here. ++ ensureTextCache is O(visible-buffer-text) and must not run on ++ every redisplay cycle. Using stale cached text for granularity ++ classification is safe: the worst case is an incorrect ++ granularity hint (defaulting to unknown), which causes VoiceOver ++ to make its own determination. Fresh text is always available ++ to VoiceOver via the AX getter path (accessibilityValue etc.). */ + NSInteger granularity = ns_ax_text_selection_granularity_unknown; +- [self ensureTextCache]; ++ BOOL singleLineMove = NO; + if (cachedText && oldPoint > 0) + { + NSUInteger tlen = [cachedText length]; +@@ -12408,7 +12680,7 @@ - (int) fullscreenState if (WINDOW_LEAF_P (w)) { @@ -514,7 +544,7 @@ index a0419bb5df..54cee74401 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -12442,7 +12703,7 @@ - (int) fullscreenState +@@ -12442,7 +12714,7 @@ - (int) fullscreenState } else { @@ -523,7 +553,7 @@ index a0419bb5df..54cee74401 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12554,7 +12815,7 @@ - (void)postAccessibilityUpdates +@@ -12554,7 +12826,7 @@ - (void)postAccessibilityUpdates accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -532,7 +562,7 @@ index a0419bb5df..54cee74401 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12563,12 +12824,12 @@ - (void)postAccessibilityUpdates +@@ -12563,12 +12835,12 @@ - (void)postAccessibilityUpdates } /* 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 b7482fe..bdac7ec 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,4 +1,4 @@ -From 137cb30bb546a9599983c25a9873d1518ad8edee Mon Sep 17 00:00:00 2001 +From b54ed57b93cb47250695106021d6e96030ffdd59 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Mon, 2 Mar 2026 18:49:13 +0100 Subject: [PATCH 9/9] ns: announce child frame completion candidates for @@ -37,8 +37,8 @@ Remove Zoom section (covered by patch 0000). Fix dangling paragraph. doc/emacs/macos.texi | 13 +- etc/NEWS | 25 +- src/nsterm.h | 21 ++ - src/nsterm.m | 561 +++++++++++++++++++++++++++++++++++++------ - 4 files changed, 529 insertions(+), 91 deletions(-) + src/nsterm.m | 560 +++++++++++++++++++++++++++++++++++++------ + 4 files changed, 528 insertions(+), 91 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi index 72ac3a9aa9..cf5ed0ff28 100644 @@ -160,7 +160,7 @@ index a210ceba14..2edd7cd6e0 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index 54cee74401..6ba2229639 100644 +index b9d3a0eb53..5e48710930 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -1275,6 +1275,12 @@ If a completion candidate is selected (overlay or child frame), @@ -309,7 +309,7 @@ index 54cee74401..6ba2229639 100644 if (b != current_buffer) set_buffer_internal_1 (b); -@@ -8605,6 +8726,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range +@@ -8609,6 +8730,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range [self ensureTextCache]; @@ -321,7 +321,7 @@ index 54cee74401..6ba2229639 100644 specpdl_ref count = SPECPDL_INDEX (); record_unwind_current_buffer (); /* Ensure block_input is always matched by unblock_input even if -@@ -9053,20 +9179,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point +@@ -9057,20 +9183,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point && granularity == ns_ax_text_selection_granularity_character); @@ -370,7 +370,7 @@ index 54cee74401..6ba2229639 100644 ns_ax_post_notification_with_info ( self, NSAccessibilitySelectedTextChangedNotification, -@@ -9166,12 +9310,17 @@ user expectation ("w" jumps to next word and reads it). */ +@@ -9170,12 +9314,17 @@ user expectation ("w" jumps to next word and reads it). */ } } @@ -393,7 +393,7 @@ index 54cee74401..6ba2229639 100644 if (cachedText && granularity == ns_ax_text_selection_granularity_line) { -@@ -9236,6 +9385,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b +@@ -9240,6 +9389,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b block_input (); specpdl_ref count2 = SPECPDL_INDEX (); @@ -405,7 +405,7 @@ index 54cee74401..6ba2229639 100644 record_unwind_protect_void (unblock_input); record_unwind_current_buffer (); if (b != current_buffer) -@@ -9412,12 +9566,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9416,12 +9570,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f if (!b) return; @@ -435,7 +435,7 @@ index 54cee74401..6ba2229639 100644 if (modiff != self.cachedModiff) { self.cachedModiff = modiff; -@@ -9431,6 +9602,7 @@ Text property changes (e.g. face updates from +@@ -9435,6 +9606,7 @@ Text property changes (e.g. face updates from { self.cachedCharsModiff = chars_modiff; [self postTextChangedNotification:point]; @@ -443,7 +443,7 @@ index 54cee74401..6ba2229639 100644 } } -@@ -9453,37 +9625,44 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9457,37 +9629,44 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property displayed in the minibuffer. In normal editing buffers, font-lock and other modes change BUF_OVERLAY_MODIFF on every redisplay, triggering O(overlays) work per keystroke. @@ -517,7 +517,7 @@ index 54cee74401..6ba2229639 100644 } } } -@@ -9497,7 +9676,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9501,7 +9680,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property self.cachedPoint = point; self.cachedMarkActive = markActive; @@ -537,15 +537,7 @@ index 54cee74401..6ba2229639 100644 NSInteger direction = ns_ax_text_selection_direction_discontiguous; if (point > oldPoint) direction = ns_ax_text_selection_direction_next; -@@ -9509,6 +9699,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property - - /* --- Granularity detection --- */ - NSInteger granularity = ns_ax_text_selection_granularity_unknown; -+ BOOL singleLineMove = NO; - [self ensureTextCache]; - if (cachedText && oldPoint > 0) - { -@@ -9523,7 +9714,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9534,7 +9724,18 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */ NSRange newLine = [cachedText lineRangeForRange: NSMakeRange (newIdx, 0)]; if (oldLine.location != newLine.location) @@ -565,7 +557,7 @@ index 54cee74401..6ba2229639 100644 else { NSUInteger dist = (newIdx > oldIdx -@@ -9545,38 +9747,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9556,38 +9757,23 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */ granularity = ns_ax_text_selection_granularity_line; } @@ -617,7 +609,7 @@ index 54cee74401..6ba2229639 100644 { NSWindow *win = [self.emacsView window]; if (win) -@@ -9735,6 +9922,13 @@ - (NSRect)accessibilityFrame +@@ -9746,6 +9932,13 @@ - (NSRect)accessibilityFrame if (vis_start >= vis_end) return @[]; @@ -631,7 +623,7 @@ index 54cee74401..6ba2229639 100644 block_input (); specpdl_ref blk_count = SPECPDL_INDEX (); record_unwind_protect_void (unblock_input); -@@ -10042,6 +10236,10 @@ - (void)dealloc +@@ -10053,6 +10246,10 @@ - (void)dealloc #endif [accessibilityElements release]; @@ -642,7 +634,7 @@ index 54cee74401..6ba2229639 100644 [[self menu] release]; [super dealloc]; } -@@ -11491,6 +11689,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f +@@ -11502,6 +11699,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f windowClosing = NO; processingCompose = NO; @@ -652,7 +644,7 @@ index 54cee74401..6ba2229639 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; fs_before_fs = next_maximized = -1; -@@ -12799,6 +13000,156 @@ - (id)accessibilityFocusedUIElement +@@ -12810,6 +13010,156 @@ - (id)accessibilityFocusedUIElement 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. */ @@ -809,7 +801,7 @@ index 54cee74401..6ba2229639 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12809,11 +13160,69 @@ - (void)postAccessibilityUpdates +@@ -12820,11 +13170,69 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us