patches: add Zoom completion tracking (overlay + child frame)

Zoom patch 0000 now tracks completion candidates:
- Overlay: Vertico, Icomplete, Ivy (face heuristic on before-string)
- Child frame: Corfu, Company-box (scan buffer text for selected face)
Also fixes duplicate lastCursorRect ivar when applied with VoiceOver.
This commit is contained in:
2026-03-01 03:38:58 +01:00
parent 9110eee881
commit b283068f82
9 changed files with 340 additions and 86 deletions

View File

@@ -1,4 +1,4 @@
From 8a45d478b23b3bc2a1ae039493ba90b07eb89c72 Mon Sep 17 00:00:00 2001
From 45076d26a15ae82b489349d481f3c1a1792730a5 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 22:39:35 +0100
Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking
@@ -6,32 +6,46 @@ Subject: [PATCH 1/9] 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.
Basic cursor tracking:
* src/nsterm.h (EmacsView): Add lastCursorRect, zoomCursorUpdated.
* src/nsterm.m (ns_draw_window_cursor): Store cursor rect in
lastCursorRect; call UAZoomChangeFocus with CG-space coordinates
when UAZoomEnabled returns true. Set zoomCursorUpdated flag.
(ns_update_end): Call UAZoomChangeFocus as fallback when cursor
was not physically redrawn in this cycle (e.g., after C-x o window
switch). Gated by zoomCursorUpdated to avoid double calls.
was not physically redrawn (e.g. after C-x o window switch).
Gated by zoomCursorUpdated to avoid double calls.
Completion candidate tracking:
* src/nsterm.m (ns_zoom_face_is_selected): New predicate.
Match 'current', 'selected', and 'selection' in face symbol
names to identify the highlighted completion candidate.
(ns_zoom_find_overlay_candidate_line): Scan overlay
before-string/after-string for the selected candidate line.
Handles Vertico, Icomplete, Ivy, and similar overlay frameworks.
(ns_zoom_find_child_frame_candidate): Scan child frame buffer
text for the selected candidate. Handles Corfu, Company-box,
and similar child frame frameworks.
(ns_zoom_track_completion): Called from ns_update_end after
cursor tracking. Overrides Zoom focus to the selected
completion candidate when one is found.
Coordinate conversion: EmacsView pixels (AppKit, flipped) ->
NSWindow -> NSScreen -> CGRect with y-flip for CoreGraphics
top-left origin. UAZoomEnabled returns false when Zoom is inactive,
so overhead is a single function call per redisplay cycle.
top-left origin.
Tested on macOS 14 with Zoom enabled: cursor tracking works across
window splits, switches (C-x o), and normal navigation.
window splits, switches (C-x o), and completion frameworks.
---
etc/NEWS | 8 +++++++
src/nsterm.h | 6 +++++
src/nsterm.m | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 82 insertions(+)
etc/NEWS | 11 ++
src/nsterm.h | 6 ++
src/nsterm.m | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 302 insertions(+)
diff --git a/etc/NEWS b/etc/NEWS
index ef36df5..f10d17e 100644
index ef36df5..80661a9 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -82,6 +82,14 @@ 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
@@ -41,7 +55,10 @@ index ef36df5..f10d17e 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.
+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.
@@ -64,10 +81,228 @@ index 7c1ee4c..ea6e7ba 100644
}
diff --git a/src/nsterm.m b/src/nsterm.m
index 74e4ad5..cd721c8 100644
index 74e4ad5..05ec3d1 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1104,6 +1104,35 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
@@ -1081,6 +1081,217 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
}
+
+#ifdef NS_IMPL_COCOA
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+
+/* Check whether FACE (a Lisp symbol or list) has a name suggesting
+ it marks the currently selected completion candidate. Matches
+ vertico-current, icomplete-selected-match, ivy-current-match,
+ company-tooltip-selection, corfu-current, and similar. */
+static bool
+ns_zoom_face_is_selected (Lisp_Object face)
+{
+ if (SYMBOLP (face) && !NILP (face))
+ {
+ const char *name = SSDATA (SYMBOL_NAME (face));
+ if (strstr (name, "current")
+ || strstr (name, "selected")
+ || strstr (name, "selection"))
+ return true;
+ }
+ /* Handle face list (face1 face2 ...). */
+ if (CONSP (face))
+ {
+ Lisp_Object tail;
+ for (tail = face; CONSP (tail); tail = XCDR (tail))
+ if (ns_zoom_face_is_selected (XCAR (tail)))
+ return true;
+ }
+ return false;
+}
+
+/* Scan overlay before-string / after-string properties in the
+ selected window for a completion candidate with a "selected"
+ face. Return the 0-based visual line index of the selected
+ candidate, or -1 if none found. */
+static int
+ns_zoom_find_overlay_candidate_line (struct window *w)
+{
+ struct buffer *b = XBUFFER (w->contents);
+ ptrdiff_t beg = marker_position (w->start);
+ ptrdiff_t end = ZV_S (b);
+ Lisp_Object overlays = Foverlays_in (make_fixnum (beg),
+ make_fixnum (end));
+ Lisp_Object tail;
+
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail))
+ {
+ Lisp_Object ov = XCAR (tail);
+ Lisp_Object str = Foverlay_get (ov, Qbefore_string);
+
+ if (NILP (str))
+ str = Foverlay_get (ov, Qafter_string);
+ if (!STRINGP (str) || SCHARS (str) < 2)
+ continue;
+
+ /* Walk the string line by line, checking faces. */
+ ptrdiff_t len = SCHARS (str);
+ int line = 0;
+ ptrdiff_t line_start = 0;
+
+ for (ptrdiff_t i = 0; i <= len; i++)
+ {
+ bool at_newline = (i == len
+ || SREF (str, i) == '\n');
+ if (at_newline && i > line_start)
+ {
+ /* Check the face at line_start. */
+ Lisp_Object face
+ = Fget_text_property (make_fixnum (line_start),
+ Qface, str);
+ if (ns_zoom_face_is_selected (face))
+ return line;
+ line++;
+ line_start = i + 1;
+ }
+ else if (at_newline)
+ {
+ line++;
+ line_start = i + 1;
+ }
+ }
+ }
+ return -1;
+}
+
+/* Scan child frames for a completion popup with a selected
+ candidate. Return the 0-based line index, or -1 if none.
+ Set *CHILD_FRAME to the child frame if found. */
+static int
+ns_zoom_find_child_frame_candidate (struct frame *f,
+ struct frame **child_frame)
+{
+ Lisp_Object frames, tail;
+
+ FOR_EACH_FRAME (tail, frames)
+ {
+ struct frame *cf = XFRAME (frames);
+ if (!FRAME_NS_P (cf) || !FRAME_LIVE_P (cf))
+ continue;
+ if (FRAME_PARENT_FRAME (cf) != f)
+ continue;
+ /* Small buffer = likely completion popup. */
+ struct buffer *b = XBUFFER (cf->current_buffer);
+ if (BUF_ZV (b) - BUF_BEGV (b) > 10000)
+ continue;
+
+ ptrdiff_t beg = BUF_BEGV (b);
+ ptrdiff_t zv = BUF_ZV (b);
+ int line = 0;
+
+ ptrdiff_t pos = beg;
+ while (pos < zv)
+ {
+ Lisp_Object face
+ = Fget_char_property (make_fixnum (pos), Qface,
+ cf->current_buffer);
+ if (ns_zoom_face_is_selected (face))
+ {
+ *child_frame = cf;
+ return line;
+ }
+ /* Advance to next line. */
+ ptrdiff_t next = find_newline (pos, -1, zv, -1,
+ 1, NULL, NULL, false);
+ if (next <= pos)
+ break;
+ pos = next;
+ line++;
+ }
+ }
+ return -1;
+}
+
+/* Update Zoom focus based on completion candidates.
+ Called from ns_update_end after normal cursor tracking.
+ If a completion candidate is selected (overlay or child frame),
+ move Zoom to that candidate instead of the text cursor. */
+static void
+ns_zoom_track_completion (struct frame *f, EmacsView *view)
+{
+ if (!UAZoomEnabled ())
+ return;
+
+ struct window *w = XWINDOW (f->selected_window);
+ int line_h = FRAME_LINE_HEIGHT (f);
+
+ /* 1. Check overlay completions (Vertico, Icomplete, Ivy). */
+ int ov_line = ns_zoom_find_overlay_candidate_line (w);
+ if (ov_line >= 0)
+ {
+ /* Overlay candidates typically start after the input line,
+ so the visual offset is (ov_line + 1) * line_h from
+ the window top. */
+ int y_off = (ov_line + 1) * line_h;
+ if (y_off < w->pixel_height)
+ {
+ NSRect r = NSMakeRect (
+ WINDOW_TEXT_TO_FRAME_PIXEL_X (w, 0),
+ WINDOW_TO_FRAME_PIXEL_Y (w, y_off),
+ FRAME_COLUMN_WIDTH (f),
+ line_h);
+
+ NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect
+ = [[view window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ return;
+ }
+ }
+
+ /* 2. Check child frame completions (Corfu, Company-box). */
+ struct frame *cf = NULL;
+ int cf_line = ns_zoom_find_child_frame_candidate (f, &cf);
+ if (cf_line >= 0 && cf)
+ {
+ EmacsView *cv = FRAME_NS_VIEW (cf);
+ struct window *cw
+ = XWINDOW (cf->selected_window);
+ int cf_line_h = FRAME_LINE_HEIGHT (cf);
+ int y_off = cf_line * cf_line_h;
+
+ NSRect r = NSMakeRect (
+ WINDOW_TEXT_TO_FRAME_PIXEL_X (cw, 0),
+ WINDOW_TO_FRAME_PIXEL_Y (cw, y_off),
+ FRAME_COLUMN_WIDTH (cf),
+ cf_line_h);
+
+ NSRect windowRect = [cv convertRect:r toView:nil];
+ NSRect screenRect
+ = [[cv window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ }
+}
+
+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
+#endif /* NS_IMPL_COCOA */
+
static void
ns_update_end (struct frame *f)
/* --------------------------------------------------------------------------
@@ -1104,6 +1315,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
unblock_input ();
ns_updating_frame = NULL;
@@ -99,11 +334,17 @@ index 74e4ad5..cd721c8 100644
+ if (view)
+ view->zoomCursorUpdated = NO;
+#endif
+
+ /* Track completion candidates for Zoom (overlay and child frame).
+ Runs after cursor tracking so the selected candidate overrides
+ the default cursor position. */
+ if (view)
+ ns_zoom_track_completion (f, view);
+#endif /* NS_IMPL_COCOA */
}
static void
@@ -3232,6 +3261,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
@@ -3232,6 +3478,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));

View File

@@ -1,4 +1,4 @@
From 67bf4786bb3896241bf7ab75cdada0e3da924ec5 Mon Sep 17 00:00:00 2001
From 59ec39bdda532d02c748a7e27f9a90ba3be3c338 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 2/9] ns: add accessibility base classes and text extraction
@@ -188,7 +188,7 @@ index ea6e7ba..6e830de 100644
diff --git a/src/nsterm.m b/src/nsterm.m
index cd721c8..1320a9f 100644
index 05ec3d1..a4d0e02 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -199,7 +199,7 @@ index cd721c8..1320a9f 100644
#include "systime.h"
#include "character.h"
#include "xwidget.h"
@@ -6924,6 +6925,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
@@ -7141,6 +7142,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
}
#endif
@@ -630,7 +630,7 @@ index cd721c8..1320a9f 100644
/* ==========================================================================
EmacsView implementation
@@ -11380,6 +11805,28 @@ Convert an X font name (XLFD) to an NS font name.
@@ -11597,6 +12022,28 @@ 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");
@@ -659,7 +659,7 @@ index cd721c8..1320a9f 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));
@@ -11528,6 +11975,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
@@ -11745,6 +12192,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;

View File

@@ -1,4 +1,4 @@
From 80e590420ce74bb886849d91c245d0c0bece39bf Mon Sep 17 00:00:00 2001
From 80f6de2020c50717e2238528e6241e00017f7230 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 3/9] ns: implement buffer accessibility element (core
@@ -22,10 +22,10 @@ line-by-line navigation, word/character announcements.
1 file changed, 1097 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m
index 1320a9f..ab9a287 100644
index a4d0e02..e281073 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7346,6 +7346,1103 @@ - (id)accessibilityTopLevelUIElement
@@ -7563,6 +7563,1103 @@ - (id)accessibilityTopLevelUIElement
@end

View File

@@ -1,4 +1,4 @@
From afadafa16eef6e8e6d01ef4f340c451132198ba8 Mon Sep 17 00:00:00 2001
From 672223b6ced41898156aa476b8c77b27d3719e4a Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 4/9] ns: add buffer notification dispatch and mode-line
@@ -24,10 +24,10 @@ region selection feedback, completion popups, mode-line reading.
1 file changed, 545 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m
index ab9a287..330667d 100644
index e281073..a29f039 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -8443,6 +8443,551 @@ - (NSRect)accessibilityFrame
@@ -8660,6 +8660,551 @@ - (NSRect)accessibilityFrame
@end

View File

@@ -1,4 +1,4 @@
From 010ec7a6e568653e12b904c5bb812a1b3c8e52cf Mon Sep 17 00:00:00 2001
From 64b373bebadd9b9c52c90da297173ea108c6e394 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 5/9] ns: add interactive span elements for Tab navigation
@@ -17,10 +17,10 @@ Tested on macOS 14. Verified: Tab-cycling through org-mode links,
1 file changed, 286 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m
index 330667d..907ce47 100644
index a29f039..86bb7b6 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -8988,6 +8988,292 @@ - (NSRect)accessibilityFrame
@@ -9205,6 +9205,292 @@ - (NSRect)accessibilityFrame
@end

View File

@@ -1,4 +1,4 @@
From f10036eeadf681cd87bbec7ec0581b572053c38a Mon Sep 17 00:00:00 2001
From be594f584a94e6fccd649be029c1af30242a12ee Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 6/9] ns: integrate accessibility with EmacsView and redisplay
@@ -23,14 +23,14 @@ block cursor, org-mode folded headings, indirect buffers.
Known limitations documented in patch 6 Texinfo node.
---
etc/NEWS | 13 ++
src/nsterm.m | 369 +++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 374 insertions(+), 8 deletions(-)
src/nsterm.m | 376 +++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 376 insertions(+), 13 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS
index f10d17e..f48d05b 100644
index 80661a9..2b1f9e6 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -4397,6 +4397,19 @@ allowing Emacs users access to speech recognition utilities.
@@ -4400,6 +4400,19 @@ allowing Emacs users access to speech recognition utilities.
Note: Accepting this permission allows the use of system APIs, which may
send user data to Apple's speech recognition servers.
@@ -51,20 +51,28 @@ index f10d17e..f48d05b 100644
** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.m b/src/nsterm.m
index 907ce47..d813274 100644
index 86bb7b6..2e8ae20 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1133,6 +1133,9 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
@@ -1343,13 +1343,16 @@ so the visual offset is (ov_line + 1) * line_h from
}
if (view)
view->zoomCursorUpdated = NO;
#endif
-#endif
/* Track completion candidates for Zoom (overlay and child frame).
Runs after cursor tracking so the selected candidate overrides
the default cursor position. */
if (view)
ns_zoom_track_completion (f, view);
+#endif
+
+ /* Post accessibility notifications after each redisplay cycle. */
+ [view postAccessibilityUpdates];
#endif /* NS_IMPL_COCOA */
}
@@ -3263,11 +3266,11 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
@@ -3480,15 +3483,12 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
#ifdef NS_IMPL_COCOA
@@ -73,15 +81,20 @@ index 907ce47..d813274 100644
- element to keep the zoomed viewport centered on the cursor.
-
- Coordinate conversion:
+ /* Store cursor rect and inform macOS Zoom / VoiceOver.
+ lastCursorRect is used by:
+ - Zoom: UAZoomChangeFocus below (unconditional when active)
+ - VoiceOver: accessibilityBoundsForRange: fallback
- EmacsView pixels (AppKit, flipped, top-left origin)
- -> NSWindow (convertRect:toView:nil)
- -> NSScreen (convertRectToScreen:)
- -> CGRect with y-flip for CoreGraphics top-left origin. */
+ /* Store cursor rect for Zoom and VoiceOver bounds queries.
+ Zoom: UAZoomChangeFocus below.
+ VoiceOver: accessibilityBoundsForRange: fallback.
+ Coordinate conversion for Zoom:
EmacsView pixels (AppKit, flipped, top-left origin)
-> NSWindow (convertRect:toView:nil)
-> NSScreen (convertRectToScreen:)
@@ -7349,7 +7352,6 @@ - (id)accessibilityTopLevelUIElement
+ EmacsView (AppKit, flipped) -> NSWindow -> NSScreen
+ -> CGRect with y-flip for CoreGraphics top-left origin. */
{
EmacsView *view = FRAME_NS_VIEW (f);
if (view && on_p && active_p)
@@ -7566,7 +7566,6 @@ - (id)accessibilityTopLevelUIElement
@@ -89,7 +102,7 @@ index 907ce47..d813274 100644
static BOOL
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
ptrdiff_t *out_start,
@@ -8444,7 +8446,6 @@ - (NSRect)accessibilityFrame
@@ -8661,7 +8660,6 @@ - (NSRect)accessibilityFrame
@end
@@ -97,7 +110,7 @@ index 907ce47..d813274 100644
/* ===================================================================
EmacsAccessibilityBuffer (Notifications) — AX event dispatch
@@ -8989,7 +8990,6 @@ - (NSRect)accessibilityFrame
@@ -9206,7 +9204,6 @@ - (NSRect)accessibilityFrame
@end
@@ -105,7 +118,7 @@ index 907ce47..d813274 100644
/* ===================================================================
EmacsAccessibilityInteractiveSpan — helpers and implementation
=================================================================== */
@@ -9319,6 +9319,7 @@ - (void)dealloc
@@ -9536,6 +9533,7 @@ - (void)dealloc
[layer release];
#endif
@@ -113,7 +126,7 @@ index 907ce47..d813274 100644
[[self menu] release];
[super dealloc];
}
@@ -10667,6 +10668,32 @@ - (void)windowDidBecomeKey /* for direct calls */
@@ -10884,6 +10882,32 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -146,7 +159,7 @@ index 907ce47..d813274 100644
}
@@ -11904,6 +11931,332 @@ - (int) fullscreenState
@@ -12121,6 +12145,332 @@ - (int) fullscreenState
return fs_state;
}

