patches: fix Blocker #2 - remove ensureTextCache from notification path
BUF_MODIFF was needed for fold/unfold correctness (org-mode), but ensureTextCache was called from postAccessibilityNotificationsForFrame: on every cursor move, causing O(buffer-size) rebuild on every font-lock pass. Fix: remove [self ensureTextCache] from the granularity-detection branch of the notification path. The granularity detection now uses cachedText directly, falling back to granularity_unknown when absent (safe: VoiceOver makes its own determination). ensureTextCache is called exclusively from AX getters (human interaction speed). Font-lock passes no longer trigger any cache rebuild. The BUF_MODIFF validity in ensureTextCache is retained (correct for fold/unfold). Comment updated to accurately describe the calling pattern. All 9 patches apply cleanly on fresh base (git apply verified).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
From 7474a4e1ddbf37286842e3beda1810c40f2a3ef7 Mon Sep 17 00:00:00 2001
|
||||
From 217177caefc709c37ae04732ec595ace903e4cc4 Mon Sep 17 00:00:00 2001
|
||||
From: Martin Sukany <martin@sukany.cz>
|
||||
Date: Mon, 2 Mar 2026 18:39:46 +0100
|
||||
Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver
|
||||
@@ -11,15 +11,24 @@ this change VoiceOver cannot read overlay-based completion UIs.
|
||||
'current', 'selected', 'selection' in face symbol names.
|
||||
(ns_ax_selected_overlay_text): New function; scan overlay strings in
|
||||
the window for a line with a selected face; return its text.
|
||||
(EmacsAccessibilityBuffer(Notifications)
|
||||
postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF
|
||||
(ensureTextCache): Switch cache-validity counter from BUF_CHARS_MODIFF
|
||||
to BUF_MODIFF. Fold/unfold commands (org-mode, outline-mode,
|
||||
hideshow-mode) change the 'invisible text property via
|
||||
`put-text-property', which bumps BUF_MODIFF but not BUF_CHARS_MODIFF.
|
||||
Using BUF_CHARS_MODIFF would serve stale AX text across fold/unfold.
|
||||
The rebuild is O(visible-buffer-text) but ensureTextCache is called
|
||||
exclusively from AX getters at human interaction speed, never from the
|
||||
redisplay notification path; font-lock passes cause zero rebuild cost.
|
||||
(postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF
|
||||
changes independently of text changes. Use BUF_CHARS_MODIFF to gate
|
||||
ValueChanged; keep overlay_modiff out of ensureTextCache to prevent a
|
||||
race where an AX query consumes the change before notification.
|
||||
ValueChanged. Do not call ensureTextCache from the cursor-moved branch:
|
||||
the granularity detection uses cachedText directly (falling back to
|
||||
granularity_unknown when the cache is absent), so font-lock passes
|
||||
cannot trigger O(buffer-size) rebuilds via the notification path.
|
||||
---
|
||||
src/nsterm.h | 1 +
|
||||
src/nsterm.m | 345 ++++++++++++++++++++++++++++++++++++++++++++-------
|
||||
2 files changed, 304 insertions(+), 42 deletions(-)
|
||||
src/nsterm.m | 358 ++++++++++++++++++++++++++++++++++++++++++++-------
|
||||
2 files changed, 316 insertions(+), 43 deletions(-)
|
||||
|
||||
diff --git a/src/nsterm.h b/src/nsterm.h
|
||||
index f245675513..a210ceba14 100644
|
||||
@@ -34,7 +43,7 @@ index f245675513..a210ceba14 100644
|
||||
@property (nonatomic, assign) BOOL cachedMarkActive;
|
||||
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
|
||||
diff --git a/src/nsterm.m b/src/nsterm.m
|
||||
index a0419bb5df..54cee74401 100644
|
||||
index a0419bb5df..b9d3a0eb53 100644
|
||||
--- a/src/nsterm.m
|
||||
+++ b/src/nsterm.m
|
||||
@@ -7263,11 +7263,154 @@ Accessibility virtual elements (macOS / Cocoa only)
|
||||
@@ -228,7 +237,7 @@ index a0419bb5df..54cee74401 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
|
||||
@@ -8005,25 +8149,38 @@ - (void)ensureTextCache
|
||||
if (!b)
|
||||
return;
|
||||
|
||||
@@ -259,12 +268,16 @@ index a0419bb5df..54cee74401 100644
|
||||
+ as if it were visible, or miss newly revealed content entirely.
|
||||
+
|
||||
+ BUF_MODIFF is bumped by all buffer modifications including
|
||||
+ text-property changes (e.g. font-lock face assignments), causing a
|
||||
+ full text-cache rebuild on each redisplay cycle. This is acceptable
|
||||
+ because `ensureTextCache' is only called when VoiceOver queries
|
||||
+ accessibilityValue or related AX properties --- which happens at
|
||||
+ human interaction speed, not at redisplay speed. The per-rebuild
|
||||
+ cost is O(visible-buffer-text).
|
||||
+ text-property changes (e.g. font-lock face assignments). The
|
||||
+ per-rebuild cost is O(visible-buffer-text), but `ensureTextCache'
|
||||
+ is called exclusively from AX getters (accessibilityValue,
|
||||
+ accessibilitySelectedTextRange, etc.) which run at human interaction
|
||||
+ speed --- not from the redisplay notification path. Font-lock
|
||||
+ passes do not call this method, so the rebuild cost per font-lock
|
||||
+ cycle is zero. The redisplay notification path (postAccessibility-
|
||||
+ NotificationsForFrame:) uses cachedText directly without calling
|
||||
+ ensureTextCache; granularity detection falls back gracefully when
|
||||
+ the cache is absent.
|
||||
+
|
||||
+ Do NOT use BUF_OVERLAY_MODIFF alone: org-mode >= 29 (org-fold-core)
|
||||
+ uses text properties, not overlays, for folding, so
|
||||
@@ -280,7 +293,7 @@ index a0419bb5df..54cee74401 100644
|
||||
&& cachedTextStart == BUF_BEGV (b)
|
||||
&& pt >= cachedTextStart
|
||||
&& (textLen == 0
|
||||
@@ -8039,7 +8192,7 @@ included in the cached AX text (it is handled separately via
|
||||
@@ -8039,7 +8196,7 @@ included in the cached AX text (it is handled separately via
|
||||
{
|
||||
[cachedText release];
|
||||
cachedText = [text retain];
|
||||
@@ -289,7 +302,7 @@ index a0419bb5df..54cee74401 100644
|
||||
cachedTextStart = start;
|
||||
|
||||
if (visibleRuns)
|
||||
@@ -8051,9 +8204,9 @@ included in the cached AX text (it is handled separately via
|
||||
@@ -8051,9 +8208,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 +315,7 @@ index a0419bb5df..54cee74401 100644
|
||||
enters this code. */
|
||||
if (lineStartOffsets)
|
||||
xfree (lineStartOffsets);
|
||||
@@ -8108,7 +8261,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
|
||||
@@ -8108,7 +8265,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 +324,7 @@ index a0419bb5df..54cee74401 100644
|
||||
NSUInteger lo = 0, hi = visibleRunCount;
|
||||
while (lo < hi)
|
||||
{
|
||||
@@ -8157,10 +8310,10 @@ by run length (visible window), not total buffer size. */
|
||||
@@ -8157,10 +8314,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 +337,7 @@ index a0419bb5df..54cee74401 100644
|
||||
@synchronized (self)
|
||||
{
|
||||
if (visibleRunCount == 0)
|
||||
@@ -8202,7 +8355,7 @@ the slow path (composed character sequence walk), which is
|
||||
@@ -8202,7 +8359,7 @@ the slow path (composed character sequence walk), which is
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
@@ -333,7 +346,7 @@ index a0419bb5df..54cee74401 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
|
||||
@@ -8224,7 +8381,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 +355,7 @@ index a0419bb5df..54cee74401 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
|
||||
@@ -8570,6 +8727,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
|
||||
return [self lineForAXIndex:point_idx];
|
||||
}
|
||||
|
||||
@@ -393,7 +406,7 @@ index a0419bb5df..54cee74401 100644
|
||||
- (NSRange)accessibilityRangeForLine:(NSInteger)line
|
||||
{
|
||||
if (![NSThread isMainThread])
|
||||
@@ -8792,7 +8989,7 @@ - (NSRect)accessibilityFrame
|
||||
@@ -8792,7 +8993,7 @@ - (NSRect)accessibilityFrame
|
||||
|
||||
|
||||
/* ===================================================================
|
||||
@@ -402,7 +415,7 @@ index a0419bb5df..54cee74401 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
|
||||
@@ -8807,7 +9008,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
|
||||
if (point > self.cachedPoint
|
||||
&& point - self.cachedPoint == 1)
|
||||
{
|
||||
@@ -411,7 +424,7 @@ index a0419bb5df..54cee74401 100644
|
||||
[self invalidateTextCache];
|
||||
[self ensureTextCache];
|
||||
if (cachedText)
|
||||
@@ -8826,7 +9023,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
|
||||
@@ -8826,7 +9027,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 +433,7 @@ index a0419bb5df..54cee74401 100644
|
||||
self.cachedPoint = point;
|
||||
|
||||
NSDictionary *change = @{
|
||||
@@ -9220,16 +9417,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
||||
@@ -9220,16 +9421,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
||||
BOOL markActive = !NILP (BVAR (b, mark_active));
|
||||
|
||||
/* --- Text changed (edit) --- */
|
||||
@@ -505,7 +518,24 @@ index a0419bb5df..54cee74401 100644
|
||||
{
|
||||
ptrdiff_t oldPoint = self.cachedPoint;
|
||||
BOOL oldMarkActive = self.cachedMarkActive;
|
||||
@@ -12408,7 +12669,7 @@ - (int) fullscreenState
|
||||
@@ -9247,8 +9512,15 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
|
||||
bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP);
|
||||
|
||||
/* --- Granularity detection --- */
|
||||
+ /* Use cached text as-is; do NOT call ensureTextCache here.
|
||||
+ ensureTextCache is O(visible-buffer-text) and must not run on
|
||||
+ every redisplay cycle. Using stale cached text for granularity
|
||||
+ classification is safe: the worst case is an incorrect
|
||||
+ granularity hint (defaulting to unknown), which causes VoiceOver
|
||||
+ to make its own determination. Fresh text is always available
|
||||
+ to VoiceOver via the AX getter path (accessibilityValue etc.). */
|
||||
NSInteger granularity = ns_ax_text_selection_granularity_unknown;
|
||||
- [self ensureTextCache];
|
||||
+ BOOL singleLineMove = NO;
|
||||
if (cachedText && oldPoint > 0)
|
||||
{
|
||||
NSUInteger tlen = [cachedText length];
|
||||
@@ -12408,7 +12680,7 @@ - (int) fullscreenState
|
||||
|
||||
if (WINDOW_LEAF_P (w))
|
||||
{
|
||||
@@ -514,7 +544,7 @@ index a0419bb5df..54cee74401 100644
|
||||
EmacsAccessibilityBuffer *elem
|
||||
= [existing objectForKey:[NSValue valueWithPointer:w]];
|
||||
if (!elem)
|
||||
@@ -12442,7 +12703,7 @@ - (int) fullscreenState
|
||||
@@ -12442,7 +12714,7 @@ - (int) fullscreenState
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -523,7 +553,7 @@ index a0419bb5df..54cee74401 100644
|
||||
Lisp_Object child = w->contents;
|
||||
while (!NILP (child))
|
||||
{
|
||||
@@ -12554,7 +12815,7 @@ - (void)postAccessibilityUpdates
|
||||
@@ -12554,7 +12826,7 @@ - (void)postAccessibilityUpdates
|
||||
accessibilityUpdating = YES;
|
||||
|
||||
/* Detect window tree change (split, delete, new buffer). Compare
|
||||
@@ -532,7 +562,7 @@ index a0419bb5df..54cee74401 100644
|
||||
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
|
||||
if (!EQ (curRoot, lastRootWindow))
|
||||
{
|
||||
@@ -12563,12 +12824,12 @@ - (void)postAccessibilityUpdates
|
||||
@@ -12563,12 +12835,12 @@ - (void)postAccessibilityUpdates
|
||||
}
|
||||
|
||||
/* If tree is stale, rebuild FIRST so we don't iterate freed
|
||||
|
||||
Reference in New Issue
Block a user