patches: fix B1 (live window ref), B2 (matrix guard), B3 (invisibility-spec), H1 (thread safety), H4 (main thread assert)

This commit is contained in:
2026-02-27 09:57:10 +01:00
parent 081d1c01e7
commit 1245253e15

View File

@@ -1,19 +1,19 @@
From 4626c5d0688db9d878120b120cb19e12fa2f3a94 Mon Sep 17 00:00:00 2001
From 1caa0476b3109ad583715c2f8a90c943780ffcb9 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Fri, 27 Feb 2026 09:39:39 +0100
Date: Fri, 27 Feb 2026 09:57:00 +0100
Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
nav, completions)
---
src/nsterm.h | 71 ++
src/nsterm.m | 2195 ++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 2126 insertions(+), 140 deletions(-)
src/nsterm.h | 73 ++
src/nsterm.m | 2249 +++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 2187 insertions(+), 135 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..22828f2 100644
index 7c1ee4c..717a838 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -453,6 +453,62 @@ enum ns_return_frame_mode
@@ -453,6 +453,64 @@ enum ns_return_frame_mode
@end
@@ -29,7 +29,9 @@ index 7c1ee4c..22828f2 100644
+/* Base class for virtual accessibility elements attached to EmacsView. */
+@interface EmacsAccessibilityElement : NSAccessibilityElement
+@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
+@property (nonatomic, assign) struct window *emacsWindow;
+/* Lisp window object — safe across GC cycles. NULL_LISP when unset. */
+@property (nonatomic, assign) Lisp_Object lispWindow;
+- (struct window *)validWindow; /* Returns live window or NULL. */
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
+@end
+
@@ -76,7 +78,7 @@ index 7c1ee4c..22828f2 100644
/* ==========================================================================
The main Emacs view
@@ -471,6 +527,14 @@ enum ns_return_frame_mode
@@ -471,6 +529,14 @@ enum ns_return_frame_mode
#ifdef NS_IMPL_COCOA
char *old_title;
BOOL maximizing_resize;
@@ -91,7 +93,7 @@ index 7c1ee4c..22828f2 100644
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
@@ -528,6 +592,13 @@ enum ns_return_frame_mode
@@ -528,6 +594,13 @@ enum ns_return_frame_mode
- (void)windowWillExitFullScreen;
- (void)windowDidExitFullScreen;
- (void)windowDidBecomeKey;
@@ -106,10 +108,18 @@ index 7c1ee4c..22828f2 100644
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..06ee636 100644
index 932d209..220dccf 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1104,6 +1104,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
#include "blockinput.h"
#include "sysselect.h"
#include "nsterm.h"
+#include "intervals.h"
#include "systime.h"
#include "character.h"
#include "xwidget.h"
@@ -1104,6 +1105,11 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
unblock_input ();
ns_updating_frame = NULL;
@@ -121,7 +131,7 @@ index 932d209..06ee636 100644
}
static void
@@ -3232,6 +3237,37 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
@@ -3232,6 +3238,37 @@ 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));
@@ -159,7 +169,7 @@ index 932d209..06ee636 100644
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6849,216 +6885,1781 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
@@ -6849,207 +6886,1829 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
/* ==========================================================================
@@ -227,16 +237,14 @@ index 932d209..06ee636 100644
+
+ while (pos < zv)
+ {
+ /* Check invisible property (text properties + overlays). */
+ /* Check invisible property (text properties + overlays).
+ Use TEXT_PROP_MEANS_INVISIBLE which respects buffer-invisibility-spec,
+ matching the logic in xdisp.c. This correctly handles org-mode,
+ outline-mode, hideshow and any mode using spec-controlled
+ invisibility (not just `invisible t'). */
+ Lisp_Object invis = Fget_char_property (make_fixnum (pos),
+ Qinvisible, Qnil);
+ /* Check if invisible property means truly invisible.
+ TEXT_PROP_MEANS_INVISIBLE is defined only in xdisp.c,
+ so we replicate: EQ(invis, Qt), or invis is on the
+ buffer's invisibility-spec list. Simplified: any
+ non-nil invisible property hides the text. This matches
+ the common case (invisible t) and org-mode/dired usage. */
+ if (!NILP (invis))
+ if (TEXT_PROP_MEANS_INVISIBLE (invis))
+ {
+ /* Skip to the next position where invisible changes. */
+ Lisp_Object next = Fnext_single_char_property_change (
@@ -730,8 +738,34 @@ index 932d209..06ee636 100644
+@implementation EmacsAccessibilityElement
- return Qnil;
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh
+- (instancetype)init
+{
+ self = [super init];
+ if (self)
+ self.lispWindow = Qnil;
+ return self;
}
-- (BOOL)acceptsFirstResponder
+/* Return the associated Emacs window if it is still live, else NULL.
+ Use this instead of storing a raw struct window * which can become a
+ dangling pointer after delete-window or kill-buffer. */
+- (struct window *)validWindow
{
- NSTRACE ("[EmacsView acceptsFirstResponder]");
- return YES;
+ if (NILP (self.lispWindow) || !WINDOW_LIVE_P (self.lispWindow))
+ return NULL;
+ return XWINDOW (self.lispWindow);
}
-/* Tell NS we want to accept clicks that activate the window */
-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh
{
- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld",
- [theEvent type], [theEvent clickCount]);
- return ns_click_through;
+ EmacsView *view = self.emacsView;
+ if (!view || ![view window])
+ return NSZeroRect;
@@ -740,39 +774,25 @@ index 932d209..06ee636 100644
+ NSRect winRect = [view convertRect:r toView:nil];
+ return [[view window] convertRectToScreen:winRect];
}
-- (BOOL)acceptsFirstResponder
-- (void)resetCursorRects
+
+- (BOOL)isAccessibilityElement
{
- NSTRACE ("[EmacsView acceptsFirstResponder]");
return YES;
}
-/* Tell NS we want to accept clicks that activate the window */
-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
+/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */
+
+- (id)accessibilityParent
{
- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld",
- [theEvent type], [theEvent clickCount]);
- return ns_click_through;
+ return NSAccessibilityUnignoredAncestor (self.emacsView);
}
-- (void)resetCursorRects
-{
- NSRect visible = [self visibleRect];
- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe);
- NSTRACE ("[EmacsView resetCursorRects]");
-
+ return YES;
+}
- if (currentCursor == nil)
- currentCursor = [NSCursor arrowCursor];
+/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */
- if (!NSIsEmptyRect (visible))
- [self addCursorRect: visible cursor: currentCursor];
+- (id)accessibilityWindow
+- (id)accessibilityParent
+{
+ return [self.emacsView window];
+ return NSAccessibilityUnignoredAncestor (self.emacsView);
+}
-#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300
@@ -781,17 +801,19 @@ index 932d209..06ee636 100644
-#endif
- [currentCursor setOnMouseEntered: YES];
-#endif
+- (id)accessibilityWindow
+{
+ return [self.emacsView window];
+}
+
+- (id)accessibilityTopLevelUIElement
+{
+ return [self.emacsView window];
}
+}
+
+@end
-/*****************************************************************************/
-/* Keyboard handling. */
-#define NS_KEYLOG 0
+
+
+@implementation EmacsAccessibilityBuffer
+@synthesize cachedText;
+@synthesize cachedTextModiff;
@@ -803,8 +825,7 @@ index 932d209..06ee636 100644
+@synthesize cachedCompletionOverlayStart;
+@synthesize cachedCompletionOverlayEnd;
+@synthesize cachedCompletionPoint;
-- (void)keyDown: (NSEvent *)theEvent
+
+- (void)dealloc
+{
+ [cachedText release];
@@ -830,7 +851,7 @@ index 932d209..06ee636 100644
+
+- (void)ensureTextCache
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return;
+
@@ -868,7 +889,7 @@ index 932d209..06ee636 100644
+/* Convert buffer charpos to accessibility string index. */
+- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL;
+
+ for (NSUInteger i = 0; i < visibleRunCount; i++)
@@ -901,7 +922,7 @@ index 932d209..06ee636 100644
+/* Convert accessibility string index to buffer charpos. */
+- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL;
+
+ for (NSUInteger i = 0; i < visibleRunCount; i++)
@@ -950,7 +971,7 @@ index 932d209..06ee636 100644
+
+- (NSString *)accessibilityRoleDescription
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (w && MINI_WINDOW_P (w))
+ return @"minibuffer";
+ return @"editor";
@@ -958,7 +979,7 @@ index 932d209..06ee636 100644
+
+- (NSString *)accessibilityLabel
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (w && WINDOW_LEAF_P (w))
+ {
+ if (MINI_WINDOW_P (w))
@@ -977,7 +998,7 @@ index 932d209..06ee636 100644
+
+- (BOOL)isAccessibilityFocused
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w)
+ return NO;
+ EmacsView *view = self.emacsView;
@@ -989,6 +1010,14 @@ index 932d209..06ee636 100644
+
+- (id)accessibilityValue
+{
+ /* AX getters can be called from any thread by the AT subsystem.
+ Dispatch to main thread where Emacs buffer state is consistent. */
+ if (![NSThread isMainThread])
+ {
+ __block id result;
+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityValue]; });
+ return result;
+ }
+ [self ensureTextCache];
+ return cachedText ? cachedText : @"";
+}
@@ -1001,7 +1030,7 @@ index 932d209..06ee636 100644
+
+- (NSString *)accessibilitySelectedText
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return @"";
+
@@ -1019,7 +1048,13 @@ index 932d209..06ee636 100644
+
+- (NSRange)accessibilitySelectedTextRange
+{
+ struct window *w = self.emacsWindow;
+ if (![NSThread isMainThread])
+ {
+ __block NSRange result;
+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilitySelectedTextRange]; });
+ return result;
+ }
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return NSMakeRange (0, 0);
+
@@ -1045,7 +1080,7 @@ index 932d209..06ee636 100644
+
+- (void)setAccessibilitySelectedTextRange:(NSRange)range
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return;
+
@@ -1104,7 +1139,7 @@ index 932d209..06ee636 100644
+ if (!flag)
+ return;
+
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return;
+
@@ -1132,7 +1167,13 @@ index 932d209..06ee636 100644
+
+- (NSInteger)accessibilityInsertionPointLineNumber
+{
+ struct window *w = self.emacsWindow;
+ if (![NSThread isMainThread])
+ {
+ __block NSInteger result;
+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityInsertionPointLineNumber]; });
+ return result;
+ }
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return 0;
+
@@ -1246,7 +1287,13 @@ index 932d209..06ee636 100644
+
+- (NSRect)accessibilityFrameForRange:(NSRange)range
+{
+ struct window *w = self.emacsWindow;
+ if (![NSThread isMainThread])
+ {
+ __block NSRect result;
+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityFrameForRange:range]; });
+ return result;
+ }
+ struct window *w = [self validWindow];
+ EmacsView *view = self.emacsView;
+ if (!w || !view)
+ return NSZeroRect;
@@ -1261,8 +1308,14 @@ index 932d209..06ee636 100644
+
+- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
+{
+ if (![NSThread isMainThread])
+ {
+ __block NSRange result;
+ dispatch_sync (dispatch_get_main_queue (), ^{ result = [self accessibilityRangeForPosition:screenPoint]; });
+ return result;
+ }
+ /* Hit test: convert screen point to buffer character index. */
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ EmacsView *view = self.emacsView;
+ if (!w || !view || !w->current_matrix)
+ return NSMakeRange (0, 0);
@@ -1278,6 +1331,10 @@ index 932d209..06ee636 100644
+ if (x < 0 || y < 0 || x >= w->pixel_width || y >= w->pixel_height)
+ return NSMakeRange (0, 0);
+
+ /* Block input to prevent concurrent redisplay from modifying the
+ glyph matrix while we traverse it. */
+ block_input ();
+
+ /* Find the glyph row at this y coordinate. */
+ struct glyph_matrix *matrix = w->current_matrix;
+ struct glyph_row *hit_row = NULL;
@@ -1297,7 +1354,10 @@ index 932d209..06ee636 100644
+ }
+
+ if (!hit_row)
+ {
+ unblock_input ();
+ return NSMakeRange (0, 0);
+ }
+
+ /* Find the glyph at this x coordinate within the row. */
+ struct glyph *glyph = hit_row->glyphs[TEXT_AREA];
@@ -1324,6 +1384,8 @@ index 932d209..06ee636 100644
+ NSUInteger ax_idx = [self accessibilityIndexForCharpos:best_charpos];
+ if (cachedText && ax_idx > [cachedText length])
+ ax_idx = [cachedText length];
+
+ unblock_input ();
+ return NSMakeRange (ax_idx, 1);
+}
+
@@ -1338,7 +1400,7 @@ index 932d209..06ee636 100644
+
+- (NSRect)accessibilityFrame
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w)
+ return NSZeroRect;
+
@@ -1366,7 +1428,7 @@ index 932d209..06ee636 100644
+
+- (void)postAccessibilityNotificationsForFrame:(struct frame *)f
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w || !WINDOW_LEAF_P (w))
+ return;
+
@@ -1812,7 +1874,7 @@ index 932d209..06ee636 100644
+
+- (NSString *)accessibilityLabel
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (w && WINDOW_LEAF_P (w))
+ {
+ struct buffer *b = XBUFFER (w->contents);
@@ -1831,7 +1893,7 @@ index 932d209..06ee636 100644
+
+- (id)accessibilityValue
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w)
+ return @"";
+ return ns_ax_mode_line_text (w);
@@ -1839,7 +1901,7 @@ index 932d209..06ee636 100644
+
+- (NSRect)accessibilityFrame
+{
+ struct window *w = self.emacsWindow;
+ struct window *w = [self validWindow];
+ if (!w || !w->current_matrix)
+ return NSZeroRect;
+
@@ -2069,19 +2131,10 @@ index 932d209..06ee636 100644
+#endif
+ [currentCursor setOnMouseEntered: YES];
+#endif
+}
+
+
+
+/*****************************************************************************/
+/* Keyboard handling. */
+#define NS_KEYLOG 0
+
+- (void)keyDown: (NSEvent *)theEvent
{
Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe);
int code;
@@ -8237,6 +9838,28 @@ - (void)windowDidBecomeKey /* for direct calls */
}
@@ -8237,6 +9896,28 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -2110,7 +2163,7 @@ index 932d209..06ee636 100644
}
@@ -9474,6 +11097,298 @@ - (int) fullscreenState
@@ -9474,6 +11155,304 @@ - (int) fullscreenState
return fs_state;
}
@@ -2147,7 +2200,7 @@ index 932d209..06ee636 100644
+ {
+ [elem retain];
+ }
+ elem.emacsWindow = w;
+ elem.lispWindow = window;
+ [elements addObject:elem];
+ [elem release];
+
@@ -2157,7 +2210,7 @@ index 932d209..06ee636 100644
+ EmacsAccessibilityModeLine *ml
+ = [[EmacsAccessibilityModeLine alloc] init];
+ ml.emacsView = view;
+ ml.emacsWindow = w;
+ ml.lispWindow = window;
+ [elements addObject:ml];
+ [ml release];
+ }
@@ -2186,10 +2239,10 @@ index 932d209..06ee636 100644
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
+ {
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]
+ && elem.emacsWindow)
+ && !NILP (elem.lispWindow))
+ [existing setObject:elem
+ forKey:[NSValue valueWithPointer:
+ elem.emacsWindow]];
+ XWINDOW (elem.lispWindow)]];
+ }
+ }
+
@@ -2248,7 +2301,7 @@ index 932d209..06ee636 100644
+ for (EmacsAccessibilityElement *elem in accessibilityElements)
+ {
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]
+ && elem.emacsWindow == sel)
+ && EQ (elem.lispWindow, emacsframe->selected_window))
+ return elem;
+ }
+ return self;
@@ -2262,6 +2315,9 @@ index 932d209..06ee636 100644
+ elements with current values, making change detection impossible. */
+- (void)postAccessibilityUpdates
+{
+ NSCAssert ([NSThread isMainThread],
+ @"postAccessibilityUpdates must be called on the main thread");
+
+ if (!emacsframe)
+ return;
+
@@ -2271,6 +2327,7 @@ index 932d209..06ee636 100644
+ if (accessibilityUpdating)
+ return;
+ accessibilityUpdating = YES;
+ @try {
+
+ /* Detect window tree change (split, delete, new buffer). Compare
+ FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */
@@ -2308,7 +2365,7 @@ index 932d209..06ee636 100644
+ {
+ if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]])
+ {
+ struct window *w = elem.emacsWindow;
+ struct window *w = [elem validWindow];
+ if (w && WINDOW_LEAF_P (w)
+ && BUFFERP (w->contents) && XBUFFER (w->contents))
+ [(EmacsAccessibilityBuffer *) elem
@@ -2338,7 +2395,9 @@ index 932d209..06ee636 100644
+ NSAccessibilityFocusedUIElementChangedNotification);
+ }
+
+ } @finally {
+ accessibilityUpdating = NO;
+ }
+}
+
+/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ----