From 23e2549bd2ce3c3180f0f9a5ead326bc0183c1fb 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 in ns_ax_buffer_text only reads buffer content, making these overlay-based UIs invisible to VoiceOver. This patch adds two enhancements: 1. Walk overlays in the visible range after buffer text extraction and append their before-string / after-string content. Each overlay string gets a virtual visible-run entry mapped to the overlay's buffer position for cursor tracking. 2. Detect overlay-only changes (BUF_OVERLAY_MODIFF) in the notification dispatch and fire AXValueChanged, so VoiceOver re-reads content when completion candidates change. * src/nsterm.m (ns_ax_buffer_text): Append overlay before-string and after-string content with virtual visible-run entries. (EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): Check BUF_OVERLAY_MODIFF and fire postTextChangedNotification when overlays change. Tested on macOS 14 with VoiceOver and Vertico completion framework. Verified: M-x candidate navigation announced, file finder candidates read by VoiceOver when moving up/down. --- src/nsterm.m | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/nsterm.m b/src/nsterm.m index 1780194..88458aa 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -7021,6 +7021,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 +8857,16 @@ 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. Fire ValueChanged so + VoiceOver re-reads the buffer content including overlay strings. */ + else if (BUF_OVERLAY_MODIFF (b) != self.cachedOverlayModiff) + { + self.cachedOverlayModiff = BUF_OVERLAY_MODIFF (b); + [self postTextChangedNotification:point]; + } + /* --- Cursor moved or selection changed --- Use 'else if' — edits and selection moves are mutually exclusive per the WebKit/Chromium pattern. */ -- 2.43.0