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 c9d78e9..e2194dc 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 @@ -1,7 +1,7 @@ From 0470786c91eb4a464d8580387b83a4a8d4e4f8eb Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 22:39:35 +0100 -Subject: [PATCH 0/8] ns: integrate with macOS Zoom for cursor tracking +Subject: [PATCH] ns: integrate with macOS Zoom for cursor tracking Inform macOS Zoom of the text cursor position so the zoomed viewport follows keyboard focus in Emacs. Also track completion candidates so @@ -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 | 11 ++ + etc/NEWS | 10 ++ src/nsterm.h | 6 + - src/nsterm.m | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 371 insertions(+) + src/nsterm.m | 357 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 373 insertions(+) diff --git a/etc/NEWS b/etc/NEWS index 7367e3ccbd..4c149e41d6 100644 --- a/etc/NEWS +++ b/etc/NEWS -@@ -82,6 +82,17 @@ other directory on your system. You can also invoke the +@@ -82,6 +82,16 @@ other directory on your system. You can also invoke the * Changes in Emacs 31.1 @@ -45,10 +45,9 @@ 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. 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. ++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. + +++ ** 'line-spacing' now supports specifying spacing above the line. @@ -86,7 +85,7 @@ index 932d209f56..88c9251c18 100644 #endif static EmacsMenu *dockMenu; -@@ -1081,6 +1086,281 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1081,6 +1086,284 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) } @@ -124,6 +123,9 @@ 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. @@ -368,7 +370,7 @@ index 932d209f56..88c9251c18 100644 static void ns_update_end (struct frame *f) /* -------------------------------------------------------------------------- -@@ -1104,6 +1384,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) +@@ -1104,6 +1387,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) unblock_input (); ns_updating_frame = NULL; @@ -410,7 +412,7 @@ index 932d209f56..88c9251c18 100644 } static void -@@ -3232,6 +3547,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. +@@ -3232,6 +3550,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 1acec93..3939a10 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 @@ -44,7 +44,7 @@ index ea6e7ba4f5..5746e9e9bd 100644 + + Accessibility virtual elements (macOS / Cocoa only) + -+ ========================================================================== */ ++ ========================================================================= */ + +#ifdef NS_IMPL_COCOA +@class EmacsView; @@ -52,11 +52,11 @@ index ea6e7ba4f5..5746e9e9bd 100644 +/* Base class for virtual accessibility elements attached to EmacsView. */ +@interface EmacsAccessibilityElement : NSAccessibilityElement +@property (nonatomic, unsafe_unretained) EmacsView *emacsView; -+/* Lisp window object — safe across GC cycles. ++/* Lisp window object --- safe across GC cycles. + GC safety: these Lisp_Objects are NOT visible to GC via staticpro + or the specpdl stack. This is safe because: + (1) Emacs GC runs only on the main thread, at well-defined safe -+ points during Lisp evaluation — never during redisplay. ++ points during Lisp evaluation --- never during redisplay. + (2) Accessibility elements are owned by EmacsView which belongs to + an active frame; windows referenced here are always reachable + from the frame's window tree until rebuildAccessibilityTree @@ -81,7 +81,7 @@ index ea6e7ba4f5..5746e9e9bd 100644 + NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */ +} ns_ax_visible_run; + -+/* Virtual AXTextArea element — one per visible Emacs window (buffer). */ ++/* Virtual AXTextArea element --- one per visible Emacs window (buffer). */ +@interface EmacsAccessibilityBuffer + : EmacsAccessibilityElement +{ @@ -119,7 +119,7 @@ index ea6e7ba4f5..5746e9e9bd 100644 +- (void)invalidateInteractiveSpans; +@end + -+/* Virtual AXStaticText element — one per mode line. */ ++/* Virtual AXStaticText element --- one per mode line. */ +@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement +@end + @@ -200,7 +200,7 @@ index 88c9251c18..9d36de66f9 100644 #include "systime.h" #include "character.h" #include "xwidget.h" -@@ -7201,6 +7202,432 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg +@@ -7204,6 +7205,432 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg } #endif @@ -208,7 +208,7 @@ index 88c9251c18..9d36de66f9 100644 + + Accessibility virtual elements (macOS / Cocoa only) + -+ ========================================================================== */ ++ ========================================================================= */ + +#ifdef NS_IMPL_COCOA + @@ -248,9 +248,9 @@ index 88c9251c18..9d36de66f9 100644 + return @""; + + specpdl_ref count = SPECPDL_INDEX (); ++ block_input (); + record_unwind_current_buffer (); + record_unwind_protect_void (unblock_input); -+ block_input (); + if (b != current_buffer) + set_buffer_internal_1 (b); + @@ -291,7 +291,7 @@ index 88c9251c18..9d36de66f9 100644 + + /* 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 + include garbage bytes when the run spans the gap position. */ + Lisp_Object lstr = Fbuffer_substring_no_properties ( + make_fixnum (pos), make_fixnum (run_end)); @@ -372,7 +372,7 @@ index 88c9251c18..9d36de66f9 100644 + 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 + charposForAccessibilityIndex which handles invisible text. */ + ptrdiff_t cp_start = charpos_start; + ptrdiff_t cp_end = cp_start + charpos_len; @@ -633,7 +633,7 @@ index 88c9251c18..9d36de66f9 100644 /* ========================================================================== EmacsView implementation -@@ -11657,6 +12084,24 @@ Convert an X font name (XLFD) to an NS font name. +@@ -11660,6 +12087,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)); -@@ -11805,6 +12250,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -11808,6 +12253,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 09ee937..a7d9066 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 -@@ -7625,6 +7625,1121 @@ - (id)accessibilityTopLevelUIElement +@@ -7628,6 +7628,1121 @@ - (id)accessibilityTopLevelUIElement @end @@ -352,7 +352,7 @@ index 9d36de66f9..6256dbc22e 100644 + 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 + write section at the end needs synchronization to protect + against concurrent reads from AX server thread. */ + eassert ([NSThread isMainThread]); @@ -467,7 +467,7 @@ index 9d36de66f9..6256dbc22e 100644 + /* 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. */ + NSUInteger lo = 0, hi = visibleRunCount; + while (lo < hi) + { @@ -516,10 +516,10 @@ index 9d36de66f9..6256dbc22e 100644 + +/* 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. */ +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +{ -+ /* May be called from AX server thread — synchronize. */ ++ /* May be called from AX server thread --- synchronize. */ + @synchronized (self) + { + if (visibleRunCount == 0) @@ -561,7 +561,7 @@ index 9d36de66f9..6256dbc22e 100644 + return cp; + } + } -+ /* Past end — return last charpos. */ ++ /* Past end --- return last charpos. */ + if (lo > 0) + { + ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; @@ -583,7 +583,7 @@ index 9d36de66f9..6256dbc22e 100644 + 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. + 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 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 815f5df..f976e9b 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,14 +29,14 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 6256dbc22e..9e0e317237 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -8740,6 +8740,612 @@ - (NSRect)accessibilityFrame +@@ -8743,6 +8743,612 @@ - (NSRect)accessibilityFrame @end + + +/* =================================================================== -+ 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). @@ -51,7 +51,7 @@ index 6256dbc22e..9e0e317237 100644 + if (point > self.cachedPoint + && point - self.cachedPoint == 1) + { -+ /* Single char inserted — refresh cache and grab it. */ ++ /* Single char inserted --- refresh cache and grab it. */ + [self invalidateTextCache]; + [self ensureTextCache]; + if (cachedText) @@ -70,7 +70,7 @@ index 6256dbc22e..9e0e317237 100644 + /* 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. */ + self.cachedPoint = point; + + NSDictionary *change = @{ @@ -471,7 +471,7 @@ index 6256dbc22e..9e0e317237 100644 + } + + /* --- 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) + { 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 193f852..afe82fc 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 -@@ -9346,6 +9346,298 @@ - (NSRect)accessibilityFrame +@@ -9349,6 +9349,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 55f68ea..fd86e34 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, 475 insertions(+), 12 deletions(-) + 2 files changed, 479 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 -@@ -1275,7 +1275,7 @@ If a completion candidate is selected (overlay or child frame), +@@ -1278,7 +1278,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; -@@ -1393,7 +1393,8 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -1396,7 +1396,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; -@@ -1420,6 +1421,9 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -1423,6 +1424,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 -@@ -3567,7 +3571,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) +@@ -3570,7 +3574,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 -@@ -6723,9 +6727,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification +@@ -6726,9 +6730,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification } #endif @@ -149,7 +149,7 @@ index 8aa5b6ac1b..32eb04acef 100644 - (void)antialiasThresholdDidChange:(NSNotification *)notification { #ifdef NS_IMPL_COCOA -@@ -7628,7 +7679,6 @@ - (id)accessibilityTopLevelUIElement +@@ -7631,7 +7682,6 @@ - (id)accessibilityTopLevelUIElement @@ -157,7 +157,7 @@ index 8aa5b6ac1b..32eb04acef 100644 static BOOL ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ptrdiff_t *out_start, -@@ -8741,7 +8791,6 @@ - (NSRect)accessibilityFrame +@@ -8744,7 +8794,6 @@ - (NSRect)accessibilityFrame @end @@ -165,12 +165,12 @@ index 8aa5b6ac1b..32eb04acef 100644 /* =================================================================== EmacsAccessibilityBuffer (Notifications) — AX event dispatch -@@ -9235,6 +9284,50 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9238,6 +9287,50 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f 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 @@ -179,6 +179,10 @@ 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 @@ -216,7 +220,7 @@ index 8aa5b6ac1b..32eb04acef 100644 /* Post notifications for focused and non-focused elements. */ if ([self isAccessibilityFocused]) [self postFocusedCursorNotification:point -@@ -9347,7 +9440,6 @@ - (NSRect)accessibilityFrame +@@ -9350,7 +9443,6 @@ - (NSRect)accessibilityFrame @end @@ -224,7 +228,7 @@ index 8aa5b6ac1b..32eb04acef 100644 /* =================================================================== EmacsAccessibilityInteractiveSpan --- helpers and implementation =================================================================== */ -@@ -9683,6 +9775,7 @@ - (void)dealloc +@@ -9686,6 +9778,7 @@ - (void)dealloc [layer release]; #endif @@ -232,7 +236,7 @@ index 8aa5b6ac1b..32eb04acef 100644 [[self menu] release]; [super dealloc]; } -@@ -11031,6 +11124,32 @@ - (void)windowDidBecomeKey /* for direct calls */ +@@ -11034,6 +11127,32 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -265,7 +269,7 @@ index 8aa5b6ac1b..32eb04acef 100644 } -@@ -12268,6 +12387,332 @@ - (int) fullscreenState +@@ -12271,6 +12390,332 @@ - (int) fullscreenState return fs_state; } @@ -285,7 +289,7 @@ index 8aa5b6ac1b..32eb04acef 100644 + + if (WINDOW_LEAF_P (w)) + { -+ /* Buffer element — reuse existing if available. */ ++ /* Buffer element --- reuse existing if available. */ + EmacsAccessibilityBuffer *elem + = [existing objectForKey:[NSValue valueWithPointer:w]]; + if (!elem) @@ -319,7 +323,7 @@ index 8aa5b6ac1b..32eb04acef 100644 + } + else + { -+ /* Internal (combination) window — recurse into children. */ ++ /* Internal (combination) window --- recurse into children. */ + Lisp_Object child = w->contents; + while (!NILP (child)) + { @@ -431,7 +435,7 @@ index 8aa5b6ac1b..32eb04acef 100644 + 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. */ + Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); + if (!EQ (curRoot, lastRootWindow)) + { @@ -440,12 +444,12 @@ index 8aa5b6ac1b..32eb04acef 100644 + } + + /* 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 + 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. */ + for (EmacsAccessibilityElement *elem in accessibilityElements) + if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) + [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans]; @@ -598,7 +602,7 @@ index 8aa5b6ac1b..32eb04acef 100644 @end /* EmacsView */ -@@ -14264,12 +14709,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -14267,12 +14712,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 e6bda59..82d2dcc 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 | 76 ++++++++++++++++++++++++++++++++++++++++++++ + doc/emacs/macos.texi | 77 ++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 10 ++++-- - 2 files changed, 83 insertions(+), 3 deletions(-) + 2 files changed, 84 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,81 @@ and return the result as a string. You can also use the Lisp function +@@ -272,6 +273,82 @@ 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,7 +101,8 @@ 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. + @@ -113,7 +114,7 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 32eb04acef..8e5cc7e1d7 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -14710,9 +14710,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with +@@ -14713,9 +14713,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 1b4e8f7..0dedba1 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: Daneel +From: Martin Sukany Date: Mon, 2 Mar 2026 18:39:46 +0100 Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver -Completion frameworks such as Vertico, Ivy, and Icomplete render -candidates via overlay before-string/after-string properties. Without -this change VoiceOver cannot read overlay-based completion UIs. +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. * 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 | 348 ++++++++++++++++++++++++++++++++++++++++++++------- - 2 files changed, 307 insertions(+), 42 deletions(-) + src/nsterm.m | 352 ++++++++++++++++++++++++++++++++++++++++++++------- + 2 files changed, 311 insertions(+), 42 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 5746e9e9bd..21a93bc799 100644 @@ -37,10 +37,13 @@ diff --git a/src/nsterm.m b/src/nsterm.m index 8e5cc7e1d7..8ef344d9fe 100644 --- a/src/nsterm.m +++ b/src/nsterm.m -@@ -7263,11 +7263,154 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7266,11 +7266,158 @@ 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, @@ -185,6 +188,7 @@ 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 @@ -193,7 +197,7 @@ 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) -@@ -7340,7 +7483,7 @@ Accessibility virtual elements (macOS / Cocoa only) +@@ -7343,7 +7489,7 @@ Accessibility virtual elements (macOS / Cocoa only) /* Extract this visible run's text. Use Fbuffer_substring_no_properties which correctly handles the @@ -202,7 +206,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 include garbage bytes when the run spans the gap position. */ Lisp_Object lstr = Fbuffer_substring_no_properties ( make_fixnum (pos), make_fixnum (run_end)); -@@ -7421,7 +7564,7 @@ Mode lines using icon fonts (e.g. nerd-font icons) +@@ -7424,7 +7570,7 @@ Mode lines using icon fonts (e.g. nerd-font icons) return NSZeroRect; /* charpos_start and charpos_len are already in buffer charpos @@ -211,7 +215,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 charposForAccessibilityIndex which handles invisible text. */ ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_end = cp_start + charpos_len; -@@ -7896,6 +8039,7 @@ @implementation EmacsAccessibilityBuffer +@@ -7899,6 +8045,7 @@ @implementation EmacsAccessibilityBuffer @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; @@ -219,7 +223,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; -@@ -7993,7 +8137,7 @@ - (void)ensureTextCache +@@ -7996,7 +8143,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 @@ -228,7 +232,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); -@@ -8005,25 +8149,34 @@ - (void)ensureTextCache +@@ -8008,25 +8155,34 @@ - (void)ensureTextCache if (!b) return; @@ -280,7 +284,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -8039,7 +8192,7 @@ included in the cached AX text (it is handled separately via +@@ -8042,7 +8198,7 @@ included in the cached AX text (it is handled separately via { [cachedText release]; cachedText = [text retain]; @@ -289,7 +293,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 cachedTextStart = start; if (visibleRuns) -@@ -8051,9 +8204,9 @@ included in the cached AX text (it is handled separately via +@@ -8054,9 +8210,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 @@ -302,7 +306,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 enters this code. */ if (lineStartOffsets) xfree (lineStartOffsets); -@@ -8108,7 +8261,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +@@ -8111,7 +8267,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 @@ -311,7 +315,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { -@@ -8157,10 +8310,10 @@ by run length (visible window), not total buffer size. */ +@@ -8160,10 +8316,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 @@ -324,7 +328,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 @synchronized (self) { if (visibleRunCount == 0) -@@ -8202,7 +8355,7 @@ the slow path (composed character sequence walk), which is +@@ -8205,7 +8361,7 @@ the slow path (composed character sequence walk), which is return cp; } } @@ -333,7 +337,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; -@@ -8224,7 +8377,7 @@ the slow path (composed character sequence walk), which is +@@ -8227,7 +8383,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 @@ -342,7 +346,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 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 -@@ -8570,6 +8723,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber +@@ -8573,6 +8729,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber return [self lineForAXIndex:point_idx]; } @@ -393,7 +397,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 - (NSRange)accessibilityRangeForLine:(NSInteger)line { if (![NSThread isMainThread]) -@@ -8792,7 +8989,7 @@ - (NSRect)accessibilityFrame +@@ -8795,7 +8995,7 @@ - (NSRect)accessibilityFrame /* =================================================================== @@ -402,7 +406,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). -@@ -8807,7 +9004,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8810,7 +9010,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point if (point > self.cachedPoint && point - self.cachedPoint == 1) { @@ -411,7 +415,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) -@@ -8826,7 +9023,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point +@@ -8829,7 +9029,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 @@ -420,7 +424,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -9220,16 +9417,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9223,16 +9423,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -508,7 +512,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; -@@ -12403,7 +12667,7 @@ - (int) fullscreenState +@@ -12406,7 +12673,7 @@ - (int) fullscreenState if (WINDOW_LEAF_P (w)) { @@ -517,7 +521,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -12437,7 +12701,7 @@ - (int) fullscreenState +@@ -12440,7 +12707,7 @@ - (int) fullscreenState } else { @@ -526,7 +530,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12549,7 +12813,7 @@ - (void)postAccessibilityUpdates +@@ -12552,7 +12819,7 @@ - (void)postAccessibilityUpdates accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -535,7 +539,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12558,12 +12822,12 @@ - (void)postAccessibilityUpdates +@@ -12561,12 +12828,12 @@ - (void)postAccessibilityUpdates } /* If tree is stale, rebuild FIRST so we don't iterate freed 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 29af831..0fdc1f8 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: Daneel +From: Martin Sukany Date: Mon, 2 Mar 2026 18:49:13 +0100 Subject: [PATCH 8/8] ns: announce child frame completion candidates for 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. +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. * 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(+), 64 deletions(-) + 4 files changed, 510 insertions(+), 68 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 -@@ -1275,7 +1275,13 @@ If a completion candidate is selected (overlay or child frame), +@@ -1278,7 +1278,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; -@@ -1393,7 +1399,7 @@ so the visual offset is (ov_line + 1) * line_h from +@@ -1396,7 +1402,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)) { -@@ -3571,7 +3577,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) +@@ -3574,7 +3580,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 -@@ -7407,6 +7413,117 @@ visual line index for Zoom (skip whitespace-only lines +@@ -7413,6 +7419,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 -@@ -7440,9 +7557,13 @@ visual line index for Zoom (skip whitespace-only lines +@@ -7446,9 +7563,13 @@ visual line index for Zoom (skip whitespace-only lines return @""; specpdl_ref count = SPECPDL_INDEX (); @@ -326,14 +326,13 @@ 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); -@@ -8605,6 +8726,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range +@@ -8611,6 +8732,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range [self ensureTextCache]; @@ -345,7 +344,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 -@@ -9053,20 +9179,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point +@@ -9059,20 +9185,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point && granularity == ns_ax_text_selection_granularity_character); @@ -394,7 +393,7 @@ index 8ef344d9fe..1acb64630a 100644 ns_ax_post_notification_with_info ( self, NSAccessibilitySelectedTextChangedNotification, -@@ -9166,12 +9310,17 @@ user expectation ("w" jumps to next word and reads it). */ +@@ -9172,12 +9316,17 @@ user expectation ("w" jumps to next word and reads it). */ } } @@ -407,7 +406,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. @@ -417,7 +416,7 @@ index 8ef344d9fe..1acb64630a 100644 if (cachedText && granularity == ns_ax_text_selection_granularity_line) { -@@ -9236,6 +9385,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b +@@ -9242,6 +9391,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b block_input (); specpdl_ref count2 = SPECPDL_INDEX (); @@ -429,7 +428,7 @@ index 8ef344d9fe..1acb64630a 100644 record_unwind_protect_void (unblock_input); record_unwind_current_buffer (); if (b != current_buffer) -@@ -9412,12 +9566,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f +@@ -9418,12 +9572,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f if (!b) return; @@ -437,7 +436,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; @@ -459,7 +458,7 @@ index 8ef344d9fe..1acb64630a 100644 if (modiff != self.cachedModiff) { self.cachedModiff = modiff; -@@ -9431,6 +9602,7 @@ Text property changes (e.g. face updates from +@@ -9437,6 +9608,7 @@ Text property changes (e.g. face updates from { self.cachedCharsModiff = chars_modiff; [self postTextChangedNotification:point]; @@ -467,7 +466,7 @@ index 8ef344d9fe..1acb64630a 100644 } } -@@ -9453,8 +9625,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9459,8 +9631,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. @@ -485,7 +484,7 @@ index 8ef344d9fe..1acb64630a 100644 goto skip_overlay_scan; int selected_line = -1; -@@ -9500,7 +9679,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9506,7 +9685,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property self.cachedPoint = point; self.cachedMarkActive = markActive; @@ -505,7 +504,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 +@@ -9518,6 +9708,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property /* --- Granularity detection --- */ NSInteger granularity = ns_ax_text_selection_granularity_unknown; @@ -513,7 +512,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 +@@ -9532,7 +9723,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property NSRange newLine = [cachedText lineRangeForRange: NSMakeRange (newIdx, 0)]; if (oldLine.location != newLine.location) @@ -522,7 +521,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, evil j/k, outline-next-heading, etc. --- is ++ C-n/C-p, 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. */ @@ -533,7 +532,7 @@ index 8ef344d9fe..1acb64630a 100644 else { NSUInteger dist = (newIdx > oldIdx -@@ -9548,34 +9750,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property +@@ -9554,38 +9756,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property granularity = ns_ax_text_selection_granularity_line; } @@ -545,13 +544,19 @@ index 8ef344d9fe..1acb64630a 100644 - 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 @@ -581,7 +586,7 @@ index 8ef344d9fe..1acb64630a 100644 { NSWindow *win = [self.emacsView window]; if (win) -@@ -9734,6 +9925,13 @@ - (NSRect)accessibilityFrame +@@ -9740,6 +9931,13 @@ - (NSRect)accessibilityFrame if (vis_start >= vis_end) return @[]; @@ -595,7 +600,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 +@@ -10046,6 +10244,10 @@ - (void)dealloc #endif [accessibilityElements release]; @@ -606,7 +611,7 @@ index 8ef344d9fe..1acb64630a 100644 [[self menu] release]; [super dealloc]; } -@@ -11489,6 +11691,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f +@@ -11495,6 +11697,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f windowClosing = NO; processingCompose = NO; @@ -616,7 +621,7 @@ index 8ef344d9fe..1acb64630a 100644 scrollbarsNeedingUpdate = 0; fs_state = FULLSCREEN_NONE; fs_before_fs = next_maximized = -1; -@@ -12797,6 +13002,161 @@ - (id)accessibilityFocusedUIElement +@@ -12803,6 +13008,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. */ @@ -778,7 +783,7 @@ index 8ef344d9fe..1acb64630a 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12807,11 +13167,69 @@ - (void)postAccessibilityUpdates +@@ -12813,11 +13173,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 a73d255..3c3e9c7 100644 --- a/patches/README.txt +++ b/patches/README.txt @@ -88,7 +88,9 @@ ARCHITECTURE PERFORMANCE ----------- - ns-accessibility-enabled (DEFVAR_BOOL, default t): + ns-accessibility-enabled (DEFVAR_BOOL, default nil): + Set automatically at startup when macOS Zoom or an assistive + technology (VoiceOver, Switch Control) is detected. 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. @@ -117,7 +119,7 @@ KNOWN LIMITATIONS - Overlay face matching: string containment ("current", "selected") - GNUstep excluded (#ifdef NS_IMPL_COCOA) - No multi-frame coordination - - Child frame static lastCandidate leaks at exit (minor) + - Child frame childFrameLastCandidate (per-view ivar, freed in dealloc) TESTING diff --git a/patches/TESTING.txt b/patches/TESTING.txt index 03a9269..333921d 100644 --- a/patches/TESTING.txt +++ b/patches/TESTING.txt @@ -112,7 +112,8 @@ PASS — Folded text NOT read, unfolded text read correctly. 11. ERT — ns-accessibility-enabled Variable -------------------------------------------- -PASS — ns-accessibility-enabled bound, defaults to t. +PASS — ns-accessibility-enabled bound, defaults to nil. + Set automatically to t when AT is detected at startup. 12. VoiceOver — Overlay Completion (Patch 0007) ------------------------------------------------ @@ -143,3 +144,10 @@ 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. diff --git a/patches/fix_0008.py b/patches/fix_0008.py new file mode 100644 index 0000000..517ed51 --- /dev/null +++ b/patches/fix_0008.py @@ -0,0 +1,50 @@ +import sys + +with open('0008-ns-announce-child-frame-completion-candidates-for-Vo.patch', 'r') as f: + lines = f.readlines() + +# Find and fix the problematic hunk +new_lines = [] +i = 0 +while i < len(lines): + line = lines[i] + + # Skip lines 555-562 (0-indexed: 554-561) - the "If Emacs moved" DELETE block + if i == 554: + # Skip 8 lines (555-562) + i += 8 + continue + + # Skip lines 568-575 (0-indexed: 567-574) after adjustment (now 559-566) + # After removing 8 lines, line 568 is now 560 in 0-indexed terms: 567-8=559 + if i == 559: + # Skip 8 lines (568-575 became 560-567) + i += 8 + continue + + # Change context lines to ADD lines + # Line 566 (0-indexed 565, after adjustments 565-8=557) + if i == 557: + # Change the context line to ADD + if line.startswith(' '): + line = '+' + line[1:] + # Line 567 (0-indexed 566, after adjustments 558) + if i == 558: + if line.startswith(' '): + line = '+' + line[1:] + # Line 581 (0-indexed 580, after adjustments: 580-16=564) + if i == 564: + if line.startswith(' '): + line = '+' + line[1:] + # Line 582 (0-indexed 581, after adjustments: 565) + if i == 565: + if line.startswith(' '): + line = '+' + line[1:] + + new_lines.append(line) + i += 1 + +with open('0008-ns-announce-child-frame-completion-candidates-for-Vo.patch', 'w') as f: + f.writelines(new_lines) + +print("Done")