diff --git a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch index 63fbadf..8e0ccc7 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,19 +1,19 @@ -From 6a256eb9269e0cfc4d1270ef61c228dd9f9989da Mon Sep 17 00:00:00 2001 +From 06e39ff5c5e1cf836ff4c78dafc2939ef0b05851 Mon Sep 17 00:00:00 2001 From: Daneel -Date: Thu, 26 Feb 2026 18:41:28 +0100 -Subject: [PATCH] ns: implement AXBoundsForRange and VoiceOver interaction - fixes +Date: Thu, 26 Feb 2026 17:02:31 +0100 +Subject: [PATCH 1/9] v15.8: fix C-n/C-p AX state mapping and completions + candidate announcement --- - nsterm.h | 71 ++ - nsterm.m | 2201 ++++++++++++++++++++++++++++++++++++++++++++++++++---- - 2 files changed, 2133 insertions(+), 139 deletions(-) + nsterm.h | 67 ++ + nsterm.m | 1837 ++++++++++++++++++++++++++++++++++++++++++++++++------ + 2 files changed, 1723 insertions(+), 181 deletions(-) -diff --git a/src/nsterm.h b/src/nsterm.h -index 7c1ee4c..22828f2 100644 ---- a/src/nsterm.h -+++ b/src/nsterm.h -@@ -453,6 +453,62 @@ enum ns_return_frame_mode +diff --git a/nsterm.h b/nsterm.h +index 7c1ee4c..2e2c80f 100644 +--- a/nsterm.h ++++ b/nsterm.h +@@ -453,6 +453,58 @@ enum ns_return_frame_mode @end @@ -57,10 +57,6 @@ index 7c1ee4c..22828f2 100644 +@property (nonatomic, assign) ptrdiff_t cachedModiff; +@property (nonatomic, assign) ptrdiff_t cachedPoint; +@property (nonatomic, assign) BOOL cachedMarkActive; -+@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; -+@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart; -+@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd; -+@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint; +- (void)invalidateTextCache; +- (void)postAccessibilityNotificationsForFrame:(struct frame *)f; +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx; @@ -76,7 +72,7 @@ index 7c1ee4c..22828f2 100644 /* ========================================================================== The main Emacs view -@@ -471,6 +527,14 @@ enum ns_return_frame_mode +@@ -471,6 +523,14 @@ enum ns_return_frame_mode #ifdef NS_IMPL_COCOA char *old_title; BOOL maximizing_resize; @@ -91,7 +87,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 +588,13 @@ enum ns_return_frame_mode - (void)windowWillExitFullScreen; - (void)windowDidExitFullScreen; - (void)windowDidBecomeKey; @@ -105,10 +101,10 @@ index 7c1ee4c..22828f2 100644 @end -diff --git a/src/nsterm.m b/src/nsterm.m -index 932d209..da40369 100644 ---- a/src/nsterm.m -+++ b/src/nsterm.m +diff --git a/nsterm.m b/nsterm.m +index 932d209..416e5a4 100644 +--- a/nsterm.m ++++ b/nsterm.m @@ -1104,6 +1104,11 @@ ns_update_end (struct frame *f) unblock_input (); @@ -159,7 +155,7 @@ index 932d209..da40369 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,214 +6885,1801 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) +@@ -6849,261 +6885,1380 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) /* ========================================================================== @@ -174,62 +170,93 @@ index 932d209..da40369 100644 +/* ---- Helper: extract buffer text for accessibility ---- */ -- (void)windowDidEndLiveResize:(NSNotification *)notification +-{ +- [self updateFramePosition]; +-} +/* Maximum characters exposed via accessibilityValue. */ +#define NS_AX_TEXT_CAP 100000 -+ + +-/* Needed to inform when window closed from lisp. */ +-- (void) setWindowClosing: (BOOL)closing +-{ +- NSTRACE ("[EmacsView setWindowClosing:%d]", closing); +/* 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 + with the count. Caller must free *OUT_RUNS with xfree(). */ -+ + +- windowClosing = closing; +-} +static NSString * +ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, + ns_ax_visible_run **out_runs, NSUInteger *out_nruns) - { -- [self updateFramePosition]; ++{ + *out_runs = NULL; + *out_nruns = 0; -+ + + if (!w || !WINDOW_LEAF_P (w)) + { + *out_start = 0; + return @""; + } -+ -+ if (!BUFFERP (w->contents)) -+ { -+ *out_start = 0; -+ return @""; -+ } + +-- (void)dealloc +-{ +- NSTRACE ("[EmacsView dealloc]"); + struct buffer *b = XBUFFER (w->contents); + if (!b) + { + *out_start = 0; + return @""; + } -+ + +- /* Clear the view resize notification. */ +- [[NSNotificationCenter defaultCenter] +- removeObserver:self +- name:NSViewFrameDidChangeNotification +- object:nil]; + ptrdiff_t begv = BUF_BEGV (b); + ptrdiff_t zv = BUF_ZV (b); -+ + +- if (fs_state == FULLSCREEN_BOTH) +- [nonfs_window release]; + *out_start = begv; -+ + +-#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 +- /* Release layer and menu */ +- EmacsLayer *layer = (EmacsLayer *)[self layer]; +- [layer release]; +-#endif + if (zv <= begv) + return @""; -+ + +- [[self menu] release]; +- [super dealloc]; +-} + struct buffer *oldb = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); -+ + + /* First pass: count visible runs to allocate the mapping array. */ + NSUInteger run_capacity = 64; + ns_ax_visible_run *runs = xmalloc (run_capacity + * sizeof (ns_ax_visible_run)); + NSUInteger nruns = 0; + NSUInteger ax_offset = 0; -+ + +-/* Called on font panel selection. */ +-- (void) changeFont: (id) sender +-{ +- struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; +- NSFont *nsfont; + NSMutableString *result = [NSMutableString string]; + ptrdiff_t pos = begv; -+ + +-#ifdef NS_IMPL_GNUSTEP +- nsfont = ((struct nsfont_info *) font)->nsfont; +-#else +- nsfont = (NSFont *) macfont_get_nsctfont (font); +-#endif + while (pos < zv) + { + /* Check invisible property (text properties + overlays). */ @@ -249,12 +276,16 @@ index 932d209..da40369 100644 + pos = FIXNUMP (next) ? XFIXNUM (next) : zv; + continue; + } -+ + +- if (!font_panel_active) +- return; + /* Find end of this visible run: where invisible property changes. */ + Lisp_Object next = Fnext_single_char_property_change ( + make_fixnum (pos), Qinvisible, Qnil, make_fixnum (zv)); + ptrdiff_t run_end = FIXNUMP (next) ? XFIXNUM (next) : zv; -+ + +- if (font_panel_result) +- [font_panel_result release]; + /* Cap total text at NS_AX_TEXT_CAP. */ + ptrdiff_t run_len = run_end - pos; + if (ax_offset + (NSUInteger) run_len > NS_AX_TEXT_CAP) @@ -285,41 +316,54 @@ index 932d209..da40369 100644 + runs[nruns].ax_start = ax_offset; + runs[nruns].ax_length = ns_len; + nruns++; -+ + +- font_panel_result = (NSFont *) [sender convertFont: nsfont]; + ax_offset += ns_len; + pos = run_end; + } -+ + +- if (font_panel_result) +- [font_panel_result retain]; + if (b != oldb) + set_buffer_internal_1 (oldb); -+ + +-#ifndef NS_IMPL_COCOA +- font_panel_active = NO; +- [NSApp stop: self]; +-#endif + *out_runs = runs; + *out_nruns = nruns; + return result; } --/* Needed to inform when window closed from lisp. */ --- (void) setWindowClosing: (BOOL)closing +-#ifdef NS_IMPL_COCOA +-- (void) noteUserSelectedFont + +/* ---- Helper: extract mode line text from glyph rows ---- */ + +static NSString * +ns_ax_mode_line_text (struct window *w) { -- NSTRACE ("[EmacsView setWindowClosing:%d]", closing); +- font_panel_active = NO; + if (!w || !w->current_matrix) + return @""; -- windowClosing = closing; +- /* If no font was previously selected, use the currently selected +- font. */ + struct glyph_matrix *matrix = w->current_matrix; + NSMutableString *text = [NSMutableString string]; -+ + +- if (!font_panel_result && FRAME_FONT (emacsframe)) + for (int i = 0; i < matrix->nrows; i++) -+ { + { +- font_panel_result +- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || !row->mode_line_p) + continue; -+ + +- if (font_panel_result) +- [font_panel_result retain]; + struct glyph *g = row->glyphs[TEXT_AREA]; + struct glyph *end = g + row->used[TEXT_AREA]; + for (; g < end; g++) @@ -331,42 +375,64 @@ index 932d209..da40369 100644 + length:1]]; + } + } -+ } + } +- +- [NSApp stop: self]; + return text; } +-- (void) noteUserCancelledSelection +-{ +- font_panel_active = NO; +- +- if (font_panel_result) +- [font_panel_result release]; +- font_panel_result = nil; --- (void)dealloc +- [NSApp stop: self]; +-} +-#endif +/* ---- Helper: screen rect for a character range via glyph matrix ---- */ -+ + +-- (Lisp_Object) showFontPanel +static NSRect +ns_ax_frame_for_range (struct window *w, EmacsView *view, + ptrdiff_t text_start, NSRange range) { -- NSTRACE ("[EmacsView dealloc]"); +- id fm = [NSFontManager sharedFontManager]; +- struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; +- NSFont *nsfont, *result; +- struct timespec timeout; +-#ifdef NS_IMPL_COCOA +- NSView *buttons; +- BOOL canceled; +-#endif + if (!w || !w->current_matrix || !view) + return NSZeroRect; -- /* Clear the view resize notification. */ -- [[NSNotificationCenter defaultCenter] -- removeObserver:self -- name:NSViewFrameDidChangeNotification -- object:nil]; +-#ifdef NS_IMPL_GNUSTEP +- nsfont = ((struct nsfont_info *) font)->nsfont; +-#else +- nsfont = (NSFont *) macfont_get_nsctfont (font); +-#endif + /* Convert range indices back to buffer charpos. */ + ptrdiff_t cp_start = text_start + (ptrdiff_t) range.location; + ptrdiff_t cp_end = cp_start + (ptrdiff_t) range.length; -- if (fs_state == FULLSCREEN_BOTH) -- [nonfs_window release]; +-#ifdef NS_IMPL_COCOA +- buttons +- = ns_create_font_panel_buttons (self, +- @selector (noteUserSelectedFont), +- @selector (noteUserCancelledSelection)); +- [[fm fontPanel: YES] setAccessoryView: buttons]; +- [buttons release]; +-#endif + struct glyph_matrix *matrix = w->current_matrix; + NSRect result = NSZeroRect; + BOOL found = NO; --#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 -- /* Release layer and menu */ -- EmacsLayer *layer = (EmacsLayer *)[self layer]; -- [layer release]; --#endif +- [fm setSelectedFont: nsfont isMultiple: NO]; +- [fm orderFrontFontPanel: NSApp]; + for (int i = 0; i < matrix->nrows; i++) + { + struct glyph_row *row = matrix->rows + i; @@ -375,11 +441,21 @@ index 932d209..da40369 100644 + if (!row->displays_text_p && !row->ends_at_zv_p) + continue; -- [[self menu] release]; -- [super dealloc]; +- font_panel_active = YES; +- timeout = make_timespec (0, 100000000); + ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row); + ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row); -+ + +- block_input (); +- while (font_panel_active +-#ifdef NS_IMPL_COCOA +- && (canceled = [[fm fontPanel: YES] isVisible]) +-#else +- && [[fm fontPanel: YES] isVisible] +-#endif +- ) +- ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES); +- unblock_input (); + if (row_start < cp_end && row_end > cp_start) + { + int window_x, window_y, window_width; @@ -402,10 +478,16 @@ index 932d209..da40369 100644 + result = NSUnionRect (result, rowRect); + } + } -+ + +- if (font_panel_result) +- [font_panel_result autorelease]; + if (!found) + return NSZeroRect; -+ + +-#ifdef NS_IMPL_COCOA +- if (!canceled) +- font_panel_result = nil; +-#endif + /* Clip result to text area bounds. */ + { + int text_area_x, text_area_y, text_area_w, text_area_h; @@ -415,315 +497,12 @@ index 932d209..da40369 100644 + if (NSMaxY (result) > max_y) + result.size.height = max_y - result.origin.y; + } -+ -+ /* Convert from EmacsView (flipped) coords to screen coords. */ -+ NSRect winRect = [view convertRect:result toView:nil]; -+ return [[view window] convertRectToScreen:winRect]; - } - -+/* AX enum numeric compatibility for NSAccessibility notifications. -+ Values match WebKit AXObjectCacheMac fallback enums -+ (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection / -+ AXTextSelectionGranularity). */ -+enum { -+ ns_ax_text_state_change_unknown = 0, -+ ns_ax_text_state_change_edit = 1, -+ ns_ax_text_state_change_selection_move = 2, - --/* Called on font panel selection. */ --- (void) changeFont: (id) sender --{ -- struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; -- NSFont *nsfont; -+ ns_ax_text_edit_type_typing = 3, - --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif -+ ns_ax_text_selection_direction_unknown = 0, -+ ns_ax_text_selection_direction_previous = 3, -+ ns_ax_text_selection_direction_next = 4, -+ ns_ax_text_selection_direction_discontiguous = 5, - -- if (!font_panel_active) -- return; -+ ns_ax_text_selection_granularity_unknown = 0, -+ ns_ax_text_selection_granularity_character = 1, -+ ns_ax_text_selection_granularity_line = 3, -+}; - -- if (font_panel_result) -- [font_panel_result release]; -+static NSUInteger -+ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start, -+ ptrdiff_t end) -+{ -+ if (!b || end <= start) -+ return 0; - -- font_panel_result = (NSFont *) [sender convertFont: nsfont]; -+ struct buffer *oldb = current_buffer; -+ if (b != current_buffer) -+ set_buffer_internal_1 (b); - -- if (font_panel_result) -- [font_panel_result retain]; -+ Lisp_Object lstr = Fbuffer_substring_no_properties (make_fixnum (start), -+ make_fixnum (end)); -+ NSString *nsstr = [NSString stringWithLispString:lstr]; -+ NSUInteger len = [nsstr length]; - --#ifndef NS_IMPL_COCOA -- font_panel_active = NO; -- [NSApp stop: self]; --#endif -+ if (b != oldb) -+ set_buffer_internal_1 (oldb); -+ -+ return len; - } - --#ifdef NS_IMPL_COCOA --- (void) noteUserSelectedFont -+static BOOL -+ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, -+ ptrdiff_t *out_start, -+ ptrdiff_t *out_end) - { -- font_panel_active = NO; -+ if (!b || !out_start || !out_end) -+ return NO; - -- /* If no font was previously selected, use the currently selected -- font. */ -+ Lisp_Object faceSym = intern ("completions-highlight"); -+ ptrdiff_t begv = BUF_BEGV (b); -+ ptrdiff_t zv = BUF_ZV (b); -+ ptrdiff_t best_start = 0; -+ ptrdiff_t best_end = 0; -+ ptrdiff_t best_dist = PTRDIFF_MAX; -+ BOOL found = NO; - -- if (!font_panel_result && FRAME_FONT (emacsframe)) -+ /* Fast path: look at point and immediate neighbors first. */ -+ ptrdiff_t probes[3] = { point, point - 1, point + 1 }; -+ for (int i = 0; i < 3 && !found; i++) - { -- font_panel_result -- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); -+ ptrdiff_t p = probes[i]; -+ if (p < begv || p > zv) -+ continue; - -- if (font_panel_result) -- [font_panel_result retain]; -+ Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil); -+ Lisp_Object tail; -+ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) -+ { -+ Lisp_Object ov = XCAR (tail); -+ Lisp_Object face = Foverlay_get (ov, Qface); -+ if (!(EQ (face, faceSym) -+ || (CONSP (face) && !NILP (Fmemq (faceSym, face))))) -+ continue; -+ -+ ptrdiff_t ov_start = OVERLAY_START (ov); -+ ptrdiff_t ov_end = OVERLAY_END (ov); -+ if (ov_end <= ov_start) -+ continue; -+ -+ best_start = ov_start; -+ best_end = ov_end; -+ best_dist = 0; -+ found = YES; -+ break; -+ } - } - -- [NSApp stop: self]; -+ if (!found) -+ { -+ for (ptrdiff_t scan = begv; scan < zv; scan++) -+ { -+ Lisp_Object overlays = Foverlays_at (make_fixnum (scan), Qnil); -+ Lisp_Object tail; -+ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) -+ { -+ Lisp_Object ov = XCAR (tail); -+ Lisp_Object face = Foverlay_get (ov, Qface); -+ if (!(EQ (face, faceSym) -+ || (CONSP (face) && !NILP (Fmemq (faceSym, face))))) -+ continue; -+ -+ ptrdiff_t ov_start = OVERLAY_START (ov); -+ ptrdiff_t ov_end = OVERLAY_END (ov); -+ if (ov_end <= ov_start) -+ continue; -+ -+ ptrdiff_t dist = 0; -+ if (point < ov_start) -+ dist = ov_start - point; -+ else if (point > ov_end) -+ dist = point - ov_end; -+ -+ if (!found || dist < best_dist -+ || (dist == best_dist -+ && (ov_start < point && best_start >= point))) -+ { -+ best_start = ov_start; -+ best_end = ov_end; -+ best_dist = dist; -+ found = YES; -+ } -+ } -+ } -+ } -+ -+ if (!found) -+ return NO; -+ -+ *out_start = best_start; -+ *out_end = best_end; -+ return YES; - } - --- (void) noteUserCancelledSelection -+static bool -+ns_ax_event_is_ctrl_n_or_p (int *which) - { -- font_panel_active = NO; -+ Lisp_Object ev = last_command_event; -+ if (CONSP (ev)) -+ ev = EVENT_HEAD (ev); - -- if (font_panel_result) -- [font_panel_result release]; -- font_panel_result = nil; -+ if (!FIXNUMP (ev)) -+ return false; - -- [NSApp stop: self]; -+ EMACS_INT c = XFIXNUM (ev); -+ if (c == ('n' & 0x1f)) -+ { -+ if (which) -+ *which = 1; -+ return true; -+ } -+ if (c == ('p' & 0x1f)) -+ { -+ if (which) -+ *which = -1; -+ return true; -+ } -+ return false; - } --#endif - --- (Lisp_Object) showFontPanel -+static bool -+ns_ax_command_is_basic_line_move (void) - { -- id fm = [NSFontManager sharedFontManager]; -- struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; -- NSFont *nsfont, *result; -- struct timespec timeout; --#ifdef NS_IMPL_COCOA -- NSView *buttons; -- BOOL canceled; --#endif -+ if (!SYMBOLP (Vreal_this_command)) -+ return false; - --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif -+ Lisp_Object next = intern ("next-line"); -+ Lisp_Object prev = intern ("previous-line"); -+ return EQ (Vreal_this_command, next) || EQ (Vreal_this_command, prev); -+} - --#ifdef NS_IMPL_COCOA -- buttons -- = ns_create_font_panel_buttons (self, -- @selector (noteUserSelectedFont), -- @selector (noteUserCancelledSelection)); -- [[fm fontPanel: YES] setAccessoryView: buttons]; -- [buttons release]; --#endif -+static NSString * -+ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, -+ struct buffer *b, -+ ptrdiff_t start, -+ ptrdiff_t end, -+ NSString *cachedText) -+{ -+ if (!elem || !b || !cachedText || end <= start) -+ return nil; - -- [fm setSelectedFont: nsfont isMultiple: NO]; -- [fm orderFrontFontPanel: NSApp]; -+ NSString *text = nil; -+ struct buffer *oldb = current_buffer; -+ if (b != current_buffer) -+ set_buffer_internal_1 (b); - -- font_panel_active = YES; -- timeout = make_timespec (0, 100000000); -+ /* Prefer canonical completion candidate string from text property. */ -+ ptrdiff_t probes[2] = { start, end - 1 }; -+ for (int i = 0; i < 2 && !text; i++) -+ { -+ ptrdiff_t p = probes[i]; -+ Lisp_Object cstr = Fget_char_property (make_fixnum (p), -+ intern ("completion--string"), -+ Qnil); -+ if (STRINGP (cstr)) -+ text = [NSString stringWithLispString:cstr]; -+ } - -- block_input (); -- while (font_panel_active --#ifdef NS_IMPL_COCOA -- && (canceled = [[fm fontPanel: YES] isVisible]) --#else -- && [[fm fontPanel: YES] isVisible] --#endif -- ) -- ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES); -- unblock_input (); -+ if (!text) -+ { -+ NSUInteger ax_s = [elem accessibilityIndexForCharpos:start]; -+ NSUInteger ax_e = [elem accessibilityIndexForCharpos:end]; -+ if (ax_e > ax_s && ax_e <= [cachedText length]) -+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)]; -+ } - -- if (font_panel_result) -- [font_panel_result autorelease]; -+ if (b != oldb) -+ set_buffer_internal_1 (oldb); - --#ifdef NS_IMPL_COCOA -- if (!canceled) -- font_panel_result = nil; --#endif -+ if (text) -+ { -+ text = [text stringByTrimmingCharactersInSet: -+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; -+ if ([text length] == 0) -+ text = nil; -+ } - result = font_panel_result; - font_panel_result = nil; -+ return text; ++ /* Convert from EmacsView (flipped) coords to screen coords. */ ++ NSRect winRect = [view convertRect:result toView:nil]; ++ return [[view window] convertRectToScreen:winRect]; +} - [[fm fontPanel: YES] setIsVisible: NO]; @@ -765,38 +544,34 @@ index 932d209..da40369 100644 + return NSAccessibilityUnignoredAncestor (self.emacsView); } -- (void)resetCursorRects --{ ++ ++- (id)accessibilityWindow + { - NSRect visible = [self visibleRect]; - NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); - NSTRACE ("[EmacsView resetCursorRects]"); -- ++ return [self.emacsView window]; ++} + - if (currentCursor == nil) - currentCursor = [NSCursor arrowCursor]; - -- if (!NSIsEmptyRect (visible)) -- [self addCursorRect: visible cursor: currentCursor]; -+- (id)accessibilityWindow ++- (id)accessibilityTopLevelUIElement +{ + return [self.emacsView window]; +} +- if (!NSIsEmptyRect (visible)) +- [self addCursorRect: visible cursor: currentCursor]; ++@end + -#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300 -#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 - if ([currentCursor respondsToSelector: @selector(setOnMouseEntered:)]) -#endif - [currentCursor setOnMouseEntered: YES]; -#endif -+- (id)accessibilityTopLevelUIElement -+{ -+ return [self.emacsView window]; - } +-} -+@end - - --/*****************************************************************************/ --/* Keyboard handling. */ --#define NS_KEYLOG 0 +@implementation EmacsAccessibilityBuffer +@synthesize cachedText; +@synthesize cachedTextModiff; @@ -804,24 +579,28 @@ index 932d209..da40369 100644 +@synthesize cachedModiff; +@synthesize cachedPoint; +@synthesize cachedMarkActive; -+@synthesize cachedCompletionAnnouncement; -+@synthesize cachedCompletionOverlayStart; -+@synthesize cachedCompletionOverlayEnd; -+@synthesize cachedCompletionPoint; -+ + +- (void)dealloc +{ + [cachedText release]; -+ [cachedCompletionAnnouncement release]; + if (visibleRuns) + xfree (visibleRuns); + [super dealloc]; +} -+ + +-/*****************************************************************************/ +-/* Keyboard handling. */ +-#define NS_KEYLOG 0 +/* ---- Text cache ---- */ -+ + +-- (void)keyDown: (NSEvent *)theEvent +- (void)invalidateTextCache -+{ + { +- Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); +- int code; +- unsigned fnKeysym = 0; +- static NSMutableArray *nsEvArray; +- unsigned int flags = [theEvent modifierFlags]; + [cachedText release]; + cachedText = nil; + if (visibleRuns) @@ -831,19 +610,21 @@ index 932d209..da40369 100644 + } + visibleRunCount = 0; +} -+ + +- NSTRACE ("[EmacsView keyDown:]"); +- (void)ensureTextCache +{ + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) + return; -+ -+ if (!BUFFERP (w->contents)) -+ return; + +- /* Rhapsody and macOS give up and down events for the arrow keys. */ +- if ([theEvent type] != NSEventTypeKeyDown) + struct buffer *b = XBUFFER (w->contents); + if (!b) -+ return; -+ + return; + +- if (!emacs_event) + ptrdiff_t modiff = BUF_MODIFF (b); + ptrdiff_t pt = BUF_PT (b); + NSUInteger textLen = cachedText ? [cachedText length] : 0; @@ -851,45 +632,56 @@ index 932d209..da40369 100644 + && pt >= cachedTextStart + && (textLen == 0 + || [self accessibilityIndexForCharpos:pt] <= textLen)) -+ return; -+ + return; + +- if (![[self window] isKeyWindow] +- && [[theEvent window] isKindOfClass: [EmacsWindow class]] +- /* We must avoid an infinite loop here. */ +- && (EmacsView *)[[theEvent window] delegate] != self) +- { +- /* XXX: There is an occasional condition in which, when Emacs display +- updates a different frame from the current one, and temporarily +- selects it, then processes some interrupt-driven input +- (dispnew.c:3878), OS will send the event to the correct NSWindow, but +- for some reason that window has its first responder set to the NSView +- most recently updated (I guess), which is not the correct one. */ +- [(EmacsView *)[[theEvent window] delegate] keyDown: theEvent]; +- return; +- } + ptrdiff_t start; + ns_ax_visible_run *runs = NULL; + NSUInteger nruns = 0; + NSString *text = ns_ax_buffer_text (w, &start, &runs, &nruns); -+ + +- if (nsEvArray == nil) +- nsEvArray = [[NSMutableArray alloc] initWithCapacity: 1]; + [cachedText release]; + cachedText = [text retain]; + cachedTextModiff = modiff; + cachedTextStart = start; -+ + +- [NSCursor setHiddenUntilMouseMoves:! NILP (Vmake_pointer_invisible)]; + if (visibleRuns) + xfree (visibleRuns); + visibleRuns = runs; + visibleRunCount = nruns; +} -+ + +- if (!hlinfo->mouse_face_hidden +- && FIXNUMP (Vmouse_highlight) +- && !EQ (emacsframe->tab_bar_window, hlinfo->mouse_face_window)) +/* ---- Index mapping ---- */ + +/* Convert buffer charpos to accessibility string index. */ +- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos +{ -+ struct window *w = self.emacsWindow; -+ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; -+ + for (NSUInteger i = 0; i < visibleRunCount; i++) -+ { + { +- clear_mouse_face (hlinfo); +- hlinfo->mouse_face_hidden = true; + ns_ax_visible_run *r = &visibleRuns[i]; + if (charpos >= r->charpos && charpos < r->charpos + r->length) -+ { -+ if (!b) -+ return r->ax_start; -+ NSUInteger delta = ns_ax_utf16_length_for_buffer_range (b, r->charpos, -+ charpos); -+ if (delta > r->ax_length) -+ delta = r->ax_length; -+ return r->ax_start + delta; -+ } ++ return r->ax_start + (NSUInteger) (charpos - r->charpos); + /* If charpos falls in an invisible gap before the next run, + map it to the start of the next visible run. */ + if (charpos < r->charpos) @@ -900,43 +692,20 @@ index 932d209..da40369 100644 + { + ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; + return last->ax_start + last->ax_length; -+ } + } + return 0; +} -+ + +- if (!processingCompose) +/* Convert accessibility string index to buffer charpos. */ +- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx +{ -+ struct window *w = self.emacsWindow; -+ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; -+ + for (NSUInteger i = 0; i < visibleRunCount; i++) + { + ns_ax_visible_run *r = &visibleRuns[i]; + if (ax_idx >= r->ax_start + && ax_idx < r->ax_start + r->ax_length) -+ { -+ if (!b) -+ return r->charpos; -+ -+ NSUInteger target = ax_idx - r->ax_start; -+ ptrdiff_t lo = r->charpos; -+ ptrdiff_t hi = r->charpos + r->length; -+ -+ while (lo < hi) -+ { -+ ptrdiff_t mid = lo + (hi - lo) / 2; -+ NSUInteger mid_len = ns_ax_utf16_length_for_buffer_range (b, -+ r->charpos, -+ mid); -+ if (mid_len < target) -+ lo = mid + 1; -+ else -+ hi = mid; -+ } -+ -+ return lo; -+ } ++ return r->charpos + (ptrdiff_t) (ax_idx - r->ax_start); + } + /* Past end — return last charpos. */ + if (visibleRunCount > 0) @@ -1011,8 +780,6 @@ index 932d209..da40369 100644 + if (!w || !WINDOW_LEAF_P (w)) + return @""; + -+ if (!BUFFERP (w->contents)) -+ return @""; + struct buffer *b = XBUFFER (w->contents); + if (!b || NILP (BVAR (b, mark_active))) + return @""; @@ -1031,8 +798,6 @@ index 932d209..da40369 100644 + if (!w || !WINDOW_LEAF_P (w)) + return NSMakeRange (0, 0); + -+ if (!BUFFERP (w->contents)) -+ return 0; + struct buffer *b = XBUFFER (w->contents); + if (!b) + return NSMakeRange (0, 0); @@ -1057,63 +822,50 @@ index 932d209..da40369 100644 + if (!w || !WINDOW_LEAF_P (w)) + return; + -+ if (!BUFFERP (w->contents)) ++ struct buffer *b = XBUFFER (w->contents); ++ if (!b) + return; + -+ dispatch_async (dispatch_get_main_queue (), ^{ -+ struct window *w2 = self.emacsWindow; -+ if (!w2 || !WINDOW_LEAF_P (w2)) -+ return; ++ [self ensureTextCache]; + -+ if (!BUFFERP (w2->contents)) -+ return; -+ struct buffer *b = XBUFFER (w2->contents); ++ /* Convert accessibility index to buffer charpos via mapping. */ ++ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location]; + -+ [self ensureTextCache]; ++ /* Clamp to buffer bounds. */ ++ if (charpos < BUF_BEGV (b)) ++ charpos = BUF_BEGV (b); ++ if (charpos > BUF_ZV (b)) ++ charpos = BUF_ZV (b); + -+ /* Convert accessibility index to buffer charpos via mapping. */ -+ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location]; ++ block_input (); + -+ /* Clamp to buffer bounds. */ -+ if (charpos < BUF_BEGV (b)) -+ charpos = BUF_BEGV (b); -+ if (charpos > BUF_ZV (b)) -+ charpos = BUF_ZV (b); ++ /* Move point directly in the buffer. Use set_point_both which ++ operates on the current buffer — temporarily switch if needed. */ ++ struct buffer *oldb = current_buffer; ++ if (b != current_buffer) ++ set_buffer_internal_1 (b); + -+ block_input (); ++ SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos)); + -+ /* Move point directly in the buffer. Use set_point_both which -+ operates on the current buffer — temporarily switch if needed. */ -+ struct buffer *oldb = current_buffer; -+ if (b != current_buffer) -+ set_buffer_internal_1 (b); ++ /* If range has nonzero length, activate the mark. */ ++ if (range.length > 0) ++ { ++ ptrdiff_t mark_charpos = [self charposForAccessibilityIndex: ++ range.location + range.length]; ++ if (mark_charpos > BUF_ZV (b)) ++ mark_charpos = BUF_ZV (b); ++ Fset_marker (BVAR (b, mark), make_fixnum (mark_charpos), ++ Fcurrent_buffer ()); ++ } + -+ SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos)); ++ if (b != oldb) ++ set_buffer_internal_1 (oldb); + -+ /* Keep mark state aligned with requested selection range. */ -+ if (range.length > 0) -+ { -+ ptrdiff_t mark_charpos = [self charposForAccessibilityIndex: -+ range.location + range.length]; -+ if (mark_charpos > BUF_ZV (b)) -+ mark_charpos = BUF_ZV (b); -+ Fset_marker (BVAR (b, mark), make_fixnum (mark_charpos), -+ Fcurrent_buffer ()); -+ bset_mark_active (b, Qt); -+ } -+ else -+ bset_mark_active (b, Qnil); ++ unblock_input (); + -+ if (b != oldb) -+ set_buffer_internal_1 (oldb); -+ -+ unblock_input (); -+ -+ /* Update cached state so the next notification cycle doesn't -+ re-announce this movement. */ -+ self.cachedPoint = charpos; -+ self.cachedMarkActive = (range.length > 0); -+ }); ++ /* Update cached state so the next notification cycle doesn't ++ re-announce this movement. */ ++ self.cachedPoint = charpos; +} + +- (void)setAccessibilityFocused:(BOOL)flag @@ -1140,9 +892,8 @@ index 932d209..da40369 100644 + + /* Post SelectedTextChanged so VoiceOver reads the current line + upon entering text interaction mode. -+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. */ -+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), -+ @"AXTextChangeElement": self}; ++ kAXTextStateChangeTypeSelectionMove = 1. */ ++ NSDictionary *info = @{@"AXTextStateChangeType": @1}; + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilitySelectedTextChangedNotification, info); +} @@ -1153,8 +904,6 @@ index 932d209..da40369 100644 + if (!w || !WINDOW_LEAF_P (w)) + return 0; + -+ if (!BUFFERP (w->contents)) -+ return 0; + struct buffer *b = XBUFFER (w->contents); + if (!b) + return 0; @@ -1389,8 +1138,6 @@ index 932d209..da40369 100644 + if (!w || !WINDOW_LEAF_P (w)) + return; + -+ if (!BUFFERP (w->contents)) -+ return; + struct buffer *b = XBUFFER (w->contents); + if (!b) + return; @@ -1400,7 +1147,7 @@ index 932d209..da40369 100644 + BOOL markActive = !NILP (BVAR (b, mark_active)); + + /* --- Text changed → typing echo --- -+ WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */ ++ kAXTextStateChangeTypeEdit = 0, kAXTextEditTypeTyping = 3. */ + if (modiff != self.cachedModiff) + { + /* Capture changed char before invalidating cache. */ @@ -1432,21 +1179,20 @@ index 932d209..da40369 100644 + self.cachedPoint = point; + + NSDictionary *change = @{ -+ @"AXTextEditType": @(ns_ax_text_edit_type_typing), ++ @"AXTextEditType": @3, + @"AXTextChangeValue": changedChar, + @"AXTextChangeValueLength": @([changedChar length]) + }; + NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @(ns_ax_text_state_change_edit), -+ @"AXTextChangeValues": @[change], -+ @"AXTextChangeElement": self ++ @"AXTextStateChangeType": @0, ++ @"AXTextChangeValues": @[change] + }; + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilityValueChangedNotification, userInfo); + } + + /* --- Cursor moved or selection changed → line reading --- -+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. ++ kAXTextStateChangeTypeSelectionMove = 1. + Use 'else if' — edits and selection moves are mutually exclusive + per the WebKit/Chromium pattern. VoiceOver gets confused if + both notifications arrive in the same runloop iteration. */ @@ -1456,104 +1202,54 @@ index 932d209..da40369 100644 + self.cachedPoint = point; + self.cachedMarkActive = markActive; + -+ /* Compute direction. */ -+ NSInteger direction = ns_ax_text_selection_direction_discontiguous; ++ /* Compute direction: 3=Previous, 4=Next, 5=Discontiguous. */ ++ NSInteger direction = 5; + if (point > oldPoint) -+ direction = ns_ax_text_selection_direction_next; ++ direction = 4; + else if (point < oldPoint) -+ direction = ns_ax_text_selection_direction_previous; -+ -+ int ctrlNP = 0; -+ bool isCtrlNP = ns_ax_event_is_ctrl_n_or_p (&ctrlNP); ++ direction = 3; + + /* Compute granularity from movement distance. -+ Prefer robust line-range comparison for vertical movement, -+ otherwise single char (1) or unknown (0). */ -+ NSInteger granularity = ns_ax_text_selection_granularity_unknown; ++ Check if we crossed a newline → line movement (3). ++ Otherwise single char (1) or unknown (0). */ ++ NSInteger granularity = 0; + [self ensureTextCache]; + if (cachedText && oldPoint > 0) + { + ptrdiff_t delta = point - oldPoint; + if (delta == 1 || delta == -1) -+ granularity = ns_ax_text_selection_granularity_character; /* Character. */ ++ granularity = 1; /* Character. */ + else + { ++ /* Check for line crossing by looking for newlines ++ between old and new position. */ ++ NSUInteger lo = [self accessibilityIndexForCharpos: ++ MIN (oldPoint, point)]; ++ NSUInteger hi = [self accessibilityIndexForCharpos: ++ MAX (oldPoint, point)]; + NSUInteger tlen = [cachedText length]; -+ NSUInteger oldIdx = [self accessibilityIndexForCharpos:oldPoint]; -+ NSUInteger newIdx = [self accessibilityIndexForCharpos:point]; -+ if (oldIdx > tlen) -+ oldIdx = tlen; -+ if (newIdx > tlen) -+ newIdx = tlen; -+ -+ NSRange oldLine = [cachedText lineRangeForRange: -+ NSMakeRange (oldIdx, 0)]; -+ NSRange newLine = [cachedText lineRangeForRange: -+ NSMakeRange (newIdx, 0)]; -+ if (oldLine.location != newLine.location) -+ granularity = ns_ax_text_selection_granularity_line; /* Line. */ -+ ++ 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 = 3; /* Line. */ ++ } + } + } + -+ /* Force line semantics for explicit C-n/C-p keystrokes. -+ This isolates the key-path difference from arrow-down/up. */ -+ if (isCtrlNP) -+ { -+ direction = (ctrlNP > 0 -+ ? ns_ax_text_selection_direction_next -+ : ns_ax_text_selection_direction_previous); -+ granularity = ns_ax_text_selection_granularity_line; -+ } -+ + NSDictionary *moveInfo = @{ -+ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), ++ @"AXTextStateChangeType": @1, + @"AXTextSelectionDirection": @(direction), -+ @"AXTextSelectionGranularity": @(granularity), -+ @"AXTextChangeElement": self ++ @"AXTextSelectionGranularity": @(granularity) + }; + NSAccessibilityPostNotificationWithUserInfo ( + self, + NSAccessibilitySelectedTextChangedNotification, + moveInfo); + -+ /* C-n/C-p (`next-line' / `previous-line') can diverge from -+ arrow-down/up command paths in some major modes (completion list, -+ Dired, etc.). Emit an explicit line announcement for this basic -+ line-motion path so VoiceOver tracks the Emacs point reliably. */ -+ if ([self isAccessibilityFocused] -+ && cachedText -+ && (isCtrlNP || ns_ax_command_is_basic_line_move ()) -+ && (direction == ns_ax_text_selection_direction_next -+ || direction == ns_ax_text_selection_direction_previous)) -+ { -+ NSUInteger point_idx = [self accessibilityIndexForCharpos:point]; -+ if (point_idx <= [cachedText length]) -+ { -+ NSInteger lineNum = [self accessibilityLineForIndex:point_idx]; -+ NSRange lineRange = [self accessibilityRangeForLine:lineNum]; -+ if (lineRange.location != NSNotFound -+ && lineRange.length > 0 -+ && lineRange.location + lineRange.length <= [cachedText length]) -+ { -+ NSString *lineText = [cachedText substringWithRange:lineRange]; -+ lineText = [lineText stringByTrimmingCharactersInSet: -+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; -+ if ([lineText length] > 0) -+ { -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: lineText, -+ NSAccessibilityPriorityKey: @(NSAccessibilityPriorityHigh) -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); -+ } -+ } -+ } -+ } -+ + /* --- Completions announcement --- + When point changes in a non-focused buffer (e.g. *Completions* + while the minibuffer has keyboard focus), VoiceOver won't read @@ -1566,8 +1262,6 @@ index 932d209..da40369 100644 + if (![self isAccessibilityFocused] && cachedText) + { + NSString *announceText = nil; -+ ptrdiff_t currentOverlayStart = 0; -+ ptrdiff_t currentOverlayEnd = 0; + + struct buffer *oldb2 = current_buffer; + if (b != current_buffer) @@ -1624,53 +1318,37 @@ index 932d209..da40369 100644 + } + } + -+ /* 3) Fallback: check completions-highlight overlay span at point. */ ++ /* 3) Fallback: check completions-highlight overlay span. */ + if (!announceText) + { -+ Lisp_Object faceSym = intern ("completions-highlight"); + Lisp_Object overlays = Foverlays_at (make_fixnum (point), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + { + Lisp_Object ov = XCAR (tail); + Lisp_Object face = Foverlay_get (ov, Qface); -+ if (EQ (face, faceSym) ++ if (EQ (face, intern ("completions-highlight")) + || (CONSP (face) -+ && !NILP (Fmemq (faceSym, face)))) ++ && !NILP (Fmemq (intern ("completions-highlight"), ++ face)))) + { + ptrdiff_t ov_start = OVERLAY_START (ov); + ptrdiff_t ov_end = OVERLAY_END (ov); + if (ov_end > ov_start) + { -+ announceText = ns_ax_completion_text_for_span (self, b, -+ ov_start, -+ ov_end, -+ cachedText); -+ currentOverlayStart = ov_start; -+ currentOverlayEnd = ov_end; ++ NSUInteger ax_s = [self accessibilityIndexForCharpos: ++ ov_start]; ++ NSUInteger ax_e = [self accessibilityIndexForCharpos: ++ ov_end]; ++ if (ax_e > ax_s && ax_e <= [cachedText length]) ++ announceText = [cachedText substringWithRange: ++ NSMakeRange (ax_s, ax_e - ax_s)]; + } + break; + } + } + } + -+ /* 4) Fallback: select the best completions-highlight overlay. -+ Prefer overlay nearest to point over first-found in buffer. */ -+ if (!announceText) -+ { -+ ptrdiff_t ov_start = 0; -+ ptrdiff_t ov_end = 0; -+ if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end)) -+ { -+ announceText = ns_ax_completion_text_for_span (self, b, -+ ov_start, -+ ov_end, -+ cachedText); -+ currentOverlayStart = ov_start; -+ currentOverlayEnd = ov_end; -+ } -+ } -+ + if (b != oldb2) + set_buffer_internal_1 (oldb2); + @@ -1697,127 +1375,19 @@ index 932d209..da40369 100644 + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([announceText length] > 0) + { -+ BOOL textChanged = ![announceText isEqualToString: -+ self.cachedCompletionAnnouncement]; -+ BOOL overlayChanged = -+ (currentOverlayStart != self.cachedCompletionOverlayStart -+ || currentOverlayEnd != self.cachedCompletionOverlayEnd); -+ BOOL pointChanged = (point != self.cachedCompletionPoint); -+ if (textChanged || overlayChanged || pointChanged) -+ { -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: announceText, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); -+ } -+ self.cachedCompletionAnnouncement = announceText; -+ self.cachedCompletionOverlayStart = currentOverlayStart; -+ self.cachedCompletionOverlayEnd = currentOverlayEnd; -+ self.cachedCompletionPoint = point; -+ } -+ else -+ { -+ self.cachedCompletionAnnouncement = nil; -+ self.cachedCompletionOverlayStart = 0; -+ self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; -+ } -+ } -+ else -+ { -+ self.cachedCompletionAnnouncement = nil; -+ self.cachedCompletionOverlayStart = 0; -+ self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; -+ } -+ } -+ -+ } -+ else -+ { -+ if ([self isAccessibilityFocused]) -+ { -+ self.cachedCompletionAnnouncement = nil; -+ self.cachedCompletionOverlayStart = 0; -+ self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; -+ } -+ else -+ { -+ [self ensureTextCache]; -+ if (cachedText) -+ { -+ NSString *announceText = nil; -+ ptrdiff_t currentOverlayStart = 0; -+ ptrdiff_t currentOverlayEnd = 0; -+ struct buffer *oldb2 = current_buffer; -+ if (b != current_buffer) -+ set_buffer_internal_1 (b); -+ -+ if (ns_ax_find_completion_overlay_range (b, point, -+ ¤tOverlayStart, -+ ¤tOverlayEnd)) -+ { -+ announceText = ns_ax_completion_text_for_span (self, b, -+ currentOverlayStart, -+ currentOverlayEnd, -+ cachedText); -+ } -+ -+ if (b != oldb2) -+ set_buffer_internal_1 (oldb2); -+ -+ if (announceText) -+ { -+ announceText = [announceText stringByTrimmingCharactersInSet: -+ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; -+ if ([announceText length] > 0) -+ { -+ BOOL textChanged = ![announceText isEqualToString: -+ self.cachedCompletionAnnouncement]; -+ BOOL overlayChanged = -+ (currentOverlayStart != self.cachedCompletionOverlayStart -+ || currentOverlayEnd != self.cachedCompletionOverlayEnd); -+ BOOL pointChanged = (point != self.cachedCompletionPoint); -+ if (textChanged || overlayChanged || pointChanged) -+ { -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: announceText, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); -+ } -+ self.cachedCompletionAnnouncement = announceText; -+ self.cachedCompletionOverlayStart = currentOverlayStart; -+ self.cachedCompletionOverlayEnd = currentOverlayEnd; -+ self.cachedCompletionPoint = point; -+ } -+ else -+ { -+ self.cachedCompletionAnnouncement = nil; -+ self.cachedCompletionOverlayStart = 0; -+ self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; -+ } -+ } -+ else -+ { -+ self.cachedCompletionAnnouncement = nil; -+ self.cachedCompletionOverlayStart = 0; -+ self.cachedCompletionOverlayEnd = 0; -+ self.cachedCompletionPoint = 0; ++ NSDictionary *annInfo = @{ ++ NSAccessibilityAnnouncementKey: announceText, ++ NSAccessibilityPriorityKey: ++ @(NSAccessibilityPriorityHigh) ++ }; ++ NSAccessibilityPostNotificationWithUserInfo ( ++ NSApp, ++ NSAccessibilityAnnouncementRequestedNotification, ++ annInfo); + } + } + } ++ + } +} + @@ -2097,10 +1667,57 @@ index 932d209..da40369 100644 +/*****************************************************************************/ +/* Keyboard handling. */ +#define NS_KEYLOG 0 - - - (void)keyDown: (NSEvent *)theEvent - { -@@ -8237,6 +9860,28 @@ ns_in_echo_area (void) ++ ++- (void)keyDown: (NSEvent *)theEvent ++{ ++ Mouse_HLInfo *hlinfo = MOUSE_HL_INFO (emacsframe); ++ int code; ++ unsigned fnKeysym = 0; ++ static NSMutableArray *nsEvArray; ++ unsigned int flags = [theEvent modifierFlags]; ++ ++ NSTRACE ("[EmacsView keyDown:]"); ++ ++ /* Rhapsody and macOS give up and down events for the arrow keys. */ ++ if ([theEvent type] != NSEventTypeKeyDown) ++ return; ++ ++ if (!emacs_event) ++ return; ++ ++ if (![[self window] isKeyWindow] ++ && [[theEvent window] isKindOfClass: [EmacsWindow class]] ++ /* We must avoid an infinite loop here. */ ++ && (EmacsView *)[[theEvent window] delegate] != self) ++ { ++ /* XXX: There is an occasional condition in which, when Emacs display ++ updates a different frame from the current one, and temporarily ++ selects it, then processes some interrupt-driven input ++ (dispnew.c:3878), OS will send the event to the correct NSWindow, but ++ for some reason that window has its first responder set to the NSView ++ most recently updated (I guess), which is not the correct one. */ ++ [(EmacsView *)[[theEvent window] delegate] keyDown: theEvent]; ++ return; ++ } ++ ++ if (nsEvArray == nil) ++ nsEvArray = [[NSMutableArray alloc] initWithCapacity: 1]; ++ ++ [NSCursor setHiddenUntilMouseMoves:! NILP (Vmake_pointer_invisible)]; ++ ++ if (!hlinfo->mouse_face_hidden ++ && FIXNUMP (Vmouse_highlight) ++ && !EQ (emacsframe->tab_bar_window, hlinfo->mouse_face_window)) ++ { ++ clear_mouse_face (hlinfo); ++ hlinfo->mouse_face_hidden = true; ++ } ++ ++ if (!processingCompose) + { + /* FIXME: What should happen for key sequences with more than + one character? */ +@@ -8237,6 +9392,27 @@ ns_in_echo_area (void) XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -2116,8 +1733,7 @@ index 932d209..da40369 100644 + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); -+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), -+ @"AXTextChangeElement": focused}; ++ NSDictionary *info = @{@"AXTextStateChangeType": @1}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } @@ -2129,7 +1745,7 @@ index 932d209..da40369 100644 } -@@ -9474,6 +11119,298 @@ ns_in_echo_area (void) +@@ -9474,6 +10650,297 @@ ns_in_echo_area (void) return fs_state; } @@ -2347,8 +1963,7 @@ index 932d209..da40369 100644 + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); -+ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), -+ @"AXTextChangeElement": focused}; ++ NSDictionary *info = @{@"AXTextStateChangeType": @1}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } @@ -2428,7 +2043,7 @@ index 932d209..da40369 100644 @end /* EmacsView */ -@@ -9941,6 +11878,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) +@@ -9941,6 +11408,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) return [super accessibilityAttributeValue:attribute]; } @@ -2446,3 +2061,1512 @@ index 932d209..da40369 100644 -- 2.43.0 + +From d8dff0694720a366cb38636b1873499679433790 Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Thu, 26 Feb 2026 17:28:40 +0100 +Subject: [PATCH 2/9] ns: improve VO selection granularity and completions + announce + +--- + nsterm.h | 1 + + nsterm.m | 191 ++++++++++++++++++++++++++++++++++++++++++++++--------- + 2 files changed, 162 insertions(+), 30 deletions(-) + +diff --git a/nsterm.h b/nsterm.h +index 2e2c80f..719eeba 100644 +--- a/nsterm.h ++++ b/nsterm.h +@@ -493,6 +493,7 @@ typedef struct ns_ax_visible_run + @property (nonatomic, assign) ptrdiff_t cachedModiff; + @property (nonatomic, assign) ptrdiff_t cachedPoint; + @property (nonatomic, assign) BOOL cachedMarkActive; ++@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; + - (void)invalidateTextCache; + - (void)postAccessibilityNotificationsForFrame:(struct frame *)f; + - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx; +diff --git a/nsterm.m b/nsterm.m +index 416e5a4..336150a 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7160,10 +7160,12 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + @synthesize cachedModiff; + @synthesize cachedPoint; + @synthesize cachedMarkActive; ++@synthesize cachedCompletionAnnouncement; + + - (void)dealloc + { + [cachedText release]; ++ [cachedCompletionAnnouncement release]; + if (visibleRuns) + xfree (visibleRuns); + [super dealloc]; +@@ -7755,8 +7757,8 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + direction = 3; + + /* Compute granularity from movement distance. +- Check if we crossed a newline → line movement (3). +- Otherwise single char (1) or unknown (0). */ ++ Prefer robust line-range comparison for vertical movement, ++ otherwise single char (1) or unknown (0). */ + NSInteger granularity = 0; + [self ensureTextCache]; + if (cachedText && oldPoint > 0) +@@ -7766,22 +7768,21 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + granularity = 1; /* Character. */ + else + { +- /* Check for line crossing by looking for newlines +- between old and new position. */ +- NSUInteger lo = [self accessibilityIndexForCharpos: +- MIN (oldPoint, point)]; +- NSUInteger hi = [self accessibilityIndexForCharpos: +- MAX (oldPoint, point)]; + 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 = 3; /* Line. */ +- } ++ NSUInteger oldIdx = [self accessibilityIndexForCharpos:oldPoint]; ++ NSUInteger newIdx = [self accessibilityIndexForCharpos:point]; ++ if (oldIdx > tlen) ++ oldIdx = tlen; ++ if (newIdx > tlen) ++ newIdx = tlen; ++ ++ NSRange oldLine = [cachedText lineRangeForRange: ++ NSMakeRange (oldIdx, 0)]; ++ NSRange newLine = [cachedText lineRangeForRange: ++ NSMakeRange (newIdx, 0)]; ++ if (oldLine.location != newLine.location) ++ granularity = 3; /* Line. */ ++ + } + } + +@@ -7863,19 +7864,19 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + } + } + +- /* 3) Fallback: check completions-highlight overlay span. */ ++ /* 3) Fallback: check completions-highlight overlay span at point. */ + if (!announceText) + { ++ Lisp_Object faceSym = intern ("completions-highlight"); + Lisp_Object overlays = Foverlays_at (make_fixnum (point), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + { + Lisp_Object ov = XCAR (tail); + Lisp_Object face = Foverlay_get (ov, Qface); +- if (EQ (face, intern ("completions-highlight")) ++ if (EQ (face, faceSym) + || (CONSP (face) +- && !NILP (Fmemq (intern ("completions-highlight"), +- face)))) ++ && !NILP (Fmemq (faceSym, face)))) + { + ptrdiff_t ov_start = OVERLAY_START (ov); + ptrdiff_t ov_end = OVERLAY_END (ov); +@@ -7894,6 +7895,47 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + } + } + ++ /* 4) Fallback: scan for completions-highlight anywhere in buffer. ++ TAB cycling can move highlight without moving point. */ ++ if (!announceText) ++ { ++ Lisp_Object faceSym = intern ("completions-highlight"); ++ ptrdiff_t begv2 = BUF_BEGV (b); ++ ptrdiff_t zv2 = BUF_ZV (b); ++ ptrdiff_t scanPos; ++ BOOL found = NO; ++ ++ for (scanPos = begv2; scanPos < zv2 && !found; scanPos++) ++ { ++ Lisp_Object overlays = Foverlays_at (make_fixnum (scanPos), Qnil); ++ Lisp_Object tail; ++ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) ++ { ++ Lisp_Object ov = XCAR (tail); ++ Lisp_Object face = Foverlay_get (ov, Qface); ++ if (EQ (face, faceSym) ++ || (CONSP (face) ++ && !NILP (Fmemq (faceSym, face)))) ++ { ++ ptrdiff_t ov_start = OVERLAY_START (ov); ++ ptrdiff_t ov_end = OVERLAY_END (ov); ++ if (ov_end > ov_start) ++ { ++ NSUInteger ax_s = [self accessibilityIndexForCharpos: ++ ov_start]; ++ NSUInteger ax_e = [self accessibilityIndexForCharpos: ++ ov_end]; ++ if (ax_e > ax_s && ax_e <= [cachedText length]) ++ announceText = [cachedText substringWithRange: ++ NSMakeRange (ax_s, ax_e - ax_s)]; ++ } ++ found = YES; ++ break; ++ } ++ } ++ } ++ } ++ + if (b != oldb2) + set_buffer_internal_1 (oldb2); + +@@ -7920,20 +7962,109 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([announceText length] > 0) + { +- NSDictionary *annInfo = @{ +- NSAccessibilityAnnouncementKey: announceText, +- NSAccessibilityPriorityKey: +- @(NSAccessibilityPriorityHigh) +- }; +- NSAccessibilityPostNotificationWithUserInfo ( +- NSApp, +- NSAccessibilityAnnouncementRequestedNotification, +- annInfo); ++ if (![announceText isEqualToString:self.cachedCompletionAnnouncement]) ++ { ++ NSDictionary *annInfo = @{ ++ NSAccessibilityAnnouncementKey: announceText, ++ NSAccessibilityPriorityKey: ++ @(NSAccessibilityPriorityHigh) ++ }; ++ NSAccessibilityPostNotificationWithUserInfo ( ++ NSApp, ++ NSAccessibilityAnnouncementRequestedNotification, ++ annInfo); ++ } ++ self.cachedCompletionAnnouncement = announceText; + } ++ else ++ self.cachedCompletionAnnouncement = nil; + } ++ else ++ self.cachedCompletionAnnouncement = nil; + } + + } ++ else ++ { ++ if ([self isAccessibilityFocused]) ++ self.cachedCompletionAnnouncement = nil; ++ else ++ { ++ [self ensureTextCache]; ++ if (cachedText) ++ { ++ NSString *announceText = nil; ++ struct buffer *oldb2 = current_buffer; ++ if (b != current_buffer) ++ set_buffer_internal_1 (b); ++ ++ Lisp_Object faceSym = intern ("completions-highlight"); ++ ptrdiff_t begv2 = BUF_BEGV (b); ++ ptrdiff_t zv2 = BUF_ZV (b); ++ ptrdiff_t scanPos; ++ BOOL found = NO; ++ ++ for (scanPos = begv2; scanPos < zv2 && !found; scanPos++) ++ { ++ Lisp_Object overlays = Foverlays_at (make_fixnum (scanPos), Qnil); ++ Lisp_Object tail; ++ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) ++ { ++ Lisp_Object ov = XCAR (tail); ++ Lisp_Object face = Foverlay_get (ov, Qface); ++ if (EQ (face, faceSym) ++ || (CONSP (face) ++ && !NILP (Fmemq (faceSym, face)))) ++ { ++ ptrdiff_t ov_start = OVERLAY_START (ov); ++ ptrdiff_t ov_end = OVERLAY_END (ov); ++ if (ov_end > ov_start) ++ { ++ NSUInteger ax_s = [self accessibilityIndexForCharpos: ++ ov_start]; ++ NSUInteger ax_e = [self accessibilityIndexForCharpos: ++ ov_end]; ++ if (ax_e > ax_s && ax_e <= [cachedText length]) ++ announceText = [cachedText substringWithRange: ++ NSMakeRange (ax_s, ax_e - ax_s)]; ++ } ++ found = YES; ++ break; ++ } ++ } ++ } ++ ++ if (b != oldb2) ++ set_buffer_internal_1 (oldb2); ++ ++ if (announceText) ++ { ++ announceText = [announceText stringByTrimmingCharactersInSet: ++ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; ++ if ([announceText length] > 0) ++ { ++ if (![announceText isEqualToString:self.cachedCompletionAnnouncement]) ++ { ++ NSDictionary *annInfo = @{ ++ NSAccessibilityAnnouncementKey: announceText, ++ NSAccessibilityPriorityKey: ++ @(NSAccessibilityPriorityHigh) ++ }; ++ NSAccessibilityPostNotificationWithUserInfo ( ++ NSApp, ++ NSAccessibilityAnnouncementRequestedNotification, ++ annInfo); ++ } ++ self.cachedCompletionAnnouncement = announceText; ++ } ++ else ++ self.cachedCompletionAnnouncement = nil; ++ } ++ else ++ self.cachedCompletionAnnouncement = nil; ++ } ++ } ++ } + } + + @end +-- +2.43.0 + + +From 4f0f0fff013d58ebb145aba2b8f5402f6c2005b1 Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Thu, 26 Feb 2026 17:49:40 +0100 +Subject: [PATCH 3/9] ns: harden AX mapping and completions overlay selection + +--- + nsterm.h | 2 + + nsterm.m | 268 +++++++++++++++++++++++++++++++++++++++---------------- + 2 files changed, 194 insertions(+), 76 deletions(-) + +diff --git a/nsterm.h b/nsterm.h +index 719eeba..97da979 100644 +--- a/nsterm.h ++++ b/nsterm.h +@@ -494,6 +494,8 @@ typedef struct ns_ax_visible_run + @property (nonatomic, assign) ptrdiff_t cachedPoint; + @property (nonatomic, assign) BOOL cachedMarkActive; + @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; ++@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart; ++@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd; + - (void)invalidateTextCache; + - (void)postAccessibilityNotificationsForFrame:(struct frame *)f; + - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx; +diff --git a/nsterm.m b/nsterm.m +index 336150a..8673194 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7114,6 +7114,87 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + return [[view window] convertRectToScreen:winRect]; + } + ++static NSUInteger ++ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start, ++ ptrdiff_t end) ++{ ++ if (!b || end <= start) ++ return 0; ++ ++ struct buffer *oldb = current_buffer; ++ if (b != current_buffer) ++ set_buffer_internal_1 (b); ++ ++ Lisp_Object lstr = Fbuffer_substring_no_properties (make_fixnum (start), ++ make_fixnum (end)); ++ NSString *nsstr = [NSString stringWithLispString:lstr]; ++ NSUInteger len = [nsstr length]; ++ ++ if (b != oldb) ++ set_buffer_internal_1 (oldb); ++ ++ return len; ++} ++ ++static BOOL ++ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, ++ ptrdiff_t *out_start, ++ ptrdiff_t *out_end) ++{ ++ if (!b || !out_start || !out_end) ++ return NO; ++ ++ Lisp_Object faceSym = intern ("completions-highlight"); ++ ptrdiff_t begv = BUF_BEGV (b); ++ ptrdiff_t zv = BUF_ZV (b); ++ ptrdiff_t best_start = 0; ++ ptrdiff_t best_end = 0; ++ ptrdiff_t best_dist = PTRDIFF_MAX; ++ BOOL found = NO; ++ ++ for (ptrdiff_t scan = begv; scan < zv; scan++) ++ { ++ Lisp_Object overlays = Foverlays_at (make_fixnum (scan), Qnil); ++ Lisp_Object tail; ++ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) ++ { ++ Lisp_Object ov = XCAR (tail); ++ Lisp_Object face = Foverlay_get (ov, Qface); ++ if (!(EQ (face, faceSym) ++ || (CONSP (face) && !NILP (Fmemq (faceSym, face))))) ++ continue; ++ ++ ptrdiff_t ov_start = OVERLAY_START (ov); ++ ptrdiff_t ov_end = OVERLAY_END (ov); ++ if (ov_end <= ov_start) ++ continue; ++ ++ ptrdiff_t dist = 0; ++ if (point < ov_start) ++ dist = ov_start - point; ++ else if (point > ov_end) ++ dist = point - ov_end; ++ ++ if (!found || dist < best_dist ++ || (dist == best_dist ++ && (ov_start < point && best_start >= point))) ++ { ++ best_start = ov_start; ++ best_end = ov_end; ++ best_dist = dist; ++ found = YES; ++ } ++ } ++ } ++ ++ if (!found) ++ return NO; ++ ++ *out_start = best_start; ++ *out_end = best_end; ++ return YES; ++} ++ + + @implementation EmacsAccessibilityElement + +@@ -7161,6 +7242,8 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + @synthesize cachedPoint; + @synthesize cachedMarkActive; + @synthesize cachedCompletionAnnouncement; ++@synthesize cachedCompletionOverlayStart; ++@synthesize cachedCompletionOverlayEnd; + + - (void)dealloc + { +@@ -7225,11 +7308,22 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + /* Convert buffer charpos to accessibility string index. */ + - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos + { ++ struct window *w = self.emacsWindow; ++ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; ++ + for (NSUInteger i = 0; i < visibleRunCount; i++) + { + ns_ax_visible_run *r = &visibleRuns[i]; + if (charpos >= r->charpos && charpos < r->charpos + r->length) +- return r->ax_start + (NSUInteger) (charpos - r->charpos); ++ { ++ if (!b) ++ return r->ax_start; ++ NSUInteger delta = ns_ax_utf16_length_for_buffer_range (b, r->charpos, ++ charpos); ++ if (delta > r->ax_length) ++ delta = r->ax_length; ++ return r->ax_start + delta; ++ } + /* If charpos falls in an invisible gap before the next run, + map it to the start of the next visible run. */ + if (charpos < r->charpos) +@@ -7247,12 +7341,36 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + /* Convert accessibility string index to buffer charpos. */ + - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx + { ++ struct window *w = self.emacsWindow; ++ struct buffer *b = (w && WINDOW_LEAF_P (w)) ? XBUFFER (w->contents) : NULL; ++ + for (NSUInteger i = 0; i < visibleRunCount; i++) + { + ns_ax_visible_run *r = &visibleRuns[i]; + if (ax_idx >= r->ax_start + && ax_idx < r->ax_start + r->ax_length) +- return r->charpos + (ptrdiff_t) (ax_idx - r->ax_start); ++ { ++ if (!b) ++ return r->charpos; ++ ++ NSUInteger target = ax_idx - r->ax_start; ++ ptrdiff_t lo = r->charpos; ++ ptrdiff_t hi = r->charpos + r->length; ++ ++ while (lo < hi) ++ { ++ ptrdiff_t mid = lo + (hi - lo) / 2; ++ NSUInteger mid_len = ns_ax_utf16_length_for_buffer_range (b, ++ r->charpos, ++ mid); ++ if (mid_len < target) ++ lo = mid + 1; ++ else ++ hi = mid; ++ } ++ ++ return lo; ++ } + } + /* Past end — return last charpos. */ + if (visibleRunCount > 0) +@@ -7394,7 +7512,7 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + + SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos)); + +- /* If range has nonzero length, activate the mark. */ ++ /* Keep mark state aligned with requested selection range. */ + if (range.length > 0) + { + ptrdiff_t mark_charpos = [self charposForAccessibilityIndex: +@@ -7403,7 +7521,10 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + mark_charpos = BUF_ZV (b); + Fset_marker (BVAR (b, mark), make_fixnum (mark_charpos), + Fcurrent_buffer ()); ++ bset_mark_active (b, Qt); + } ++ else ++ bset_mark_active (b, Qnil); + + if (b != oldb) + set_buffer_internal_1 (oldb); +@@ -7413,6 +7534,7 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + /* Update cached state so the next notification cycle doesn't + re-announce this movement. */ + self.cachedPoint = charpos; ++ self.cachedMarkActive = (range.length > 0); + } + + - (void)setAccessibilityFocused:(BOOL)flag +@@ -7808,6 +7930,8 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + if (![self isAccessibilityFocused] && cachedText) + { + NSString *announceText = nil; ++ ptrdiff_t currentOverlayStart = 0; ++ ptrdiff_t currentOverlayEnd = 0; + + struct buffer *oldb2 = current_buffer; + if (b != current_buffer) +@@ -7895,43 +8019,22 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + } + } + +- /* 4) Fallback: scan for completions-highlight anywhere in buffer. +- TAB cycling can move highlight without moving point. */ ++ /* 4) Fallback: select the best completions-highlight overlay. ++ Prefer overlay nearest to point over first-found in buffer. */ + if (!announceText) + { +- Lisp_Object faceSym = intern ("completions-highlight"); +- ptrdiff_t begv2 = BUF_BEGV (b); +- ptrdiff_t zv2 = BUF_ZV (b); +- ptrdiff_t scanPos; +- BOOL found = NO; +- +- for (scanPos = begv2; scanPos < zv2 && !found; scanPos++) ++ ptrdiff_t ov_start = 0; ++ ptrdiff_t ov_end = 0; ++ if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end)) + { +- Lisp_Object overlays = Foverlays_at (make_fixnum (scanPos), Qnil); +- Lisp_Object tail; +- for (tail = overlays; CONSP (tail); tail = XCDR (tail)) ++ NSUInteger ax_s = [self accessibilityIndexForCharpos:ov_start]; ++ NSUInteger ax_e = [self accessibilityIndexForCharpos:ov_end]; ++ if (ax_e > ax_s && ax_e <= [cachedText length]) + { +- Lisp_Object ov = XCAR (tail); +- Lisp_Object face = Foverlay_get (ov, Qface); +- if (EQ (face, faceSym) +- || (CONSP (face) +- && !NILP (Fmemq (faceSym, face)))) +- { +- ptrdiff_t ov_start = OVERLAY_START (ov); +- ptrdiff_t ov_end = OVERLAY_END (ov); +- if (ov_end > ov_start) +- { +- NSUInteger ax_s = [self accessibilityIndexForCharpos: +- ov_start]; +- NSUInteger ax_e = [self accessibilityIndexForCharpos: +- ov_end]; +- if (ax_e > ax_s && ax_e <= [cachedText length]) +- announceText = [cachedText substringWithRange: +- NSMakeRange (ax_s, ax_e - ax_s)]; +- } +- found = YES; +- break; +- } ++ announceText = [cachedText substringWithRange: ++ NSMakeRange (ax_s, ax_e - ax_s)]; ++ currentOverlayStart = ov_start; ++ currentOverlayEnd = ov_end; + } + } + } +@@ -7962,7 +8065,12 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([announceText length] > 0) + { +- if (![announceText isEqualToString:self.cachedCompletionAnnouncement]) ++ BOOL textChanged = ![announceText isEqualToString: ++ self.cachedCompletionAnnouncement]; ++ BOOL overlayChanged = ++ (currentOverlayStart != self.cachedCompletionOverlayStart ++ || currentOverlayEnd != self.cachedCompletionOverlayEnd); ++ if (textChanged || overlayChanged) + { + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: announceText, +@@ -7975,63 +8083,56 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + annInfo); + } + self.cachedCompletionAnnouncement = announceText; ++ self.cachedCompletionOverlayStart = currentOverlayStart; ++ self.cachedCompletionOverlayEnd = currentOverlayEnd; + } + else +- self.cachedCompletionAnnouncement = nil; ++ { ++ self.cachedCompletionAnnouncement = nil; ++ self.cachedCompletionOverlayStart = 0; ++ self.cachedCompletionOverlayEnd = 0; ++ } + } + else +- self.cachedCompletionAnnouncement = nil; ++ { ++ self.cachedCompletionAnnouncement = nil; ++ self.cachedCompletionOverlayStart = 0; ++ self.cachedCompletionOverlayEnd = 0; ++ } + } + + } + else + { + if ([self isAccessibilityFocused]) +- self.cachedCompletionAnnouncement = nil; ++ { ++ self.cachedCompletionAnnouncement = nil; ++ self.cachedCompletionOverlayStart = 0; ++ self.cachedCompletionOverlayEnd = 0; ++ } + else + { + [self ensureTextCache]; + if (cachedText) + { + NSString *announceText = nil; ++ ptrdiff_t currentOverlayStart = 0; ++ ptrdiff_t currentOverlayEnd = 0; + struct buffer *oldb2 = current_buffer; + if (b != current_buffer) + set_buffer_internal_1 (b); + +- Lisp_Object faceSym = intern ("completions-highlight"); +- ptrdiff_t begv2 = BUF_BEGV (b); +- ptrdiff_t zv2 = BUF_ZV (b); +- ptrdiff_t scanPos; +- BOOL found = NO; +- +- for (scanPos = begv2; scanPos < zv2 && !found; scanPos++) ++ if (ns_ax_find_completion_overlay_range (b, point, ++ ¤tOverlayStart, ++ ¤tOverlayEnd)) + { +- Lisp_Object overlays = Foverlays_at (make_fixnum (scanPos), Qnil); +- Lisp_Object tail; +- for (tail = overlays; CONSP (tail); tail = XCDR (tail)) +- { +- Lisp_Object ov = XCAR (tail); +- Lisp_Object face = Foverlay_get (ov, Qface); +- if (EQ (face, faceSym) +- || (CONSP (face) +- && !NILP (Fmemq (faceSym, face)))) +- { +- ptrdiff_t ov_start = OVERLAY_START (ov); +- ptrdiff_t ov_end = OVERLAY_END (ov); +- if (ov_end > ov_start) +- { +- NSUInteger ax_s = [self accessibilityIndexForCharpos: +- ov_start]; +- NSUInteger ax_e = [self accessibilityIndexForCharpos: +- ov_end]; +- if (ax_e > ax_s && ax_e <= [cachedText length]) +- announceText = [cachedText substringWithRange: +- NSMakeRange (ax_s, ax_e - ax_s)]; +- } +- found = YES; +- break; +- } +- } ++ NSUInteger ax_s = [self accessibilityIndexForCharpos: ++ currentOverlayStart]; ++ NSUInteger ax_e = [self accessibilityIndexForCharpos: ++ currentOverlayEnd]; ++ if (ax_e > ax_s && ax_e <= [cachedText length]) ++ announceText = [cachedText substringWithRange: ++ NSMakeRange (ax_s, ax_e - ax_s)]; + } + + if (b != oldb2) +@@ -8043,7 +8144,12 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([announceText length] > 0) + { +- if (![announceText isEqualToString:self.cachedCompletionAnnouncement]) ++ BOOL textChanged = ![announceText isEqualToString: ++ self.cachedCompletionAnnouncement]; ++ BOOL overlayChanged = ++ (currentOverlayStart != self.cachedCompletionOverlayStart ++ || currentOverlayEnd != self.cachedCompletionOverlayEnd); ++ if (textChanged || overlayChanged) + { + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: announceText, +@@ -8056,12 +8162,22 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + annInfo); + } + self.cachedCompletionAnnouncement = announceText; ++ self.cachedCompletionOverlayStart = currentOverlayStart; ++ self.cachedCompletionOverlayEnd = currentOverlayEnd; + } + else +- self.cachedCompletionAnnouncement = nil; ++ { ++ self.cachedCompletionAnnouncement = nil; ++ self.cachedCompletionOverlayStart = 0; ++ self.cachedCompletionOverlayEnd = 0; ++ } + } + else +- self.cachedCompletionAnnouncement = nil; ++ { ++ self.cachedCompletionAnnouncement = nil; ++ self.cachedCompletionOverlayStart = 0; ++ self.cachedCompletionOverlayEnd = 0; ++ } + } + } + } +-- +2.43.0 + + +From 9965f5317b145be9b5a808d24b72df3f4f23ea11 Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Thu, 26 Feb 2026 18:06:00 +0100 +Subject: [PATCH 4/9] ns: fix AX enum mapping and completion line announcements + +--- + nsterm.m | 118 +++++++++++++++++++++++++++++++++++++------------------ + 1 file changed, 80 insertions(+), 38 deletions(-) + +diff --git a/nsterm.m b/nsterm.m +index 8673194..e7af9a3 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7114,6 +7114,27 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, + return [[view window] convertRectToScreen:winRect]; + } + ++/* AX enum numeric compatibility for NSAccessibility notifications. ++ Values match WebKit AXObjectCacheMac fallback enums ++ (AXTextStateChangeType / AXTextEditType / AXTextSelectionDirection / ++ AXTextSelectionGranularity). */ ++enum { ++ ns_ax_text_state_change_unknown = 0, ++ ns_ax_text_state_change_edit = 1, ++ ns_ax_text_state_change_selection_move = 2, ++ ++ ns_ax_text_edit_type_typing = 3, ++ ++ ns_ax_text_selection_direction_unknown = 0, ++ ns_ax_text_selection_direction_previous = 3, ++ ns_ax_text_selection_direction_next = 4, ++ ns_ax_text_selection_direction_discontiguous = 5, ++ ++ ns_ax_text_selection_granularity_unknown = 0, ++ ns_ax_text_selection_granularity_character = 1, ++ ns_ax_text_selection_granularity_line = 3, ++}; ++ + static NSUInteger + ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start, + ptrdiff_t end) +@@ -7561,8 +7582,9 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + + /* Post SelectedTextChanged so VoiceOver reads the current line + upon entering text interaction mode. +- kAXTextStateChangeTypeSelectionMove = 1. */ +- NSDictionary *info = @{@"AXTextStateChangeType": @1}; ++ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. */ ++ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": self}; + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilitySelectedTextChangedNotification, info); + } +@@ -7816,7 +7838,7 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + BOOL markActive = !NILP (BVAR (b, mark_active)); + + /* --- Text changed → typing echo --- +- kAXTextStateChangeTypeEdit = 0, kAXTextEditTypeTyping = 3. */ ++ WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */ + if (modiff != self.cachedModiff) + { + /* Capture changed char before invalidating cache. */ +@@ -7848,20 +7870,21 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + self.cachedPoint = point; + + NSDictionary *change = @{ +- @"AXTextEditType": @3, ++ @"AXTextEditType": @(ns_ax_text_edit_type_typing), + @"AXTextChangeValue": changedChar, + @"AXTextChangeValueLength": @([changedChar length]) + }; + NSDictionary *userInfo = @{ +- @"AXTextStateChangeType": @0, +- @"AXTextChangeValues": @[change] ++ @"AXTextStateChangeType": @(ns_ax_text_state_change_edit), ++ @"AXTextChangeValues": @[change], ++ @"AXTextChangeElement": self + }; + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilityValueChangedNotification, userInfo); + } + + /* --- Cursor moved or selection changed → line reading --- +- kAXTextStateChangeTypeSelectionMove = 1. ++ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. + Use 'else if' — edits and selection moves are mutually exclusive + per the WebKit/Chromium pattern. VoiceOver gets confused if + both notifications arrive in the same runloop iteration. */ +@@ -7871,23 +7894,23 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + self.cachedPoint = point; + self.cachedMarkActive = markActive; + +- /* Compute direction: 3=Previous, 4=Next, 5=Discontiguous. */ +- NSInteger direction = 5; ++ /* Compute direction. */ ++ NSInteger direction = ns_ax_text_selection_direction_discontiguous; + if (point > oldPoint) +- direction = 4; ++ direction = ns_ax_text_selection_direction_next; + else if (point < oldPoint) +- direction = 3; ++ direction = ns_ax_text_selection_direction_previous; + + /* Compute granularity from movement distance. + Prefer robust line-range comparison for vertical movement, + otherwise single char (1) or unknown (0). */ +- NSInteger granularity = 0; ++ NSInteger granularity = ns_ax_text_selection_granularity_unknown; + [self ensureTextCache]; + if (cachedText && oldPoint > 0) + { + ptrdiff_t delta = point - oldPoint; + if (delta == 1 || delta == -1) +- granularity = 1; /* Character. */ ++ granularity = ns_ax_text_selection_granularity_character; /* Character. */ + else + { + NSUInteger tlen = [cachedText length]; +@@ -7903,15 +7926,16 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + NSRange newLine = [cachedText lineRangeForRange: + NSMakeRange (newIdx, 0)]; + if (oldLine.location != newLine.location) +- granularity = 3; /* Line. */ ++ granularity = ns_ax_text_selection_granularity_line; /* Line. */ + + } + } + + NSDictionary *moveInfo = @{ +- @"AXTextStateChangeType": @1, ++ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), + @"AXTextSelectionDirection": @(direction), +- @"AXTextSelectionGranularity": @(granularity) ++ @"AXTextSelectionGranularity": @(granularity), ++ @"AXTextChangeElement": self + }; + NSAccessibilityPostNotificationWithUserInfo ( + self, +@@ -8006,13 +8030,20 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t ov_end = OVERLAY_END (ov); + if (ov_end > ov_start) + { +- NSUInteger ax_s = [self accessibilityIndexForCharpos: +- ov_start]; +- NSUInteger ax_e = [self accessibilityIndexForCharpos: +- ov_end]; +- if (ax_e > ax_s && ax_e <= [cachedText length]) +- announceText = [cachedText substringWithRange: +- NSMakeRange (ax_s, ax_e - ax_s)]; ++ NSUInteger ax_idx = [self accessibilityIndexForCharpos: ++ ov_start]; ++ if (ax_idx <= [cachedText length]) ++ { ++ NSInteger lineNum = [self accessibilityLineForIndex:ax_idx]; ++ NSRange lineRange = [self accessibilityRangeForLine:lineNum]; ++ if (lineRange.location != NSNotFound ++ && lineRange.length > 0 ++ && lineRange.location + lineRange.length ++ <= [cachedText length]) ++ announceText = [cachedText substringWithRange:lineRange]; ++ } ++ currentOverlayStart = ov_start; ++ currentOverlayEnd = ov_end; + } + break; + } +@@ -8027,15 +8058,19 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t ov_end = 0; + if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end)) + { +- NSUInteger ax_s = [self accessibilityIndexForCharpos:ov_start]; +- NSUInteger ax_e = [self accessibilityIndexForCharpos:ov_end]; +- if (ax_e > ax_s && ax_e <= [cachedText length]) ++ NSUInteger ax_idx = [self accessibilityIndexForCharpos:ov_start]; ++ if (ax_idx <= [cachedText length]) + { +- announceText = [cachedText substringWithRange: +- NSMakeRange (ax_s, ax_e - ax_s)]; +- currentOverlayStart = ov_start; +- currentOverlayEnd = ov_end; ++ NSInteger lineNum = [self accessibilityLineForIndex:ax_idx]; ++ NSRange lineRange = [self accessibilityRangeForLine:lineNum]; ++ if (lineRange.location != NSNotFound ++ && lineRange.length > 0 ++ && lineRange.location + lineRange.length ++ <= [cachedText length]) ++ announceText = [cachedText substringWithRange:lineRange]; + } ++ currentOverlayStart = ov_start; ++ currentOverlayEnd = ov_end; + } + } + +@@ -8126,13 +8161,18 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ¤tOverlayStart, + ¤tOverlayEnd)) + { +- NSUInteger ax_s = [self accessibilityIndexForCharpos: ++ NSUInteger ax_idx = [self accessibilityIndexForCharpos: + currentOverlayStart]; +- NSUInteger ax_e = [self accessibilityIndexForCharpos: +- currentOverlayEnd]; +- if (ax_e > ax_s && ax_e <= [cachedText length]) +- announceText = [cachedText substringWithRange: +- NSMakeRange (ax_s, ax_e - ax_s)]; ++ if (ax_idx <= [cachedText length]) ++ { ++ NSInteger lineNum = [self accessibilityLineForIndex:ax_idx]; ++ NSRange lineRange = [self accessibilityRangeForLine:lineNum]; ++ if (lineRange.location != NSNotFound ++ && lineRange.length > 0 ++ && lineRange.location + lineRange.length ++ <= [cachedText length]) ++ announceText = [cachedText substringWithRange:lineRange]; ++ } + } + + if (b != oldb2) +@@ -9651,7 +9691,8 @@ ns_in_echo_area (void) + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); +- NSDictionary *info = @{@"AXTextStateChangeType": @1}; ++ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": focused}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } +@@ -11111,7 +11152,8 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); +- NSDictionary *info = @{@"AXTextStateChangeType": @1}; ++ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": focused}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } +-- +2.43.0 + + +From 9ea70d95944224c27be460bb5c289a58867ec385 Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Thu, 26 Feb 2026 18:24:45 +0100 +Subject: [PATCH 5/9] ns: align completion announce with candidate text model + +--- + nsterm.h | 1 + + nsterm.m | 172 ++++++++++++++++++++++++++++++++++++++----------------- + 2 files changed, 122 insertions(+), 51 deletions(-) + +diff --git a/nsterm.h b/nsterm.h +index 97da979..22828f2 100644 +--- a/nsterm.h ++++ b/nsterm.h +@@ -496,6 +496,7 @@ typedef struct ns_ax_visible_run + @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; + @property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart; + @property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd; ++@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint; + - (void)invalidateTextCache; + - (void)postAccessibilityNotificationsForFrame:(struct frame *)f; + - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx; +diff --git a/nsterm.m b/nsterm.m +index e7af9a3..add827f 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7173,9 +7173,15 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t best_dist = PTRDIFF_MAX; + BOOL found = NO; + +- for (ptrdiff_t scan = begv; scan < zv; scan++) ++ /* Fast path: look at point and immediate neighbors first. */ ++ ptrdiff_t probes[3] = { point, point - 1, point + 1 }; ++ for (int i = 0; i < 3 && !found; i++) + { +- Lisp_Object overlays = Foverlays_at (make_fixnum (scan), Qnil); ++ ptrdiff_t p = probes[i]; ++ if (p < begv || p > zv) ++ continue; ++ ++ Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil); + Lisp_Object tail; + for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + { +@@ -7190,20 +7196,48 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + if (ov_end <= ov_start) + continue; + +- ptrdiff_t dist = 0; +- if (point < ov_start) +- dist = ov_start - point; +- else if (point > ov_end) +- dist = point - ov_end; ++ best_start = ov_start; ++ best_end = ov_end; ++ best_dist = 0; ++ found = YES; ++ break; ++ } ++ } + +- if (!found || dist < best_dist +- || (dist == best_dist +- && (ov_start < point && best_start >= point))) ++ if (!found) ++ { ++ for (ptrdiff_t scan = begv; scan < zv; scan++) ++ { ++ Lisp_Object overlays = Foverlays_at (make_fixnum (scan), Qnil); ++ Lisp_Object tail; ++ for (tail = overlays; CONSP (tail); tail = XCDR (tail)) + { +- best_start = ov_start; +- best_end = ov_end; +- best_dist = dist; +- found = YES; ++ Lisp_Object ov = XCAR (tail); ++ Lisp_Object face = Foverlay_get (ov, Qface); ++ if (!(EQ (face, faceSym) ++ || (CONSP (face) && !NILP (Fmemq (faceSym, face))))) ++ continue; ++ ++ ptrdiff_t ov_start = OVERLAY_START (ov); ++ ptrdiff_t ov_end = OVERLAY_END (ov); ++ if (ov_end <= ov_start) ++ continue; ++ ++ ptrdiff_t dist = 0; ++ if (point < ov_start) ++ dist = ov_start - point; ++ else if (point > ov_end) ++ dist = point - ov_end; ++ ++ if (!found || dist < best_dist ++ || (dist == best_dist ++ && (ov_start < point && best_start >= point))) ++ { ++ best_start = ov_start; ++ best_end = ov_end; ++ best_dist = dist; ++ found = YES; ++ } + } + } + } +@@ -7216,6 +7250,55 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + return YES; + } + ++static NSString * ++ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, ++ struct buffer *b, ++ ptrdiff_t start, ++ ptrdiff_t end, ++ NSString *cachedText) ++{ ++ if (!elem || !b || !cachedText || end <= start) ++ return nil; ++ ++ NSString *text = nil; ++ struct buffer *oldb = current_buffer; ++ if (b != current_buffer) ++ set_buffer_internal_1 (b); ++ ++ /* Prefer canonical completion candidate string from text property. */ ++ ptrdiff_t probes[2] = { start, end - 1 }; ++ for (int i = 0; i < 2 && !text; i++) ++ { ++ ptrdiff_t p = probes[i]; ++ Lisp_Object cstr = Fget_char_property (make_fixnum (p), ++ intern ("completion--string"), ++ Qnil); ++ if (STRINGP (cstr)) ++ text = [NSString stringWithLispString:cstr]; ++ } ++ ++ if (!text) ++ { ++ NSUInteger ax_s = [elem accessibilityIndexForCharpos:start]; ++ NSUInteger ax_e = [elem accessibilityIndexForCharpos:end]; ++ if (ax_e > ax_s && ax_e <= [cachedText length]) ++ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)]; ++ } ++ ++ if (b != oldb) ++ set_buffer_internal_1 (oldb); ++ ++ if (text) ++ { ++ text = [text stringByTrimmingCharactersInSet: ++ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; ++ if ([text length] == 0) ++ text = nil; ++ } ++ ++ return text; ++} ++ + + @implementation EmacsAccessibilityElement + +@@ -7265,6 +7348,7 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + @synthesize cachedCompletionAnnouncement; + @synthesize cachedCompletionOverlayStart; + @synthesize cachedCompletionOverlayEnd; ++@synthesize cachedCompletionPoint; + + - (void)dealloc + { +@@ -8030,18 +8114,10 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t ov_end = OVERLAY_END (ov); + if (ov_end > ov_start) + { +- NSUInteger ax_idx = [self accessibilityIndexForCharpos: +- ov_start]; +- if (ax_idx <= [cachedText length]) +- { +- NSInteger lineNum = [self accessibilityLineForIndex:ax_idx]; +- NSRange lineRange = [self accessibilityRangeForLine:lineNum]; +- if (lineRange.location != NSNotFound +- && lineRange.length > 0 +- && lineRange.location + lineRange.length +- <= [cachedText length]) +- announceText = [cachedText substringWithRange:lineRange]; +- } ++ announceText = ns_ax_completion_text_for_span (self, b, ++ ov_start, ++ ov_end, ++ cachedText); + currentOverlayStart = ov_start; + currentOverlayEnd = ov_end; + } +@@ -8058,17 +8134,10 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ptrdiff_t ov_end = 0; + if (ns_ax_find_completion_overlay_range (b, point, &ov_start, &ov_end)) + { +- NSUInteger ax_idx = [self accessibilityIndexForCharpos:ov_start]; +- if (ax_idx <= [cachedText length]) +- { +- NSInteger lineNum = [self accessibilityLineForIndex:ax_idx]; +- NSRange lineRange = [self accessibilityRangeForLine:lineNum]; +- if (lineRange.location != NSNotFound +- && lineRange.length > 0 +- && lineRange.location + lineRange.length +- <= [cachedText length]) +- announceText = [cachedText substringWithRange:lineRange]; +- } ++ announceText = ns_ax_completion_text_for_span (self, b, ++ ov_start, ++ ov_end, ++ cachedText); + currentOverlayStart = ov_start; + currentOverlayEnd = ov_end; + } +@@ -8105,7 +8174,8 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + BOOL overlayChanged = + (currentOverlayStart != self.cachedCompletionOverlayStart + || currentOverlayEnd != self.cachedCompletionOverlayEnd); +- if (textChanged || overlayChanged) ++ BOOL pointChanged = (point != self.cachedCompletionPoint); ++ if (textChanged || overlayChanged || pointChanged) + { + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: announceText, +@@ -8120,12 +8190,14 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + self.cachedCompletionAnnouncement = announceText; + self.cachedCompletionOverlayStart = currentOverlayStart; + self.cachedCompletionOverlayEnd = currentOverlayEnd; ++ self.cachedCompletionPoint = point; + } + else + { + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; ++ self.cachedCompletionPoint = 0; + } + } + else +@@ -8133,6 +8205,7 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; ++ self.cachedCompletionPoint = 0; + } + } + +@@ -8144,6 +8217,7 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; ++ self.cachedCompletionPoint = 0; + } + else + { +@@ -8161,18 +8235,10 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + ¤tOverlayStart, + ¤tOverlayEnd)) + { +- NSUInteger ax_idx = [self accessibilityIndexForCharpos: +- currentOverlayStart]; +- if (ax_idx <= [cachedText length]) +- { +- NSInteger lineNum = [self accessibilityLineForIndex:ax_idx]; +- NSRange lineRange = [self accessibilityRangeForLine:lineNum]; +- if (lineRange.location != NSNotFound +- && lineRange.length > 0 +- && lineRange.location + lineRange.length +- <= [cachedText length]) +- announceText = [cachedText substringWithRange:lineRange]; +- } ++ announceText = ns_ax_completion_text_for_span (self, b, ++ currentOverlayStart, ++ currentOverlayEnd, ++ cachedText); + } + + if (b != oldb2) +@@ -8189,7 +8255,8 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + BOOL overlayChanged = + (currentOverlayStart != self.cachedCompletionOverlayStart + || currentOverlayEnd != self.cachedCompletionOverlayEnd); +- if (textChanged || overlayChanged) ++ BOOL pointChanged = (point != self.cachedCompletionPoint); ++ if (textChanged || overlayChanged || pointChanged) + { + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: announceText, +@@ -8204,12 +8271,14 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + self.cachedCompletionAnnouncement = announceText; + self.cachedCompletionOverlayStart = currentOverlayStart; + self.cachedCompletionOverlayEnd = currentOverlayEnd; ++ self.cachedCompletionPoint = point; + } + else + { + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; ++ self.cachedCompletionPoint = 0; + } + } + else +@@ -8217,6 +8286,7 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + self.cachedCompletionAnnouncement = nil; + self.cachedCompletionOverlayStart = 0; + self.cachedCompletionOverlayEnd = 0; ++ self.cachedCompletionPoint = 0; + } + } + } +-- +2.43.0 + + +From 74496754b9011d2f48d9787b77ad93669d257f5f Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Thu, 26 Feb 2026 18:36:42 +0100 +Subject: [PATCH 6/9] ns: handle basic C-n/C-p line motion for VoiceOver + +--- + nsterm.m | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 48 insertions(+) + +diff --git a/nsterm.m b/nsterm.m +index add827f..b7f0614 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7250,6 +7250,17 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + return YES; + } + ++static bool ++ns_ax_command_is_basic_line_move (void) ++{ ++ if (!SYMBOLP (real_this_command)) ++ return false; ++ ++ Lisp_Object next = intern ("next-line"); ++ Lisp_Object prev = intern ("previous-line"); ++ return EQ (real_this_command, next) || EQ (real_this_command, prev); ++} ++ + static NSString * + ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + struct buffer *b, +@@ -8026,6 +8037,43 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + NSAccessibilitySelectedTextChangedNotification, + moveInfo); + ++ /* C-n/C-p (`next-line' / `previous-line') can diverge from ++ arrow-down/up command paths in some major modes (completion list, ++ Dired, etc.). Emit an explicit line announcement for this basic ++ line-motion path so VoiceOver tracks the Emacs point reliably. */ ++ if ([self isAccessibilityFocused] ++ && cachedText ++ && ns_ax_command_is_basic_line_move () ++ && (direction == ns_ax_text_selection_direction_next ++ || direction == ns_ax_text_selection_direction_previous)) ++ { ++ NSUInteger point_idx = [self accessibilityIndexForCharpos:point]; ++ if (point_idx <= [cachedText length]) ++ { ++ NSInteger lineNum = [self accessibilityLineForIndex:point_idx]; ++ NSRange lineRange = [self accessibilityRangeForLine:lineNum]; ++ if (lineRange.location != NSNotFound ++ && lineRange.length > 0 ++ && lineRange.location + lineRange.length <= [cachedText length]) ++ { ++ NSString *lineText = [cachedText substringWithRange:lineRange]; ++ lineText = [lineText stringByTrimmingCharactersInSet: ++ [NSCharacterSet whitespaceAndNewlineCharacterSet]]; ++ if ([lineText length] > 0) ++ { ++ NSDictionary *annInfo = @{ ++ NSAccessibilityAnnouncementKey: lineText, ++ NSAccessibilityPriorityKey: @(NSAccessibilityPriorityHigh) ++ }; ++ NSAccessibilityPostNotificationWithUserInfo ( ++ NSApp, ++ NSAccessibilityAnnouncementRequestedNotification, ++ annInfo); ++ } ++ } ++ } ++ } ++ + /* --- Completions announcement --- + When point changes in a non-focused buffer (e.g. *Completions* + while the minibuffer has keyboard focus), VoiceOver won't read +-- +2.43.0 + + +From 0f29e2ced4d0e3b737f0e0ee862a933aec0e02ac Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Thu, 26 Feb 2026 18:41:19 +0100 +Subject: [PATCH 7/9] ns: detect C-n/C-p keypath and force line semantics + +--- + nsterm.m | 43 ++++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 42 insertions(+), 1 deletion(-) + +diff --git a/nsterm.m b/nsterm.m +index b7f0614..da40369 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7250,6 +7250,34 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + return YES; + } + ++extern Lisp_Object last_command_event; ++ ++static bool ++ns_ax_event_is_ctrl_n_or_p (int *which) ++{ ++ Lisp_Object ev = last_command_event; ++ if (CONSP (ev)) ++ ev = EVENT_HEAD (ev); ++ ++ if (!FIXNUMP (ev)) ++ return false; ++ ++ EMACS_INT c = XFIXNUM (ev); ++ if (c == '\C-n') ++ { ++ if (which) ++ *which = 1; ++ return true; ++ } ++ if (c == '\C-p') ++ { ++ if (which) ++ *which = -1; ++ return true; ++ } ++ return false; ++} ++ + static bool + ns_ax_command_is_basic_line_move (void) + { +@@ -7996,6 +8024,9 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + else if (point < oldPoint) + direction = ns_ax_text_selection_direction_previous; + ++ int ctrlNP = 0; ++ bool isCtrlNP = ns_ax_event_is_ctrl_n_or_p (&ctrlNP); ++ + /* Compute granularity from movement distance. + Prefer robust line-range comparison for vertical movement, + otherwise single char (1) or unknown (0). */ +@@ -8026,6 +8057,16 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + } + } + ++ /* Force line semantics for explicit C-n/C-p keystrokes. ++ This isolates the key-path difference from arrow-down/up. */ ++ if (isCtrlNP) ++ { ++ direction = (ctrlNP > 0 ++ ? ns_ax_text_selection_direction_next ++ : ns_ax_text_selection_direction_previous); ++ granularity = ns_ax_text_selection_granularity_line; ++ } ++ + NSDictionary *moveInfo = @{ + @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), + @"AXTextSelectionDirection": @(direction), +@@ -8043,7 +8084,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + line-motion path so VoiceOver tracks the Emacs point reliably. */ + if ([self isAccessibilityFocused] + && cachedText +- && ns_ax_command_is_basic_line_move () ++ && (isCtrlNP || ns_ax_command_is_basic_line_move ()) + && (direction == ns_ax_text_selection_direction_next + || direction == ns_ax_text_selection_direction_previous)) + { +-- +2.43.0 + + +From ea8100f3110bf937aa69b098d2227352524f34e8 Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Thu, 26 Feb 2026 21:37:16 +0100 +Subject: [PATCH 8/9] ns: fix build errors in C-n/C-p key detection + +Remove redundant extern declaration of last_command_event which +is already a macro in globals.h (globals.f_last_command_event). +Replace invalid C escape sequences '\C-n'/'\C-p' with their ASCII +integer values 14 (Ctrl+N) and 16 (Ctrl+P). +--- + nsterm.m | 6 ++---- + 1 file changed, 2 insertions(+), 4 deletions(-) + +diff --git a/nsterm.m b/nsterm.m +index da40369..5ca0deb 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7250,8 +7250,6 @@ ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, + return YES; + } + +-extern Lisp_Object last_command_event; +- + static bool + ns_ax_event_is_ctrl_n_or_p (int *which) + { +@@ -7263,13 +7261,13 @@ ns_ax_event_is_ctrl_n_or_p (int *which) + return false; + + EMACS_INT c = XFIXNUM (ev); +- if (c == '\C-n') ++ if (c == 14) /* C-n */ + { + if (which) + *which = 1; + return true; + } +- if (c == '\C-p') ++ if (c == 16) /* C-p */ + { + if (which) + *which = -1; +-- +2.43.0 + + +From 32f820e094a1dda540ad64c105b6eec5d5b70f28 Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Fri, 27 Feb 2026 07:29:33 +0100 +Subject: [PATCH 9/9] ns: fix NSRange return type in + accessibilitySelectedTextRange guard + +--- + nsterm.m | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/nsterm.m b/nsterm.m +index 5ca0deb..cfc5b4c 100644 +--- a/nsterm.m ++++ b/nsterm.m +@@ -7605,6 +7605,8 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + if (!w || !WINDOW_LEAF_P (w)) + return NSMakeRange (0, 0); + ++ if (!BUFFERP (w->contents)) ++ return NSMakeRange (0, 0); + struct buffer *b = XBUFFER (w->contents); + if (!b) + return NSMakeRange (0, 0); +-- +2.43.0 +