diff --git a/patches/0007-ns-include-overlay-display-strings-in-accessibility-.patch b/patches/0007-ns-include-overlay-display-strings-in-accessibility-.patch index 90bafd7..5d20d0a 100644 --- a/patches/0007-ns-include-overlay-display-strings-in-accessibility-.patch +++ b/patches/0007-ns-include-overlay-display-strings-in-accessibility-.patch @@ -1,57 +1,52 @@ -From d9a3c249cc55792e7cfbe12fa8b69861a6bf1f96 Mon Sep 17 00:00:00 2001 +From e12982f4ac111f9814a198763124a64540f4640b Mon Sep 17 00:00:00 2001 From: Martin Sukany -Date: Sat, 28 Feb 2026 14:16:29 +0100 +Date: Sat, 28 Feb 2026 14:46:25 +0100 Subject: [PATCH] ns: include overlay display strings in accessibility text -Completion frameworks like Vertico, Ivy, and Icomplete render -candidates via overlay before-string / after-string properties -rather than as buffer text. The accessibility text extraction -only reads buffer content, making overlay-based UIs invisible -to VoiceOver. +Completion frameworks such as Vertico, Ivy, and Icomplete render +candidates via overlay before-string/after-string properties rather +than buffer text. Without this patch, VoiceOver cannot read any +overlay-based completion UI. -This patch adds three enhancements: +This patch adds three pieces: -1. Walk overlays in the visible range after buffer text extraction - and append their before-string / after-string content, with - virtual visible-run entries for index mapping. +1. ns_ax_selected_overlay_text: extract the currently highlighted + candidate from overlay strings. Compare each line's face against + the first line's face via Fequal; the line with a different face + (e.g. vertico-current) is the selected candidate. -2. Detect overlay-only changes (BUF_OVERLAY_MODIFF) in the - notification dispatch. +2. ns_ax_buffer_text: append overlay before-string and after-string + content after the buffer text, with virtual visible-run entries + anchored at the overlay's buffer position. -3. When overlays change, find the highlighted candidate (the text - with a face property in the overlay string) and announce it via - NSAccessibilityAnnouncementRequestedNotification. This makes - VoiceOver read the specific selected candidate rather than - announcing 'new line'. +3. Notification dispatch: detect overlay-only changes via + BUF_OVERLAY_MODIFF. Post SelectedTextChanged to interrupt + VoiceOver, then AnnouncementRequested (to NSApp) with the + candidate text. -* src/nsterm.m (ns_ax_selected_overlay_text): New function. Walk -overlay before-string / after-string text properties to find the -face-highlighted (selected) portion, extract its full line. -(ns_ax_buffer_text): Append overlay display strings with virtual -visible-run entries. +* src/nsterm.m (ns_ax_selected_overlay_text): New function. +(ns_ax_buffer_text): Append overlay display strings. (EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): -Check BUF_OVERLAY_MODIFF; announce selected candidate text. - -Tested on macOS 14 with VoiceOver and icomplete-vertical-mode. +Handle BUF_OVERLAY_MODIFF changes with candidate announcement. --- - src/nsterm.m | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++- - 1 file changed, 178 insertions(+), 1 deletion(-) + src/nsterm.m | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 200 insertions(+), 1 deletion(-) diff --git a/src/nsterm.m b/src/nsterm.m -index 1780194..c7bba5b 100644 +index 1780194..35edd39 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -6915,11 +6915,96 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) +@@ -6915,11 +6915,99 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) are truncated for accessibility purposes. */ #define NS_AX_TEXT_CAP 100000 +/* Extract the currently selected candidate text from overlay display -+ strings in window W. Completion frameworks (Vertico, Ivy, Icomplete) -+ highlight the current candidate by applying a face property to a -+ portion of the overlay's before-string or after-string. We find -+ that highlighted portion and return it as an NSString. ++ strings. Completion frameworks (Vertico, Ivy, Icomplete) highlight ++ the current candidate with a distinct face. We find the line whose ++ face DIFFERS from the first line's face (the "normal" candidate ++ face) — that is the selected candidate. + -+ Returns nil if no highlighted overlay text is found. */ ++ Returns nil if no distinctly-faced line is found. */ +static NSString * +ns_ax_selected_overlay_text (struct buffer *b, + ptrdiff_t beg, ptrdiff_t end) @@ -72,57 +67,60 @@ index 1780194..c7bba5b 100644 + continue; + + Lisp_Object str = strings[s]; -+ ptrdiff_t len = SCHARS (str); -+ ptrdiff_t pos = 0; ++ ptrdiff_t slen = SCHARS (str); ++ if (slen == 0) ++ continue; + -+ while (pos < len) ++ /* Collect line boundaries. */ ++ ptrdiff_t line_starts[512]; ++ ptrdiff_t line_ends[512]; ++ int nlines = 0; ++ ptrdiff_t lstart = 0; ++ ++ for (ptrdiff_t i = 0; i <= slen && nlines < 512; i++) + { -+ Lisp_Object face -+ = Fget_text_property (make_fixnum (pos), -+ Qface, str); -+ if (!NILP (face)) ++ bool is_nl = false; ++ if (i < slen) + { -+ /* Found highlighted text. Extract the full line -+ containing this position. */ -+ ptrdiff_t line_start = pos; -+ while (line_start > 0) ++ Lisp_Object ch = Faref (str, make_fixnum (i)); ++ is_nl = (FIXNUMP (ch) && XFIXNUM (ch) == '\n'); ++ } ++ if (is_nl || i == slen) ++ { ++ if (i > lstart) + { -+ /* Check character before line_start. */ -+ Lisp_Object ch -+ = Faref (str, make_fixnum (line_start - 1)); -+ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n') -+ break; -+ line_start--; ++ line_starts[nlines] = lstart; ++ line_ends[nlines] = i; ++ nlines++; + } ++ lstart = i + 1; ++ } ++ } + -+ ptrdiff_t line_end = pos; -+ while (line_end < len) -+ { -+ Lisp_Object ch -+ = Faref (str, make_fixnum (line_end)); -+ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n') -+ break; -+ line_end++; -+ } ++ if (nlines < 2) ++ continue; + ++ /* Get the face of the first line (the "normal" face). */ ++ Lisp_Object normal_face ++ = Fget_text_property (make_fixnum (line_starts[0]), ++ Qface, str); ++ ++ /* Find the first line with a DIFFERENT face. */ ++ for (int li = 0; li < nlines; li++) ++ { ++ Lisp_Object line_face ++ = Fget_text_property (make_fixnum (line_starts[li]), ++ Qface, str); ++ if (NILP (Fequal (line_face, normal_face))) ++ { + Lisp_Object line -+ = Fsubstring_no_properties (str, -+ make_fixnum (line_start), -+ make_fixnum (line_end)); ++ = Fsubstring_no_properties ( ++ str, ++ make_fixnum (line_starts[li]), ++ make_fixnum (line_ends[li])); + if (SCHARS (line) > 0) + return [NSString stringWithLispString:line]; + } -+ -+ /* Skip to next face change. */ -+ Lisp_Object next -+ = Fnext_single_property_change (make_fixnum (pos), -+ Qface, str, -+ make_fixnum (len)); -+ ptrdiff_t npos -+ = FIXNUMP (next) ? XFIXNUM (next) : len; -+ if (npos <= pos) -+ break; -+ pos = npos; + } + } + } @@ -139,7 +137,7 @@ index 1780194..c7bba5b 100644 static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -7021,6 +7106,68 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, +@@ -7021,6 +7109,68 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, pos = run_end; } @@ -208,7 +206,7 @@ index 1780194..c7bba5b 100644 unbind_to (count, Qnil); *out_runs = runs; -@@ -8795,6 +8942,36 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8795,6 +8945,55 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, [self postTextChangedNotification:point]; } @@ -230,15 +228,34 @@ index 1780194..c7bba5b 100644 + = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b)); + if (candidate) + { -+ NSDictionary *info = @{ -+ NSAccessibilityAnnouncementKey: candidate, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) ++ /* Post SelectedTextChanged first to interrupt VoiceOver, ++ then AnnouncementRequested with candidate text. ++ Target NSApp for announcements (Apple docs require it). */ ++ NSDictionary *moveInfo = @{ ++ @"AXTextStateChangeType": ++ @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": self + }; + ns_ax_post_notification_with_info ( + self, -+ NSAccessibilityAnnouncementRequestedNotification, -+ info); ++ NSAccessibilitySelectedTextChangedNotification, ++ moveInfo); ++ ++ candidate = [candidate ++ stringByTrimmingCharactersInSet: ++ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; ++ if ([candidate length] > 0) ++ { ++ NSDictionary *annInfo = @{ ++ NSAccessibilityAnnouncementKey: candidate, ++ NSAccessibilityPriorityKey: ++ @(NSAccessibilityPriorityHigh) ++ }; ++ ns_ax_post_notification_with_info ( ++ NSApp, ++ NSAccessibilityAnnouncementRequestedNotification, ++ annInfo); ++ } + } + } + diff --git a/patches/0008-ns-fix-overlay-candidate-announcement.patch b/patches/0008-ns-fix-overlay-candidate-announcement.patch deleted file mode 100644 index 9bee489..0000000 --- a/patches/0008-ns-fix-overlay-candidate-announcement.patch +++ /dev/null @@ -1,84 +0,0 @@ -From 988b041f1fe0dc730014fdac82e746412a5afc70 Mon Sep 17 00:00:00 2001 -From: Martin Sukany -Date: Sat, 28 Feb 2026 14:41:42 +0100 -Subject: [PATCH] ns: fix overlay candidate announcement for VoiceOver -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Fix three bugs in patch 0007: - -1. Post AnnouncementRequested to NSApp, not self. VoiceOver ignores - announcements from non-application elements. - -2. Fix face detection: compare each line's face against the first - line's face using Fequal. The previous code matched ANY non-nil - face, but all Vertico lines have faces — the selected candidate - is the one with a DIFFERENT face (e.g. vertico-current). - -3. Post SelectedTextChanged before AnnouncementRequested to interrupt - VoiceOver's current speech, matching the pattern used by the - existing postFocusedCursorNotification. - -* src/nsterm.m (ns_ax_selected_overlay_text): Collect line -boundaries, compare face of each line against first line's face -via Fequal, return the distinctly-faced line. -(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): -Post SelectedTextChanged then AnnouncementRequested to NSApp. ---- - src/nsterm.m | 32 ++++++++++++++++++++++++++------ - 1 file changed, 26 insertions(+), 6 deletions(-) - -diff --git a/src/nsterm.m b/src/nsterm.m -index c7bba5b..43d30f9 100644 ---- a/src/nsterm.m -+++ b/src/nsterm.m -@@ -8956,19 +8956,39 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - properties. Completion frameworks highlight the current - candidate with a text face (e.g. vertico-current, - icomplete-selected-match). */ -+ NSString *candidate - NSString *candidate - = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b)); - if (candidate) - { -- NSDictionary *info = @{ -- NSAccessibilityAnnouncementKey: candidate, -- NSAccessibilityPriorityKey: -- @(NSAccessibilityPriorityHigh) -+ /* Post SelectedTextChanged first to interrupt VoiceOver, -+ then AnnouncementRequested with candidate text. -+ Target NSApp for announcements (Apple docs require it). */ -+ NSDictionary *moveInfo = @{ -+ @"AXTextStateChangeType": -+ @(ns_ax_text_state_change_selection_move), -+ @"AXTextChangeElement": self - }; - ns_ax_post_notification_with_info ( - self, -- NSAccessibilityAnnouncementRequestedNotification, -- info); -+ NSAccessibilitySelectedTextChangedNotification, -+ moveInfo); -+ -+ candidate = [candidate -+ stringByTrimmingCharactersInSet: -+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; -+ if ([candidate length] > 0) -+ { -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: candidate, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) -+ }; -+ ns_ax_post_notification_with_info ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); -+ } - } - } - --- -2.43.0 - diff --git a/patches/0009-ns-fix-overlay-candidate-detection.patch b/patches/0009-ns-fix-overlay-candidate-detection.patch deleted file mode 100644 index 72a7411..0000000 --- a/patches/0009-ns-fix-overlay-candidate-detection.patch +++ /dev/null @@ -1,152 +0,0 @@ -From 2e5505f044e403ccaef8c43bdc66480c71dcf05a Mon Sep 17 00:00:00 2001 -From: Martin Sukany -Date: Sat, 28 Feb 2026 14:44:37 +0100 -Subject: [PATCH] ns: fix overlay candidate detection (Fequal face comparison) - -The previous ns_ax_selected_overlay_text matched ANY non-nil face, -but all Vertico lines have faces. Fix: collect line boundaries, -compare each line's face against the first line's face via Fequal, -return the line with a DIFFERENT face (the selected candidate). - -Also fix duplicate 'NSString *candidate' declaration. - -* src/nsterm.m (ns_ax_selected_overlay_text): Rewrite to compare -faces line-by-line via Fequal instead of matching first non-nil face. ---- - src/nsterm.m | 94 +++++++++++++++++++++++++++------------------------- - 1 file changed, 48 insertions(+), 46 deletions(-) - -diff --git a/src/nsterm.m b/src/nsterm.m -index 43d30f9..35edd39 100644 ---- a/src/nsterm.m -+++ b/src/nsterm.m -@@ -6916,12 +6916,12 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) - #define NS_AX_TEXT_CAP 100000 - - /* Extract the currently selected candidate text from overlay display -- strings in window W. Completion frameworks (Vertico, Ivy, Icomplete) -- highlight the current candidate by applying a face property to a -- portion of the overlay's before-string or after-string. We find -- that highlighted portion and return it as an NSString. -+ strings. Completion frameworks (Vertico, Ivy, Icomplete) highlight -+ the current candidate with a distinct face. We find the line whose -+ face DIFFERS from the first line's face (the "normal" candidate -+ face) — that is the selected candidate. - -- Returns nil if no highlighted overlay text is found. */ -+ Returns nil if no distinctly-faced line is found. */ - static NSString * - ns_ax_selected_overlay_text (struct buffer *b, - ptrdiff_t beg, ptrdiff_t end) -@@ -6942,57 +6942,60 @@ ns_ax_selected_overlay_text (struct buffer *b, - continue; - - Lisp_Object str = strings[s]; -- ptrdiff_t len = SCHARS (str); -- ptrdiff_t pos = 0; -+ ptrdiff_t slen = SCHARS (str); -+ if (slen == 0) -+ continue; -+ -+ /* Collect line boundaries. */ -+ ptrdiff_t line_starts[512]; -+ ptrdiff_t line_ends[512]; -+ int nlines = 0; -+ ptrdiff_t lstart = 0; - -- while (pos < len) -+ for (ptrdiff_t i = 0; i <= slen && nlines < 512; i++) - { -- Lisp_Object face -- = Fget_text_property (make_fixnum (pos), -- Qface, str); -- if (!NILP (face)) -+ bool is_nl = false; -+ if (i < slen) -+ { -+ Lisp_Object ch = Faref (str, make_fixnum (i)); -+ is_nl = (FIXNUMP (ch) && XFIXNUM (ch) == '\n'); -+ } -+ if (is_nl || i == slen) - { -- /* Found highlighted text. Extract the full line -- containing this position. */ -- ptrdiff_t line_start = pos; -- while (line_start > 0) -+ if (i > lstart) - { -- /* Check character before line_start. */ -- Lisp_Object ch -- = Faref (str, make_fixnum (line_start - 1)); -- if (FIXNUMP (ch) && XFIXNUM (ch) == '\n') -- break; -- line_start--; -+ line_starts[nlines] = lstart; -+ line_ends[nlines] = i; -+ nlines++; - } -+ lstart = i + 1; -+ } -+ } - -- ptrdiff_t line_end = pos; -- while (line_end < len) -- { -- Lisp_Object ch -- = Faref (str, make_fixnum (line_end)); -- if (FIXNUMP (ch) && XFIXNUM (ch) == '\n') -- break; -- line_end++; -- } -+ if (nlines < 2) -+ continue; - -+ /* Get the face of the first line (the "normal" face). */ -+ Lisp_Object normal_face -+ = Fget_text_property (make_fixnum (line_starts[0]), -+ Qface, str); -+ -+ /* Find the first line with a DIFFERENT face. */ -+ for (int li = 0; li < nlines; li++) -+ { -+ Lisp_Object line_face -+ = Fget_text_property (make_fixnum (line_starts[li]), -+ Qface, str); -+ if (NILP (Fequal (line_face, normal_face))) -+ { - Lisp_Object line -- = Fsubstring_no_properties (str, -- make_fixnum (line_start), -- make_fixnum (line_end)); -+ = Fsubstring_no_properties ( -+ str, -+ make_fixnum (line_starts[li]), -+ make_fixnum (line_ends[li])); - if (SCHARS (line) > 0) - return [NSString stringWithLispString:line]; - } -- -- /* Skip to next face change. */ -- Lisp_Object next -- = Fnext_single_property_change (make_fixnum (pos), -- Qface, str, -- make_fixnum (len)); -- ptrdiff_t npos -- = FIXNUMP (next) ? XFIXNUM (next) : len; -- if (npos <= pos) -- break; -- pos = npos; - } - } - } -@@ -8956,7 +8959,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, - properties. Completion frameworks highlight the current - candidate with a text face (e.g. vertico-current, - icomplete-selected-match). */ -- NSString *candidate - NSString *candidate - = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b)); - if (candidate) --- -2.43.0 -