View File

@@ -1,4 +1,4 @@
From 40bf8a0163b876e1d8f71fa8bd3c4aaa2a28ad52 Mon Sep 17 00:00:00 2001
From 10ae74cefffd3733b8ff60e07a2b7f0dcb4d62cb Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 7/9] doc: add VoiceOver accessibility section to macOS

View File

@@ -1,4 +1,4 @@
From 456c6a734fb94943b1d9acfaa8c1ffd0f1321ab5 Mon Sep 17 00:00:00 2001
From 43b0625a7b9ce634006811e814fff037af8a51cd Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver
@@ -61,10 +61,10 @@ index 6e830de..2102fb9 100644
@property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
diff --git a/src/nsterm.m b/src/nsterm.m
index d813274..9e089f3 100644
index 2e8ae20..b9b2a80 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -6938,11 +6938,154 @@ Accessibility virtual elements (macOS / Cocoa only)
@@ -7152,11 +7152,154 @@ Accessibility virtual elements (macOS / Cocoa only)
/* ---- Helper: extract buffer text for accessibility ---- */
@@ -220,7 +220,7 @@ index d813274..9e089f3 100644
static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -7013,7 +7156,7 @@ Accessibility virtual elements (macOS / Cocoa only)
@@ -7227,7 +7370,7 @@ Accessibility virtual elements (macOS / Cocoa only)
/* Extract this visible run's text. Use
Fbuffer_substring_no_properties which correctly handles the
@@ -229,7 +229,7 @@ index d813274..9e089f3 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));
@@ -7094,7 +7237,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font)
@@ -7308,7 +7451,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font)
return NSZeroRect;
/* charpos_start and charpos_len are already in buffer charpos
@@ -238,7 +238,7 @@ index d813274..9e089f3 100644
charposForAccessibilityIndex which handles invisible text. */
ptrdiff_t cp_start = charpos_start;
ptrdiff_t cp_end = cp_start + charpos_len;
@@ -7573,6 +7716,7 @@ @implementation EmacsAccessibilityBuffer
@@ -7787,6 +7930,7 @@ @implementation EmacsAccessibilityBuffer
@synthesize cachedOverlayModiff;
@synthesize cachedTextStart;
@synthesize cachedModiff;
@@ -246,7 +246,7 @@ index d813274..9e089f3 100644
@synthesize cachedPoint;
@synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement;
@@ -7670,7 +7814,7 @@ - (void)ensureTextCache
@@ -7884,7 +8028,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
@@ -255,7 +255,7 @@ index d813274..9e089f3 100644
write section at the end needs synchronization to protect
against concurrent reads from AX server thread. */
eassert ([NSThread isMainThread]);
@@ -7683,16 +7827,15 @@ - (void)ensureTextCache
@@ -7897,16 +8041,15 @@ - (void)ensureTextCache
return;
ptrdiff_t modiff = BUF_MODIFF (b);
@@ -278,7 +278,7 @@ index d813274..9e089f3 100644
&& cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart
&& (textLen == 0
@@ -7709,7 +7852,6 @@ - (void)ensureTextCache
@@ -7923,7 +8066,6 @@ - (void)ensureTextCache
[cachedText release];
cachedText = [text retain];
cachedTextModiff = modiff;
@@ -286,7 +286,7 @@ index d813274..9e089f3 100644
cachedTextStart = start;
if (visibleRuns)
@@ -7774,7 +7916,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
@@ -7988,7 +8130,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
@@ -295,7 +295,7 @@ index d813274..9e089f3 100644
NSUInteger lo = 0, hi = visibleRunCount;
while (lo < hi)
{
@@ -7787,7 +7929,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
@@ -8001,7 +8143,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
else
{
/* Found: charpos is inside this run. Compute UTF-16 delta
@@ -304,7 +304,7 @@ index d813274..9e089f3 100644
NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
if (chars_in == 0 || !cachedText)
return r->ax_start;
@@ -7812,10 +7954,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
@@ -8026,10 +8168,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
/* Convert accessibility string index to buffer charpos.
Safe to call from any thread: uses only cachedText (NSString) and
@@ -317,7 +317,7 @@ index d813274..9e089f3 100644
@synchronized (self)
{
if (visibleRunCount == 0)
@@ -7849,7 +7991,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
@@ -8063,7 +8205,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
return cp;
}
}
@@ -326,7 +326,7 @@ index d813274..9e089f3 100644
if (lo > 0)
{
ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
@@ -7871,7 +8013,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
@@ -8085,7 +8227,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
deadlocking the AX server thread. This is prevented by:
1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
@@ -335,7 +335,7 @@ index d813274..9e089f3 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
@@ -8225,6 +8367,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
@@ -8439,6 +8581,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
return [self lineForAXIndex:point_idx];
}
@@ -386,7 +386,7 @@ index d813274..9e089f3 100644
- (NSRange)accessibilityRangeForLine:(NSInteger)line
{
if (![NSThread isMainThread])
@@ -8447,7 +8633,7 @@ - (NSRect)accessibilityFrame
@@ -8661,7 +8847,7 @@ - (NSRect)accessibilityFrame
/* ===================================================================
@@ -395,7 +395,7 @@ index d813274..9e089f3 100644
These methods notify VoiceOver of text and selection changes.
Called from the redisplay cycle (postAccessibilityUpdates).
@@ -8462,7 +8648,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8676,7 +8862,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
if (point > self.cachedPoint
&& point - self.cachedPoint == 1)
{
@@ -404,7 +404,7 @@ index d813274..9e089f3 100644
[self invalidateTextCache];
[self ensureTextCache];
if (cachedText)
@@ -8481,7 +8667,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8695,7 +8881,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
@@ -413,7 +413,7 @@ index d813274..9e089f3 100644
self.cachedPoint = point;
NSDictionary *change = @{
@@ -8814,14 +9000,72 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
@@ -9028,14 +9214,72 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */
@@ -488,7 +488,7 @@ index d813274..9e089f3 100644
per the WebKit/Chromium pattern. */
else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
{
@@ -8991,7 +9235,7 @@ - (NSRect)accessibilityFrame
@@ -9205,7 +9449,7 @@ - (NSRect)accessibilityFrame
/* ===================================================================
@@ -497,7 +497,7 @@ index d813274..9e089f3 100644
=================================================================== */
/* Scan visible range of window W for interactive spans.
@@ -9199,7 +9443,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@@ -9413,7 +9657,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
dispatch_async (dispatch_get_main_queue (), ^{
/* lwin is a Lisp_Object captured by value. This is GC-safe
because Lisp_Objects are tagged integers/pointers that
@@ -506,7 +506,7 @@ index d813274..9e089f3 100644
Emacs. The WINDOW_LIVE_P check below guards against the
window being deleted between capture and execution. */
if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin)))
@@ -9225,7 +9469,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@@ -9439,7 +9683,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@end
@@ -515,7 +515,7 @@ index d813274..9e089f3 100644
Methods are kept here (same .m file) so they access the ivars
declared in the @interface ivar block. */
@implementation EmacsAccessibilityBuffer (InteractiveSpans)
@@ -11947,7 +12191,7 @@ - (int) fullscreenState
@@ -12161,7 +12405,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w))
{
@@ -524,7 +524,7 @@ index d813274..9e089f3 100644
EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem)
@@ -11981,7 +12225,7 @@ - (int) fullscreenState
@@ -12195,7 +12439,7 @@ - (int) fullscreenState
}
else
{
@@ -533,7 +533,7 @@ index d813274..9e089f3 100644
Lisp_Object child = w->contents;
while (!NILP (child))
{
@@ -12093,7 +12337,7 @@ - (void)postAccessibilityUpdates
@@ -12307,7 +12551,7 @@ - (void)postAccessibilityUpdates
accessibilityUpdating = YES;
/* Detect window tree change (split, delete, new buffer). Compare
@@ -542,7 +542,7 @@ index d813274..9e089f3 100644
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow))
{
@@ -12102,12 +12346,12 @@ - (void)postAccessibilityUpdates
@@ -12316,12 +12560,12 @@ - (void)postAccessibilityUpdates
}
/* If tree is stale, rebuild FIRST so we don't iterate freed

View File

@@ -1,4 +1,4 @@
From 16e60a514212d8e5e541eb731db242b2bcd1bca2 Mon Sep 17 00:00:00 2001
From 62e619d508d4ce3b3bf0f8dd959041bcd9a75350 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 9/9] ns: announce child frame completion candidates for
@@ -71,10 +71,10 @@ index 4825cf9..97777e2 100644
To disable the accessibility interface entirely (for instance, to
eliminate overhead on systems where assistive technology is not in
diff --git a/etc/NEWS b/etc/NEWS
index f48d05b..ec4b95e 100644
index 2b1f9e6..8a40850 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -4401,8 +4401,8 @@ send user data to Apple's speech recognition servers.
@@ -4404,8 +4404,8 @@ send user data to Apple's speech recognition servers.
** VoiceOver accessibility support on macOS.
Emacs now exposes buffer content, cursor position, and interactive
elements to the macOS accessibility subsystem (VoiceOver). This
@@ -109,10 +109,10 @@ index 2102fb9..2fc4de4 100644
@end
diff --git a/src/nsterm.m b/src/nsterm.m
index 9e089f3..1a623be 100644
index b9b2a80..dc49417 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7082,6 +7082,112 @@ visual line index for Zoom (skip whitespace-only lines
@@ -7296,6 +7296,112 @@ visual line index for Zoom (skip whitespace-only lines
return nil;
}
@@ -225,7 +225,7 @@ index 9e089f3..1a623be 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
@@ -12321,6 +12427,77 @@ - (id)accessibilityFocusedUIElement
@@ -12535,6 +12641,77 @@ - (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. */
@@ -303,7 +303,7 @@ index 9e089f3..1a623be 100644
- (void)postAccessibilityUpdates
{
NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12331,11 +12508,59 @@ - (void)postAccessibilityUpdates
@@ -12545,11 +12722,59 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us