v15.1: fix freeze on window split, C-n/C-p cursor tracking, new window detection

- Re-entrance guard (accessibilityUpdating) prevents infinite recursion
  when VoiceOver callbacks trigger redisplay during notification posting
- Detect window tree change via FRAME_ROOT_WINDOW comparison; rebuild
  tree BEFORE iterating elements (prevents accessing freed windows)
- Validate window+buffer pointers before posting notifications
- Dynamic direction (Previous/Next/Discontiguous) and granularity
  (Character/Line) in SelectedTextChanged notifications based on
  actual point movement — fixes C-n/C-p not moving VoiceOver cursor
- New windows (completions, splits) detected via lastRootWindow change
This commit is contained in:
2026-02-26 13:43:34 +01:00
parent 64c40f4867
commit cd48418a3c

View File

@@ -1,5 +1,5 @@
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..8bf21f6 100644 index 7c1ee4c..4abeafe 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -453,6 +453,40 @@ enum ns_return_frame_mode @@ -453,6 +453,40 @@ enum ns_return_frame_mode
@@ -43,20 +43,22 @@ index 7c1ee4c..8bf21f6 100644
/* ========================================================================== /* ==========================================================================
The main Emacs view The main Emacs view
@@ -471,6 +505,12 @@ enum ns_return_frame_mode @@ -471,6 +505,14 @@ enum ns_return_frame_mode
#ifdef NS_IMPL_COCOA #ifdef NS_IMPL_COCOA
char *old_title; char *old_title;
BOOL maximizing_resize; BOOL maximizing_resize;
+ NSMutableArray *accessibilityElements; + NSMutableArray *accessibilityElements;
+ Lisp_Object lastSelectedWindow; + Lisp_Object lastSelectedWindow;
+ Lisp_Object lastRootWindow;
+ BOOL accessibilityTreeValid; + BOOL accessibilityTreeValid;
+ BOOL accessibilityUpdating;
+ @public + @public
+ NSRect lastAccessibilityCursorRect; + NSRect lastAccessibilityCursorRect;
+ @protected + @protected
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -528,6 +568,13 @@ enum ns_return_frame_mode @@ -528,6 +570,13 @@ enum ns_return_frame_mode
- (void)windowWillExitFullScreen; - (void)windowWillExitFullScreen;
- (void)windowDidExitFullScreen; - (void)windowDidExitFullScreen;
- (void)windowDidBecomeKey; - (void)windowDidBecomeKey;
@@ -71,7 +73,7 @@ index 7c1ee4c..8bf21f6 100644
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..6543e3b 100644 index 932d209..e830f54 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f) @@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f)
@@ -124,7 +126,7 @@ index 932d209..6543e3b 100644
ns_focus (f, NULL, 0); ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6847,6 +6883,717 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) @@ -6847,6 +6883,756 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
} }
#endif #endif
@@ -759,13 +761,52 @@ index 932d209..6543e3b 100644
+ kAXTextStateChangeTypeSelectionMove = 1. */ + kAXTextStateChangeTypeSelectionMove = 1. */
+ if (point != self.cachedPoint || markActive != self.cachedMarkActive) + if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ { + {
+ ptrdiff_t oldPoint = self.cachedPoint;
+ self.cachedPoint = point; + self.cachedPoint = point;
+ self.cachedMarkActive = markActive; + self.cachedMarkActive = markActive;
+ +
+ /* Compute direction: 2=Previous, 3=Next, 4=Discontiguous. */
+ NSInteger direction = 4;
+ if (point > oldPoint)
+ direction = 3;
+ else if (point < oldPoint)
+ direction = 2;
+
+ /* Compute granularity from movement distance.
+ Check if we crossed a newline → line movement (2).
+ Otherwise single char (0) or discontiguous (5=unknown). */
+ NSInteger granularity = 5;
+ [self ensureTextCache];
+ if (cachedText && oldPoint > 0)
+ {
+ ptrdiff_t delta = point - oldPoint;
+ if (delta == 1 || delta == -1)
+ granularity = 0; /* Character. */
+ else
+ {
+ /* Check for line crossing by looking for newlines
+ between old and new position. */
+ NSUInteger lo = (NSUInteger)
+ (MIN (oldPoint, point) - cachedTextStart);
+ NSUInteger hi = (NSUInteger)
+ (MAX (oldPoint, point) - cachedTextStart);
+ NSUInteger tlen = [cachedText length];
+ if (lo < tlen && hi <= tlen)
+ {
+ NSRange searchRange = NSMakeRange (lo, hi - lo);
+ NSRange nl = [cachedText rangeOfString:@"\n"
+ options:0
+ range:searchRange];
+ if (nl.location != NSNotFound)
+ granularity = 2; /* Line. */
+ }
+ }
+ }
+
+ NSDictionary *moveInfo = @{ + NSDictionary *moveInfo = @{
+ @"AXTextStateChangeType": @1, + @"AXTextStateChangeType": @1,
+ @"AXTextSelectionDirection": @4, + @"AXTextSelectionDirection": @(direction),
+ @"AXTextSelectionGranularity": @3 + @"AXTextSelectionGranularity": @(granularity)
+ }; + };
+ NSAccessibilityPostNotificationWithUserInfo ( + NSAccessibilityPostNotificationWithUserInfo (
+ self, + self,
@@ -842,7 +883,7 @@ index 932d209..6543e3b 100644
/* ========================================================================== /* ==========================================================================
EmacsView implementation EmacsView implementation
@@ -6889,6 +7636,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) @@ -6889,6 +7675,7 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action)
[layer release]; [layer release];
#endif #endif
@@ -850,7 +891,7 @@ index 932d209..6543e3b 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -8237,6 +8985,18 @@ ns_in_echo_area (void) @@ -8237,6 +9024,18 @@ ns_in_echo_area (void)
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
@@ -869,7 +910,7 @@ index 932d209..6543e3b 100644
} }
@@ -9474,6 +10234,259 @@ ns_in_echo_area (void) @@ -9474,6 +10273,290 @@ ns_in_echo_area (void)
return fs_state; return fs_state;
} }
@@ -1028,37 +1069,68 @@ index 932d209..6543e3b 100644
+ if (!emacsframe) + if (!emacsframe)
+ return; + return;
+ +
+ /* Re-entrance guard: VoiceOver callbacks during notification posting
+ can trigger redisplay, which calls ns_update_end, which calls us
+ again. Prevent infinite recursion. */
+ if (accessibilityUpdating)
+ return;
+ accessibilityUpdating = YES;
+
+ /* Detect window tree change (split, delete, new buffer). Compare
+ FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */
+ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
+ if (!EQ (curRoot, lastRootWindow))
+ {
+ lastRootWindow = curRoot;
+ accessibilityTreeValid = NO;
+ }
+
+ /* If tree is stale, rebuild FIRST so we don't iterate freed
+ window pointers. Skip notifications for this cycle — the
+ freshly-built elements have no previous state to diff against. */
+ if (!accessibilityTreeValid)
+ {
+ [self rebuildAccessibilityTree];
+
+ /* Post focus change so VoiceOver picks up the new tree. */
+ id focused = [self accessibilityFocusedUIElement];
+ if (focused && focused != self)
+ NSAccessibilityPostNotification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification);
+
+ lastSelectedWindow = emacsframe->selected_window;
+ accessibilityUpdating = NO;
+ return;
+ }
+
+ /* Post per-buffer notifications using EXISTING elements that have + /* Post per-buffer notifications using EXISTING elements that have
+ cached state from the previous cycle. */ + cached state from the previous cycle. Validate each window
+ pointer before use. */
+ for (EmacsAccessibilityElement *elem in accessibilityElements) + for (EmacsAccessibilityElement *elem in accessibilityElements)
+ { + {
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]) + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]])
+ { + {
+ struct window *w = elem.emacsWindow; + struct window *w = elem.emacsWindow;
+ if (w && WINDOW_LEAF_P (w)) + if (w && WINDOW_LEAF_P (w)
+ && BUFFERP (w->contents) && XBUFFER (w->contents))
+ [(EmacsAccessibilityBuffer *) elem + [(EmacsAccessibilityBuffer *) elem
+ postAccessibilityNotificationsForFrame:emacsframe]; + postAccessibilityNotificationsForFrame:emacsframe];
+ } + }
+ } + }
+ +
+ /* Check for window switch (C-x o) before rebuild. */ + /* Check for window switch (C-x o). */
+ Lisp_Object curSel = emacsframe->selected_window; + Lisp_Object curSel = emacsframe->selected_window;
+ BOOL windowSwitched = !EQ (curSel, lastSelectedWindow); + BOOL windowSwitched = !EQ (curSel, lastSelectedWindow);
+ if (windowSwitched) + if (windowSwitched)
+ lastSelectedWindow = curSel;
+
+ /* Rebuild tree only if window configuration changed. */
+ if (!accessibilityTreeValid)
+ [self rebuildAccessibilityTree];
+
+ /* Post focus change AFTER rebuild so the new element exists. */
+ if (windowSwitched)
+ { + {
+ lastSelectedWindow = curSel;
+ id focused = [self accessibilityFocusedUIElement]; + id focused = [self accessibilityFocusedUIElement];
+ if (focused && focused != self) + if (focused && focused != self)
+ NSAccessibilityPostNotification (focused, + NSAccessibilityPostNotification (focused,
+ NSAccessibilityFocusedUIElementChangedNotification); + NSAccessibilityFocusedUIElementChangedNotification);
+ } + }
+
+ accessibilityUpdating = NO;
+} +}
+ +
+/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- +/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ----
@@ -1129,7 +1201,7 @@ index 932d209..6543e3b 100644
@end /* EmacsView */ @end /* EmacsView */
@@ -9941,6 +10954,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) @@ -9941,6 +11024,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c)
return [super accessibilityAttributeValue:attribute]; return [super accessibilityAttributeValue:attribute];
} }