patches: fix all safe pre-submission issues

- Blocker #1: add BOOL emacsMovedCursor = YES in patch 0005 so series
  compiles standalone; patch 0008 replaces it with !voiceoverSetPoint
- Blocker #3: change all Daneel authorship to Martin Sukany
- Zoom gate: remove ns_accessibility_enabled guards from Zoom code
  paths (0005 no longer adds them; 0008 retains the clarifying comment)
- eassert: remove redundant BUFFER_LIVE_P eassert with contradictory
  comment in patch 0008
- macos.texi: integrate orphan 'Block-style cursors' paragraph as
  @item in the Known Limitations list
- cindex: restore @cindex Zoom, cursor tracking (macOS) removed in 0008
- ChangeLog 0002: list only functions actually added in that patch
- ChangeLog 0008: accurate description (remove wrong BUF_CHARS_MODIFF
  claim for ensureTextCache; ns_ax_buffer_text block_input was in 0001)
- git apply --check: all 9 patches apply cleanly on fresh base

Remaining open issue: BUF_MODIFF regression in patch 0007 (ensureTextCache
O(N) rebuild per font-lock pass) requires design decision before
submission.
This commit is contained in:
2026-03-03 17:50:18 +01:00
parent 3a3fbbac19
commit 70f0cb9a86
9 changed files with 270 additions and 247 deletions

View File

