From 1dcc7f8352f67723d417a608c4967b3d1465460d Mon Sep 17 00:00:00 2001 From: Daneel Date: Sun, 1 Mar 2026 21:05:10 +0100 Subject: [PATCH] =?UTF-8?q?patches:=20fix=20minibuffer=20=E2=80=94=20typin?= =?UTF-8?q?g=20priority,=20echo=20area,=20C-g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...d-frame-completion-candidates-for-Vo.patch | 192 +++++++++++------- 1 file changed, 120 insertions(+), 72 deletions(-) diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index db207fd..cb693a1 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -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 Date: Sat, 28 Feb 2026 16:01:29 +0100 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 +- etc/NEWS | 16 +- - src/nsterm.h | 9 ++ - src/nsterm.m | 373 ++++++++++++++++++++++++++++++++++++++++--- - 4 files changed, 381 insertions(+), 31 deletions(-) + src/nsterm.h | 13 ++ + src/nsterm.m | 397 ++++++++++++++++++++++++++++++++++++++++--- + 4 files changed, 407 insertions(+), 33 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi index 6514dfc..95a8d15 100644 @@ -83,7 +83,7 @@ index 2b1f9e6..d276aa6 100644 interface and eliminate the associated overhead. diff --git a/src/nsterm.h b/src/nsterm.h -index 21a93bc..bbce9fe 100644 +index 21a93bc..8f2143b 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -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 cachedTextStart; @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; BOOL accessibilityTreeValid; BOOL accessibilityUpdating; @@ -105,10 +105,14 @@ index 21a93bc..bbce9fe 100644 + char *childFrameLastCandidate; + Lisp_Object childFrameLastBuffer; + 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 BOOL font_panel_active; 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)invalidateAccessibilityTree; - (void)postAccessibilityUpdates; @@ -117,7 +121,7 @@ index 21a93bc..bbce9fe 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index 8d44b5f..e39a012 100644 +index 8d44b5f..48b6aa0 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -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 (); record_unwind_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) return; -+ /* --- Echo area announcements --- -+ When the minibuffer is not active for user input (minibuf_level == 0) -+ and an echo area message arrives, announce it to VoiceOver. This -+ surfaces messages such as "Git finished", "Wrote file", and error -+ strings that appear in the echo area during background operations. -+ -+ 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. */ ++ /* Echo area announcements are handled in ++ 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 ++ meaningful cursor to track. */ + if (MINI_WINDOW_P (w) && minibuf_level == 0) -+ { -+ 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; -+ } ++ return; + ptrdiff_t modiff = BUF_MODIFF (b); ptrdiff_t point = BUF_PT (b); 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; } @@ -428,7 +421,7 @@ index 8d44b5f..e39a012 100644 /* Post notifications for focused and non-focused elements. */ if ([self isAccessibilityFocused]) [self postFocusedCursorNotification:point -@@ -9931,6 +10135,10 @@ - (void)dealloc +@@ -9931,6 +10100,10 @@ - (void)dealloc #endif [accessibilityElements release]; @@ -439,7 +432,7 @@ index 8d44b5f..e39a012 100644 [[self menu] release]; [super dealloc]; } -@@ -11380,6 +11588,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f +@@ -11380,6 +11553,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f windowClosing = NO; processingCompose = NO; @@ -449,11 +442,61 @@ index 8d44b5f..e39a012 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; 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 previous redisplay cycle. Rebuilding first would create fresh 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. + Handles Corfu, Company-box, and similar frameworks that render + candidates in a separate child frame rather than as overlay strings @@ -530,7 +573,7 @@ index 8d44b5f..e39a012 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12698,11 +12983,59 @@ - (void)postAccessibilityUpdates +@@ -12698,11 +12998,64 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us @@ -542,6 +585,11 @@ index 8d44b5f..e39a012 100644 return; 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 frames don't participate in the accessibility tree; + announce the selected candidate directly. */