diff --git a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch index e2194dc..a893c59 100644 --- a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch +++ b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch @@ -26,16 +26,16 @@ to the selected completion candidate after normal cursor tracking. (ns_update_end): Call ns_zoom_track_completion. (ns_draw_window_cursor): Store cursor rect; call UAZoomChangeFocus. --- - etc/NEWS | 10 ++ + etc/NEWS | 11 ++ src/nsterm.h | 6 + - src/nsterm.m | 357 +++++++++++++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 373 insertions(+) + src/nsterm.m | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 371 insertions(+) diff --git a/etc/NEWS b/etc/NEWS index 7367e3ccbd..4c149e41d6 100644 --- a/etc/NEWS +++ b/etc/NEWS -@@ -82,6 +82,16 @@ other directory on your system. You can also invoke the +@@ -82,6 +82,17 @@ other directory on your system. You can also invoke the * Changes in Emacs 31.1 @@ -45,9 +45,10 @@ index 7367e3ccbd..4c149e41d6 100644 +Follow keyboard focus), Emacs informs Zoom of the text cursor position +after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport +automatically tracks the insertion point across window splits and -+switches. Overlay-based completion frameworks and child-frame popup -+completions are also tracked: Zoom follows the selected candidate -+rather than the text cursor during completion. ++switches. Completion frameworks (Vertico, Icomplete, Ivy for overlay ++candidates; Corfu, Company-box for child frame popups) are also ++tracked: Zoom follows the selected candidate rather than the text ++cursor during completion. + +++ ** 'line-spacing' now supports specifying spacing above the line. @@ -85,7 +86,7 @@ index 932d209f56..88c9251c18 100644 #endif static EmacsMenu *dockMenu; -@@ -1081,6 +1086,284 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1081,6 +1086,281 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) } @@ -102,9 +103,9 @@ index 932d209f56..88c9251c18 100644 + +/* Cached wrapper around ns_zoom_enabled_p (). + ns_zoom_enabled_p () performs a synchronous Mach IPC roundtrip to the -+ macOS Accessibility server (~50-200 µs per call). With call sites ++ macOS Accessibility server (~50-200 uss per call). With call sites + in ns_draw_window_cursor, ns_update_end, and ns_zoom_track_completion, -+ the overhead accumulates to ~150-600 µs per redisplay cycle. Zoom ++ the overhead accumulates to ~150-600 uss per redisplay cycle. Zoom + state changes only on explicit user action in System Settings, so a + 1-second TTL is safe and indistinguishable from querying every frame. + Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */ @@ -123,9 +124,6 @@ index 932d209f56..88c9251c18 100644 + return ns_zoom_cached_enabled; +} + -+/* Note: ns_ax_face_is_selected in the VoiceOver series has identical -+ logic. The duplication is intentional: both series are independent -+ and must compile standalone. */ +/* Identify faces that mark a selected completion candidate. + Matches vertico-current, corfu-current, icomplete-selected-match, + ivy-current-match, etc. by checking the face symbol name. @@ -370,7 +368,7 @@ index 932d209f56..88c9251c18 100644 static void ns_update_end (struct frame *f) /* -------------------------------------------------------------------------- -@@ -1104,6 +1387,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1104,6 +1384,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) unblock_input (); ns_updating_frame = NULL; @@ -412,7 +410,7 @@ index 932d209f56..88c9251c18 100644 } static void -@@ -3232,6 +3550,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3232,6 +3547,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. /* Prevent the cursor from being drawn outside the text area. */ r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); diff --git a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch index 3939a10..58f38d3 100644 --- a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch +++ b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch @@ -200,7 +200,7 @@ index 88c9251c18..9d36de66f9 100644 #include "systime.h" #include "character.h" #include "xwidget.h" -@@ -7204,6 +7205,432 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -7201,6 +7202,432 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg } #endif @@ -248,8 +248,8 @@ index 88c9251c18..9d36de66f9 100644 + return @""; + + specpdl_ref count = SPECPDL_INDEX (); -+ block_input (); + record_unwind_current_buffer (); ++ block_input (); + record_unwind_protect_void (unblock_input); + if (b != current_buffer) + set_buffer_internal_1 (b); @@ -514,7 +514,7 @@ index 88c9251c18..9d36de66f9 100644 +} + +/* Build label for span [START, END) in BUF_OBJ. -+ Priority: completion--string → buffer text → help-echo. */ ++ Priority: completion--string -> buffer text -> help-echo. */ +static NSString * +ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end, + Lisp_Object buf_obj) @@ -633,7 +633,7 @@ index 88c9251c18..9d36de66f9 100644 /* ========================================================================== EmacsView implementation -@@ -11660,6 +12087,24 @@ Convert an X font name (XLFD) to an NS font name. +@@ -11657,6 +12084,24 @@ Convert an X font name (XLFD) to an NS font name. DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); @@ -658,7 +658,7 @@ index 88c9251c18..9d36de66f9 100644 Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); -@@ -11808,6 +12253,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -11805,6 +12250,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with This variable is ignored on Mac OS X < 10.7 and GNUstep. */); ns_use_srgb_colorspace = YES; diff --git a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch index a7d9066..1325edf 100644 --- a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch +++ b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch @@ -30,7 +30,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 9d36de66f9..6256dbc22e 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7628,6 +7628,1121 @@ - (id)accessibilityTopLevelUIElement +@@ -7625,6 +7625,1121 @@ - (id)accessibilityTopLevelUIElement @end diff --git a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch index f976e9b..6e69ecb 100644 --- a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch +++ b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch @@ -29,7 +29,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 6256dbc22e..9e0e317237 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -8743,6 +8743,612 @@ - (NSRect)accessibilityFrame +@@ -8740,6 +8740,612 @@ - (NSRect)accessibilityFrame @end diff --git a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch index afe82fc..193f852 100644 --- a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch +++ b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch @@ -21,7 +21,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 9e0e317237..8aa5b6ac1b 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -9349,6 +9349,298 @@ - (NSRect)accessibilityFrame +@@ -9346,6 +9346,298 @@ - (NSRect)accessibilityFrame @end diff --git a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch index fd86e34..3a7fbb9 100644 --- a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch +++ b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch @@ -24,7 +24,7 @@ com.apple.accessibility.api distributed notification. --- etc/NEWS | 13 ++ src/nsterm.m | 474 +++++++++++++++++++++++++++++++++++++++++++++++++-- - 2 files changed, 479 insertions(+), 12 deletions(-) + 2 files changed, 475 insertions(+), 12 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 4c149e41d6..7f917f93b2 100644 @@ -54,7 +54,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 8aa5b6ac1b..32eb04acef 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1278,7 +1278,7 @@ If a completion candidate is selected (overlay or child frame), +@@ -1275,7 +1275,7 @@ If a completion candidate is selected (overlay or child frame), static void ns_zoom_track_completion (struct frame *f, EmacsView *view) { @@ -63,7 +63,7 @@ index 8aa5b6ac1b..32eb04acef 100644 return; if (!WINDOWP (f->selected_window)) return; -@@ -1396,7 +1396,8 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -1393,7 +1393,8 @@ 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 @@ -73,7 +73,7 @@ index 8aa5b6ac1b..32eb04acef 100644 && !NSIsEmptyRect (view->lastCursorRect)) { NSRect r = view->lastCursorRect; -@@ -1423,6 +1424,9 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -1420,6 +1421,9 @@ so the visual offset is (ov_line + 1) * line_h from if (view) ns_zoom_track_completion (f, view); #endif /* NS_IMPL_COCOA */ @@ -83,7 +83,7 @@ index 8aa5b6ac1b..32eb04acef 100644 } static void -@@ -3570,7 +3574,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) +@@ -3567,7 +3571,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) #if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 @@ -92,7 +92,7 @@ index 8aa5b6ac1b..32eb04acef 100644 { NSRect windowRect = [view convertRect:r toView:nil]; NSRect screenRect -@@ -6726,9 +6730,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification +@@ -6723,9 +6727,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification } #endif @@ -149,7 +149,7 @@ index 8aa5b6ac1b..32eb04acef 100644 - (void)antialiasThresholdDidChange:(NSNotification *)notification { #ifdef NS_IMPL_COCOA -@@ -7631,7 +7682,6 @@ - (id)accessibilityTopLevelUIElement +@@ -7628,7 +7679,6 @@ - (id)accessibilityTopLevelUIElement @@ -157,15 +157,15 @@ index 8aa5b6ac1b..32eb04acef 100644 static BOOL ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ptrdiff_t *out_start, -@@ -8744,7 +8794,6 @@ - (NSRect)accessibilityFrame +@@ -8741,7 +8791,6 @@ - (NSRect)accessibilityFrame @end - /* =================================================================== - EmacsAccessibilityBuffer (Notifications) — AX event dispatch + EmacsAccessibilityBuffer (Notifications) --- AX event dispatch -@@ -9238,6 +9287,50 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9235,6 +9284,50 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f granularity = ns_ax_text_selection_granularity_line; } @@ -179,10 +179,6 @@ index 8aa5b6ac1b..32eb04acef 100644 + if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line) + direction = ns_ax_text_selection_direction_discontiguous; + -+ /* Until voiceoverSetPoint tracking is added, treat all cursor -+ moves as Emacs-initiated (refined in a later patch). */ -+ BOOL emacsMovedCursor = YES; -+ + /* If Emacs moved the cursor (not VoiceOver), force discontiguous + so VoiceOver re-anchors its browse cursor to the current + accessibilitySelectedTextRange. This covers all Emacs-initiated @@ -220,7 +216,7 @@ index 8aa5b6ac1b..32eb04acef 100644 /* Post notifications for focused and non-focused elements. */ if ([self isAccessibilityFocused]) [self postFocusedCursorNotification:point -@@ -9350,7 +9443,6 @@ - (NSRect)accessibilityFrame +@@ -9347,7 +9440,6 @@ - (NSRect)accessibilityFrame @end @@ -228,7 +224,7 @@ index 8aa5b6ac1b..32eb04acef 100644 /* =================================================================== EmacsAccessibilityInteractiveSpan --- helpers and implementation =================================================================== */ -@@ -9686,6 +9778,7 @@ - (void)dealloc +@@ -9683,6 +9775,7 @@ - (void)dealloc [layer release]; #endif @@ -236,7 +232,7 @@ index 8aa5b6ac1b..32eb04acef 100644 [[self menu] release]; [super dealloc]; } -@@ -11034,6 +11127,32 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -11031,6 +11124,32 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -269,7 +265,7 @@ index 8aa5b6ac1b..32eb04acef 100644 } -@@ -12271,6 +12390,332 @@ - (int) fullscreenState +@@ -12268,6 +12387,332 @@ - (int) fullscreenState return fs_state; } @@ -602,7 +598,7 @@ index 8aa5b6ac1b..32eb04acef 100644 @end /* EmacsView */ -@@ -14267,12 +14712,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -14264,12 +14709,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with ns_use_srgb_colorspace = YES; DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, diff --git a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch index 82d2dcc..e6bda59 100644 --- a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch +++ b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch @@ -11,9 +11,9 @@ enabled, and known limitations. Use @xref for cross-reference at sentence start. Correct description of ns-accessibility-enabled default: initial value is nil, set automatically at startup. --- - doc/emacs/macos.texi | 77 ++++++++++++++++++++++++++++++++++++++++++++ + doc/emacs/macos.texi | 76 ++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 10 ++++-- - 2 files changed, 84 insertions(+), 3 deletions(-) + 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi index 6bd334f48e..8d4a7825d8 100644 @@ -27,7 +27,7 @@ index 6bd334f48e..8d4a7825d8 100644 * GNUstep Support:: Details on status of GNUstep support. @end menu -@@ -272,6 +273,82 @@ and return the result as a string. You can also use the Lisp function +@@ -272,6 +273,81 @@ and return the result as a string. You can also use the Lisp function services and receive the results back. Note that you may need to restart Emacs to access newly-available services. @@ -101,8 +101,7 @@ index 6bd334f48e..8d4a7825d8 100644 + + This support is available only on the Cocoa build. GNUstep has a +different accessibility model and is not yet supported. -+ -+ Block-style cursors are handled ++Block-style cursors are handled +correctly: character navigation announces the character at the cursor +position, not the character before it. + @@ -114,7 +113,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 32eb04acef..8e5cc7e1d7 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -14713,9 +14713,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -14710,9 +14710,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support. diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index 0dedba1..3ac6f8a 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,11 +1,11 @@ From affdaf60d28ad4d9836c6505216e19599b31c437 Mon Sep 17 00:00:00 2001 -From: Martin Sukany +From: Daneel Date: Mon, 2 Mar 2026 18:39:46 +0100 Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver -Overlay-based completion frameworks render candidates via overlay -before-string/after-string properties (e.g., Vertico, Icomplete, -Ivy). Without this change VoiceOver cannot read these completion UIs. +Completion frameworks such as overlay-based completion frameworks, Ivy, and Icomplete render +candidates via overlay before-string/after-string properties. Without +this change VoiceOver cannot read overlay-based completion UIs. * src/nsterm.m (ns_ax_face_is_selected): New static function; matches 'current', 'selected', 'selection' in face symbol names. @@ -18,8 +18,8 @@ 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 | 352 ++++++++++++++++++++++++++++++++++++++++++++------- - 2 files changed, 311 insertions(+), 42 deletions(-) + src/nsterm.m | 348 ++++++++++++++++++++++++++++++++++++++++++++------- + 2 files changed, 307 insertions(+), 42 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 5746e9e9bd..21a93bc799 100644 @@ -37,13 +37,10 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 8e5cc7e1d7..8ef344d9fe 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7266,11 +7266,158 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7263,11 +7263,154 @@ Accessibility virtual elements (macOS / Cocoa only) /* ---- Helper: extract buffer text for accessibility ---- */ -+/* Note: ns_zoom_face_is_selected in the Zoom series has identical -+ logic. The duplication is intentional: both series are independent -+ and must compile standalone. */ +/* Return true if FACE is or contains a face symbol whose name + includes "current" or "selected", indicating a highlighted + completion candidate. Works for vertico-current, @@ -188,7 +185,6 @@ index 8e5cc7e1d7..8ef344d9fe 100644 + + return nil; +} -+ /* 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 @@ -197,25 +193,25 @@ index 8e5cc7e1d7..8ef344d9fe 100644 static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -7343,7 +7489,7 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7340,7 +7483,7 @@ Accessibility virtual elements (macOS / Cocoa only) /* Extract this visible run's text. Use Fbuffer_substring_no_properties which correctly handles the -- buffer gap — raw BUF_BYTE_ADDRESS reads across the gap would +- buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would + buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would include garbage bytes when the run spans the gap position. */ Lisp_Object lstr = Fbuffer_substring_no_properties ( make_fixnum (pos), make_fixnum (run_end)); -@@ -7424,7 +7570,7 @@ Mode lines using icon fonts (e.g. nerd-font icons) +@@ -7421,7 +7564,7 @@ Mode lines using icon fonts (e.g. nerd-font icons) return NSZeroRect; /* charpos_start and charpos_len are already in buffer charpos -- space — the caller maps AX string indices through +- space --- the caller maps AX string indices through + space --- the caller maps AX string indices through charposForAccessibilityIndex which handles invisible text. */ ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_end = cp_start + charpos_len; -@@ -7899,6 +8045,7 @@ @implementation EmacsAccessibilityBuffer +@@ -7896,6 +8039,7 @@ @implementation EmacsAccessibilityBuffer @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; @@ -223,16 +219,16 @@ index 8e5cc7e1d7..8ef344d9fe 100644 @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; -@@ -7996,7 +8143,7 @@ - (void)ensureTextCache +@@ -7993,7 +8137,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 -- below are therefore safe without @synchronized — only the +- below are therefore safe without @synchronized --- only the + below are therefore safe without @synchronized --- only the write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); -@@ -8008,25 +8155,34 @@ - (void)ensureTextCache +@@ -8005,25 +8149,34 @@ - (void)ensureTextCache if (!b) return; @@ -284,7 +280,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -8042,7 +8198,7 @@ included in the cached AX text (it is handled separately via +@@ -8039,7 +8192,7 @@ included in the cached AX text (it is handled separately via { [cachedText release]; cachedText = [text retain]; @@ -293,7 +289,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 cachedTextStart = start; if (visibleRuns) -@@ -8054,9 +8210,9 @@ included in the cached AX text (it is handled separately via +@@ -8051,9 +8204,9 @@ included in the cached AX text (it is handled separately via Walk the cached text once, recording the start offset of each line. Uses NSString lineRangeForRange: --- O(N) in the total text --- but this loop runs only on cache rebuild, which is @@ -306,47 +302,47 @@ index 8e5cc7e1d7..8ef344d9fe 100644 enters this code. */ if (lineStartOffsets) xfree (lineStartOffsets); -@@ -8111,7 +8267,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -8108,7 +8261,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 -- O(n) — matters for org-mode with many folded sections. */ +- O(n) --- matters for org-mode with many folded sections. */ + O(n) --- matters for org-mode with many folded sections. */ NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { -@@ -8160,10 +8316,10 @@ by run length (visible window), not total buffer size. */ +@@ -8157,10 +8310,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 -- visibleRuns — no Lisp calls. */ +- visibleRuns --- no Lisp calls. */ + visibleRuns --- no Lisp calls. */ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx { -- /* May be called from AX server thread — synchronize. */ +- /* May be called from AX server thread --- synchronize. */ + /* May be called from AX server thread --- synchronize. */ @synchronized (self) { if (visibleRunCount == 0) -@@ -8205,7 +8361,7 @@ the slow path (composed character sequence walk), which is +@@ -8202,7 +8355,7 @@ the slow path (composed character sequence walk), which is return cp; } } -- /* Past end — return last charpos. */ +- /* Past end --- return last charpos. */ + /* Past end --- return last charpos. */ if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -8227,7 +8383,7 @@ the slow path (composed character sequence walk), which is +@@ -8224,7 +8377,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 -- Lisp access — the window and buffer are verified live. +- Lisp access --- the window and buffer are verified live. + Lisp access --- the window and buffer are verified live. 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 -@@ -8573,6 +8729,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber +@@ -8570,6 +8723,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber return [self lineForAXIndex:point_idx]; } @@ -397,34 +393,34 @@ index 8e5cc7e1d7..8ef344d9fe 100644 - (NSRange)accessibilityRangeForLine:(NSInteger)line { if (![NSThread isMainThread]) -@@ -8795,7 +8995,7 @@ - (NSRect)accessibilityFrame +@@ -8792,7 +8989,7 @@ - (NSRect)accessibilityFrame /* =================================================================== -- EmacsAccessibilityBuffer (Notifications) — AX event dispatch +- EmacsAccessibilityBuffer (Notifications) --- AX event dispatch + EmacsAccessibilityBuffer (Notifications) --- AX event dispatch These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8810,7 +9010,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8807,7 +9004,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point if (point > self.cachedPoint && point - self.cachedPoint == 1) { -- /* Single char inserted — refresh cache and grab it. */ +- /* Single char inserted --- refresh cache and grab it. */ + /* Single char inserted --- refresh cache and grab it. */ [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) -@@ -8829,7 +9029,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8826,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 -- same user action — they are mutually exclusive. */ +- same user action --- they are mutually exclusive. */ + same user action --- they are mutually exclusive. */ self.cachedPoint = point; NSDictionary *change = @{ -@@ -9223,16 +9423,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9220,16 +9417,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -503,7 +499,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 + skip_overlay_scan:; /* --- Cursor moved or selection changed --- -- Use 'else if' — edits and selection moves are mutually exclusive +- Use 'else if' --- edits and selection moves are mutually exclusive - per the WebKit/Chromium pattern. */ - else if (point != self.cachedPoint || markActive != self.cachedMarkActive) + Independent check: the goto above may jump here from the overlay @@ -512,44 +508,44 @@ index 8e5cc7e1d7..8ef344d9fe 100644 { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; -@@ -12406,7 +12673,7 @@ - (int) fullscreenState +@@ -12403,7 +12667,7 @@ - (int) fullscreenState if (WINDOW_LEAF_P (w)) { -- /* Buffer element — reuse existing if available. */ +- /* Buffer element --- reuse existing if available. */ + /* Buffer element --- reuse existing if available. */ EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -12440,7 +12707,7 @@ - (int) fullscreenState +@@ -12437,7 +12701,7 @@ - (int) fullscreenState } else { -- /* Internal (combination) window — recurse into children. */ +- /* Internal (combination) window --- recurse into children. */ + /* Internal (combination) window --- recurse into children. */ Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12552,7 +12819,7 @@ - (void)postAccessibilityUpdates +@@ -12549,7 +12813,7 @@ - (void)postAccessibilityUpdates accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare -- FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */ +- FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ + FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12561,12 +12828,12 @@ - (void)postAccessibilityUpdates +@@ -12558,12 +12822,12 @@ - (void)postAccessibilityUpdates } /* If tree is stale, rebuild FIRST so we don't iterate freed -- window pointers. Skip notifications for this cycle — the +- window pointers. Skip notifications for this cycle --- the + window pointers. Skip notifications for this cycle --- the freshly-built elements have no previous state to diff against. */ if (!accessibilityTreeValid) { [self rebuildAccessibilityTree]; -- /* Invalidate span cache — window layout changed. */ +- /* Invalidate span cache --- window layout changed. */ + /* Invalidate span cache --- window layout changed. */ for (EmacsAccessibilityElement *elem in accessibilityElements) if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) 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 0fdc1f8..f60f6d9 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,13 +1,13 @@ From 1e3d3919fd41e4480a02190fb89bee1ef8107d62 Mon Sep 17 00:00:00 2001 -From: Martin Sukany +From: Daneel Date: Mon, 2 Mar 2026 18:49:13 +0100 Subject: [PATCH 8/8] ns: announce child frame completion candidates for VoiceOver -Child-frame completion frameworks render candidates in a separate -frame whose buffer is not accessible via the minibuffer overlay path -(e.g., Corfu, Company-box). This patch scans child frame buffers -for selected candidates and announces them via VoiceOver. +Child frame popups (child-frame completion frameworks, 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 @@ -34,7 +34,7 @@ area announcements. etc/NEWS | 25 ++- src/nsterm.h | 21 ++ src/nsterm.m | 514 +++++++++++++++++++++++++++++++++++++++---- - 4 files changed, 510 insertions(+), 68 deletions(-) + 4 files changed, 510 insertions(+), 64 deletions(-) diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi index 8d4a7825d8..03a657f970 100644 @@ -167,7 +167,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 8ef344d9fe..1acb64630a 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -1278,7 +1278,13 @@ If a completion candidate is selected (overlay or child frame), +@@ -1275,7 +1275,13 @@ If a completion candidate is selected (overlay or child frame), static void ns_zoom_track_completion (struct frame *f, EmacsView *view) { @@ -182,7 +182,7 @@ index 8ef344d9fe..1acb64630a 100644 return; if (!WINDOWP (f->selected_window)) return; -@@ -1396,7 +1402,7 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -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 @@ -191,7 +191,7 @@ index 8ef344d9fe..1acb64630a 100644 && ns_zoom_enabled_p () && !NSIsEmptyRect (view->lastCursorRect)) { -@@ -3574,7 +3580,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) +@@ -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 @@ -200,7 +200,7 @@ index 8ef344d9fe..1acb64630a 100644 { NSRect windowRect = [view convertRect:r toView:nil]; NSRect screenRect -@@ -7413,6 +7419,117 @@ visual line index for Zoom (skip whitespace-only lines +@@ -7407,6 +7413,117 @@ visual line index for Zoom (skip whitespace-only lines return nil; } @@ -318,7 +318,7 @@ 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 -@@ -7446,9 +7563,13 @@ visual line index for Zoom (skip whitespace-only lines +@@ -7440,9 +7557,13 @@ visual line index for Zoom (skip whitespace-only lines return @""; specpdl_ref count = SPECPDL_INDEX (); @@ -326,13 +326,14 @@ index 8ef344d9fe..1acb64630a 100644 + 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 (); ++ block_input (); record_unwind_current_buffer (); record_unwind_protect_void (unblock_input); +- block_input (); if (b != current_buffer) set_buffer_internal_1 (b); -@@ -8611,6 +8732,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range +@@ -8605,6 +8726,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range [self ensureTextCache]; @@ -344,7 +345,7 @@ index 8ef344d9fe..1acb64630a 100644 specpdl_ref count = SPECPDL_INDEX (); record_unwind_current_buffer (); /* Ensure block_input is always matched by unblock_input even if -@@ -9059,20 +9185,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point +@@ -9053,20 +9179,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point && granularity == ns_ax_text_selection_granularity_character); @@ -393,7 +394,7 @@ index 8ef344d9fe..1acb64630a 100644 ns_ax_post_notification_with_info ( self, NSAccessibilitySelectedTextChangedNotification, -@@ -9172,12 +9316,17 @@ user expectation ("w" jumps to next word and reads it). */ +@@ -9166,12 +9310,17 @@ user expectation ("w" jumps to next word and reads it). */ } } @@ -416,7 +417,7 @@ index 8ef344d9fe..1acb64630a 100644 if (cachedText && granularity == ns_ax_text_selection_granularity_line) { -@@ -9242,6 +9391,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b +@@ -9236,6 +9385,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b block_input (); specpdl_ref count2 = SPECPDL_INDEX (); @@ -428,7 +429,7 @@ index 8ef344d9fe..1acb64630a 100644 record_unwind_protect_void (unblock_input); record_unwind_current_buffer (); if (b != current_buffer) -@@ -9418,12 +9572,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9412,12 +9566,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f if (!b) return; @@ -458,7 +459,7 @@ index 8ef344d9fe..1acb64630a 100644 if (modiff != self.cachedModiff) { self.cachedModiff = modiff; -@@ -9437,6 +9608,7 @@ Text property changes (e.g. face updates from +@@ -9431,6 +9602,7 @@ Text property changes (e.g. face updates from { self.cachedCharsModiff = chars_modiff; [self postTextChangedNotification:point]; @@ -466,7 +467,7 @@ index 8ef344d9fe..1acb64630a 100644 } } -@@ -9459,8 +9631,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9453,8 +9625,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. @@ -484,7 +485,7 @@ index 8ef344d9fe..1acb64630a 100644 goto skip_overlay_scan; int selected_line = -1; -@@ -9506,7 +9685,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9500,7 +9679,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property self.cachedPoint = point; self.cachedMarkActive = markActive; @@ -504,7 +505,7 @@ index 8ef344d9fe..1acb64630a 100644 NSInteger direction = ns_ax_text_selection_direction_discontiguous; if (point > oldPoint) direction = ns_ax_text_selection_direction_next; -@@ -9518,6 +9708,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9512,6 +9702,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property /* --- Granularity detection --- */ NSInteger granularity = ns_ax_text_selection_granularity_unknown; @@ -512,7 +513,7 @@ index 8ef344d9fe..1acb64630a 100644 [self ensureTextCache]; if (cachedText && oldPoint > 0) { -@@ -9532,7 +9723,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9526,7 +9717,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property NSRange newLine = [cachedText lineRangeForRange: NSMakeRange (newIdx, 0)]; if (oldLine.location != newLine.location) @@ -521,7 +522,7 @@ index 8ef344d9fe..1acb64630a 100644 + granularity = ns_ax_text_selection_granularity_line; + /* Detect single adjacent-line move while oldLine/newLine + are in scope. Any command that steps exactly one line --- -+ C-n/C-p, outline-next-heading, etc. --- is ++ C-n/C-p, evil j/k, outline-next-heading, etc. --- is + sequential. Multi-line teleports (]], M-<, xref, ...) are + not adjacent and will be marked discontiguous below. + Detected structurally: no package-specific code needed. */ @@ -532,31 +533,25 @@ index 8ef344d9fe..1acb64630a 100644 else { NSUInteger dist = (newIdx > oldIdx -@@ -9554,38 +9756,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9548,34 +9750,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property granularity = ns_ax_text_selection_granularity_line; } - /* 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 - accessibilitySelectedTextRange rather than advancing linearly - from its previous internal position. */ - if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line) -- direction = ns_ax_text_selection_direction_discontiguous; -- -- /* Until voiceoverSetPoint tracking is added, treat all cursor -- moves as Emacs-initiated (refined in a later patch). */ -- BOOL emacsMovedCursor = YES; -- + + /* Multi-line teleports are discontiguous; single adjacent-line + steps stay sequential. */ + if (!isCtrlNP && !singleLineMove + && granularity == ns_ax_text_selection_granularity_line) -+ direction = ns_ax_text_selection_direction_discontiguous; -+ + direction = ns_ax_text_selection_direction_discontiguous; + - /* If Emacs moved the cursor (not VoiceOver), force discontiguous - so VoiceOver re-anchors its browse cursor to the current - accessibilitySelectedTextRange. This covers all Emacs-initiated @@ -586,7 +581,7 @@ index 8ef344d9fe..1acb64630a 100644 { NSWindow *win = [self.emacsView window]; if (win) -@@ -9740,6 +9931,13 @@ - (NSRect)accessibilityFrame +@@ -9734,6 +9925,13 @@ - (NSRect)accessibilityFrame if (vis_start >= vis_end) return @[]; @@ -600,7 +595,7 @@ index 8ef344d9fe..1acb64630a 100644 block_input (); specpdl_ref blk_count = SPECPDL_INDEX (); record_unwind_protect_void (unblock_input); -@@ -10046,6 +10244,10 @@ - (void)dealloc +@@ -10040,6 +10238,10 @@ - (void)dealloc #endif [accessibilityElements release]; @@ -611,7 +606,7 @@ index 8ef344d9fe..1acb64630a 100644 [[self menu] release]; [super dealloc]; } -@@ -11495,6 +11697,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f +@@ -11489,6 +11691,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f windowClosing = NO; processingCompose = NO; @@ -621,7 +616,7 @@ index 8ef344d9fe..1acb64630a 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; fs_before_fs = next_maximized = -1; -@@ -12803,6 +13008,161 @@ - (id)accessibilityFocusedUIElement +@@ -12797,6 +13002,161 @@ - (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. */ @@ -783,7 +778,7 @@ index 8ef344d9fe..1acb64630a 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12813,11 +13173,69 @@ - (void)postAccessibilityUpdates +@@ -12807,11 +13167,69 @@ - (void)postAccessibilityUpdates /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us diff --git a/patches/README.txt b/patches/README.txt index 3c3e9c7..a73d255 100644 --- a/patches/README.txt +++ b/patches/README.txt @@ -88,9 +88,7 @@ ARCHITECTURE PERFORMANCE ----------- - ns-accessibility-enabled (DEFVAR_BOOL, default nil): - Set automatically at startup when macOS Zoom or an assistive - technology (VoiceOver, Switch Control) is detected. + ns-accessibility-enabled (DEFVAR_BOOL, default t): When nil, no virtual elements are built, no notifications are posted, and ns_draw_window_cursor skips the cursor rect store. Zero overhead for users who do not use assistive technology. @@ -119,7 +117,7 @@ KNOWN LIMITATIONS - Overlay face matching: string containment ("current", "selected") - GNUstep excluded (#ifdef NS_IMPL_COCOA) - No multi-frame coordination - - Child frame childFrameLastCandidate (per-view ivar, freed in dealloc) + - Child frame static lastCandidate leaks at exit (minor) TESTING diff --git a/patches/TESTING.txt b/patches/TESTING.txt index 333921d..03a9269 100644 --- a/patches/TESTING.txt +++ b/patches/TESTING.txt @@ -112,8 +112,7 @@ PASS — Folded text NOT read, unfolded text read correctly. 11. ERT — ns-accessibility-enabled Variable -------------------------------------------- -PASS — ns-accessibility-enabled bound, defaults to nil. - Set automatically to t when AT is detected at startup. +PASS — ns-accessibility-enabled bound, defaults to t. 12. VoiceOver — Overlay Completion (Patch 0007) ------------------------------------------------ @@ -144,10 +143,3 @@ PASS — When set to nil: ----------------- PASS — Texinfo node accessible via C-h i g (emacs)VoiceOver. etc/NEWS entry present and accurate. - -16. Patch Compilation Order ---------------------------- -VERIFIED — Each patch 0001-0008 declares all variables before use. - - 0005: emacsMovedCursor declared locally (BOOL YES) before use. - - 0008: replaces with voiceoverSetPoint-based detection. - - No forward references to code in later patches.