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 119cfaa..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,4 +1,4 @@ -From bb69ef51db4c87dfe88861927264121f95fc627f 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/2] ns: announce overlay completion candidates for VoiceOver @@ -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 | 335 +++++++++++++++++++++++++++++++++++++++++++++------ - 2 files changed, 300 insertions(+), 38 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..6efeb1d 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, @@ -570,22 +570,6 @@ index 1780194..6efeb1d 100644 Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -10520,13 +10779,13 @@ ns_in_echo_area (void) - if (old_title == 0) - { - char *t = strdup ([[[self window] title] UTF8String]); -- char *pos = strstr (t, " — "); -+ char *pos = strstr (t, " --- "); - if (pos) - *pos = '\0'; - old_title = t; - } - size_title = xmalloc (strlen (old_title) + 40); -- esprintf (size_title, "%s — (%d × %d)", old_title, cols, rows); -+ esprintf (size_title, "%s --- (%d × %d)", old_title, cols, rows); - [window setTitle: [NSString stringWithUTF8String: size_title]]; - [window display]; - xfree (size_title); @@ -11922,7 +12181,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, if (WINDOW_LEAF_P (w)) 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 29d16dc..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,4 +1,4 @@ -From bdb5aefe515662fb294b719ad32fabc4008a5cb8 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/2] ns: announce child frame completion candidates for @@ -43,8 +43,8 @@ childFrameCompletionActive flag. refocus parent buffer element when child frame closes. --- src/nsterm.h | 2 + - src/nsterm.m | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++- - 2 files changed, 254 insertions(+), 1 deletion(-) + 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 6efeb1d..0255239 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 6efeb1d..0255239 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 -@@ -12311,6 +12415,105 @@ 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. */ @@ -194,9 +194,7 @@ index 6efeb1d..0255239 100644 + after the parent's draw_window_cursor. */ +- (void)announceChildFrameCompletion +{ -+ static char *lastCandidate; -+ static struct buffer *lastBuffer; -+ static EMACS_INT lastModiff; ++ + + /* Validate frame state --- child frames may be partially + initialized during creation. */ @@ -212,10 +210,11 @@ index 6efeb1d..0255239 100644 + also guards against re-entrance: if Lisp calls below + trigger redisplay, the modiff check short-circuits. */ + EMACS_INT modiff = BUF_MODIFF (b); -+ if (b == lastBuffer && modiff == lastModiff) ++ if (EQ (w->contents, lastChildFrameBuffer) ++ && modiff == lastChildFrameModiff) + return; -+ lastBuffer = b; -+ lastModiff = modiff; ++ lastChildFrameBuffer = w->contents; ++ lastChildFrameModiff = modiff; + + /* Skip buffers larger than a typical completion popup. + This avoids scanning eldoc, which-key, or other child @@ -232,10 +231,10 @@ index 6efeb1d..0255239 100644 + + /* Deduplicate --- avoid re-announcing the same candidate. */ + const char *cstr = [candidate UTF8String]; -+ if (lastCandidate && strcmp (cstr, lastCandidate) == 0) ++ if (lastChildFrameCandidate && strcmp (cstr, lastChildFrameCandidate) == 0) + return; -+ xfree (lastCandidate); -+ lastCandidate = xstrdup (cstr); ++ xfree (lastChildFrameCandidate); ++ lastChildFrameCandidate = xstrdup (cstr); + + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: candidate, @@ -283,11 +282,29 @@ index 6efeb1d..0255239 100644 + kUAZoomFocusTypeInsertionPoint); + } +} ++ ++/* 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; ++} + - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12321,11 +12524,59 @@ 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 @@ -298,14 +315,16 @@ index 6efeb1d..0255239 100644 if (accessibilityUpdating) return; accessibilityUpdating = YES; - ++ specpdl_ref axCount = SPECPDL_INDEX (); ++ record_unwind_protect_ptr (ns_ax_reset_accessibility_updating, self); ++ + /* Child frame completion popup (Corfu, Company-box, etc.). + Child frames don't participate in the accessibility tree; + announce the selected candidate directly. */ + if (FRAME_PARENT_FRAME (emacsframe)) + { + [self announceChildFrameCompletion]; -+ accessibilityUpdating = NO; ++ unbind_to (axCount, Qnil); + return; + } + @@ -344,10 +363,37 @@ index 6efeb1d..0255239 100644 + NSAccessibilityFocusedUIElementChangedNotification); + } + } -+ + /* Detect window tree change (split, delete, new buffer). Compare FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ - Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); +@@ -12355,7 +12625,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, + NSAccessibilityFocusedUIElementChangedNotification); + + lastSelectedWindow = emacsframe->selected_window; +- accessibilityUpdating = NO; ++ unbind_to (axCount, Qnil); + return; + } + +@@ -12399,7 +12669,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, + NSAccessibilityFocusedUIElementChangedNotification); + } + +- accessibilityUpdating = NO; ++ unbind_to (axCount, Qnil); + } + + /* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- +@@ -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); + /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */ + Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); + Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); -- 2.43.0 diff --git a/patches/README.txt b/patches/README.txt index b15ef06..85484ef 100644 --- a/patches/README.txt +++ b/patches/README.txt @@ -92,7 +92,9 @@ ARCHITECTURE text range, line/index/range conversions, frame-for-range, range-for-position, and insertion-point-line-number. - Maintains a text cache (cachedText / visibleRuns) keyed on - BUF_MODIFF, BUF_OVERLAY_MODIFF, and BUF_BEGV (narrowing). + BUF_MODIFF and BUF_BEGV (narrowing). BUF_OVERLAY_MODIFF is + tracked separately for notification dispatch (patch 0007) + but not for cache invalidation. The cache is the single source of truth for all index-to-charpos and charpos-to-index mappings. - Detects buffer edits (modiff change), cursor movement (point