patches: squash 0007+0008+0009 into single clean 0007

All overlay fixes in one patch: Fequal face detection,
NSApp announcement target, SelectedTextChanged interrupt.
This commit is contained in:
2026-02-28 14:46:43 +01:00
parent 8a48e72493
commit 6c502c7af5
3 changed files with 100 additions and 319 deletions

View File

@@ -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 <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
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 Subject: [PATCH] ns: include overlay display strings in accessibility text
Completion frameworks like Vertico, Ivy, and Icomplete render Completion frameworks such as Vertico, Ivy, and Icomplete render
candidates via overlay before-string / after-string properties candidates via overlay before-string/after-string properties rather
rather than as buffer text. The accessibility text extraction than buffer text. Without this patch, VoiceOver cannot read any
only reads buffer content, making overlay-based UIs invisible overlay-based completion UI.
to VoiceOver.
This patch adds three enhancements: This patch adds three pieces:
1. Walk overlays in the visible range after buffer text extraction 1. ns_ax_selected_overlay_text: extract the currently highlighted
and append their before-string / after-string content, with candidate from overlay strings. Compare each line's face against
virtual visible-run entries for index mapping. 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 2. ns_ax_buffer_text: append overlay before-string and after-string
notification dispatch. 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 3. Notification dispatch: detect overlay-only changes via
with a face property in the overlay string) and announce it via BUF_OVERLAY_MODIFF. Post SelectedTextChanged to interrupt
NSAccessibilityAnnouncementRequestedNotification. This makes VoiceOver, then AnnouncementRequested (to NSApp) with the
VoiceOver read the specific selected candidate rather than candidate text.
announcing 'new line'.
* src/nsterm.m (ns_ax_selected_overlay_text): New function. Walk * src/nsterm.m (ns_ax_selected_overlay_text): New function.
overlay before-string / after-string text properties to find the (ns_ax_buffer_text): Append overlay display strings.
face-highlighted (selected) portion, extract its full line.
(ns_ax_buffer_text): Append overlay display strings with virtual
visible-run entries.
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): (EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
Check BUF_OVERLAY_MODIFF; announce selected candidate text. Handle BUF_OVERLAY_MODIFF changes with candidate announcement.
Tested on macOS 14 with VoiceOver and icomplete-vertical-mode.
--- ---
src/nsterm.m | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/nsterm.m | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 178 insertions(+), 1 deletion(-) 1 file changed, 200 insertions(+), 1 deletion(-)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 1780194..c7bba5b 100644 index 1780194..35edd39 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/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. */ are truncated for accessibility purposes. */
#define NS_AX_TEXT_CAP 100000 #define NS_AX_TEXT_CAP 100000
+/* Extract the currently selected candidate text from overlay display +/* Extract the currently selected candidate text from overlay display
+ strings in window W. Completion frameworks (Vertico, Ivy, Icomplete) + strings. Completion frameworks (Vertico, Ivy, Icomplete) highlight
+ highlight the current candidate by applying a face property to a + the current candidate with a distinct face. We find the line whose
+ portion of the overlay's before-string or after-string. We find + face DIFFERS from the first line's face (the "normal" candidate
+ that highlighted portion and return it as an NSString. + 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 * +static NSString *
+ns_ax_selected_overlay_text (struct buffer *b, +ns_ax_selected_overlay_text (struct buffer *b,
+ ptrdiff_t beg, ptrdiff_t end) + ptrdiff_t beg, ptrdiff_t end)
@@ -72,57 +67,60 @@ index 1780194..c7bba5b 100644
+ continue; + continue;
+ +
+ Lisp_Object str = strings[s]; + Lisp_Object str = strings[s];
+ ptrdiff_t len = SCHARS (str); + ptrdiff_t slen = SCHARS (str);
+ ptrdiff_t pos = 0; + 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 + bool is_nl = false;
+ = Fget_text_property (make_fixnum (pos), + if (i < slen)
+ {
+ Lisp_Object ch = Faref (str, make_fixnum (i));
+ is_nl = (FIXNUMP (ch) && XFIXNUM (ch) == '\n');
+ }
+ if (is_nl || i == slen)
+ {
+ if (i > lstart)
+ {
+ line_starts[nlines] = lstart;
+ line_ends[nlines] = i;
+ nlines++;
+ }
+ lstart = i + 1;
+ }
+ }
+
+ 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); + 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; + /* Find the first line with a DIFFERENT face. */
+ while (line_end < len) + 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 ch
+ = Faref (str, make_fixnum (line_end));
+ if (FIXNUMP (ch) && XFIXNUM (ch) == '\n')
+ break;
+ line_end++;
+ }
+
+ Lisp_Object line + Lisp_Object line
+ = Fsubstring_no_properties (str, + = Fsubstring_no_properties (
+ make_fixnum (line_start), + str,
+ make_fixnum (line_end)); + make_fixnum (line_starts[li]),
+ make_fixnum (line_ends[li]));
+ if (SCHARS (line) > 0) + if (SCHARS (line) > 0)
+ return [NSString stringWithLispString:line]; + 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 * static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns) 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; pos = run_end;
} }
@@ -208,7 +206,7 @@ index 1780194..c7bba5b 100644
unbind_to (count, Qnil); unbind_to (count, Qnil);
*out_runs = runs; *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]; [self postTextChangedNotification:point];
} }
@@ -230,15 +228,34 @@ index 1780194..c7bba5b 100644
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b)); + = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b));
+ if (candidate) + if (candidate)
+ { + {
+ NSDictionary *info = @{ + /* 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,
+ NSAccessibilitySelectedTextChangedNotification,
+ moveInfo);
+
+ candidate = [candidate
+ stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if ([candidate length] > 0)
+ {
+ NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: candidate, + NSAccessibilityAnnouncementKey: candidate,
+ NSAccessibilityPriorityKey: + NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh) + @(NSAccessibilityPriorityHigh)
+ }; + };
+ ns_ax_post_notification_with_info ( + ns_ax_post_notification_with_info (
+ self, + NSApp,
+ NSAccessibilityAnnouncementRequestedNotification, + NSAccessibilityAnnouncementRequestedNotification,
+ info); + annInfo);
+ }
+ } + }
+ } + }
+ +

View File

@@ -1,84 +0,0 @@
From 988b041f1fe0dc730014fdac82e746412a5afc70 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
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

View File

@@ -1,152 +0,0 @@
From 2e5505f044e403ccaef8c43bdc66480c71dcf05a Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
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