patches: add 0007 overlay display string support
Appends overlay before-string/after-string to AX text extraction. Fires AXValueChanged on BUF_OVERLAY_MODIFF changes. Enables VoiceOver for Vertico, Ivy, Icomplete and other overlay-based completion frameworks.
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
From 23e2549bd2ce3c3180f0f9a5ead326bc0183c1fb 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
|
||||||
|
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
|
||||||
|
|
||||||
Reference in New Issue
Block a user