- Appends overlay before-string/after-string to AX text - Detects BUF_OVERLAY_MODIFF changes - Finds highlighted candidate via face text property - Announces selected candidate via AnnouncementRequested (fixes 'new line' instead of reading candidate)
246 lines
7.8 KiB
Diff
246 lines
7.8 KiB
Diff
From 8d1b276ce29e5c4cc193ce4207a2b929a550c8ce Mon Sep 17 00:00:00 2001
|
|
From: Martin Sukany <martin@sukany.cz>
|
|
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
|
|
|