patches: full maintainer review pass — all blocking issues fixed

This commit is contained in:
2026-03-01 21:10:35 +01:00
parent 1dcc7f8352
commit 57086b88ef

View File

@@ -1,4 +1,4 @@
From e8bd8ac29d75cb665d18d8730963868caa1b0454 Mon Sep 17 00:00:00 2001 From 10d8d56ed0364c9bc387600431a66dabe37b3e2a Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100 Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for Subject: [PATCH 8/8] ns: announce child frame completion candidates for
@@ -17,14 +17,14 @@ FRAME_PARENT_FRAME; call announceChildFrameCompletion. Post
NSAccessibilityFocusedUIElementChangedNotification on the parent buffer NSAccessibilityFocusedUIElementChangedNotification on the parent buffer
element when a child frame completion closes. element when a child frame completion closes.
--- ---
doc/emacs/macos.texi | 14 +- doc/emacs/macos.texi | 18 +-
etc/NEWS | 16 +- etc/NEWS | 18 +-
src/nsterm.h | 13 ++ src/nsterm.h | 13 ++
src/nsterm.m | 397 ++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 468 +++++++++++++++++++++++++++++++++++++++----
4 files changed, 407 insertions(+), 33 deletions(-) 4 files changed, 466 insertions(+), 51 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 6514dfc..95a8d15 100644 index 6514dfc..bcf74b3 100644
--- a/doc/emacs/macos.texi --- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi +++ b/doc/emacs/macos.texi
@@ -278,7 +278,6 @@ restart Emacs to access newly-available services. @@ -278,7 +278,6 @@ restart Emacs to access newly-available services.
@@ -55,11 +55,27 @@ index 6514dfc..95a8d15 100644
@vindex ns-accessibility-enabled @vindex ns-accessibility-enabled
To disable the accessibility interface entirely (for instance, to To disable the accessibility interface entirely (for instance, to
@@ -341,8 +345,8 @@ but @code{accessibilityRangeForPosition} hit-testing assumes
left-to-right glyph layout.
@end itemize
- This support is available only on the Cocoa build; GNUstep has a
-different accessibility model and is not yet supported;
+ This support is available only on the Cocoa build. GNUstep has a
+different accessibility model and is not yet supported.
@xref{GNUstep Support}. Evil-mode block cursors are handled
correctly: character navigation announces the character at the cursor
position, not the character before it.
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index 2b1f9e6..d276aa6 100644 index 2b1f9e6..5766428 100644
--- a/etc/NEWS --- a/etc/NEWS
+++ b/etc/NEWS +++ b/etc/NEWS
@@ -4404,12 +4404,16 @@ send user data to Apple's speech recognition servers. @@ -4400,16 +4400,20 @@ allowing Emacs users access to speech recognition utilities.
Note: Accepting this permission allows the use of system APIs, which may
send user data to Apple's speech recognition servers.
----
++++
** VoiceOver accessibility support on macOS. ** VoiceOver accessibility support on macOS.
Emacs now exposes buffer content, cursor position, and interactive Emacs now exposes buffer content, cursor position, and interactive
elements to the macOS accessibility subsystem (VoiceOver). This elements to the macOS accessibility subsystem (VoiceOver). This
@@ -121,10 +137,58 @@ index 21a93bc..8f2143b 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 8d44b5f..48b6aa0 100644 index 8d44b5f..34d7ee2 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7415,6 +7415,112 @@ visual line index for Zoom (skip whitespace-only lines @@ -1126,24 +1126,19 @@ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */
ivy-current-match, etc. by checking the face symbol name.
Defined here so the Zoom patch compiles independently of the
VoiceOver patches. */
+/* Forward declaration --- ns_ax_face_is_selected is defined in the
+ VoiceOver section below; ns_zoom_face_is_selected delegates to it. */
+static bool ns_ax_face_is_selected (Lisp_Object face);
+
static bool
ns_zoom_face_is_selected (Lisp_Object face)
{
- if (SYMBOLP (face))
- {
- const char *name = SSDATA (SYMBOL_NAME (face));
- return (strstr (name, "current") != NULL
- || strstr (name, "selected") != NULL
- || strstr (name, "selection") != NULL);
- }
- if (CONSP (face))
- {
- Lisp_Object tail;
- for (tail = face; CONSP (tail); tail = XCDR (tail))
- if (ns_zoom_face_is_selected (XCAR (tail)))
- return true;
- }
- return false;
+ /* Forward to ns_ax_face_is_selected (defined in the VoiceOver section
+ below) so that Zoom and VoiceOver agree on what constitutes a
+ "selected" face. Identical logic in two places would diverge over
+ time; one canonical implementation is preferable.
+ The forward declaration appears in nsterm.h. */
+ return ns_ax_face_is_selected (face);
}
/* Scan overlay before-string / after-string properties in the
@@ -1356,6 +1351,12 @@ so the visual offset is (ov_line + 1) * line_h from
UAZoomChangeFocus (&cgRect, &cgRect,
kUAZoomFocusTypeInsertionPoint);
}
+ /* Unbind record_unwind_current_buffer for both overlay and child
+ frame paths. The overlay path calls unbind_to before return;
+ this call covers the child frame path and the no-candidate path.
+ unbind_to is idempotent if count equals the current specpdl
+ index (i.e. already unbound by the overlay path). */
+ unbind_to (count, Qnil);
}
#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
@@ -7415,6 +7416,112 @@ visual line index for Zoom (skip whitespace-only lines
return nil; return nil;
} }
@@ -237,7 +301,7 @@ index 8d44b5f..48b6aa0 100644
/* Build accessibility text for window W, skipping invisible text. /* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos. Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -8046,6 +8152,7 @@ that remapped bindings (e.g., C-j -> next-line) are recognized. @@ -8046,6 +8153,7 @@ that remapped bindings (e.g., C-j -> next-line) are recognized.
@implementation EmacsAccessibilityBuffer @implementation EmacsAccessibilityBuffer
@synthesize cachedText; @synthesize cachedText;
@synthesize cachedTextModiff; @synthesize cachedTextModiff;
@@ -245,7 +309,7 @@ index 8d44b5f..48b6aa0 100644
@synthesize cachedOverlayModiff; @synthesize cachedOverlayModiff;
@synthesize cachedTextStart; @synthesize cachedTextStart;
@synthesize cachedModiff; @synthesize cachedModiff;
@@ -8159,16 +8266,34 @@ - (void)ensureTextCache @@ -8159,16 +8267,34 @@ - (void)ensureTextCache
if (!b) if (!b)
return; return;
@@ -288,7 +352,7 @@ index 8d44b5f..48b6aa0 100644
&& cachedTextStart == BUF_BEGV (b) && cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart && pt >= cachedTextStart
&& (textLen == 0 && (textLen == 0
@@ -8184,7 +8309,8 @@ included in the cached AX text (it is handled separately via @@ -8184,7 +8310,8 @@ included in the cached AX text (it is handled separately via
{ {
[cachedText release]; [cachedText release];
cachedText = [text retain]; cachedText = [text retain];
@@ -298,7 +362,7 @@ index 8d44b5f..48b6aa0 100644
cachedTextStart = start; cachedTextStart = start;
if (visibleRuns) if (visibleRuns)
@@ -9060,11 +9186,13 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point @@ -9060,11 +9187,13 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
= @(ns_ax_text_state_change_selection_move); = @(ns_ax_text_state_change_selection_move);
moveInfo[@"AXTextSelectionDirection"] = @(direction); moveInfo[@"AXTextSelectionDirection"] = @(direction);
moveInfo[@"AXTextChangeElement"] = self; moveInfo[@"AXTextChangeElement"] = self;
@@ -317,7 +381,7 @@ index 8d44b5f..48b6aa0 100644
moveInfo[@"AXTextSelectionGranularity"] = @(granularity); moveInfo[@"AXTextSelectionGranularity"] = @(granularity);
ns_ax_post_notification_with_info ( ns_ax_post_notification_with_info (
@@ -9107,12 +9235,17 @@ derive its own speech (it would read the wrong character @@ -9107,12 +9236,17 @@ derive its own speech (it would read the wrong character
} }
} }
@@ -340,15 +404,22 @@ index 8d44b5f..48b6aa0 100644
if (cachedText if (cachedText
&& granularity == ns_ax_text_selection_granularity_line) && granularity == ns_ax_text_selection_granularity_line)
{ {
@@ -9175,6 +9308,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b @@ -9175,7 +9309,14 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
ptrdiff_t currentOverlayStart = 0; ptrdiff_t currentOverlayStart = 0;
ptrdiff_t currentOverlayEnd = 0; ptrdiff_t currentOverlayEnd = 0;
+ block_input (); + block_input ();
specpdl_ref count2 = SPECPDL_INDEX (); specpdl_ref count2 = SPECPDL_INDEX ();
+ /* Register unblock_input as an unwind action so that if any Lisp
+ call below signals (triggering a longjmp through unbind_to),
+ block_input is always paired with an unblock_input. The explicit
+ unblock_input() at the end of the function is still needed for
+ the normal (non-signal) path. */
+ record_unwind_protect_void (unblock_input);
record_unwind_current_buffer (); record_unwind_current_buffer ();
if (b != current_buffer) if (b != current_buffer)
@@ -9352,12 +9486,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f set_buffer_internal_1 (b);
@@ -9352,12 +9493,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
if (!b) if (!b)
return; return;
@@ -378,7 +449,7 @@ index 8d44b5f..48b6aa0 100644
if (modiff != self.cachedModiff) if (modiff != self.cachedModiff)
{ {
self.cachedModiff = modiff; self.cachedModiff = modiff;
@@ -9371,6 +9522,7 @@ Text property changes (e.g. face updates from @@ -9371,6 +9529,7 @@ Text property changes (e.g. face updates from
{ {
self.cachedCharsModiff = chars_modiff; self.cachedCharsModiff = chars_modiff;
[self postTextChangedNotification:point]; [self postTextChangedNotification:point];
@@ -386,7 +457,7 @@ index 8d44b5f..48b6aa0 100644
} }
} }
@@ -9393,8 +9545,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9393,8 +9552,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
displayed in the minibuffer. In normal editing buffers, displayed in the minibuffer. In normal editing buffers,
font-lock and other modes change BUF_OVERLAY_MODIFF on font-lock and other modes change BUF_OVERLAY_MODIFF on
every redisplay, triggering O(overlays) work per keystroke. every redisplay, triggering O(overlays) work per keystroke.
@@ -404,7 +475,7 @@ index 8d44b5f..48b6aa0 100644
goto skip_overlay_scan; goto skip_overlay_scan;
int selected_line = -1; int selected_line = -1;
@@ -9488,6 +9647,16 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9488,6 +9654,16 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
granularity = ns_ax_text_selection_granularity_line; granularity = ns_ax_text_selection_granularity_line;
} }
@@ -421,7 +492,33 @@ index 8d44b5f..48b6aa0 100644
/* Post notifications for focused and non-focused elements. */ /* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused]) if ([self isAccessibilityFocused])
[self postFocusedCursorNotification:point [self postFocusedCursorNotification:point
@@ -9931,6 +10100,10 @@ - (void)dealloc @@ -9630,6 +9806,17 @@ - (NSRect)accessibilityFrame
if (vis_start >= vis_end)
return @[];
+ /* block_input for the duration of the scan: the Lisp calls below
+ (Ftext_properties_at, Fplist_get, Foverlays_in, Foverlay_get,
+ Fnext_single_property_change, Fbuffer_substring_no_properties)
+ must not be interleaved with timer events or process sentinels
+ that could modify buffer state (e.g. invalidate vis_end).
+ record_unwind_protect_void guarantees unblock_input even if
+ a Lisp call signals. */
+ block_input ();
+ specpdl_ref blk_count = SPECPDL_INDEX ();
+ record_unwind_protect_void (unblock_input);
+
/* Symbols are interned once at startup via DEFSYM in syms_of_nsterm;
reference them directly here (GC-safe, no repeated obarray lookup). */
@@ -9750,6 +9937,7 @@ than O(chars). Fall back to pos+1 as safety net. */
pos = span_end;
}
+ unbind_to (blk_count, Qnil);
return [[spans copy] autorelease];
}
@@ -9931,6 +10119,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
@@ -432,7 +529,7 @@ index 8d44b5f..48b6aa0 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11380,6 +11553,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -11380,6 +11572,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;
@@ -442,7 +539,7 @@ index 8d44b5f..48b6aa0 100644
scrollbarsNeedingUpdate = 0; scrollbarsNeedingUpdate = 0;
fs_state = FULLSCREEN_NONE; fs_state = FULLSCREEN_NONE;
fs_before_fs = next_maximized = -1; fs_before_fs = next_maximized = -1;
@@ -12688,6 +12864,130 @@ - (id)accessibilityFocusedUIElement @@ -12688,6 +12883,152 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */ elements with current values, making change detection impossible. */
@@ -477,10 +574,15 @@ index 8d44b5f..48b6aa0 100644
+ if (echo_chars == lastEchoCharsModiff || BUF_ZV (eb) <= BUF_BEGV (eb)) + if (echo_chars == lastEchoCharsModiff || BUF_ZV (eb) <= BUF_BEGV (eb))
+ return; + return;
+ lastEchoCharsModiff = echo_chars; + lastEchoCharsModiff = echo_chars;
+ struct buffer *prev = current_buffer; + /* Use specpdl to restore current_buffer if Fbuffer_string signals.
+ set_buffer_internal (eb); + set_buffer_internal_1 is preferred over set_buffer_internal in
+ a redisplay context: it skips point-motion hooks that could
+ trigger further redisplay or modify buffer state unexpectedly. */
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ set_buffer_internal_1 (eb);
+ Lisp_Object ls = Fbuffer_string (); + Lisp_Object ls = Fbuffer_string ();
+ set_buffer_internal (prev); + unbind_to (count, Qnil);
+ /* stringWithLispString: converts Emacs's internal multibyte encoding + /* stringWithLispString: converts Emacs's internal multibyte encoding
+ to NSString correctly; a raw SSDATA cast would produce invalid + to NSString correctly; a raw SSDATA cast would produce invalid
+ UTF-8 for non-ASCII characters. */ + UTF-8 for non-ASCII characters. */
@@ -520,12 +622,20 @@ index 8d44b5f..48b6aa0 100644
+ EMACS_INT modiff = BUF_MODIFF (b); + EMACS_INT modiff = BUF_MODIFF (b);
+ if (!BUFFER_LIVE_P (b)) + if (!BUFFER_LIVE_P (b))
+ return; + return;
+ if (EQ (childFrameLastBuffer, make_lisp_ptr (b, Lisp_Vectorlike)) + /* Compare buffer identity using the raw pointer, not a Lisp_Object.
+ A killed buffer can be GC'd even if we hold a Lisp_Object for it
+ (EmacsView is not GC-visible). Storing and comparing struct buffer *
+ is safe because we only test identity (not dereference) here, and
+ we guard all actual buffer field reads with BUFFER_LIVE_P below. */
+ if ((struct buffer *) XLP (childFrameLastBuffer) == b
+ && modiff == childFrameLastModiff) + && modiff == childFrameLastModiff)
+ return; + return;
+ childFrameLastBuffer = make_lisp_ptr (b, Lisp_Vectorlike); + childFrameLastBuffer = make_lisp_ptr (b, Lisp_Vectorlike);
+ childFrameLastModiff = modiff; + childFrameLastModiff = modiff;
+ +
+ if (!BUFFER_LIVE_P (b))
+ return;
+
+ /* Skip buffers larger than a typical completion popup. + /* Skip buffers larger than a typical completion popup.
+ This avoids scanning eldoc, which-key, or other child + This avoids scanning eldoc, which-key, or other child
+ frame buffers that are not completion UIs. */ + frame buffers that are not completion UIs. */
@@ -533,8 +643,17 @@ index 8d44b5f..48b6aa0 100644
+ return; + return;
+ +
+ int selected_line = -1; + int selected_line = -1;
+ /* block_input prevents timer events and process output from
+ interleaving with the Lisp calls inside
+ ns_ax_selected_child_frame_text (Fbuffer_substring_no_properties,
+ Fget_char_property, etc.). record_unwind_protect_void ensures
+ unblock_input is called even if a Lisp call signals. */
+ block_input ();
+ specpdl_ref blk_count = SPECPDL_INDEX ();
+ record_unwind_protect_void (unblock_input);
+ NSString *candidate + NSString *candidate
+ = ns_ax_selected_child_frame_text (b, w->contents, &selected_line); + = ns_ax_selected_child_frame_text (b, w->contents, &selected_line);
+ unbind_to (blk_count, Qnil);
+ +
+ if (!candidate) + if (!candidate)
+ return; + return;
@@ -573,7 +692,7 @@ index 8d44b5f..48b6aa0 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12698,11 +12998,64 @@ - (void)postAccessibilityUpdates @@ -12698,11 +13039,64 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting /* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us can trigger redisplay, which calls ns_update_end, which calls us