patches: fix minibuffer — typing priority, echo area, C-g

This commit is contained in:
2026-03-01 21:05:10 +01:00
parent 371d90bd4f
commit 1dcc7f8352

View File

@@ -1,4 +1,4 @@
From 240373a4a61b387dcbd1bf422ffcf4309db03039 Mon Sep 17 00:00:00 2001 From e8bd8ac29d75cb665d18d8730963868caa1b0454 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
@@ -19,9 +19,9 @@ element when a child frame completion closes.
--- ---
doc/emacs/macos.texi | 14 +- doc/emacs/macos.texi | 14 +-
etc/NEWS | 16 +- etc/NEWS | 16 +-
src/nsterm.h | 9 ++ src/nsterm.h | 13 ++
src/nsterm.m | 373 ++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 397 ++++++++++++++++++++++++++++++++++++++++---
4 files changed, 381 insertions(+), 31 deletions(-) 4 files changed, 407 insertions(+), 33 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..95a8d15 100644
@@ -83,7 +83,7 @@ index 2b1f9e6..d276aa6 100644
interface and eliminate the associated overhead. interface and eliminate the associated overhead.
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 21a93bc..bbce9fe 100644 index 21a93bc..8f2143b 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -507,6 +507,10 @@ typedef struct ns_ax_visible_run @@ -507,6 +507,10 @@ typedef struct ns_ax_visible_run
@@ -97,7 +97,7 @@ index 21a93bc..bbce9fe 100644
@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; @property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart; @property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) ptrdiff_t cachedModiff; @property (nonatomic, assign) ptrdiff_t cachedModiff;
@@ -596,6 +600,10 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -596,6 +600,14 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
Lisp_Object lastRootWindow; Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid; BOOL accessibilityTreeValid;
BOOL accessibilityUpdating; BOOL accessibilityUpdating;
@@ -105,10 +105,14 @@ index 21a93bc..bbce9fe 100644
+ char *childFrameLastCandidate; + char *childFrameLastCandidate;
+ Lisp_Object childFrameLastBuffer; + Lisp_Object childFrameLastBuffer;
+ EMACS_INT childFrameLastModiff; + EMACS_INT childFrameLastModiff;
+ /* Last BUF_CHARS_MODIFF seen for echo_area_buffer[0]. Used by
+ postEchoAreaAnnouncementIfNeeded to detect new echo area messages
+ independently of the per-element notification cycle. */
+ ptrdiff_t lastEchoCharsModiff;
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -665,6 +673,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) @@ -665,6 +677,7 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree; - (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree; - (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates; - (void)postAccessibilityUpdates;
@@ -117,7 +121,7 @@ index 21a93bc..bbce9fe 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..e39a012 100644 index 8d44b5f..48b6aa0 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 @@ -7415,6 +7415,112 @@ visual line index for Zoom (skip whitespace-only lines
@@ -344,74 +348,63 @@ index 8d44b5f..e39a012 100644
specpdl_ref count2 = SPECPDL_INDEX (); specpdl_ref count2 = SPECPDL_INDEX ();
record_unwind_current_buffer (); record_unwind_current_buffer ();
if (b != current_buffer) if (b != current_buffer)
@@ -9352,6 +9486,66 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9352,12 +9486,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
if (!b) if (!b)
return; return;
+ /* --- Echo area announcements --- + /* Echo area announcements are handled in
+ When the minibuffer is not active for user input (minibuf_level == 0) + postEchoAreaAnnouncementIfNeeded (called from postAccessibilityUpdates
+ and an echo area message arrives, announce it to VoiceOver. This + before this per-element loop) so that they are never lost to a
+ surfaces messages such as "Git finished", "Wrote file", and error + concurrent tree rebuild. For the inactive minibuffer (minibuf_level
+ strings that appear in the echo area during background operations. + == 0), skip normal cursor and completion processing — there is no
+ + meaningful cursor to track. */
+ IMPORTANT: Emacs displays echo area messages by calling
+ with_echo_area_buffer(), which sets current_buffer via
+ set_buffer_internal_1() but does NOT call Fset_window_buffer().
+ As a result, w->contents (and thus the variable `b' above) still
+ points to the inactive minibuffer buffer " *Minibuf-0*", NOT to
+ the echo area buffer. We must read echo_area_buffer[0] directly.
+
+ Priority High is appropriate: echo area status messages are
+ time-sensitive and may arrive while VoiceOver is reading other
+ text. While minibuf_level > 0 the user is composing a command;
+ fall through to normal cursor/completion tracking in that case. */
+ if (MINI_WINDOW_P (w) && minibuf_level == 0) + if (MINI_WINDOW_P (w) && minibuf_level == 0)
+ { + return;
+ Lisp_Object ea = echo_area_buffer[0];
+ if (BUFFERP (ea))
+ {
+ struct buffer *eb = XBUFFER (ea);
+ ptrdiff_t echo_chars = BUF_CHARS_MODIFF (eb);
+ /* cachedCharsModiff doubles as echo-area change detector.
+ For an inactive minibuffer element this ivar is only written
+ here, so comparing the echo buffer's chars_modiff against it
+ reliably detects new messages. Both are ptrdiff_t counters
+ that increase monotonically; a false negative is impossible. */
+ if (echo_chars != self.cachedCharsModiff
+ && BUF_ZV (eb) > BUF_BEGV (eb))
+ {
+ self.cachedCharsModiff = echo_chars;
+ struct buffer *prev = current_buffer;
+ set_buffer_internal (eb);
+ Lisp_Object ls = Fbuffer_string ();
+ set_buffer_internal (prev);
+ /* stringWithLispString: converts Emacs's internal multibyte
+ encoding to NSString correctly; SSDATA alone would produce
+ invalid UTF-8 for non-ASCII characters. */
+ NSString *raw = [NSString stringWithLispString: ls];
+ NSString *msg = [raw stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if ([msg length] > 0)
+ {
+ NSDictionary *info = @{
+ NSAccessibilityAnnouncementKey: msg,
+ NSAccessibilityPriorityKey:
+ @(NSAccessibilityPriorityHigh)
+ };
+ ns_ax_post_notification_with_info (
+ NSApp,
+ NSAccessibilityAnnouncementRequestedNotification,
+ info);
+ }
+ }
+ }
+ return;
+ }
+ +
ptrdiff_t modiff = BUF_MODIFF (b); ptrdiff_t modiff = BUF_MODIFF (b);
ptrdiff_t point = BUF_PT (b); ptrdiff_t point = BUF_PT (b);
BOOL markActive = !NILP (BVAR (b, mark_active)); BOOL markActive = !NILP (BVAR (b, mark_active));
@@ -9488,6 +9682,16 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
/* --- Text changed (edit) --- */
ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b);
+ /* Track whether the user typed a character this redisplay cycle.
+ Used below to suppress overlay completion announcements: when the
+ user types, character echo (via postTextChangedNotification) must
+ take priority over overlay candidate updates. Without this guard,
+ Vertico/Ivy updates its overlay immediately after each keystroke,
+ and the High-priority overlay announcement interrupts the character
+ echo, effectively silencing typed characters. */
+ BOOL didTextChange = NO;
if (modiff != self.cachedModiff)
{
self.cachedModiff = modiff;
@@ -9371,6 +9522,7 @@ Text property changes (e.g. face updates from
{
self.cachedCharsModiff = chars_modiff;
[self postTextChangedNotification:point];
+ didTextChange = YES;
}
}
@@ -9393,8 +9545,15 @@ 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))
+ Restrict the scan to minibuffer windows.
+ Skip overlay announcements when the user just typed a character
+ (didTextChange). Completion frameworks update their overlay
+ immediately after each keystroke; without this guard, the
+ overlay High-priority announcement would interrupt the character
+ echo produced by postTextChangedNotification, making typed
+ 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;
@@ -9488,6 +9647,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;
} }
@@ -428,7 +421,7 @@ index 8d44b5f..e39a012 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 +10135,10 @@ - (void)dealloc @@ -9931,6 +10100,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
@@ -439,7 +432,7 @@ index 8d44b5f..e39a012 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11380,6 +11588,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -11380,6 +11553,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;
@@ -449,11 +442,61 @@ index 8d44b5f..e39a012 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 +12899,80 @@ - (id)accessibilityFocusedUIElement @@ -12688,6 +12864,130 @@ - (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. */
+ +
+/* Announce new echo area messages to VoiceOver.
+
+ This is called at the top of postAccessibilityUpdates, before any
+ tree rebuild. Keeping it here, rather than in the per-element loop
+ in postAccessibilityNotificationsForFrame, guarantees that echo area
+ messages (including "Quit" from C-g) are announced even when the
+ accessibility element tree is in the process of being rebuilt.
+
+ The guard minibuf_level == 0 ensures we only announce passive status
+ messages. While the user is actively typing (minibuf_level > 0),
+ character echo and completion announcements take precedence.
+
+ Reads echo_area_buffer[0] directly because with_echo_area_buffer()
+ sets current_buffer via set_buffer_internal_1() but does NOT call
+ Fset_window_buffer(), so the minibuffer window's contents pointer
+ still points to the inactive " *Minibuf-0*" buffer. */
+- (void)postEchoAreaAnnouncementIfNeeded
+{
+ if (minibuf_level != 0)
+ return;
+ Lisp_Object ea = echo_area_buffer[0];
+ if (!BUFFERP (ea))
+ return;
+ struct buffer *eb = XBUFFER (ea);
+ if (!BUFFER_LIVE_P (eb))
+ return;
+ ptrdiff_t echo_chars = BUF_CHARS_MODIFF (eb);
+ if (echo_chars == lastEchoCharsModiff || BUF_ZV (eb) <= BUF_BEGV (eb))
+ return;
+ lastEchoCharsModiff = echo_chars;
+ struct buffer *prev = current_buffer;
+ set_buffer_internal (eb);
+ Lisp_Object ls = Fbuffer_string ();
+ set_buffer_internal (prev);
+ /* stringWithLispString: converts Emacs's internal multibyte encoding
+ to NSString correctly; a raw SSDATA cast would produce invalid
+ UTF-8 for non-ASCII characters. */
+ NSString *raw = [NSString stringWithLispString: ls];
+ NSString *msg = [raw stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if ([msg length] == 0)
+ return;
+ NSDictionary *info = @{
+ NSAccessibilityAnnouncementKey: msg,
+ NSAccessibilityPriorityKey: @(NSAccessibilityPriorityHigh)
+ };
+ ns_ax_post_notification_with_info (
+ NSApp, NSAccessibilityAnnouncementRequestedNotification, info);
+}
+
+/* Announce the selected candidate in a child frame completion popup. +/* Announce the selected candidate in a child frame completion popup.
+ Handles Corfu, Company-box, and similar frameworks that render + Handles Corfu, Company-box, and similar frameworks that render
+ candidates in a separate child frame rather than as overlay strings + candidates in a separate child frame rather than as overlay strings
@@ -530,7 +573,7 @@ index 8d44b5f..e39a012 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12698,11 +12983,59 @@ - (void)postAccessibilityUpdates @@ -12698,11 +12998,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
@@ -542,6 +585,11 @@ index 8d44b5f..e39a012 100644
return; return;
accessibilityUpdating = YES; accessibilityUpdating = YES;
+ /* Announce echo area messages (e.g. "Quit", "Wrote file") before
+ any tree-rebuild check. This must run even when the element tree
+ is being rebuilt to avoid missing time-sensitive status messages. */
+ [self postEchoAreaAnnouncementIfNeeded];
+
+ /* Child frame completion popup (Corfu, Company-box, etc.). + /* Child frame completion popup (Corfu, Company-box, etc.).
+ Child frames don't participate in the accessibility tree; + Child frames don't participate in the accessibility tree;
+ announce the selected candidate directly. */ + announce the selected candidate directly. */