diff --git a/patches/0007-ns-include-overlay-display-strings-in-accessibility-.patch b/patches/0007-ns-include-overlay-display-strings-in-accessibility-.patch new file mode 100644 index 0000000..2e68864 --- /dev/null +++ b/patches/0007-ns-include-overlay-display-strings-in-accessibility-.patch @@ -0,0 +1,245 @@ +From 8d1b276ce29e5c4cc193ce4207a2b929a550c8ce Mon Sep 17 00:00:00 2001 +From: Martin Sukany +Date: Sat, 28 Feb 2026 14:16:29 +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. + +This patch adds three enhancements: + +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. + +2. Detect overlay-only changes (BUF_OVERLAY_MODIFF) in the + notification dispatch. + +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'. + +* 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. +(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): +Check BUF_OVERLAY_MODIFF; announce selected candidate text. + +Tested on macOS 14 with VoiceOver and icomplete-vertical-mode. +--- + src/nsterm.m | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 178 insertions(+) + +diff --git a/src/nsterm.m b/src/nsterm.m +index 1780194..2a50a53 100644 +--- a/src/nsterm.m ++++ b/src/nsterm.m +@@ -6921,6 +6921,92 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) + with the count. Caller must free *OUT_RUNS with xfree(). */ + + static NSString * ++/* 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. ++ ++ Returns nil if no highlighted overlay text is found. */ ++static NSString * ++ns_ax_selected_overlay_text (struct buffer *b, ++ ptrdiff_t beg, ptrdiff_t end) ++{ ++ Lisp_Object ov_list = Foverlays_in (make_fixnum (beg), ++ make_fixnum (end)); ++ ++ for (Lisp_Object tail = ov_list; CONSP (tail); tail = XCDR (tail)) ++ { ++ Lisp_Object ov = XCAR (tail); ++ Lisp_Object strings[2]; ++ strings[0] = Foverlay_get (ov, intern_c_string ("before-string")); ++ strings[1] = Foverlay_get (ov, intern_c_string ("after-string")); ++ ++ for (int s = 0; s < 2; s++) ++ { ++ if (!STRINGP (strings[s])) ++ continue; ++ ++ Lisp_Object str = strings[s]; ++ ptrdiff_t len = SCHARS (str); ++ ptrdiff_t pos = 0; ++ ++ while (pos < len) ++ { ++ Lisp_Object face ++ = Fget_text_property (make_fixnum (pos), ++ Qface, str); ++ if (!NILP (face)) ++ { ++ /* Found highlighted text. Extract the full line ++ containing this position. */ ++ ptrdiff_t line_start = pos; ++ while (line_start > 0) ++ { ++ /* Check character before line_start. */ ++ Lisp_Object ch ++ = Faref (str, make_fixnum (line_start - 1)); ++ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n') ++ break; ++ line_start--; ++ } ++ ++ 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++; ++ } ++ ++ Lisp_Object line ++ = Fsubstring_no_properties (str, ++ make_fixnum (line_start), ++ make_fixnum (line_end)); ++ 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; ++ } ++ } ++ } ++ ++ return nil; ++} ++ ++ + ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, + ns_ax_visible_run **out_runs, NSUInteger *out_nruns) + { +@@ -7021,6 +7107,68 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, + pos = run_end; + } + ++ /* Append overlay display strings (before-string, after-string). ++ Many completion frameworks (Vertico, Ivy, Icomplete) render ++ candidates via overlay properties rather than buffer text. ++ Without this, VoiceOver cannot read overlay-based content. ++ ++ We append overlay strings after the buffer text and give each ++ a virtual run mapped to the overlay's buffer position, so cursor ++ tracking and index mapping remain functional. */ ++ { ++ Lisp_Object ov_list = Foverlays_in (make_fixnum (begv), ++ make_fixnum (zv)); ++ for (Lisp_Object tail = ov_list; CONSP (tail); tail = XCDR (tail)) ++ { ++ Lisp_Object ov = XCAR (tail); ++ Lisp_Object props[2]; ++ ptrdiff_t anchors[2]; ++ ++ props[0] = Foverlay_get (ov, intern_c_string ("before-string")); ++ anchors[0] = XFIXNUM (Foverlay_start (ov)); ++ props[1] = Foverlay_get (ov, intern_c_string ("after-string")); ++ anchors[1] = XFIXNUM (Foverlay_end (ov)); ++ ++ for (int k = 0; k < 2; k++) ++ { ++ if (!STRINGP (props[k])) ++ continue; ++ ++ /* Cap total text. */ ++ if (ax_offset >= NS_AX_TEXT_CAP) ++ break; ++ ++ NSString *nsstr ++ = [NSString stringWithLispString:props[k]]; ++ NSUInteger ns_len = [nsstr length]; ++ if (ns_len == 0) ++ continue; ++ ++ if (ax_offset + ns_len > NS_AX_TEXT_CAP) ++ ns_len = NS_AX_TEXT_CAP - ax_offset; ++ ++ if (ns_len < [nsstr length]) ++ nsstr = [nsstr substringToIndex:ns_len]; ++ ++ [result appendString:nsstr]; ++ ++ if (nruns >= run_capacity) ++ { ++ run_capacity *= 2; ++ runs = xrealloc (runs, run_capacity ++ * sizeof (ns_ax_visible_run)); ++ } ++ runs[nruns].charpos = anchors[k]; ++ runs[nruns].length = 0; /* virtual — no buffer chars */ ++ runs[nruns].ax_start = ax_offset; ++ runs[nruns].ax_length = ns_len; ++ nruns++; ++ ++ ax_offset += ns_len; ++ } ++ } ++ } ++ + unbind_to (count, Qnil); + + *out_runs = runs; +@@ -8795,6 +8943,36 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + [self postTextChangedNotification:point]; + } + ++ /* --- Overlay content changed (e.g. Vertico/Ivy candidate switch) --- ++ Overlay-only changes (before-string, after-string, display) bump ++ BUF_OVERLAY_MODIFF but not BUF_MODIFF. Instead of a generic ++ ValueChanged (which causes VoiceOver to say "new line"), find the ++ highlighted candidate in the overlay and announce it directly. */ ++ else if (BUF_OVERLAY_MODIFF (b) != self.cachedOverlayModiff) ++ { ++ self.cachedOverlayModiff = BUF_OVERLAY_MODIFF (b); ++ [self invalidateTextCache]; ++ ++ /* Find the selected candidate text from overlay face ++ properties. Completion frameworks highlight the current ++ candidate with a text face (e.g. vertico-current, ++ icomplete-selected-match). */ ++ NSString *candidate ++ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b)); ++ if (candidate) ++ { ++ NSDictionary *info = @{ ++ NSAccessibilityAnnouncementKey: candidate, ++ NSAccessibilityPriorityKey: ++ @(NSAccessibilityPriorityHigh) ++ }; ++ ns_ax_post_notification_with_info ( ++ self, ++ NSAccessibilityAnnouncementRequestedNotification, ++ info); ++ } ++ } ++ + /* --- Cursor moved or selection changed --- + Use 'else if' — edits and selection moves are mutually exclusive + per the WebKit/Chromium pattern. */ +-- +2.43.0 +