@@ -1,54 +1,50 @@
From 1e3d3919fd41e4480a02190fb89bee1ef8107d62 Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz>
From 137cb30bb546a9599983c25a9873d1518ad8edee Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Mon, 2 Mar 2026 18:49:13 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for
Subject: [PATCH 9/9] ns: announce child frame completion candidates for
VoiceOver
Child frame popups (Corfu, Company-mode) render completion candidates in
a separate frame whose buffer is not accessible via the minibuffer
overlay path. This patch scans child frame buffers for selected
candidates and announces them via VoiceOver.
Child frame popups (Corfu, Company-mode child frames) render completion
candidates in a separate frame whose buffer is not accessible via the
minibuffer overlay path. This patch scans child frame buffers for
selected candidates and announces them via VoiceOver.
* src/nsterm.h (EmacsView): Add childFrameLastBuffer, childFrameLastModiff,
childFrameLastCandidate, childFrameCompletionActive, lastEchoCharsModiff
ivars; remove cachedOverlayModiffForText (unused after BUF_OVERLAY_MODIFF
removed from ensureTextCache to prevent hl-line-mode O(N) rebuilds).
Initialize voiceoverSetPoint, childFrameLastBuffer in initFrameFromEmacs:.
ivars. Initialize childFrameLastBuffer to Qnil in initFrameFromEmacs:.
(EmacsAccessibilityBuffer): Add voiceoverSetPoint ivar.
* src/nsterm.m (ns_ax_buffer_text): Add block_input protection for
Lisp calls; use record_unwind_protect_void to guarantee unblock_input.
(ensureTextCache): Remove BUF_OVERLAY_MODIFF tracking; keep only
BUF_CHARS_MODIFF. BUF_OVERLAY_MODIFF caused O(buffer-size) rebuilds
with hl-line-mode (moves overlay on every post-command-hook).
* src/nsterm.m (ns_ax_selected_child_frame_text): New function; scans
child frame buffer text for the selected completion candidate.
(announceChildFrameCompletion): New method; scans child frame buffers
for selected completion candidates. Store childFrameLastBuffer as
BVAR(b, name) (buffer name symbol, GC-reachable via obarray) rather
than make_lisp_ptr to avoid dangling pointer after buffer kill.
than a raw buffer pointer to avoid a dangling pointer after buffer kill.
(postEchoAreaAnnouncementIfNeeded): New method; announces echo area
changes for commands like C-g.
changes (e.g., "Wrote file", "Quit") for commands that produce output
while the minibuffer is inactive.
(postAccessibilityNotificationsForFrame:): Drive child frame and echo
area announcements.
* doc/emacs/macos.texi: Fix dangling semicolon in GNUstep paragraph.
area announcements. Add voiceoverSetPoint flag and singleLineMove
adjacency detection to distinguish VoiceOver-initiated cursor moves
from Emacs-initiated moves; sequential adjacent-line moves use
next/previous direction, teleports use discontiguous. Add didTextChange
guard to suppress overlay completion announcements while the user types.
(setAccessibilitySelectedTextRange:): Set voiceoverSetPoint so that the
subsequent notification cycle uses sequential direction.
* doc/emacs/macos.texi (VoiceOver Accessibility): Update to document
echo area announcements and VoiceOver rotor cursor synchronization.
Remove Zoom section (covered by patch 0000). Fix dangling paragraph.
---
doc/emacs/macos.texi | 14 +-
etc/NEWS | 25 ++-
doc/emacs/macos.texi | 13 +-
etc/NEWS | 25 +-
src/nsterm.h | 21 ++
src/nsterm.m | 514 +++++++++++++++++++++++++++++++++++++++----
4 files changed, 510 insertions(+), 64 deletions(-)
src/nsterm.m | 561 +++++++++++++++++++++++++++++++++++++------
4 files changed, 529 insertions(+), 91 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 8d4a7825d8..03a657f970 100644
index 72ac3a9aa9..cf5ed0ff28 100644
--- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi
@@ -278,7 +278,6 @@ restart Emacs to access newly-available services.
@cindex VoiceOver
@cindex accessibility (macOS)
@cindex screen reader (macOS)
-@cindex Zoom, cursor tracking (macOS)
When built with the Cocoa interface on macOS, Emacs exposes buffer
content, cursor position, mode lines, and interactive elements to the
@@ -309,10 +308,15 @@ Shift-modified movement announces selected or deselected text.
@@ -309,10 +309,15 @@ Shift-modified movement announces selected or deselected text.
The @file{*Completions*} buffer announces each completion candidate
as you navigate, even while keyboard focus remains in the minibuffer.
@@ -115,7 +111,7 @@ index 7f917f93b2..bbec21b635 100644
interface and eliminate the associated overhead.
diff --git a/src/nsterm.h b/src/nsterm.h
index 21a93bc799..b5c9f84499 100644
index a210ceba14..2edd7cd6e0 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -504,9 +504,20 @@ typedef struct ns_ax_visible_run
@@ -139,7 +135,7 @@ index 21a93bc799..b5c9f84499 100644
@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) ptrdiff_t cachedModiff;
@@ -596,6 +607,14 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
@@ -596,6 +607,14 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid;
BOOL accessibilityUpdating;
@@ -154,7 +150,7 @@ index 21a93bc799..b5c9f84499 100644
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
@@ -665,6 +684,8 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
@@ -665,6 +684,8 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates;
@@ -164,42 +160,22 @@ index 21a93bc799..b5c9f84499 100644
@end
diff --git a/src/nsterm.m b/src/nsterm.m
index 8ef344d9fe..1acb64630a 100644
index 54cee74401..6ba2229639 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1275,7 +1275,13 @@ If a completion candidate is selected (overlay or child frame),
@@ -1275,6 +1275,12 @@ If a completion candidate is selected (overlay or child frame),
static void
ns_zoom_track_completion (struct frame *f, EmacsView *view)
{
- if (!ns_accessibility_enabled || !ns_zoom_enabled_p ())
+ /* Zoom cursor tracking is controlled exclusively by
+ ns_zoom_enabled_p (). We do NOT gate on ns_accessibility_enabled:
+ users can run Zoom without VoiceOver, and those users should still
+ get completion-candidate tracking. ns_accessibility_enabled is
+ only set when a screen reader (VoiceOver or similar) activates the
+ AX layer; it has no bearing on the Zoom feature. */
+ if (!ns_zoom_enabled_p ())
if (!ns_zoom_enabled_p ())
return;
if (!WINDOWP (f->selected_window))
return;
@@ -1393,7 +1399,7 @@ so the visual offset is (ov_line + 1) * line_h from
(zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && view && !view->zoomCursorUpdated
+ if (view && !view->zoomCursorUpdated
&& ns_zoom_enabled_p ()
&& !NSIsEmptyRect (view->lastCursorRect))
{
@@ -3571,7 +3577,7 @@ EmacsView pixels (AppKit, flipped, top-left origin)
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && ns_zoom_enabled_p ())
+ if (ns_zoom_enabled_p ())
{
NSRect windowRect = [view convertRect:r toView:nil];
NSRect screenRect
@@ -7407,6 +7413,117 @@ visual line index for Zoom (skip whitespace-only lines
return nil;
@@ -318,6 +294,21 @@ index 8ef344d9fe..1acb64630a 100644
/* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -7440,9 +7557,13 @@ visual line index for Zoom (skip whitespace-only lines
return @"";
specpdl_ref count = SPECPDL_INDEX ();
+ /* block_input must precede record_unwind_protect_void (unblock_input):
+ if anything between SPECPDL_INDEX and block_input were to throw,
+ the unwind handler would call unblock_input without a matching
+ block_input, corrupting the input-blocking reference count. */
+ block_input ();
record_unwind_current_buffer ();
record_unwind_protect_void (unblock_input);
- block_input ();
if (b != current_buffer)
set_buffer_internal_1 (b);
@@ -8605,6 +8726,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
[self ensureTextCache];
@@ -392,7 +383,7 @@ index 8ef344d9fe..1acb64630a 100644
+ - C-n/C-p: SelectedTextChanged carries granularity=line, but
+ VoiceOver processes those keystrokes specially and may not
+ produce speech; the explicit announcement is the reliable path.
+ - Discontiguous jumps (]], M-<, xref, imenu, ...): granularity=line
+ - Discontiguous jumps (]], M-<, xref, imenu, ): granularity=line
+ in the notification is omitted (see above) so VoiceOver will
+ not announce automatically; this explicit announcement fills
+ the gap.
@@ -422,7 +413,7 @@ index 8ef344d9fe..1acb64630a 100644
+ postEchoAreaAnnouncementIfNeeded (called from postAccessibilityUpdates
+ before this per-element loop) so that they are never lost to a
+ concurrent tree rebuild. For the inactive minibuffer (minibuf_level
+ == 0), skip normal cursor and completion processing --- there is no
+ == 0), skip normal cursor and completion processing there is no
+ meaningful cursor to track. */
+ if (MINI_WINDOW_P (w) && minibuf_level == 0)
+ return;
@@ -452,12 +443,12 @@ index 8ef344d9fe..1acb64630a 100644
}
}
@@ -9453,8 +9625,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
@@ -9453,37 +9625,44 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
displayed in the minibuffer. In normal editing buffers,
font-lock and other modes change BUF_OVERLAY_MODIFF on
every redisplay, triggering O(overlays) work per keystroke.
- Restrict the scan to minibuffer windows. */
- if (!MINI_WINDOW_P (w))
- if (MINI_WINDOW_P (w))
+ Restrict the scan to minibuffer windows.
+ Skip overlay announcements when the user just typed a character
+ (didTextChange). Completion frameworks update their overlay
@@ -467,10 +458,66 @@ index 8ef344d9fe..1acb64630a 100644
+ characters inaudible. VoiceOver should read the overlay
+ candidate only when the user navigates (C-n/C-p), not types. */
+ if (!MINI_WINDOW_P (w) || didTextChange)
goto skip_overlay_scan;
int selected_line = -1;
@@ -9500,7 +9679,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
+ goto skip_overlay_scan;
+
+ int selected_line = -1;
+ NSString *candidate
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line);
+ if (candidate)
{
- int selected_line = -1;
- NSString *candidate
- = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
- &selected_line);
- if (candidate)
+ /* Deduplicate: only announce when the candidate changed. */
+ if (![candidate isEqualToString:
+ self.cachedCompletionAnnouncement])
{
- /* Deduplicate: only announce when the candidate changed. */
- if (![candidate isEqualToString:
- self.cachedCompletionAnnouncement])
- {
- self.cachedCompletionAnnouncement = candidate;
-
- /* Announce the candidate text directly via NSApp.
- Do NOT post SelectedTextChanged --- that would cause
- VoiceOver to read the AX text at the cursor position
- (the minibuffer input line), not the overlay candidate.
- AnnouncementRequested with High priority interrupts
- any current speech and announces our text. */
- NSDictionary *annInfo = @{
- NSAccessibilityAnnouncementKey: candidate,
- NSAccessibilityPriorityKey:
- @(NSAccessibilityPriorityHigh)
- };
- ns_ax_post_notification_with_info (
- NSApp,
- NSAccessibilityAnnouncementRequestedNotification,
- annInfo);
- }
+ self.cachedCompletionAnnouncement = candidate;
+
+ /* Announce the candidate text directly via NSApp.
+ Do NOT post SelectedTextChanged --- that would cause
+ VoiceOver to read the AX text at the cursor position
+ (the minibuffer input line), not the overlay candidate.
+ AnnouncementRequested with High priority interrupts
+ any current speech and announces our text. */
+ NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: candidate,
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ annInfo);
}
}
}
@@ -9497,7 +9676,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
self.cachedPoint = point;
self.cachedMarkActive = markActive;
@@ -490,7 +537,7 @@ index 8ef344d9fe..1acb64630a 100644
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
if (point > oldPoint)
direction = ns_ax_text_selection_direction_next;
@@ -9512,6 +9702,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
@@ -9509,6 +9699,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
/* --- Granularity detection --- */
NSInteger granularity = ns_ax_text_selection_granularity_unknown;
@@ -498,7 +545,7 @@ index 8ef344d9fe..1acb64630a 100644
[self ensureTextCache];
if (cachedText && oldPoint > 0)
{
@@ -9526,7 +9717,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
@@ -9523,7 +9714,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
NSRange newLine = [cachedText lineRangeForRange:
NSMakeRange (newIdx, 0)];
if (oldLine.location != newLine.location)
@@ -518,12 +565,16 @@ index 8ef344d9fe..1acb64630a 100644
else
{
NSUInteger dist = (newIdx > oldIdx
@@ -9548,34 +9750,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
@@ -9545,38 +9747,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
granularity = ns_ax_text_selection_granularity_line;
}
- /* Treat all moves as Emacs-initiated until voiceoverSetPoint
- tracking is introduced (subsequent patch). */
- BOOL emacsMovedCursor = YES;
-
- /* Programmatic jumps that cross a line boundary (]], [[, M-<,
- xref, imenu, ...) are discontiguous: the cursor teleported to an
- xref, imenu, ) are discontiguous: the cursor teleported to an
- arbitrary position, not one sequential step forward/backward.
- Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver
- to re-anchor its rotor browse cursor at the new
@@ -566,7 +617,7 @@ index 8ef344d9fe..1acb64630a 100644
{
NSWindow *win = [self.emacsView window];
if (win)
@@ -9734,6 +9925,13 @@ - (NSRect)accessibilityFrame
@@ -9735,6 +9922,13 @@ - (NSRect)accessibilityFrame
if (vis_start >= vis_end)
return @[];
@@ -580,7 +631,7 @@ index 8ef344d9fe..1acb64630a 100644
block_input ();
specpdl_ref blk_count = SPECPDL_INDEX ();
record_unwind_protect_void (unblock_input);
@@ -10040,6 +10238,10 @@ - (void)dealloc
@@ -10042,6 +10236,10 @@ - (void)dealloc
#endif
[accessibilityElements release];
@@ -591,7 +642,7 @@ index 8ef344d9fe..1acb64630a 100644
[[self menu] release];
[super dealloc];
}
@@ -11489,6 +11691,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
@@ -11491,6 +11689,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO;
processingCompose = NO;
@@ -601,7 +652,7 @@ index 8ef344d9fe..1acb64630a 100644
scrollbarsNeedingUpdate = 0;
fs_state = FULLSCREEN_NONE;
fs_before_fs = next_maximized = -1;
@@ -12797,6 +13002,161 @@ - (id)accessibilityFocusedUIElement
@@ -12799,6 +13000,156 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */
@@ -702,11 +753,6 @@ index 8ef344d9fe..1acb64630a 100644
+ childFrameLastBuffer = BVAR (b, name);
+ childFrameLastModiff = modiff;
+
+ /* BUFFER_LIVE_P(b) is already checked at entry (line above the
+ EQ comparison). No code between that check and here can kill
+ the buffer, so this second check is redundant. */
+ eassert (BUFFER_LIVE_P (b));
+
+ /* Skip buffers larger than a typical completion popup.
+ This avoids scanning eldoc, which-key, or other child
+ frame buffers that are not completion UIs. */
@@ -763,7 +809,7 @@ index 8ef344d9fe..1acb64630a 100644
- (void)postAccessibilityUpdates
{
NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12807,11 +13167,69 @@ - (void)postAccessibilityUpdates
@@ -12809,11 +13160,69 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us