patches: squash perf fixes into respective patches, clean 9-patch series

Performance fixes folded back:
- BUF_CHARS_MODIFF → patch 0002 (implement buffer accessibility element)
- UAZoomEnabled cache + rate-limit → patch 0000 (Zoom integration)

Also in patch 0000: ns_zoom_face_is_selected (standalone compilation).
Also in patch 0001: ns_accessibility_enabled defaults to nil.
This commit is contained in:
2026-03-01 05:58:42 +01:00
parent 1bf05f1e22
commit 07826b61a0
10 changed files with 205 additions and 188 deletions

View File

@@ -1,7 +1,7 @@
From e0f4378588b0fea142d707e4b51269566e50536a Mon Sep 17 00:00:00 2001 From 5637c7ec4d3511e42bca8e5b1cb628cc2d2200ad Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 22:39:35 +0100 Date: Sat, 28 Feb 2026 22:39:35 +0100
Subject: [PATCH 01/11] ns: integrate with macOS Zoom for cursor tracking Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking
Inform macOS Zoom of the text cursor position so the zoomed viewport Inform macOS Zoom of the text cursor position so the zoomed viewport
follows keyboard focus in Emacs. follows keyboard focus in Emacs.
@@ -38,8 +38,8 @@ window splits, switches (C-x o), and completion frameworks.
--- ---
etc/NEWS | 11 ++ etc/NEWS | 11 ++
src/nsterm.h | 6 + src/nsterm.h | 6 +
src/nsterm.m | 307 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 341 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 324 insertions(+) 3 files changed, 358 insertions(+)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index ef36df5..80661a9 100644 index ef36df5..80661a9 100644
@@ -81,10 +81,10 @@ index 7c1ee4c..ea6e7ba 100644
} }
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 74e4ad5..40b4bc9 100644 index 74e4ad5..f189c9e 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1081,6 +1081,236 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) @@ -1081,6 +1081,270 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
} }
@@ -93,6 +93,29 @@ index 74e4ad5..40b4bc9 100644
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ +
+/* 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
+ in ns_draw_window_cursor, ns_update_end, and ns_zoom_track_completion,
+ the overhead accumulates to ~150-600 µs 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. */
+static BOOL ns_zoom_cached_enabled;
+static CFAbsoluteTime ns_zoom_cache_time;
+
+static BOOL
+ns_zoom_enabled_p (void)
+{
+ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent ();
+ if (now - ns_zoom_cache_time > 1.0)
+ {
+ ns_zoom_cached_enabled = UAZoomEnabled ();
+ ns_zoom_cache_time = now;
+ }
+ return ns_zoom_cached_enabled;
+}
+
+/* Identify faces that mark a selected completion candidate. +/* Identify faces that mark a selected completion candidate.
+ Matches vertico-current, corfu-current, icomplete-selected-match, + Matches vertico-current, corfu-current, icomplete-selected-match,
+ ivy-current-match, etc. by checking the face symbol name. + ivy-current-match, etc. by checking the face symbol name.
@@ -241,11 +264,22 @@ index 74e4ad5..40b4bc9 100644
+static void +static void
+ns_zoom_track_completion (struct frame *f, EmacsView *view) +ns_zoom_track_completion (struct frame *f, EmacsView *view)
+{ +{
+ if (!UAZoomEnabled ()) + if (!ns_zoom_enabled_p ())
+ return; + return;
+ if (!WINDOWP (f->selected_window)) + if (!WINDOWP (f->selected_window))
+ return; + return;
+ +
+ /* Rate-limit completion scan to 2 Hz. Completion state changes at
+ human interaction speed; checking every 500 ms eliminates
+ per-redisplay FOR_EACH_FRAME overhead with no perceptible lag. */
+ {
+ static CFAbsoluteTime last_check;
+ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent ();
+ if (now - last_check < 0.5)
+ return;
+ last_check = now;
+ }
+
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ +
@@ -321,7 +355,7 @@ index 74e4ad5..40b4bc9 100644
static void static void
ns_update_end (struct frame *f) ns_update_end (struct frame *f)
/* -------------------------------------------------------------------------- /* --------------------------------------------------------------------------
@@ -1104,6 +1334,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) @@ -1104,6 +1368,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
unblock_input (); unblock_input ();
ns_updating_frame = NULL; ns_updating_frame = NULL;
@@ -333,7 +367,7 @@ index 74e4ad5..40b4bc9 100644
+ (zoomCursorUpdated is NO). */ + (zoomCursorUpdated is NO). */
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ if (view && !view->zoomCursorUpdated && UAZoomEnabled () + if (view && !view->zoomCursorUpdated && ns_zoom_enabled_p ()
+ && !NSIsEmptyRect (view->lastCursorRect)) + && !NSIsEmptyRect (view->lastCursorRect))
+ { + {
+ NSRect r = view->lastCursorRect; + NSRect r = view->lastCursorRect;
@@ -363,7 +397,7 @@ index 74e4ad5..40b4bc9 100644
} }
static void static void
@@ -3232,6 +3497,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. @@ -3232,6 +3531,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
/* Prevent the cursor from being drawn outside the text area. */ /* Prevent the cursor from being drawn outside the text area. */
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
@@ -386,7 +420,7 @@ index 74e4ad5..40b4bc9 100644
+ +
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ +#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ if (UAZoomEnabled ()) + if (ns_zoom_enabled_p ())
+ { + {
+ NSRect windowRect = [view convertRect:r toView:nil]; + NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect + NSRect screenRect
@@ -409,7 +443,7 @@ index 74e4ad5..40b4bc9 100644
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -8321,6 +8625,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -8321,6 +8659,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;

View File

@@ -1,7 +1,7 @@
From 09bb05d23a015757c7471a4b06ce8a2c852745dc Mon Sep 17 00:00:00 2001 From 5afa856fd6295a8937e3a32b2571f02ff78a8ce3 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 02/11] ns: add accessibility base classes and text extraction Subject: [PATCH 2/9] ns: add accessibility base classes and text extraction
Add the foundation for macOS VoiceOver accessibility in the NS Add the foundation for macOS VoiceOver accessibility in the NS
(Cocoa) port. No existing code paths are modified. (Cocoa) port. No existing code paths are modified.
@@ -188,7 +188,7 @@ index ea6e7ba..6e830de 100644
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 40b4bc9..96805b9 100644 index f189c9e..73b1c50 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -199,7 +199,7 @@ index 40b4bc9..96805b9 100644
#include "systime.h" #include "systime.h"
#include "character.h" #include "character.h"
#include "xwidget.h" #include "xwidget.h"
@@ -7160,6 +7161,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg @@ -7194,6 +7195,430 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
} }
#endif #endif
@@ -630,7 +630,7 @@ index 40b4bc9..96805b9 100644
/* ========================================================================== /* ==========================================================================
EmacsView implementation EmacsView implementation
@@ -11619,6 +12044,28 @@ Convert an X font name (XLFD) to an NS font name. @@ -11653,6 +12078,28 @@ Convert an X font name (XLFD) to an NS font name.
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic"); DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
@@ -659,7 +659,7 @@ index 40b4bc9..96805b9 100644
Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
@@ -11751,7 +12198,7 @@ Convert an X font name (XLFD) to an NS font name. @@ -11785,7 +12232,7 @@ Convert an X font name (XLFD) to an NS font name.
doc: /* Non-nil means to use native fullscreen on Mac OS X 10.7 and later. doc: /* Non-nil means to use native fullscreen on Mac OS X 10.7 and later.
Nil means use fullscreen the old (< 10.7) way. The old way works better with Nil means use fullscreen the old (< 10.7) way. The old way works better with
multiple monitors, but lacks tool bar. This variable is ignored on multiple monitors, but lacks tool bar. This variable is ignored on
@@ -668,7 +668,7 @@ index 40b4bc9..96805b9 100644
ns_use_native_fullscreen = YES; ns_use_native_fullscreen = YES;
ns_last_use_native_fullscreen = ns_use_native_fullscreen; ns_last_use_native_fullscreen = ns_use_native_fullscreen;
@@ -11767,10 +12214,19 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with @@ -11801,10 +12248,19 @@ 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. */); This variable is ignored on Mac OS X < 10.7 and GNUstep. */);
ns_use_srgb_colorspace = YES; ns_use_srgb_colorspace = YES;
@@ -689,7 +689,7 @@ index 40b4bc9..96805b9 100644
ns_use_mwheel_acceleration = YES; ns_use_mwheel_acceleration = YES;
DEFVAR_LISP ("ns-mwheel-line-height", ns_mwheel_line_height, DEFVAR_LISP ("ns-mwheel-line-height", ns_mwheel_line_height,
@@ -11781,7 +12237,7 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with @@ -11815,7 +12271,7 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
DEFVAR_BOOL ("ns-use-mwheel-momentum", ns_use_mwheel_momentum, DEFVAR_BOOL ("ns-use-mwheel-momentum", ns_use_mwheel_momentum,
doc: /* Non-nil means mouse wheel scrolling uses momentum. doc: /* Non-nil means mouse wheel scrolling uses momentum.

View File

@@ -1,7 +1,7 @@
From d21a6a4a91093d5be0a9f1324250846610fc735f Mon Sep 17 00:00:00 2001 From a820d8168b8fabaf4255fb7eefe5bbeb6301fe47 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 03/11] ns: implement buffer accessibility element (core Subject: [PATCH 3/9] ns: implement buffer accessibility element (core
protocol) protocol)
Implement the NSAccessibility text protocol for Emacs buffer windows. Implement the NSAccessibility text protocol for Emacs buffer windows.
@@ -18,14 +18,14 @@ setAccessibilityFocused.
Tested on macOS 14 with VoiceOver. Verified: buffer reading, Tested on macOS 14 with VoiceOver. Verified: buffer reading,
line-by-line navigation, word/character announcements. line-by-line navigation, word/character announcements.
--- ---
src/nsterm.m | 1097 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 1104 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1097 insertions(+) 1 file changed, 1104 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 96805b9..ae4c581 100644 index 73b1c50..73e56d3 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7582,6 +7582,1103 @@ - (id)accessibilityTopLevelUIElement @@ -7616,6 +7616,1110 @@ - (id)accessibilityTopLevelUIElement
@end @end
@@ -363,17 +363,25 @@ index 96805b9..ae4c581 100644
+ if (!b) + if (!b)
+ return; + return;
+ +
+ ptrdiff_t modiff = BUF_MODIFF (b); + /* Use BUF_CHARS_MODIFF, not BUF_MODIFF, for cache validity.
+ ptrdiff_t overlay_modiff = BUF_OVERLAY_MODIFF (b); + BUF_MODIFF is bumped by every text-property change, including
+ font-lock face applications on every redisplay. AX text contains
+ only characters, not face data, so property-only changes do not
+ affect the cached value. Rebuilding the full buffer text on
+ each font-lock pass is O(buffer-size) per redisplay --- this
+ causes progressive slowdown when scrolling through large files.
+ BUF_CHARS_MODIFF is bumped only on actual character insertions
+ and deletions, matching the semantic of "did the text change".
+ This is the pattern used by WebKit and NSTextView.
+ Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
+ included in the cached AX text (it is handled separately via
+ explicit announcements in postAccessibilityNotificationsForFrame).
+ Including overlay_modiff would silently update cachedOverlayModiff
+ and prevent the notification dispatch from detecting changes. */
+ ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b);
+ ptrdiff_t pt = BUF_PT (b); + ptrdiff_t pt = BUF_PT (b);
+ NSUInteger textLen = cachedText ? [cachedText length] : 0; + NSUInteger textLen = cachedText ? [cachedText length] : 0;
+ /* Track both BUF_MODIFF and BUF_OVERLAY_MODIFF. Overlay-only + if (cachedText && cachedTextModiff == chars_modiff
+ changes (e.g., timer-based completion highlight move without
+ text edit) bump overlay_modiff but not modiff. Also detect
+ narrowing/widening which changes BUF_BEGV without bumping
+ either modiff counter. */
+ if (cachedText && cachedTextModiff == modiff
+ && cachedOverlayModiff == overlay_modiff
+ && cachedTextStart == BUF_BEGV (b) + && cachedTextStart == BUF_BEGV (b)
+ && pt >= cachedTextStart + && pt >= cachedTextStart
+ && (textLen == 0 + && (textLen == 0
@@ -389,8 +397,7 @@ index 96805b9..ae4c581 100644
+ { + {
+ [cachedText release]; + [cachedText release];
+ cachedText = [text retain]; + cachedText = [text retain];
+ cachedTextModiff = modiff; + cachedTextModiff = chars_modiff;
+ cachedOverlayModiff = overlay_modiff;
+ cachedTextStart = start; + cachedTextStart = start;
+ +
+ if (visibleRuns) + if (visibleRuns)

View File

@@ -1,7 +1,7 @@
From f006124d3ce2f345e14baac3dd2d453abfa652e5 Mon Sep 17 00:00:00 2001 From 7d711a2d08efbd491b664818954e078e214a89c3 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 04/11] ns: add buffer notification dispatch and mode-line Subject: [PATCH 4/9] ns: add buffer notification dispatch and mode-line
element element
Add VoiceOver notification methods and mode-line readout. Add VoiceOver notification methods and mode-line readout.
@@ -24,10 +24,10 @@ region selection feedback, completion popups, mode-line reading.
1 file changed, 545 insertions(+) 1 file changed, 545 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index ae4c581..09f47ab 100644 index 73e56d3..b48f0c2 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -8679,6 +8679,551 @@ - (NSRect)accessibilityFrame @@ -8720,6 +8720,551 @@ - (NSRect)accessibilityFrame
@end @end

View File

@@ -1,7 +1,7 @@
From 2428a8f16873dab87284da544a7da4bbffff48a5 Mon Sep 17 00:00:00 2001 From 014826c0b4de37f5e6adbd85032282dabae623a3 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 05/11] ns: add interactive span elements for Tab navigation Subject: [PATCH 5/9] ns: add interactive span elements for Tab navigation
* src/nsterm.m (ns_ax_scan_interactive_spans): New function. * src/nsterm.m (ns_ax_scan_interactive_spans): New function.
(EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink (EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink
@@ -17,10 +17,10 @@ Tested on macOS 14. Verified: Tab-cycling through org-mode links,
1 file changed, 286 insertions(+) 1 file changed, 286 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 09f47ab..a056c1b 100644 index b48f0c2..01751ce 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -9224,6 +9224,292 @@ - (NSRect)accessibilityFrame @@ -9265,6 +9265,292 @@ - (NSRect)accessibilityFrame
@end @end

View File

@@ -1,8 +1,7 @@
From 0f7896d526a9ad17fa34c75dd7eaf15f59e070cd Mon Sep 17 00:00:00 2001 From 495554a23725688fb7030e6837efff5341e8e9a2 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 06/11] ns: integrate accessibility with EmacsView and Subject: [PATCH 6/9] ns: integrate accessibility with EmacsView and redisplay
redisplay
Wire the accessibility infrastructure into EmacsView and the Wire the accessibility infrastructure into EmacsView and the
@@ -52,10 +51,10 @@ index 80661a9..2b1f9e6 100644
** Re-introduced dictation, lost in Emacs v30 (macOS). ** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient. We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index a056c1b..92a0dde 100644 index 01751ce..f7da553 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1370,6 +1370,9 @@ so the visual offset is (ov_line + 1) * line_h from @@ -1404,6 +1404,9 @@ so the visual offset is (ov_line + 1) * line_h from
if (view) if (view)
ns_zoom_track_completion (f, view); ns_zoom_track_completion (f, view);
#endif /* NS_IMPL_COCOA */ #endif /* NS_IMPL_COCOA */
@@ -65,7 +64,7 @@ index a056c1b..92a0dde 100644
} }
static void static void
@@ -7585,7 +7588,6 @@ - (id)accessibilityTopLevelUIElement @@ -7619,7 +7622,6 @@ - (id)accessibilityTopLevelUIElement
@@ -73,7 +72,7 @@ index a056c1b..92a0dde 100644
static BOOL static BOOL
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
ptrdiff_t *out_start, ptrdiff_t *out_start,
@@ -8680,7 +8682,6 @@ - (NSRect)accessibilityFrame @@ -8721,7 +8723,6 @@ - (NSRect)accessibilityFrame
@end @end
@@ -81,7 +80,7 @@ index a056c1b..92a0dde 100644
/* =================================================================== /* ===================================================================
EmacsAccessibilityBuffer (Notifications) — AX event dispatch EmacsAccessibilityBuffer (Notifications) — AX event dispatch
@@ -9225,7 +9226,6 @@ - (NSRect)accessibilityFrame @@ -9266,7 +9267,6 @@ - (NSRect)accessibilityFrame
@end @end
@@ -89,7 +88,7 @@ index a056c1b..92a0dde 100644
/* =================================================================== /* ===================================================================
EmacsAccessibilityInteractiveSpan — helpers and implementation EmacsAccessibilityInteractiveSpan — helpers and implementation
=================================================================== */ =================================================================== */
@@ -9555,6 +9555,7 @@ - (void)dealloc @@ -9596,6 +9596,7 @@ - (void)dealloc
[layer release]; [layer release];
#endif #endif
@@ -97,7 +96,7 @@ index a056c1b..92a0dde 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -10903,6 +10904,32 @@ - (void)windowDidBecomeKey /* for direct calls */ @@ -10944,6 +10945,32 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop ns_send_appdefined (-1); // Kick main loop
@@ -130,7 +129,7 @@ index a056c1b..92a0dde 100644
} }
@@ -12143,6 +12170,332 @@ - (int) fullscreenState @@ -12184,6 +12211,332 @@ - (int) fullscreenState
return fs_state; return fs_state;
} }

View File

@@ -1,7 +1,7 @@
From babd2effe8f137292af8d7ce9cb75ec29f6d2f3d Mon Sep 17 00:00:00 2001 From 6e03e0051d7f15bd1391ff5c5b9d2093711d4300 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 07/11] doc: add VoiceOver accessibility section to macOS Subject: [PATCH 7/9] doc: add VoiceOver accessibility section to macOS
appendix appendix
* doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document * doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document

