patches: address all maintainer review issues

- Issue 1: Add explicit ApplicationServices import for UAZoomEnabled/
  UAZoomChangeFocus (was implicit via Carbon.h, now explicit)
- Issue 2: Rename FOR_EACH_FRAME variable 'frames' -> 'frame' (plural
  was misleading; matches Emacs convention)
- Issue 3: Move unblock_input before ObjC calls in
  postCompletionAnnouncementForBuffer: to avoid holding block_input
  during @synchronized operations
- Issue 4: Fix DEFVAR_BOOL doc and Texinfo: initial value is nil,
  not t; auto-detection sets it at startup
- Issue 5: Replace magic 10000 with NS_AX_MAX_COMPLETION_BUFFER_CHARS
  constant with explanatory comment
- Issue 6: Add comment to lineStartOffsets loop explaining it is gated
  on BUF_CHARS_MODIFF and never runs on the hot path
- Issue 8: Rewrite all 9 commit messages to GNU ChangeLog format with
  '* file (symbol): description' entries
- Issue 9: Break 81-char @interface line in nsterm.h
- Issue 10: Add WINDOWP/BUFFERP guards before dereferencing
  cf->selected_window and cw->contents in ns_zoom_find_child_frame_candidate
- Issue 11: Fix @pxref -> @xref at sentence start in macos.texi
This commit is contained in:
2026-03-01 09:44:47 +01:00
parent e0343db56c
commit 71c81abcae
9 changed files with 275 additions and 279 deletions

View File

@@ -1,58 +1,31 @@
From 6c7d852b4667ec72a190d9a3008a46bbf3a78729 Mon Sep 17 00:00:00 2001
From c383dc0e225d831283db7fdfccc22c12951a1077 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver
Completion frameworks such as Vertico, Ivy, and Icomplete render
candidates via overlay before-string/after-string properties rather
than buffer text. Without this patch, VoiceOver cannot read
overlay-based completion UIs.
candidates via overlay before-string/after-string properties. Without
this change VoiceOver cannot read overlay-based completion UIs.
Identify the selected candidate by scanning overlay strings for a
face whose symbol name contains "current", "selected", or
"selection" --- this matches vertico-current, icomplete-selected-match,
ivy-current-match, company-tooltip-selection, and similar framework
faces without hard-coding any specific name.
Key implementation details:
- The overlay detection branch runs independently (if, not else-if)
of the text-change branch, because Vertico bumps both BUF_MODIFF
(via text property changes in vertico--prompt-selection) and
BUF_OVERLAY_MODIFF (via overlay-put) in the same command cycle.
- Use BUF_CHARS_MODIFF to gate ValueChanged notifications, since
text property changes bump BUF_MODIFF but not BUF_CHARS_MODIFF.
- Remove BUF_OVERLAY_MODIFF from ensureTextCache validity checks
to prevent a race condition where VoiceOver AX queries silently
consume the overlay change before the notification dispatch runs.
- Announce via AnnouncementRequested to NSApp with High priority.
Do not post SelectedTextChanged (that reads the AX text at cursor
position, which is the minibuffer input, not the candidate).
candidate line start. The flag is cleared when the user types
(BUF_CHARS_MODIFF changes) or when no candidate is found
(minibuffer exit, C-g).
(EmacsAccessibilityBuffer): Add cachedCharsModiff.
* src/nsterm.m (ns_ax_face_is_selected): New predicate. Match
"current", "selected", and "selection" in face symbol names.
(ns_ax_selected_overlay_text): New function.
(EmacsAccessibilityBuffer ensureTextCache): Remove overlay_modiff.
(EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:):
Independent overlay branch, BUF_CHARS_MODIFF gating, candidate
* src/nsterm.m (ns_ax_face_is_selected): New static function; matches
'current', 'selected', 'selection' in face symbol names.
(ns_ax_selected_overlay_text): New function; scan overlay strings in
the window for a line with a selected face; return its text.
(EmacsAccessibilityBuffer(Notifications)
postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF
changes independently of text changes. Use BUF_CHARS_MODIFF to gate
ValueChanged; keep overlay_modiff out of ensureTextCache to prevent a
race where an AX query consumes the change before notification.
---
src/nsterm.h | 1 +
src/nsterm.m | 330 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 289 insertions(+), 42 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index 6e830de..2102fb9 100644
index 7adbb92..483fed3 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -509,6 +509,7 @@ typedef struct ns_ax_visible_run
@@ -510,6 +510,7 @@ typedef struct ns_ax_visible_run
@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) ptrdiff_t cachedModiff;
@@ -61,10 +34,10 @@ index 6e830de..2102fb9 100644
@property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
diff --git a/src/nsterm.m b/src/nsterm.m
index 70c7521..a3104d0 100644
index 7d48e6b..20ba0b9 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7254,11 +7254,154 @@ Accessibility virtual elements (macOS / Cocoa only)
@@ -7271,11 +7271,154 @@ Accessibility virtual elements (macOS / Cocoa only)
/* ---- Helper: extract buffer text for accessibility ---- */
@@ -220,7 +193,7 @@ index 70c7521..a3104d0 100644
static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -7329,7 +7472,7 @@ Accessibility virtual elements (macOS / Cocoa only)
@@ -7346,7 +7489,7 @@ Accessibility virtual elements (macOS / Cocoa only)
/* Extract this visible run's text. Use
Fbuffer_substring_no_properties which correctly handles the
@@ -229,7 +202,7 @@ index 70c7521..a3104d0 100644
include garbage bytes when the run spans the gap position. */
Lisp_Object lstr = Fbuffer_substring_no_properties (
make_fixnum (pos), make_fixnum (run_end));
@@ -7410,7 +7553,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font)
@@ -7427,7 +7570,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font)
return NSZeroRect;
/* charpos_start and charpos_len are already in buffer charpos
@@ -238,7 +211,7 @@ index 70c7521..a3104d0 100644
charposForAccessibilityIndex which handles invisible text. */
ptrdiff_t cp_start = charpos_start;
ptrdiff_t cp_end = cp_start + charpos_len;
@@ -7889,6 +8032,7 @@ @implementation EmacsAccessibilityBuffer
@@ -7906,6 +8049,7 @@ @implementation EmacsAccessibilityBuffer
@synthesize cachedOverlayModiff;
@synthesize cachedTextStart;
@synthesize cachedModiff;
@@ -246,7 +219,7 @@ index 70c7521..a3104d0 100644
@synthesize cachedPoint;
@synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement;
@@ -7986,7 +8130,7 @@ - (void)ensureTextCache
@@ -8003,7 +8147,7 @@ - (void)ensureTextCache
NSTRACE ("EmacsAccessibilityBuffer ensureTextCache");
/* This method is only called from the main thread (AX getters
dispatch_sync to main first). Reads of cachedText/cachedTextModiff
@@ -255,7 +228,7 @@ index 70c7521..a3104d0 100644
write section at the end needs synchronization to protect
against concurrent reads from AX server thread. */
eassert ([NSThread isMainThread]);
@@ -7998,25 +8142,16 @@ - (void)ensureTextCache
@@ -8015,25 +8159,16 @@ - (void)ensureTextCache
if (!b)
return;
@@ -289,7 +262,7 @@ index 70c7521..a3104d0 100644
&& cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart
&& (textLen == 0
@@ -8032,7 +8167,7 @@ included in the cached AX text (it is handled separately via
@@ -8049,7 +8184,7 @@ included in the cached AX text (it is handled separately via
{
[cachedText release];
cachedText = [text retain];
@@ -298,7 +271,7 @@ index 70c7521..a3104d0 100644
cachedTextStart = start;
if (visibleRuns)
@@ -8097,7 +8232,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
@@ -8118,7 +8253,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
/* Binary search: runs are sorted by charpos (ascending). Find the
run whose [charpos, charpos+length) range contains the target,
or the nearest run after an invisible gap. O(log n) instead of
@@ -307,7 +280,7 @@ index 70c7521..a3104d0 100644
NSUInteger lo = 0, hi = visibleRunCount;
while (lo < hi)
{
@@ -8146,10 +8281,10 @@ by run length (visible window), not total buffer size. */
@@ -8167,10 +8302,10 @@ by run length (visible window), not total buffer size. */
/* Convert accessibility string index to buffer charpos.
Safe to call from any thread: uses only cachedText (NSString) and
@@ -320,7 +293,7 @@ index 70c7521..a3104d0 100644
@synchronized (self)
{
if (visibleRunCount == 0)
@@ -8191,7 +8326,7 @@ the slow path (composed character sequence walk), which is
@@ -8212,7 +8347,7 @@ the slow path (composed character sequence walk), which is
return cp;
}
}
@@ -329,7 +302,7 @@ index 70c7521..a3104d0 100644
if (lo > 0)
{
ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
@@ -8213,7 +8348,7 @@ the slow path (composed character sequence walk), which is
@@ -8234,7 +8369,7 @@ the slow path (composed character sequence walk), which is
deadlocking the AX server thread. This is prevented by:
1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
@@ -338,7 +311,7 @@ index 70c7521..a3104d0 100644
2. All dispatch_sync blocks run on the main thread where no
concurrent Lisp code can modify state between checks.
3. block_input prevents timer events and process output from
@@ -8567,6 +8702,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
@@ -8588,6 +8723,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
return [self lineForAXIndex:point_idx];
}
@@ -389,7 +362,7 @@ index 70c7521..a3104d0 100644
- (NSRange)accessibilityRangeForLine:(NSInteger)line
{
if (![NSThread isMainThread])
@@ -8789,7 +8968,7 @@ - (NSRect)accessibilityFrame
@@ -8810,7 +8989,7 @@ - (NSRect)accessibilityFrame
/* ===================================================================
@@ -398,7 +371,7 @@ index 70c7521..a3104d0 100644
These methods notify VoiceOver of text and selection changes.
Called from the redisplay cycle (postAccessibilityUpdates).
@@ -8804,7 +8983,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8825,7 +9004,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
if (point > self.cachedPoint
&& point - self.cachedPoint == 1)
{
@@ -407,7 +380,7 @@ index 70c7521..a3104d0 100644
[self invalidateTextCache];
[self ensureTextCache];
if (cachedText)
@@ -8823,7 +9002,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8844,7 +9023,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
/* Update cachedPoint here so the selection-move branch does NOT
fire for point changes caused by edits. WebKit and Chromium
never send both ValueChanged and SelectedTextChanged for the
@@ -416,7 +389,7 @@ index 70c7521..a3104d0 100644
self.cachedPoint = point;
NSDictionary *change = @{
@@ -9156,16 +9335,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
@@ -9178,16 +9357,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */
@@ -504,7 +477,7 @@ index 70c7521..a3104d0 100644
{
ptrdiff_t oldPoint = self.cachedPoint;
BOOL oldMarkActive = self.cachedMarkActive;
@@ -9333,7 +9579,7 @@ - (NSRect)accessibilityFrame
@@ -9355,7 +9601,7 @@ - (NSRect)accessibilityFrame
/* ===================================================================
@@ -513,7 +486,7 @@ index 70c7521..a3104d0 100644
=================================================================== */
/* Scan visible range of window W for interactive spans.
@@ -9541,7 +9787,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@@ -9563,7 +9809,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
dispatch_async (dispatch_get_main_queue (), ^{
/* lwin is a Lisp_Object captured by value. This is GC-safe
because Lisp_Objects are tagged integers/pointers that
@@ -522,7 +495,7 @@ index 70c7521..a3104d0 100644
Emacs. The WINDOW_LIVE_P check below guards against the
window being deleted between capture and execution. */
if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin)))
@@ -9567,7 +9813,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@@ -9589,7 +9835,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@end
@@ -531,7 +504,7 @@ index 70c7521..a3104d0 100644
Methods are kept here (same .m file) so they access the ivars
declared in the @interface ivar block. */
@implementation EmacsAccessibilityBuffer (InteractiveSpans)
@@ -12289,7 +12535,7 @@ - (int) fullscreenState
@@ -12311,7 +12557,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w))
{
@@ -540,7 +513,7 @@ index 70c7521..a3104d0 100644
EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem)
@@ -12323,7 +12569,7 @@ - (int) fullscreenState
@@ -12345,7 +12591,7 @@ - (int) fullscreenState
}
else
{
@@ -549,7 +522,7 @@ index 70c7521..a3104d0 100644
Lisp_Object child = w->contents;
while (!NILP (child))
{
@@ -12435,7 +12681,7 @@ - (void)postAccessibilityUpdates
@@ -12457,7 +12703,7 @@ - (void)postAccessibilityUpdates
accessibilityUpdating = YES;
/* Detect window tree change (split, delete, new buffer). Compare
@@ -558,7 +531,7 @@ index 70c7521..a3104d0 100644
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow))
{
@@ -12444,12 +12690,12 @@ - (void)postAccessibilityUpdates
@@ -12466,12 +12712,12 @@ - (void)postAccessibilityUpdates
}
/* If tree is stale, rebuild FIRST so we don't iterate freed