View File

@@ -1,8 +1,7 @@
From a979f430547512c252320e76683a288872f4f2ff Mon Sep 17 00:00:00 2001 From 3840a4b1e57f6fbdfc005ad86ef16bb0bfdf0868 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:46:25 +0100 Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH 08/11] ns: announce overlay completion candidates for Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver
VoiceOver
Completion frameworks such as Vertico, Ivy, and Icomplete render Completion frameworks such as Vertico, Ivy, and Icomplete render
candidates via overlay before-string/after-string properties rather candidates via overlay before-string/after-string properties rather
@@ -46,8 +45,8 @@ Key implementation details:
Independent overlay branch, BUF_CHARS_MODIFF gating, candidate Independent overlay branch, BUF_CHARS_MODIFF gating, candidate
--- ---
src/nsterm.h | 1 + src/nsterm.h | 1 +
src/nsterm.m | 319 +++++++++++++++++++++++++++++++++++++++++++++------ src/nsterm.m | 332 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 287 insertions(+), 33 deletions(-) 2 files changed, 290 insertions(+), 43 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 6e830de..2102fb9 100644 index 6e830de..2102fb9 100644
@@ -62,10 +61,10 @@ index 6e830de..2102fb9 100644
@property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 92a0dde..aaeed33 100644 index f7da553..e333d45 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7174,11 +7174,154 @@ Accessibility virtual elements (macOS / Cocoa only) @@ -7208,11 +7208,154 @@ Accessibility virtual elements (macOS / Cocoa only)
/* ---- Helper: extract buffer text for accessibility ---- */ /* ---- Helper: extract buffer text for accessibility ---- */
@@ -221,7 +220,7 @@ index 92a0dde..aaeed33 100644
static NSString * static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns) ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -7249,7 +7392,7 @@ Accessibility virtual elements (macOS / Cocoa only) @@ -7283,7 +7426,7 @@ Accessibility virtual elements (macOS / Cocoa only)
/* Extract this visible run's text. Use /* Extract this visible run's text. Use
Fbuffer_substring_no_properties which correctly handles the Fbuffer_substring_no_properties which correctly handles the
@@ -230,7 +229,7 @@ index 92a0dde..aaeed33 100644
include garbage bytes when the run spans the gap position. */ include garbage bytes when the run spans the gap position. */
Lisp_Object lstr = Fbuffer_substring_no_properties ( Lisp_Object lstr = Fbuffer_substring_no_properties (
make_fixnum (pos), make_fixnum (run_end)); make_fixnum (pos), make_fixnum (run_end));
@@ -7330,7 +7473,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font) @@ -7364,7 +7507,7 @@ Mode lines using icon fonts (e.g. doom-modeline with nerd-font)
return NSZeroRect; return NSZeroRect;
/* charpos_start and charpos_len are already in buffer charpos /* charpos_start and charpos_len are already in buffer charpos
@@ -239,7 +238,7 @@ index 92a0dde..aaeed33 100644
charposForAccessibilityIndex which handles invisible text. */ charposForAccessibilityIndex which handles invisible text. */
ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_start = charpos_start;
ptrdiff_t cp_end = cp_start + charpos_len; ptrdiff_t cp_end = cp_start + charpos_len;
@@ -7809,6 +7952,7 @@ @implementation EmacsAccessibilityBuffer @@ -7843,6 +7986,7 @@ @implementation EmacsAccessibilityBuffer
@synthesize cachedOverlayModiff; @synthesize cachedOverlayModiff;
@synthesize cachedTextStart; @synthesize cachedTextStart;
@synthesize cachedModiff; @synthesize cachedModiff;
@@ -247,7 +246,7 @@ index 92a0dde..aaeed33 100644
@synthesize cachedPoint; @synthesize cachedPoint;
@synthesize cachedMarkActive; @synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement; @synthesize cachedCompletionAnnouncement;
@@ -7906,7 +8050,7 @@ - (void)ensureTextCache @@ -7940,7 +8084,7 @@ - (void)ensureTextCache
NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); NSTRACE ("EmacsAccessibilityBuffer ensureTextCache");
/* This method is only called from the main thread (AX getters /* This method is only called from the main thread (AX getters
dispatch_sync to main first). Reads of cachedText/cachedTextModiff dispatch_sync to main first). Reads of cachedText/cachedTextModiff
@@ -256,38 +255,50 @@ index 92a0dde..aaeed33 100644
write section at the end needs synchronization to protect write section at the end needs synchronization to protect
against concurrent reads from AX server thread. */ against concurrent reads from AX server thread. */
eassert ([NSThread isMainThread]); eassert ([NSThread isMainThread]);
@@ -7919,16 +8063,15 @@ - (void)ensureTextCache @@ -7952,25 +8096,16 @@ - (void)ensureTextCache
if (!b)
return; return;
ptrdiff_t modiff = BUF_MODIFF (b); - /* Use BUF_CHARS_MODIFF, not BUF_MODIFF, for cache validity.
- ptrdiff_t overlay_modiff = BUF_OVERLAY_MODIFF (b); - BUF_MODIFF is bumped by every text-property change, including
- font-lock face applications on every redisplay. AX text contains
- only characters, not face data, so property-only changes do not
- affect the cached value. Rebuilding the full buffer text on
- each font-lock pass is O(buffer-size) per redisplay --- this
- causes progressive slowdown when scrolling through large files.
- BUF_CHARS_MODIFF is bumped only on actual character insertions
- and deletions, matching the semantic of "did the text change".
- This is the pattern used by WebKit and NSTextView.
- Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
- included in the cached AX text (it is handled separately via
- explicit announcements in postAccessibilityNotificationsForFrame).
- Including overlay_modiff would silently update cachedOverlayModiff
- and prevent the notification dispatch from detecting changes. */
- ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b);
+ ptrdiff_t modiff = BUF_MODIFF (b);
ptrdiff_t pt = BUF_PT (b); ptrdiff_t pt = BUF_PT (b);
NSUInteger textLen = cachedText ? [cachedText length] : 0; NSUInteger textLen = cachedText ? [cachedText length] : 0;
- /* Track both BUF_MODIFF and BUF_OVERLAY_MODIFF. Overlay-only - if (cachedText && cachedTextModiff == chars_modiff
- changes (e.g., timer-based completion highlight move without
- text edit) bump overlay_modiff but not modiff. Also detect
- narrowing/widening which changes BUF_BEGV without bumping
- either modiff counter. */
+ /* Cache validity: track BUF_MODIFF and buffer narrowing. + /* Cache validity: track BUF_MODIFF and buffer narrowing.
+ Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not + Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
+ included in the cached AX text (it is handled separately via + included in the cached AX text (it is handled separately via
+ explicit announcements). Including overlay_modiff would + explicit announcements). Including overlay_modiff would
+ silently update cachedOverlayModiff and prevent the + silently update cachedOverlayModiff and prevent the
+ notification dispatch from detecting overlay changes. */ + notification dispatch from detecting overlay changes. */
if (cachedText && cachedTextModiff == modiff + if (cachedText && cachedTextModiff == modiff
- && cachedOverlayModiff == overlay_modiff
&& cachedTextStart == BUF_BEGV (b) && cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart && pt >= cachedTextStart
&& (textLen == 0 && (textLen == 0
@@ -7945,7 +8088,6 @@ - (void)ensureTextCache @@ -7986,7 +8121,7 @@ included in the cached AX text (it is handled separately via
{
[cachedText release]; [cachedText release];
cachedText = [text retain]; cachedText = [text retain];
cachedTextModiff = modiff; - cachedTextModiff = chars_modiff;
- cachedOverlayModiff = overlay_modiff; + cachedTextModiff = modiff;
cachedTextStart = start; cachedTextStart = start;
if (visibleRuns) if (visibleRuns)
@@ -8010,7 +8152,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos @@ -8051,7 +8186,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
/* Binary search: runs are sorted by charpos (ascending). Find the /* Binary search: runs are sorted by charpos (ascending). Find the
run whose [charpos, charpos+length) range contains the target, run whose [charpos, charpos+length) range contains the target,
or the nearest run after an invisible gap. O(log n) instead of or the nearest run after an invisible gap. O(log n) instead of
@@ -296,7 +307,7 @@ index 92a0dde..aaeed33 100644
NSUInteger lo = 0, hi = visibleRunCount; NSUInteger lo = 0, hi = visibleRunCount;
while (lo < hi) while (lo < hi)
{ {
@@ -8023,7 +8165,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos @@ -8064,7 +8199,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
else else
{ {
/* Found: charpos is inside this run. Compute UTF-16 delta /* Found: charpos is inside this run. Compute UTF-16 delta
@@ -305,7 +316,7 @@ index 92a0dde..aaeed33 100644
NSUInteger chars_in = (NSUInteger)(charpos - r->charpos); NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
if (chars_in == 0 || !cachedText) if (chars_in == 0 || !cachedText)
return r->ax_start; return r->ax_start;
@@ -8048,10 +8190,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos @@ -8089,10 +8224,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
/* Convert accessibility string index to buffer charpos. /* Convert accessibility string index to buffer charpos.
Safe to call from any thread: uses only cachedText (NSString) and Safe to call from any thread: uses only cachedText (NSString) and
@@ -318,7 +329,7 @@ index 92a0dde..aaeed33 100644
@synchronized (self) @synchronized (self)
{ {
if (visibleRunCount == 0) if (visibleRunCount == 0)
@@ -8085,7 +8227,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx @@ -8126,7 +8261,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
return cp; return cp;
} }
} }
@@ -327,7 +338,7 @@ index 92a0dde..aaeed33 100644
if (lo > 0) if (lo > 0)
{ {
ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
@@ -8107,7 +8249,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx @@ -8148,7 +8283,7 @@ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
deadlocking the AX server thread. This is prevented by: deadlocking the AX server thread. This is prevented by:
1. validWindow checks WINDOW_LIVE_P and BUFFERP before every 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
@@ -336,7 +347,7 @@ index 92a0dde..aaeed33 100644
2. All dispatch_sync blocks run on the main thread where no 2. All dispatch_sync blocks run on the main thread where no
concurrent Lisp code can modify state between checks. concurrent Lisp code can modify state between checks.
3. block_input prevents timer events and process output from 3. block_input prevents timer events and process output from
@@ -8461,6 +8603,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber @@ -8502,6 +8637,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
return [self lineForAXIndex:point_idx]; return [self lineForAXIndex:point_idx];
} }
@@ -387,7 +398,7 @@ index 92a0dde..aaeed33 100644
- (NSRange)accessibilityRangeForLine:(NSInteger)line - (NSRange)accessibilityRangeForLine:(NSInteger)line
{ {
if (![NSThread isMainThread]) if (![NSThread isMainThread])
@@ -8683,7 +8869,7 @@ - (NSRect)accessibilityFrame @@ -8724,7 +8903,7 @@ - (NSRect)accessibilityFrame
/* =================================================================== /* ===================================================================
@@ -396,7 +407,7 @@ index 92a0dde..aaeed33 100644
These methods notify VoiceOver of text and selection changes. These methods notify VoiceOver of text and selection changes.
Called from the redisplay cycle (postAccessibilityUpdates). Called from the redisplay cycle (postAccessibilityUpdates).
@@ -8698,7 +8884,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point @@ -8739,7 +8918,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
if (point > self.cachedPoint if (point > self.cachedPoint
&& point - self.cachedPoint == 1) && point - self.cachedPoint == 1)
{ {
@@ -405,7 +416,7 @@ index 92a0dde..aaeed33 100644
[self invalidateTextCache]; [self invalidateTextCache];
[self ensureTextCache]; [self ensureTextCache];
if (cachedText) if (cachedText)
@@ -8717,7 +8903,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point @@ -8758,7 +8937,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
/* Update cachedPoint here so the selection-move branch does NOT /* Update cachedPoint here so the selection-move branch does NOT
fire for point changes caused by edits. WebKit and Chromium fire for point changes caused by edits. WebKit and Chromium
never send both ValueChanged and SelectedTextChanged for the never send both ValueChanged and SelectedTextChanged for the
@@ -414,7 +425,7 @@ index 92a0dde..aaeed33 100644
self.cachedPoint = point; self.cachedPoint = point;
NSDictionary *change = @{ NSDictionary *change = @{
@@ -9050,16 +9236,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9091,16 +9270,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active)); BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */ /* --- Text changed (edit) --- */
@@ -502,7 +513,7 @@ index 92a0dde..aaeed33 100644
{ {
ptrdiff_t oldPoint = self.cachedPoint; ptrdiff_t oldPoint = self.cachedPoint;
BOOL oldMarkActive = self.cachedMarkActive; BOOL oldMarkActive = self.cachedMarkActive;
@@ -9227,7 +9480,7 @@ - (NSRect)accessibilityFrame @@ -9268,7 +9514,7 @@ - (NSRect)accessibilityFrame
/* =================================================================== /* ===================================================================
@@ -511,7 +522,7 @@ index 92a0dde..aaeed33 100644
=================================================================== */ =================================================================== */
/* Scan visible range of window W for interactive spans. /* Scan visible range of window W for interactive spans.
@@ -9435,7 +9688,7 @@ - (void) setAccessibilityFocused: (BOOL) focused @@ -9476,7 +9722,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
dispatch_async (dispatch_get_main_queue (), ^{ dispatch_async (dispatch_get_main_queue (), ^{
/* lwin is a Lisp_Object captured by value. This is GC-safe /* lwin is a Lisp_Object captured by value. This is GC-safe
because Lisp_Objects are tagged integers/pointers that because Lisp_Objects are tagged integers/pointers that
@@ -520,7 +531,7 @@ index 92a0dde..aaeed33 100644
Emacs. The WINDOW_LIVE_P check below guards against the Emacs. The WINDOW_LIVE_P check below guards against the
window being deleted between capture and execution. */ window being deleted between capture and execution. */
if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin)))
@@ -9461,7 +9714,7 @@ - (void) setAccessibilityFocused: (BOOL) focused @@ -9502,7 +9748,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@end @end
@@ -529,7 +540,7 @@ index 92a0dde..aaeed33 100644
Methods are kept here (same .m file) so they access the ivars Methods are kept here (same .m file) so they access the ivars
declared in the @interface ivar block. */ declared in the @interface ivar block. */
@implementation EmacsAccessibilityBuffer (InteractiveSpans) @implementation EmacsAccessibilityBuffer (InteractiveSpans)
@@ -12186,7 +12439,7 @@ - (int) fullscreenState @@ -12227,7 +12473,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w)) if (WINDOW_LEAF_P (w))
{ {
@@ -538,7 +549,7 @@ index 92a0dde..aaeed33 100644
EmacsAccessibilityBuffer *elem EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]]; = [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem) if (!elem)
@@ -12220,7 +12473,7 @@ - (int) fullscreenState @@ -12261,7 +12507,7 @@ - (int) fullscreenState
} }
else else
{ {
@@ -547,7 +558,7 @@ index 92a0dde..aaeed33 100644
Lisp_Object child = w->contents; Lisp_Object child = w->contents;
while (!NILP (child)) while (!NILP (child))
{ {
@@ -12332,7 +12585,7 @@ - (void)postAccessibilityUpdates @@ -12373,7 +12619,7 @@ - (void)postAccessibilityUpdates
accessibilityUpdating = YES; accessibilityUpdating = YES;
/* Detect window tree change (split, delete, new buffer). Compare /* Detect window tree change (split, delete, new buffer). Compare
@@ -556,7 +567,7 @@ index 92a0dde..aaeed33 100644
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow)) if (!EQ (curRoot, lastRootWindow))
{ {
@@ -12341,12 +12594,12 @@ - (void)postAccessibilityUpdates @@ -12382,12 +12628,12 @@ - (void)postAccessibilityUpdates
} }
/* If tree is stale, rebuild FIRST so we don't iterate freed /* If tree is stale, rebuild FIRST so we don't iterate freed

View File

@@ -1,7 +1,7 @@
From 5edae69b987f3234d4545d8c77f89f5dc4201e03 Mon Sep 17 00:00:00 2001 From a5f18d7d2b4fdabb3d1ed10d536117ea8d641dad Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100 Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 09/11] ns: announce child frame completion candidates for Subject: [PATCH 9/9] ns: announce child frame completion candidates for
VoiceOver VoiceOver
Completion frameworks such as Corfu, Company-box, and similar Completion frameworks such as Corfu, Company-box, and similar
@@ -40,11 +40,11 @@ childFrameCompletionActive flag.
(EmacsView postAccessibilityUpdates): Dispatch to child frame handler, (EmacsView postAccessibilityUpdates): Dispatch to child frame handler,
refocus parent buffer element when child frame closes. refocus parent buffer element when child frame closes.
--- ---
doc/emacs/macos.texi | 6 -- doc/emacs/macos.texi | 6 -
etc/NEWS | 4 +- etc/NEWS | 4 +-
src/nsterm.h | 5 + src/nsterm.h | 5 +
src/nsterm.m | 236 ++++++++++++++++++++++++++++++++++++++++++- src/nsterm.m | 263 +++++++++++++++++++++++++++++++++++++++++--
4 files changed, 242 insertions(+), 9 deletions(-) 4 files changed, 260 insertions(+), 18 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 4825cf9..97777e2 100644 index 4825cf9..97777e2 100644
@@ -109,10 +109,10 @@ index 2102fb9..dd98d56 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index aaeed33..d576512 100644 index e333d45..27607c0 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7318,6 +7318,112 @@ visual line index for Zoom (skip whitespace-only lines @@ -7352,6 +7352,112 @@ visual line index for Zoom (skip whitespace-only lines
return nil; return nil;
} }
@@ -225,7 +225,50 @@ index aaeed33..d576512 100644
/* Build accessibility text for window W, skipping invisible text. /* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos. Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -9055,6 +9161,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b @@ -8096,16 +8202,25 @@ - (void)ensureTextCache
if (!b)
return;
- ptrdiff_t modiff = BUF_MODIFF (b);
- ptrdiff_t pt = BUF_PT (b);
- NSUInteger textLen = cachedText ? [cachedText length] : 0;
- /* Cache validity: track BUF_MODIFF and buffer narrowing.
+ /* Use BUF_CHARS_MODIFF, not BUF_MODIFF, for cache validity.
+ BUF_MODIFF is bumped by every text-property change, including
+ font-lock face applications on every redisplay. AX text contains
+ only characters, not face data, so property-only changes do not
+ affect the cached value. Rebuilding the full buffer text on
+ each font-lock pass is O(buffer-size) per redisplay --- this
+ causes progressive slowdown when scrolling through large files.
+ BUF_CHARS_MODIFF is bumped only on actual character insertions
+ and deletions, matching the semantic of "did the text change".
+ This is the pattern used by WebKit and NSTextView.
Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
included in the cached AX text (it is handled separately via
- explicit announcements). Including overlay_modiff would
- silently update cachedOverlayModiff and prevent the
- notification dispatch from detecting overlay changes. */
- if (cachedText && cachedTextModiff == modiff
+ explicit announcements in postAccessibilityNotificationsForFrame).
+ Including overlay_modiff would silently update cachedOverlayModiff
+ and prevent the notification dispatch from detecting changes. */
+ ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b);
+ ptrdiff_t pt = BUF_PT (b);
+ NSUInteger textLen = cachedText ? [cachedText length] : 0;
+ if (cachedText && cachedTextModiff == chars_modiff
&& cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart
&& (textLen == 0
@@ -8121,7 +8236,7 @@ included in the cached AX text (it is handled separately via
{
[cachedText release];
cachedText = [text retain];
- cachedTextModiff = modiff;
+ cachedTextModiff = chars_modiff;
cachedTextStart = start;
if (visibleRuns)
@@ -9089,6 +9204,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
ptrdiff_t currentOverlayStart = 0; ptrdiff_t currentOverlayStart = 0;
ptrdiff_t currentOverlayEnd = 0; ptrdiff_t currentOverlayEnd = 0;
@@ -233,7 +276,7 @@ index aaeed33..d576512 100644
specpdl_ref count2 = SPECPDL_INDEX (); specpdl_ref count2 = SPECPDL_INDEX ();
record_unwind_current_buffer (); record_unwind_current_buffer ();
if (b != current_buffer) if (b != current_buffer)
@@ -9213,6 +9320,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b @@ -9247,6 +9363,7 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
self.cachedCompletionOverlayEnd = 0; self.cachedCompletionOverlayEnd = 0;
self.cachedCompletionPoint = 0; self.cachedCompletionPoint = 0;
} }
@@ -241,7 +284,7 @@ index aaeed33..d576512 100644
} }
/* ---- Notification dispatch (main entry point) ---- */ /* ---- Notification dispatch (main entry point) ---- */
@@ -9809,6 +9917,10 @@ - (void)dealloc @@ -9843,6 +9960,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
@@ -252,7 +295,7 @@ index aaeed33..d576512 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -12569,6 +12681,80 @@ - (id)accessibilityFocusedUIElement @@ -12603,6 +12724,80 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */ elements with current values, making change detection impossible. */
@@ -333,7 +376,7 @@ index aaeed33..d576512 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12579,11 +12765,59 @@ - (void)postAccessibilityUpdates @@ -12613,11 +12808,59 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting /* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us can trigger redisplay, which calls ns_update_end, which calls us

View File

@@ -1,77 +0,0 @@
From ec00458c25db0fbf4af2eeb4c88e59c0af7d2063 Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz>
Date: Sun, 1 Mar 2026 04:56:16 +0100
Subject: [PATCH 10/11] perf: use BUF_CHARS_MODIFF for AX text cache validity
in ensureTextCache
The cache validity check in -[EmacsAccessibilityBuffer ensureTextCache]
used BUF_MODIFF, which is bumped by every text-property change ---
including face applications by font-lock on each redisplay cycle.
Since the AX text value contains only characters (no face or property
data), property-only changes do not affect it. Rebuilding the full
buffer text on each font-lock pass is O(buffer-size) per redisplay,
causing progressive slowdown proportional to how far the cursor is
from the beginning of the file.
Switch to BUF_CHARS_MODIFF, which is bumped only when characters are
actually inserted or deleted. This matches the semantic of 'did the
text change' and is the approach used by WebKit and NSTextView.
BUF_OVERLAY_MODIFF is intentionally not tracked here (unchanged):
overlay content is handled by separate announcements in
postAccessibilityNotificationsForFrame, and including it in this
check would prevent those announcements from firing.
---
src/nsterm.m | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/src/nsterm.m b/src/nsterm.m
index d576512..1da6f41 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -8168,16 +8168,25 @@ - (void)ensureTextCache
if (!b)
return;
- ptrdiff_t modiff = BUF_MODIFF (b);
- ptrdiff_t pt = BUF_PT (b);
- NSUInteger textLen = cachedText ? [cachedText length] : 0;
- /* Cache validity: track BUF_MODIFF and buffer narrowing.
+ /* Use BUF_CHARS_MODIFF, not BUF_MODIFF, for cache validity.
+ BUF_MODIFF is bumped by every text-property change, including
+ font-lock face applications on every redisplay. AX text contains
+ only characters, not face data, so property-only changes do not
+ affect the cached value. Rebuilding the full buffer text on
+ each font-lock pass is O(buffer-size) per redisplay --- this
+ causes progressive slowdown when scrolling through large files.
+ BUF_CHARS_MODIFF is bumped only on actual character insertions
+ and deletions, matching the semantic of "did the text change".
+ This is the pattern used by WebKit and NSTextView.
Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
included in the cached AX text (it is handled separately via
- explicit announcements). Including overlay_modiff would
- silently update cachedOverlayModiff and prevent the
- notification dispatch from detecting overlay changes. */
- if (cachedText && cachedTextModiff == modiff
+ explicit announcements in postAccessibilityNotificationsForFrame).
+ Including overlay_modiff would silently update cachedOverlayModiff
+ and prevent the notification dispatch from detecting changes. */
+ ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b);
+ ptrdiff_t pt = BUF_PT (b);
+ NSUInteger textLen = cachedText ? [cachedText length] : 0;
+ if (cachedText && cachedTextModiff == chars_modiff
&& cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart
&& (textLen == 0
@@ -8193,7 +8202,7 @@ included in the cached AX text (it is handled separately via
{
[cachedText release];
cachedText = [text retain];
- cachedTextModiff = modiff;
+ cachedTextModiff = chars_modiff;
cachedTextStart = start;
if (visibleRuns)
--
2.43.0