From 7419d9b0e4f501e02070fdee1b85d5fc30f223e3 Mon Sep 17 00:00:00 2001 From: Daneel Date: Fri, 27 Feb 2026 07:31:44 +0100 Subject: [PATCH] patches: regenerate from actual emacs repo (fix src/ path prefix) --- ...oundsForRange-for-macOS-Zoom-cursor-.patch | 3585 ++++++----------- 1 file changed, 1204 insertions(+), 2381 deletions(-) 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 8e0ccc7..d5bed2c 100644 --- a/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch +++ b/patches/0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch @@ -1,38 +1,31 @@ -From 06e39ff5c5e1cf836ff4c78dafc2939ef0b05851 Mon Sep 17 00:00:00 2001 -From: Daneel -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 +From 7970024f17d83610a4fd58d7ab135b2c71783049 Mon Sep 17 00:00:00 2001 +From: Martin Sukany +Date: Fri, 27 Feb 2026 07:31:34 +0100 +Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line + nav, completions) --- - nsterm.h | 67 ++ - nsterm.m | 1837 ++++++++++++++++++++++++++++++++++++++++++++++++------ - 2 files changed, 1723 insertions(+), 181 deletions(-) + src/nsterm.h | 47 +- + src/nsterm.m | 2005 +++++++++++++++++++++++++++++++++++++++----------- + 2 files changed, 1628 insertions(+), 424 deletions(-) -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 +diff --git a/src/nsterm.h b/src/nsterm.h +index 4f9a1b0..22828f2 100644 +--- a/src/nsterm.h ++++ b/src/nsterm.h +@@ -462,21 +462,49 @@ enum ns_return_frame_mode + #ifdef NS_IMPL_COCOA + @class EmacsView; + +-/* Base class for virtual accessibility elements attached to EmacsView. */ ++/* Base class for virtual accessibility elements attached to EmacsView. */ + @interface EmacsAccessibilityElement : NSAccessibilityElement + @property (nonatomic, unsafe_unretained) EmacsView *emacsView; + @property (nonatomic, assign) struct window *emacsWindow; + - (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h; @end - -+/* ========================================================================== -+ -+ Accessibility virtual elements (macOS / Cocoa only) -+ -+ ========================================================================== */ -+ -+#ifdef NS_IMPL_COCOA -+@class EmacsView; -+ -+/* Base class for virtual accessibility elements attached to EmacsView. */ -+@interface EmacsAccessibilityElement : NSAccessibilityElement -+@property (nonatomic, unsafe_unretained) EmacsView *emacsView; -+@property (nonatomic, assign) struct window *emacsWindow; -+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h; -+@end -+ +-/* Virtual AXTextArea element — one per visible Emacs window (buffer). */ +/* A visible run: maps a contiguous range of accessibility indices + to a contiguous range of buffer character positions. Invisible + text is skipped, so ax_start values are consecutive across runs @@ -46,7 +39,7 @@ index 7c1ee4c..2e2c80f 100644 +} ns_ax_visible_run; + +/* Virtual AXTextArea element — one per visible Emacs window (buffer). */ -+@interface EmacsAccessibilityBuffer : EmacsAccessibilityElement + @interface EmacsAccessibilityBuffer : EmacsAccessibilityElement +{ + ns_ax_visible_run *visibleRuns; + NSUInteger visibleRunCount; @@ -54,9 +47,17 @@ index 7c1ee4c..2e2c80f 100644 +@property (nonatomic, retain) NSString *cachedText; +@property (nonatomic, assign) ptrdiff_t cachedTextModiff; +@property (nonatomic, assign) ptrdiff_t cachedTextStart; -+@property (nonatomic, assign) ptrdiff_t cachedModiff; -+@property (nonatomic, assign) ptrdiff_t cachedPoint; + @property (nonatomic, assign) ptrdiff_t cachedModiff; + @property (nonatomic, assign) ptrdiff_t cachedPoint; +-@property (nonatomic, assign) Lisp_Object cachedSelectedWindow; +-- (void) +- postAccessibilityUpdatesForWindow:(struct window *)w +- frame:(struct frame *)f; +@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; @@ -65,19 +66,13 @@ index 7c1ee4c..2e2c80f 100644 + +/* Virtual AXStaticText element — one per mode line. */ +@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement -+@end -+#endif /* NS_IMPL_COCOA */ -+ -+ - /* ========================================================================== + @end + #endif /* NS_IMPL_COCOA */ - The main Emacs view -@@ -471,6 +523,14 @@ enum ns_return_frame_mode - #ifdef NS_IMPL_COCOA - char *old_title; +@@ -501,6 +529,12 @@ enum ns_return_frame_mode BOOL maximizing_resize; -+ NSMutableArray *accessibilityElements; -+ Lisp_Object lastSelectedWindow; + NSMutableArray *accessibilityElements; + Lisp_Object lastSelectedWindow; + Lisp_Object lastRootWindow; + BOOL accessibilityTreeValid; + BOOL accessibilityUpdating; @@ -87,37 +82,19 @@ index 7c1ee4c..2e2c80f 100644 #endif BOOL font_panel_active; NSFont *font_panel_result; -@@ -528,6 +588,13 @@ enum ns_return_frame_mode - - (void)windowWillExitFullScreen; - - (void)windowDidExitFullScreen; - - (void)windowDidBecomeKey; -+ -+#ifdef NS_IMPL_COCOA -+/* Accessibility support. */ -+- (void)rebuildAccessibilityTree; +@@ -562,6 +596,7 @@ enum ns_return_frame_mode + #ifdef NS_IMPL_COCOA + /* Accessibility support. */ + - (void)rebuildAccessibilityTree; +- (void)invalidateAccessibilityTree; -+- (void)postAccessibilityUpdates; -+#endif + - (void)postAccessibilityUpdates; + #endif @end - - -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 (); - ns_updating_frame = NULL; -+ -+#ifdef NS_IMPL_COCOA -+ /* Post accessibility notifications after each redisplay cycle. */ -+ [view postAccessibilityUpdates]; -+#endif - } - - static void -@@ -3232,6 +3237,37 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, +diff --git a/src/nsterm.m b/src/nsterm.m +index e67edbe..cfc5b4c 100644 +--- a/src/nsterm.m ++++ b/src/nsterm.m +@@ -3237,6 +3237,37 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors. /* Prevent the cursor from being drawn outside the text area. */ r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); @@ -155,54 +132,49 @@ index 932d209..416e5a4 100644 ns_focus (f, NULL, 0); NSGraphicsContext *ctx = [NSGraphicsContext currentContext]; -@@ -6849,261 +6885,1380 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) +@@ -6860,172 +6891,174 @@ Accessibility virtual elements (macOS / Cocoa only) - /* ========================================================================== + #ifdef NS_IMPL_COCOA -- EmacsView implementation -+ Accessibility virtual elements (macOS / Cocoa only) - - ========================================================================== */ - -+#ifdef NS_IMPL_COCOA - --@implementation EmacsView +-/* ---- Helper: extract visible text from glyph rows of a window ---- */ +/* ---- 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 * ++ + static NSString * +-ns_ax_text_from_glyph_rows (struct window *w) +ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, + ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -+{ + { +- if (!w || !w->current_matrix) +- return @""; +- +- struct glyph_matrix *matrix = w->current_matrix; +- NSMutableString *text = [NSMutableString stringWithCapacity:4096]; +- int nrows = matrix->nrows; + *out_runs = NULL; + *out_nruns = 0; +- for (int i = 0; i < nrows; i++) + if (!w || !WINDOW_LEAF_P (w)) -+ { + { +- struct glyph_row *row = matrix->rows + i; +- if (!row->enabled_p || row->mode_line_p) +- continue; +- if (!row->displays_text_p && !row->ends_at_zv_p) +- continue; + *out_start = 0; + return @""; + } --- (void)dealloc --{ -- NSTRACE ("[EmacsView dealloc]"); +- struct glyph *glyph = row->glyphs[TEXT_AREA]; +- struct glyph *end = glyph + row->used[TEXT_AREA]; + struct buffer *b = XBUFFER (w->contents); + if (!b) + { @@ -210,53 +182,57 @@ index 932d209..416e5a4 100644 + return @""; + } -- /* Clear the view resize notification. */ -- [[NSNotificationCenter defaultCenter] -- removeObserver:self -- name:NSViewFrameDidChangeNotification -- object:nil]; +- for (; glyph < end; glyph++) +- { +- if (glyph->type == CHAR_GLYPH && !glyph->padding_p) +- { +- unsigned ch = glyph->u.ch; +- if (ch == '\n' || ch == '\r') +- continue; /* row boundary handles newlines */ +- if (ch >= 32) +- { +- unichar uch = (unichar)ch; +- [text appendString:[NSString stringWithCharacters:&uch +- length:1]]; +- } +- } +- } + ptrdiff_t begv = BUF_BEGV (b); + ptrdiff_t zv = BUF_ZV (b); -- if (fs_state == FULLSCREEN_BOTH) -- [nonfs_window release]; +- /* Add newline between rows unless this is the last displayed row. */ +- if (i + 1 < nrows) +- { +- struct glyph_row *next = matrix->rows + i + 1; +- if (next->enabled_p && (next->displays_text_p || next->ends_at_zv_p) +- && !next->mode_line_p) +- [text appendString:@"\n"]; +- } +- } + *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 +- /* Cap at 32KB */ +- if ([text length] > 32768) +- return [text substringToIndex:32768]; + if (zv <= begv) + return @""; -- [[self menu] release]; -- [super dealloc]; +- return text; -} + 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). */ @@ -277,15 +253,12 @@ index 932d209..416e5a4 100644 + 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]; +-/* ---- Row geometry helpers ---- */ + /* 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) @@ -317,54 +290,118 @@ index 932d209..416e5a4 100644 + runs[nruns].ax_length = ns_len; + nruns++; -- font_panel_result = (NSFont *) [sender convertFont: nsfont]; +-/* Count the number of visible text rows (excluding mode line). */ +-static int +-ns_ax_visible_row_count (struct window *w) +-{ +- if (!w || !w->current_matrix) +- return 0; +- struct glyph_matrix *matrix = w->current_matrix; +- int count = 0; +- for (int i = 0; i < matrix->nrows; i++) +- { +- struct glyph_row *row = matrix->rows + i; +- if (row->enabled_p && !row->mode_line_p +- && (row->displays_text_p || row->ends_at_zv_p)) +- count++; + ax_offset += ns_len; + pos = run_end; -+ } + } +- return count; +-} -- if (font_panel_result) -- [font_panel_result retain]; +-/* Map a character index (within the glyph-extracted text) to a visual +- row number (0-based, text rows only). */ +-static int +-ns_ax_line_for_index (struct window *w, NSUInteger idx) +-{ +- if (!w || !w->current_matrix) +- return 0; +- struct glyph_matrix *matrix = w->current_matrix; +- NSUInteger pos = 0; +- int line = 0; + if (b != oldb) + set_buffer_internal_1 (oldb); --#ifndef NS_IMPL_COCOA -- font_panel_active = NO; -- [NSApp stop: self]; --#endif +- for (int i = 0; i < matrix->nrows; i++) +- { +- struct glyph_row *row = matrix->rows + i; +- if (!row->enabled_p || row->mode_line_p) +- continue; +- if (!row->displays_text_p && !row->ends_at_zv_p) +- continue; + *out_runs = runs; + *out_nruns = nruns; + return result; - } ++} --#ifdef NS_IMPL_COCOA --- (void) noteUserSelectedFont -+ +- /* Count characters in this row. */ +- int row_chars = 0; +- struct glyph *g = row->glyphs[TEXT_AREA]; +- struct glyph *gend = g + row->used[TEXT_AREA]; +- for (; g < gend; g++) +- { +- if (g->type == CHAR_GLYPH && !g->padding_p) +- { +- unsigned ch = g->u.ch; +- if (ch != '\n' && ch != '\r' && (ch >= 32)) +- row_chars++; +- } +- } + +- NSUInteger row_end = pos + row_chars + 1; /* +1 for newline */ +- if (idx < row_end) +- return line; +- pos = row_end; +- line++; +- } +- return MAX(0, line - 1); +-} +/* ---- Helper: extract mode line text from glyph rows ---- */ -+ + +-/* Return character range for a given visual line number. */ +-static NSRange +-ns_ax_range_for_line (struct window *w, int target_line) +static NSString * +ns_ax_mode_line_text (struct window *w) { -- font_panel_active = NO; -+ if (!w || !w->current_matrix) + if (!w || !w->current_matrix) +- return NSMakeRange(0, 0); + return @""; - -- /* If no font was previously selected, use the currently selected -- font. */ -+ struct glyph_matrix *matrix = w->current_matrix; ++ + struct glyph_matrix *matrix = w->current_matrix; +- NSUInteger pos = 0; +- int line = 0; + NSMutableString *text = [NSMutableString string]; -- if (!font_panel_result && FRAME_FONT (emacsframe)) -+ for (int i = 0; i < matrix->nrows; i++) + for (int i = 0; i < matrix->nrows; i++) { -- font_panel_result -- = macfont_get_nsctfont (FRAME_FONT (emacsframe)); -+ struct glyph_row *row = matrix->rows + i; + struct glyph_row *row = matrix->rows + i; +- if (!row->enabled_p || row->mode_line_p) +- continue; +- if (!row->displays_text_p && !row->ends_at_zv_p) +- continue; + if (!row->enabled_p || !row->mode_line_p) + continue; -- if (font_panel_result) -- [font_panel_result retain]; -+ struct glyph *g = row->glyphs[TEXT_AREA]; +- int row_chars = 0; + struct glyph *g = row->glyphs[TEXT_AREA]; +- struct glyph *gend = g + row->used[TEXT_AREA]; +- for (; g < gend; g++) +- { +- if (g->type == CHAR_GLYPH && !g->padding_p) +- { +- unsigned ch = g->u.ch; +- if (ch != '\n' && ch != '\r' && (ch >= 32)) +- row_chars++; +- } +- } +- +- if (line == target_line) +- return NSMakeRange(pos, row_chars); +- +- pos += row_chars + 1; /* +1 for newline */ +- line++; + struct glyph *end = g + row->used[TEXT_AREA]; + for (; g < end; g++) + { @@ -376,86 +413,79 @@ index 932d209..416e5a4 100644 + } + } } -- -- [NSApp stop: self]; +- return NSMakeRange(NSNotFound, 0); + return text; } --- (void) noteUserCancelledSelection --{ -- font_panel_active = NO; -- -- if (font_panel_result) -- [font_panel_result release]; -- font_panel_result = nil; - -- [NSApp stop: self]; --} --#endif +-/* Compute screen rect for a character range by unioning glyph row rects. */ ++ +/* ---- Helper: screen rect for a character range via glyph matrix ---- */ - --- (Lisp_Object) showFontPanel -+static NSRect ++ + static NSRect +-ns_ax_frame_for_range (struct window *w, EmacsView *view, NSRange range) +ns_ax_frame_for_range (struct window *w, EmacsView *view, + ptrdiff_t text_start, NSRange range) { -- 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; - --#ifdef NS_IMPL_GNUSTEP -- nsfont = ((struct nsfont_info *) font)->nsfont; --#else -- nsfont = (NSFont *) macfont_get_nsctfont (font); --#endif + if (!w || !w->current_matrix || !view) + return NSZeroRect; ++ + /* 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; ++ + struct glyph_matrix *matrix = w->current_matrix; +- NSUInteger pos = 0; + NSRect result = NSZeroRect; + BOOL found = NO; --#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; - -- [fm setSelectedFont: nsfont isMultiple: NO]; -- [fm orderFrontFontPanel: NSApp]; -+ for (int i = 0; i < matrix->nrows; i++) -+ { -+ struct glyph_row *row = matrix->rows + i; -+ if (!row->enabled_p || row->mode_line_p) +@@ -7033,121 +7066,274 @@ row number (0-based, text rows only). */ + { + struct glyph_row *row = matrix->rows + i; + if (!row->enabled_p || row->mode_line_p) +- continue; + continue; -+ if (!row->displays_text_p && !row->ends_at_zv_p) + if (!row->displays_text_p && !row->ends_at_zv_p) +- continue; + continue; -- font_panel_active = YES; -- timeout = make_timespec (0, 100000000); +- int row_chars = 0; +- struct glyph *g = row->glyphs[TEXT_AREA]; +- struct glyph *gend = g + row->used[TEXT_AREA]; +- for (; g < gend; g++) +- { +- if (g->type == CHAR_GLYPH && !g->padding_p) +- { +- unsigned ch = g->u.ch; +- if (ch != '\n' && ch != '\r' && (ch >= 32)) +- row_chars++; +- } +- } + 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 (); +- NSUInteger row_end = pos + row_chars + 1; +- if (pos < range.location + range.length && row_end > range.location) +- { +- /* This row overlaps the requested range. */ +- int window_x, window_y, window_width; +- window_box (w, TEXT_AREA, &window_x, &window_y, &window_width, 0); +- +- NSRect rowRect; +- rowRect.origin.x = window_x; +- rowRect.origin.y = WINDOW_TO_FRAME_PIXEL_Y (w, MAX(0, row->y)); +- rowRect.origin.y = MAX(rowRect.origin.y, window_y); +- rowRect.size.width = window_width; +- rowRect.size.height = row->visible_height; +- +- if (!found) +- { +- result = rowRect; +- found = YES; +- } +- else +- result = NSUnionRect(result, rowRect); +- } +- pos = row_end; + if (row_start < cp_end && row_end > cp_start) + { + int window_x, window_y, window_width; @@ -477,17 +507,11 @@ index 932d209..416e5a4 100644 + else + result = NSUnionRect (result, rowRect); + } -+ } + } -- if (font_panel_result) -- [font_panel_result autorelease]; -+ if (!found) -+ return NSZeroRect; + 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; @@ -497,81 +521,357 @@ index 932d209..416e5a4 100644 + if (NSMaxY (result) > max_y) + result.size.height = max_y - result.origin.y; + } - -- result = font_panel_result; -- font_panel_result = nil; -+ /* 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]; -- font_panel_active = NO; - -- if (result) -- return ns_font_desc_to_font_spec ([result fontDescriptor], -- result); -+@implementation EmacsAccessibilityElement - -- return Qnil; -+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh -+{ -+ EmacsView *view = self.emacsView; -+ if (!view || ![view window]) -+ return NSZeroRect; + -+ NSRect r = NSMakeRect (x, y, ew, eh); -+ NSRect winRect = [view convertRect:r toView:nil]; -+ return [[view window] convertRectToScreen:winRect]; + /* Convert from EmacsView (flipped) coords to screen coords. */ + NSRect winRect = [view convertRect:result toView:nil]; + return [[view window] convertRectToScreen:winRect]; } --- (BOOL)acceptsFirstResponder -+- (BOOL)isAccessibilityElement ++/* 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, ++}; + +-/* Compute the character index within glyph-extracted text that +- corresponds to the buffer point position. */ + static NSUInteger +-ns_ax_index_for_point (struct window *w) ++ns_ax_utf16_length_for_buffer_range (struct buffer *b, ptrdiff_t start, ++ ptrdiff_t end) { -- NSTRACE ("[EmacsView acceptsFirstResponder]"); +- if (!w || !w->current_matrix || !WINDOW_LEAF_P(w)) ++ if (!b || end <= start) + return 0; + +- struct buffer *b = XBUFFER(w->contents); +- if (!b) +- return 0; ++ struct buffer *oldb = current_buffer; ++ if (b != current_buffer) ++ set_buffer_internal_1 (b); + +- ptrdiff_t point = BUF_PT(b); +- struct glyph_matrix *matrix = w->current_matrix; +- NSUInteger pos = 0; ++ Lisp_Object lstr = Fbuffer_substring_no_properties (make_fixnum (start), ++ make_fixnum (end)); ++ NSString *nsstr = [NSString stringWithLispString:lstr]; ++ NSUInteger len = [nsstr length]; + +- for (int i = 0; i < matrix->nrows; i++) ++ 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; ++ ++ /* 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++) + { +- struct glyph_row *row = matrix->rows + i; +- if (!row->enabled_p || row->mode_line_p) +- continue; +- if (!row->displays_text_p && !row->ends_at_zv_p) +- continue; ++ ptrdiff_t p = probes[i]; ++ if (p < begv || p > zv) ++ continue; + +- ptrdiff_t row_start = MATRIX_ROW_START_CHARPOS (row); +- ptrdiff_t row_end = MATRIX_ROW_END_CHARPOS (row); ++ 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; ++ } ++ } + +- if (point >= row_start && point < row_end) +- { +- /* Point is within this row. Count visible glyphs whose +- buffer charpos is before point. */ +- int chars_before = 0; +- struct glyph *g = row->glyphs[TEXT_AREA]; +- struct glyph *gend = g + row->used[TEXT_AREA]; +- for (; g < gend; g++) +- { +- if (g->type == CHAR_GLYPH && !g->padding_p +- && g->charpos >= row_start +- && g->charpos < point) +- { +- unsigned ch = g->u.ch; +- if (ch != '\n' && ch != '\r' && ch >= 32) +- chars_before++; +- } +- } +- return pos + chars_before; +- } ++ 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; ++ } ++ } ++ } ++ } + +- /* Count visible chars in this row + newline. */ +- int row_chars = 0; +- struct glyph *g = row->glyphs[TEXT_AREA]; +- struct glyph *gend = g + row->used[TEXT_AREA]; +- for (; g < gend; g++) +- { +- if (g->type == CHAR_GLYPH && !g->padding_p) +- { +- unsigned ch = g->u.ch; +- if (ch != '\n' && ch != '\r' && (ch >= 32)) +- row_chars++; +- } +- } +- pos += row_chars + 1; ++ if (!found) ++ return NO; ++ ++ *out_start = best_start; ++ *out_end = best_end; ++ return YES; ++} ++ ++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 == 14) /* C-n */ ++ { ++ if (which) ++ *which = 1; ++ return true; ++ } ++ if (c == 16) /* C-p */ ++ { ++ if (which) ++ *which = -1; ++ return true; ++ } ++ return false; ++} ++ ++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, ++ 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 pos > 0 ? pos - 1 : 0; ++ ++ return text; + } + + +@@ -7159,7 +7345,7 @@ - (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh + if (!view || ![view window]) + return NSZeroRect; + +- NSRect r = NSMakeRect(x, y, ew, eh); ++ NSRect r = NSMakeRect (x, y, ew, eh); + NSRect winRect = [view convertRect:r toView:nil]; + return [[view window] convertRectToScreen:winRect]; + } +@@ -7169,130 +7355,475 @@ - (BOOL)isAccessibilityElement return YES; } --/* Tell NS we want to accept clicks that activate the window */ --- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +-@end +- +- +-@implementation EmacsAccessibilityBuffer +/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */ -+ + +-/* ---- NSAccessibility protocol ---- */ +- +-- (NSAccessibilityRole)accessibilityRole +- (id)accessibilityParent { -- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", -- [theEvent type], [theEvent clickCount]); -- return ns_click_through; +- return NSAccessibilityTextAreaRole; + return NSAccessibilityUnignoredAncestor (self.emacsView); } --- (void)resetCursorRects -+ + +-- (NSString *)accessibilityRoleDescription +- (id)accessibilityWindow { -- NSRect visible = [self visibleRect]; -- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); -- NSTRACE ("[EmacsView resetCursorRects]"); +- return @"editor"; + return [self.emacsView window]; -+} + } -- if (currentCursor == nil) -- currentCursor = [NSCursor arrowCursor]; +-- (NSString *)accessibilityLabel +- (id)accessibilityTopLevelUIElement -+{ + { +- struct window *w = self.emacsWindow; +- if (w && WINDOW_LEAF_P(w)) +- { +- struct buffer *b = XBUFFER(w->contents); +- if (b) +- { +- Lisp_Object name = BVAR(b, name); +- if (STRINGP(name)) +- return [NSString stringWithLispString:name]; +- } +- } +- return @"buffer"; + return [self.emacsView window]; -+} + } -- if (!NSIsEmptyRect (visible)) -- [self addCursorRect: visible cursor: currentCursor]; +-- (id)accessibilityValue +-{ +- struct window *w = self.emacsWindow; +- if (!w) +- return @""; +- return ns_ax_text_from_glyph_rows(w); +-} +@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 +-- (NSInteger)accessibilityNumberOfCharacters +-{ +- NSString *text = [self accessibilityValue]; +- return [text length]; -} +-- (NSString *)accessibilitySelectedText +@implementation EmacsAccessibilityBuffer +@synthesize cachedText; +@synthesize cachedTextModiff; @@ -579,28 +879,36 @@ index 932d209..416e5a4 100644 +@synthesize cachedModiff; +@synthesize cachedPoint; +@synthesize cachedMarkActive; - ++@synthesize cachedCompletionAnnouncement; ++@synthesize cachedCompletionOverlayStart; ++@synthesize cachedCompletionOverlayEnd; ++@synthesize cachedCompletionPoint; ++ +- (void)dealloc -+{ + { +- struct window *w = self.emacsWindow; +- if (!w || !WINDOW_LEAF_P(w)) +- return @""; + [cachedText release]; ++ [cachedCompletionAnnouncement release]; + if (visibleRuns) + xfree (visibleRuns); + [super dealloc]; +} --/*****************************************************************************/ --/* Keyboard handling. */ --#define NS_KEYLOG 0 +- struct buffer *b = XBUFFER(w->contents); +- if (!b || NILP(BVAR(b, mark_active))) +- return @""; +/* ---- Text cache ---- */ --- (void)keyDown: (NSEvent *)theEvent +- /* Return the selected region text. */ +- NSString *text = [self accessibilityValue]; +- NSRange sel = [self accessibilitySelectedTextRange]; +- if (sel.location == NSNotFound || sel.location + sel.length > [text length]) +- return @""; +- return [text substringWithRange:sel]; +- (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) @@ -609,22 +917,24 @@ index 932d209..416e5a4 100644 + visibleRuns = NULL; + } + visibleRunCount = 0; -+} + } -- NSTRACE ("[EmacsView keyDown:]"); +-- (NSRange)accessibilitySelectedTextRange +- (void)ensureTextCache -+{ -+ struct window *w = self.emacsWindow; + { + struct window *w = self.emacsWindow; +- if (!w || !WINDOW_LEAF_P(w)) +- return NSMakeRange(0, 0); + if (!w || !WINDOW_LEAF_P (w)) + return; -- /* Rhapsody and macOS give up and down events for the arrow keys. */ -- if ([theEvent type] != NSEventTypeKeyDown) +- struct buffer *b = XBUFFER(w->contents); + struct buffer *b = XBUFFER (w->contents); -+ if (!b) - return; + if (!b) +- return NSMakeRange(0, 0); ++ return; -- if (!emacs_event) +- NSUInteger point_idx = ns_ax_index_for_point(w); + ptrdiff_t modiff = BUF_MODIFF (b); + ptrdiff_t pt = BUF_PT (b); + NSUInteger textLen = cachedText ? [cachedText length] : 0; @@ -632,56 +942,63 @@ index 932d209..416e5a4 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; -- } +- if (NILP(BVAR(b, mark_active))) +- return NSMakeRange(point_idx, 0); + 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]; +- /* With active mark, report the selection range. Map mark +- position to accessibility index using the same glyph-based +- mapping as point. */ +- ptrdiff_t mark_pos = marker_position (BVAR (b, mark)); +- ptrdiff_t pt_pos = BUF_PT (b); +- ptrdiff_t begv = BUF_BEGV (b); +- ptrdiff_t sel_start = (mark_pos < pt_pos) ? mark_pos : pt_pos; +- ptrdiff_t sel_end = (mark_pos < pt_pos) ? pt_pos : mark_pos; +- NSUInteger start_idx = (NSUInteger) (sel_start - begv); +- NSUInteger len = (NSUInteger) (sel_end - sel_start); +- return NSMakeRange(start_idx, len); + [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)) +-- (NSInteger)accessibilityInsertionPointLineNumber +/* ---- Index mapping ---- */ + +/* Convert buffer charpos to accessibility string index. */ +- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos -+{ + { + struct window *w = self.emacsWindow; +- if (!w) +- return 0; +- NSUInteger idx = ns_ax_index_for_point(w); +- return ns_ax_line_for_index(w, idx); ++ 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) -+ 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) @@ -692,20 +1009,43 @@ index 932d209..416e5a4 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) -+ 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) @@ -798,6 +1138,8 @@ index 932d209..416e5a4 100644 + 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); @@ -847,7 +1189,7 @@ index 932d209..416e5a4 100644 + + 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: @@ -856,7 +1198,10 @@ index 932d209..416e5a4 100644 + 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); @@ -866,6 +1211,7 @@ index 932d209..416e5a4 100644 + /* 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 @@ -892,8 +1238,9 @@ index 932d209..416e5a4 100644 + + /* 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); +} @@ -925,13 +1272,16 @@ index 932d209..416e5a4 100644 + line++; + } + return line; -+} -+ -+- (NSString *)accessibilityStringForRange:(NSRange)range -+{ + } + + - (NSString *)accessibilityStringForRange:(NSRange)range + { +- NSString *text = [self accessibilityValue]; +- if (range.location + range.length > [text length]) + [self ensureTextCache]; + if (!cachedText || range.location + range.length > [cachedText length]) -+ return @""; + return @""; +- return [text substringWithRange:range]; + return [cachedText substringWithRange:range]; +} + @@ -939,13 +1289,16 @@ index 932d209..416e5a4 100644 +{ + NSString *str = [self accessibilityStringForRange:range]; + return [[[NSAttributedString alloc] initWithString:str] autorelease]; -+} -+ -+- (NSInteger)accessibilityLineForIndex:(NSInteger)index -+{ + } + + - (NSInteger)accessibilityLineForIndex:(NSInteger)index + { +- struct window *w = self.emacsWindow; +- if (!w) + [self ensureTextCache]; + if (!cachedText || index < 0) -+ return 0; + return 0; +- return ns_ax_line_for_index(w, (NSUInteger)index); + + NSUInteger idx = (NSUInteger) index; + if (idx > [cachedText length]) @@ -959,10 +1312,14 @@ index 932d209..416e5a4 100644 + line++; + } + return line; -+} -+ -+- (NSRange)accessibilityRangeForLine:(NSInteger)line -+{ + } + + - (NSRange)accessibilityRangeForLine:(NSInteger)line + { +- struct window *w = self.emacsWindow; +- if (!w) +- return NSMakeRange(0, 0); +- return ns_ax_range_for_line(w, (int)line); + [self ensureTextCache]; + if (!cachedText || line < 0) + return NSMakeRange (NSNotFound, 0); @@ -1010,14 +1367,14 @@ index 932d209..416e5a4 100644 + /* Return the range of the current line — simple approach. */ + NSInteger line = [self accessibilityLineForIndex:index]; + return [self accessibilityRangeForLine:line]; -+} -+ -+- (NSRect)accessibilityFrameForRange:(NSRange)range -+{ -+ struct window *w = self.emacsWindow; -+ EmacsView *view = self.emacsView; -+ if (!w || !view) -+ return NSZeroRect; + } + + - (NSRect)accessibilityFrameForRange:(NSRange)range +@@ -7301,14 +7832,18 @@ - (NSRect)accessibilityFrameForRange:(NSRange)range + EmacsView *view = self.emacsView; + if (!w || !view) + return NSZeroRect; +- return ns_ax_frame_for_range(w, view, range); + /* Convert ax-index range to charpos range for glyph lookup. */ + [self ensureTextCache]; + ptrdiff_t cp_start = [self charposForAccessibilityIndex:range.location]; @@ -1025,34 +1382,43 @@ index 932d209..416e5a4 100644 + range.location + range.length]; + NSRange charRange = NSMakeRange (0, (NSUInteger) (cp_end - cp_start)); + return ns_ax_frame_for_range (w, view, cp_start, charRange); -+} -+ -+- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint -+{ + } + +- + - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint + { +- /* Hit test: convert screen point to buffer character index. +- Used by VoiceOver for mouse/trackpad exploration. */ + /* Hit test: convert screen point to buffer character index. */ -+ struct window *w = self.emacsWindow; -+ EmacsView *view = self.emacsView; -+ if (!w || !view || !w->current_matrix) -+ return NSMakeRange (0, 0); -+ -+ /* Convert screen point to EmacsView coordinates. */ -+ NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint]; -+ NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; -+ + struct window *w = self.emacsWindow; + EmacsView *view = self.emacsView; + if (!w || !view || !w->current_matrix) +@@ -7318,7 +7853,7 @@ - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint + NSPoint windowPoint = [[view window] convertPointFromScreen:screenPoint]; + NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; + +- /* Convert to Emacs pixel coordinates (EmacsView is flipped). */ + /* Convert to window-relative pixel coordinates. */ -+ int x = (int) viewPoint.x - w->pixel_left; -+ int y = (int) viewPoint.y - w->pixel_top; -+ -+ if (x < 0 || y < 0 || x >= w->pixel_width || y >= w->pixel_height) -+ return NSMakeRange (0, 0); -+ -+ /* Find the glyph row at this y coordinate. */ -+ struct glyph_matrix *matrix = w->current_matrix; -+ struct glyph_row *hit_row = NULL; -+ -+ for (int i = 0; i < matrix->nrows; i++) -+ { -+ struct glyph_row *row = matrix->rows + i; + int x = (int) viewPoint.x - w->pixel_left; + int y = (int) viewPoint.y - w->pixel_top; + +@@ -7328,19 +7863,19 @@ - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint + /* Find the glyph row at this y coordinate. */ + struct glyph_matrix *matrix = w->current_matrix; + struct glyph_row *hit_row = NULL; +- int row_y = 0; + + for (int i = 0; i < matrix->nrows; i++) + { + struct glyph_row *row = matrix->rows + i; +- if (!row->enabled_p || !row->displays_text_p) +- continue; +- if (y >= row_y && y < row_y + row->visible_height) +- { +- hit_row = row; +- break; +- } +- row_y += row->visible_height; + if (!row->enabled_p || !row->displays_text_p || row->mode_line_p) + continue; + int row_top = WINDOW_TO_FRAME_PIXEL_Y (w, MAX (0, row->y)); @@ -1062,20 +1428,21 @@ index 932d209..416e5a4 100644 + hit_row = row; + break; + } -+ } -+ -+ if (!hit_row) -+ return NSMakeRange (0, 0); -+ -+ /* Find the glyph at this x coordinate within the row. */ -+ struct glyph *glyph = hit_row->glyphs[TEXT_AREA]; -+ struct glyph *end = glyph + hit_row->used[TEXT_AREA]; -+ int glyph_x = 0; -+ ptrdiff_t best_charpos = MATRIX_ROW_START_CHARPOS (hit_row); -+ -+ for (; glyph < end; glyph++) -+ { -+ if (glyph->type == CHAR_GLYPH && glyph->charpos > 0) + } + + if (!hit_row) +@@ -7355,49 +7890,32 @@ - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint + for (; glyph < end; glyph++) + { + if (glyph->type == CHAR_GLYPH && glyph->charpos > 0) +- { +- if (x >= glyph_x && x < glyph_x + glyph->pixel_width) +- { +- best_charpos = glyph->charpos; +- break; +- } +- best_charpos = glyph->charpos; +- } + { + if (x >= glyph_x && x < glyph_x + glyph->pixel_width) + { @@ -1084,31 +1451,59 @@ index 932d209..416e5a4 100644 + } + best_charpos = glyph->charpos; + } -+ glyph_x += glyph->pixel_width; -+ } -+ + glyph_x += glyph->pixel_width; + } + +- /* Convert buffer charpos to accessibility index. */ +- struct buffer *b = XBUFFER (w->contents); +- if (!b) +- return NSMakeRange (0, 0); +- +- ptrdiff_t idx = best_charpos - BUF_BEGV (b); +- if (idx < 0) idx = 0; +- +- return NSMakeRange ((NSUInteger) idx, 1); + /* Convert buffer charpos to accessibility index via mapping. */ + [self ensureTextCache]; + NSUInteger ax_idx = [self accessibilityIndexForCharpos:best_charpos]; + if (cachedText && ax_idx > [cachedText length]) + ax_idx = [cachedText length]; + return NSMakeRange (ax_idx, 1); -+} -+ -+- (NSRange)accessibilityVisibleCharacterRange -+{ + } + + - (NSRange)accessibilityVisibleCharacterRange + { +- NSString *text = [self accessibilityValue]; +- return NSMakeRange(0, [text length]); +-} +- +-- (BOOL)isAccessibilityFocused +-{ +- struct window *w = self.emacsWindow; +- if (!w) +- return NO; +- EmacsView *view = self.emacsView; +- if (!view || !view->emacsframe) +- return NO; +- struct frame *f = view->emacsframe; +- return (w == XWINDOW(f->selected_window)); +-} +- +-- (id)accessibilityParent +-{ +- return NSAccessibilityUnignoredAncestor (self.emacsView); + /* Return the full cached text range. VoiceOver interprets the + visible range boundary as end-of-text, so we must expose the + entire buffer to avoid premature "end of text" announcements. */ + [self ensureTextCache]; + return NSMakeRange (0, cachedText ? [cachedText length] : 0); -+} -+ -+- (NSRect)accessibilityFrame -+{ -+ struct window *w = self.emacsWindow; -+ if (!w) -+ return NSZeroRect; + } + + - (NSRect)accessibilityFrame +@@ -7405,50 +7923,523 @@ - (NSRect)accessibilityFrame + struct window *w = self.emacsWindow; + if (!w) + return NSZeroRect; + + /* Subtract mode line height so the buffer element does not overlap it. */ + int text_h = w->pixel_height; @@ -1124,32 +1519,42 @@ index 932d209..416e5a4 100644 + } + } + } -+ return [self screenRectFromEmacsX:w->pixel_left + return [self screenRectFromEmacsX:w->pixel_left +- y:w->pixel_top +- width:w->pixel_width +- height:w->pixel_height]; + y:w->pixel_top + width:w->pixel_width + height:text_h]; -+} -+ -+/* ---- Notification dispatch ---- */ -+ + } + + /* ---- Notification dispatch ---- */ + +-- (void)postAccessibilityUpdatesForWindow:(struct window *)w +- frame:(struct frame *)f +- (void)postAccessibilityNotificationsForFrame:(struct frame *)f -+{ + { +- if (!w || !WINDOW_LEAF_P(w)) + struct window *w = self.emacsWindow; + if (!w || !WINDOW_LEAF_P (w)) -+ return; -+ + return; + +- struct buffer *b = XBUFFER(w->contents); + struct buffer *b = XBUFFER (w->contents); -+ if (!b) -+ return; -+ + if (!b) + return; + +- ptrdiff_t modiff = BUF_MODIFF(b); +- ptrdiff_t point = BUF_PT(b); + ptrdiff_t modiff = BUF_MODIFF (b); + ptrdiff_t point = BUF_PT (b); + BOOL markActive = !NILP (BVAR (b, mark_active)); -+ + +- /* Text content changed? */ + /* --- Text changed → typing echo --- -+ kAXTextStateChangeTypeEdit = 0, kAXTextEditTypeTyping = 3. */ -+ if (modiff != self.cachedModiff) -+ { ++ WebKit AXObjectCacheMac fallback enum: Edit = 1, Typing = 3. */ + if (modiff != self.cachedModiff) + { + /* Capture changed char before invalidating cache. */ + NSString *changedChar = @""; + if (point > self.cachedPoint @@ -1171,85 +1576,147 @@ index 932d209..416e5a4 100644 + [self invalidateTextCache]; + } + -+ self.cachedModiff = modiff; + self.cachedModiff = modiff; +- NSAccessibilityPostNotification(self, +- NSAccessibilityValueChangedNotification); + /* Update cachedPoint here so the selection-move branch below + does NOT fire for point changes caused by edits. WebKit and + Chromium never send both ValueChanged and SelectedTextChanged + for the same user action — they are mutually exclusive. */ + self.cachedPoint = point; -+ + +- /* Rich typing echo for VoiceOver. */ + NSDictionary *change = @{ -+ @"AXTextEditType": @3, ++ @"AXTextEditType": @(ns_ax_text_edit_type_typing), + @"AXTextChangeValue": changedChar, + @"AXTextChangeValueLength": @([changedChar length]) + }; -+ NSDictionary *userInfo = @{ -+ @"AXTextStateChangeType": @0, -+ @"AXTextChangeValues": @[change] -+ }; + NSDictionary *userInfo = @{ +- @"AXTextStateChangeType" : @1, /* AXTextStateChangeTypeEdit */ +- @"AXTextEditType" : @0 /* kAXTextEditTypeTyping */ ++ @"AXTextStateChangeType": @(ns_ax_text_state_change_edit), ++ @"AXTextChangeValues": @[change], ++ @"AXTextChangeElement": self + }; +- NSAccessibilityPostNotificationWithUserInfo( +- self, NSAccessibilityValueChangedNotification, userInfo); + NSAccessibilityPostNotificationWithUserInfo ( + self, NSAccessibilityValueChangedNotification, userInfo); -+ } -+ + } + +- /* Cursor moved? */ +- if (point != self.cachedPoint) + /* --- 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. */ + else if (point != self.cachedPoint || markActive != self.cachedMarkActive) -+ { + { + ptrdiff_t oldPoint = self.cachedPoint; -+ self.cachedPoint = point; + self.cachedPoint = point; +- NSAccessibilityPostNotification(self, +- NSAccessibilitySelectedTextChangedNotification); + 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; ++ ++ int ctrlNP = 0; ++ bool isCtrlNP = ns_ax_event_is_ctrl_n_or_p (&ctrlNP); + + /* Compute granularity from movement distance. -+ Check if we crossed a newline → line movement (3). -+ Otherwise single char (1) or unknown (0). */ -+ NSInteger granularity = 0; ++ Prefer robust line-range comparison for vertical movement, ++ otherwise single char (1) or unknown (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 + { -+ /* 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 = ns_ax_text_selection_granularity_line; /* 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": @1, ++ @"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), + @"AXTextSelectionDirection": @(direction), -+ @"AXTextSelectionGranularity": @(granularity) ++ @"AXTextSelectionGranularity": @(granularity), ++ @"AXTextChangeElement": self + }; + 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 @@ -1262,6 +1729,8 @@ index 932d209..416e5a4 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) @@ -1318,37 +1787,53 @@ index 932d209..416e5a4 100644 + } + } + -+ /* 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); + 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)]; ++ announceText = ns_ax_completion_text_for_span (self, b, ++ ov_start, ++ ov_end, ++ cachedText); ++ currentOverlayStart = ov_start; ++ currentOverlayEnd = ov_end; + } + 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); + @@ -1375,20 +1860,128 @@ index 932d209..416e5a4 100644 + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([announceText length] > 0) + { -+ NSDictionary *annInfo = @{ -+ NSAccessibilityAnnouncementKey: announceText, -+ NSAccessibilityPriorityKey: -+ @(NSAccessibilityPriorityHigh) -+ }; -+ NSAccessibilityPostNotificationWithUserInfo ( -+ NSApp, -+ NSAccessibilityAnnouncementRequestedNotification, -+ annInfo); ++ 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; ++ } ++ } ++ } ++ } +} + +@end @@ -1447,277 +2040,20 @@ index 932d209..416e5a4 100644 + width:w->pixel_width + height:row->visible_height]; + } -+ } + } + return NSZeroRect; -+} -+ -+@end -+ -+#endif /* NS_IMPL_COCOA */ -+ -+ -+/* ========================================================================== -+ -+ EmacsView implementation -+ -+ ========================================================================== */ -+ -+ -+@implementation EmacsView -+ -+- (void)windowDidEndLiveResize:(NSNotification *)notification -+{ -+ [self updateFramePosition]; -+} -+ -+/* Needed to inform when window closed from lisp. */ -+- (void) setWindowClosing: (BOOL)closing -+{ -+ NSTRACE ("[EmacsView setWindowClosing:%d]", closing); -+ -+ windowClosing = closing; -+} -+ -+ -+- (void)dealloc -+{ -+ NSTRACE ("[EmacsView dealloc]"); -+ -+ /* Clear the view resize notification. */ -+ [[NSNotificationCenter defaultCenter] -+ removeObserver:self -+ name:NSViewFrameDidChangeNotification -+ object:nil]; -+ -+ if (fs_state == FULLSCREEN_BOTH) -+ [nonfs_window release]; -+ -+#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 -+ /* Release layer and menu */ -+ EmacsLayer *layer = (EmacsLayer *)[self layer]; -+ [layer release]; -+#endif -+ + } + + @end +@@ -7498,6 +8489,7 @@ - (void)dealloc + [layer release]; + #endif + + [accessibilityElements release]; -+ [[self menu] release]; -+ [super dealloc]; -+} -+ -+ -+/* Called on font panel selection. */ -+- (void) changeFont: (id) sender -+{ -+ struct font *font = FRAME_OUTPUT_DATA (emacsframe)->font; -+ NSFont *nsfont; -+ -+#ifdef NS_IMPL_GNUSTEP -+ nsfont = ((struct nsfont_info *) font)->nsfont; -+#else -+ nsfont = (NSFont *) macfont_get_nsctfont (font); -+#endif -+ -+ if (!font_panel_active) -+ return; -+ -+ if (font_panel_result) -+ [font_panel_result release]; -+ -+ font_panel_result = (NSFont *) [sender convertFont: nsfont]; -+ -+ if (font_panel_result) -+ [font_panel_result retain]; -+ -+#ifndef NS_IMPL_COCOA -+ font_panel_active = NO; -+ [NSApp stop: self]; -+#endif -+} -+ -+#ifdef NS_IMPL_COCOA -+- (void) noteUserSelectedFont -+{ -+ font_panel_active = NO; -+ -+ /* If no font was previously selected, use the currently selected -+ font. */ -+ -+ if (!font_panel_result && FRAME_FONT (emacsframe)) -+ { -+ font_panel_result -+ = macfont_get_nsctfont (FRAME_FONT (emacsframe)); -+ -+ if (font_panel_result) -+ [font_panel_result retain]; -+ } -+ -+ [NSApp stop: self]; -+} -+ -+- (void) noteUserCancelledSelection -+{ -+ font_panel_active = NO; -+ -+ if (font_panel_result) -+ [font_panel_result release]; -+ font_panel_result = nil; -+ -+ [NSApp stop: self]; -+} -+#endif -+ -+- (Lisp_Object) showFontPanel -+{ -+ 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 -+ -+#ifdef NS_IMPL_GNUSTEP -+ nsfont = ((struct nsfont_info *) font)->nsfont; -+#else -+ nsfont = (NSFont *) macfont_get_nsctfont (font); -+#endif -+ -+#ifdef NS_IMPL_COCOA -+ buttons -+ = ns_create_font_panel_buttons (self, -+ @selector (noteUserSelectedFont), -+ @selector (noteUserCancelledSelection)); -+ [[fm fontPanel: YES] setAccessoryView: buttons]; -+ [buttons release]; -+#endif -+ -+ [fm setSelectedFont: nsfont isMultiple: NO]; -+ [fm orderFrontFontPanel: NSApp]; -+ -+ font_panel_active = YES; -+ timeout = make_timespec (0, 100000000); -+ -+ 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 (font_panel_result) -+ [font_panel_result autorelease]; -+ -+#ifdef NS_IMPL_COCOA -+ if (!canceled) -+ font_panel_result = nil; -+#endif -+ -+ result = font_panel_result; -+ font_panel_result = nil; -+ -+ [[fm fontPanel: YES] setIsVisible: NO]; -+ font_panel_active = NO; -+ -+ if (result) -+ return ns_font_desc_to_font_spec ([result fontDescriptor], -+ result); -+ -+ return Qnil; -+} -+ -+- (BOOL)acceptsFirstResponder -+{ -+ NSTRACE ("[EmacsView acceptsFirstResponder]"); -+ return YES; -+} -+ -+/* Tell NS we want to accept clicks that activate the window */ -+- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent -+{ -+ NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld", -+ [theEvent type], [theEvent clickCount]); -+ return ns_click_through; -+} -+- (void)resetCursorRects -+{ -+ NSRect visible = [self visibleRect]; -+ NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe); -+ NSTRACE ("[EmacsView resetCursorRects]"); -+ -+ if (currentCursor == nil) -+ currentCursor = [NSCursor arrowCursor]; -+ -+ if (!NSIsEmptyRect (visible)) -+ [self addCursorRect: visible cursor: currentCursor]; -+ -+#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 -+} -+ -+ -+ -+/*****************************************************************************/ -+/* Keyboard handling. */ -+#define NS_KEYLOG 0 -+ -+- (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) + [[self menu] release]; + [super dealloc]; + } +@@ -8846,6 +9838,28 @@ - (void)windowDidBecomeKey /* for direct calls */ XSETFRAME (event.frame_or_window, emacsframe); kbd_buffer_store_event (&event); ns_send_appdefined (-1); // Kick main loop @@ -1733,7 +2069,8 @@ index 932d209..416e5a4 100644 + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); -+ NSDictionary *info = @{@"AXTextStateChangeType": @1}; ++ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": focused}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } @@ -1745,26 +2082,22 @@ index 932d209..416e5a4 100644 } -@@ -9474,6 +10650,297 @@ ns_in_echo_area (void) - return fs_state; - } +@@ -10089,7 +11103,8 @@ - (int) fullscreenState -+#ifdef NS_IMPL_COCOA -+ -+/* ---- Accessibility: walk the Emacs window tree ---- */ -+ -+static void -+ns_ax_collect_windows (Lisp_Object window, EmacsView *view, + static void + ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +- NSMutableArray *elements) + NSMutableArray *elements, + NSDictionary *existing) -+{ -+ if (NILP (window)) -+ return; -+ -+ struct window *w = XWINDOW (window); -+ -+ if (WINDOW_LEAF_P (w)) -+ { + { + if (NILP (window)) + return; +@@ -10098,32 +11113,47 @@ - (int) fullscreenState + + if (WINDOW_LEAF_P (w)) + { +- if (MINI_WINDOW_P (w)) +- return; /* Skip minibuffer for MVP. */ + /* Buffer element — reuse existing if available. */ + EmacsAccessibilityBuffer *elem + = [existing objectForKey:[NSValue valueWithPointer:w]]; @@ -1772,7 +2105,9 @@ index 932d209..416e5a4 100644 + { + elem = [[EmacsAccessibilityBuffer alloc] init]; + elem.emacsView = view; -+ + +- EmacsAccessibilityBuffer *elem = [[EmacsAccessibilityBuffer alloc] init]; +- elem.emacsView = view; + /* Initialize cached state to -1 to force first notification. */ + elem.cachedModiff = -1; + elem.cachedPoint = -1; @@ -1782,8 +2117,17 @@ index 932d209..416e5a4 100644 + { + [elem retain]; + } -+ elem.emacsWindow = w; -+ [elements addObject:elem]; + elem.emacsWindow = w; +- +- /* Initialize cached state to trigger first notification. */ +- struct buffer *b = XBUFFER (w->contents); +- if (b) +- { +- elem.cachedModiff = BUF_MODIFF (b); +- elem.cachedPoint = BUF_PT (b); +- } +- + [elements addObject:elem]; + [elem release]; + + /* Mode line element (skip for minibuffer). */ @@ -1796,24 +2140,28 @@ index 932d209..416e5a4 100644 + [elements addObject:ml]; + [ml release]; + } -+ } -+ else -+ { -+ /* Internal (combination) window — recurse into children. */ -+ Lisp_Object child = w->contents; -+ while (!NILP (child)) + } + else + { + /* Internal (combination) window — recurse into children. */ + Lisp_Object child = w->contents; + while (!NILP (child)) +- { +- ns_ax_collect_windows (child, view, elements); +- child = XWINDOW (child)->next; +- } + { + ns_ax_collect_windows (child, view, elements, existing); + child = XWINDOW (child)->next; + } -+ } -+} -+ -+- (void)rebuildAccessibilityTree -+{ -+ if (!emacsframe) -+ return; -+ + } + } + +@@ -10132,10 +11162,39 @@ - (void)rebuildAccessibilityTree + if (!emacsframe) + return; + +- NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:4]; + /* Build map of existing elements by window pointer for reuse. */ + NSMutableDictionary *existing = [NSMutableDictionary dictionary]; + if (accessibilityElements) @@ -1831,7 +2179,9 @@ index 932d209..416e5a4 100644 + NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:8]; + + /* Collect from main window tree. */ -+ Lisp_Object root = FRAME_ROOT_WINDOW (emacsframe); + Lisp_Object root = FRAME_ROOT_WINDOW (emacsframe); +- ns_ax_collect_windows (root, self, newElements); +- accessibilityElements = newElements; + ns_ax_collect_windows (root, self, newElements, existing); + + /* Include minibuffer. */ @@ -1847,59 +2197,44 @@ index 932d209..416e5a4 100644 +- (void)invalidateAccessibilityTree +{ + accessibilityTreeValid = NO; -+} -+ -+- (NSAccessibilityRole)accessibilityRole -+{ -+ return NSAccessibilityGroupRole; -+} -+ -+- (NSString *)accessibilityLabel -+{ -+ return @"Emacs"; -+} -+ -+- (BOOL)isAccessibilityElement -+{ -+ return YES; -+} -+ -+- (NSArray *)accessibilityChildren -+{ + } + + - (NSAccessibilityRole)accessibilityRole +@@ -10155,7 +11214,7 @@ - (BOOL)isAccessibilityElement + + - (NSArray *)accessibilityChildren + { +- if (!accessibilityElements || [accessibilityElements count] == 0) + if (!accessibilityElements || !accessibilityTreeValid) -+ [self rebuildAccessibilityTree]; -+ return accessibilityElements; -+} -+ -+- (id)accessibilityFocusedUIElement -+{ -+ if (!emacsframe) -+ return self; -+ + [self rebuildAccessibilityTree]; + return accessibilityElements; + } +@@ -10165,16 +11224,15 @@ - (id)accessibilityFocusedUIElement + if (!emacsframe) + return self; + +- /* Ensure tree exists (lazy init); avoid redundant rebuild since +- postAccessibilityUpdates already rebuilds each cycle. */ +- if (!accessibilityElements || [accessibilityElements count] == 0) + if (!accessibilityElements || !accessibilityTreeValid) -+ [self rebuildAccessibilityTree]; -+ -+ struct window *sel = XWINDOW (emacsframe->selected_window); + [self rebuildAccessibilityTree]; + + struct window *sel = XWINDOW (emacsframe->selected_window); +- for (EmacsAccessibilityBuffer *elem in accessibilityElements) + for (EmacsAccessibilityElement *elem in accessibilityElements) -+ { + { +- if (elem.emacsWindow == sel) +- return elem; + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]] + && elem.emacsWindow == sel) + return elem; -+ } -+ return self; -+} -+ -+/* Called from ns_update_end to post AX notifications. -+ -+ Important: post notifications BEFORE rebuilding the tree. -+ The existing elements carry cached state (modiff, point) from the -+ previous redisplay cycle. Rebuilding first would create fresh -+ elements with current values, making change detection impossible. */ -+- (void)postAccessibilityUpdates -+{ -+ if (!emacsframe) -+ return; -+ + } + return self; + } +@@ -10190,32 +11248,143 @@ - (void)postAccessibilityUpdates + if (!emacsframe) + return; + + /* Re-entrance guard: VoiceOver callbacks during notification posting + can trigger redisplay, which calls ns_update_end, which calls us + again. Prevent infinite recursion. */ @@ -1936,11 +2271,16 @@ index 932d209..416e5a4 100644 + return; + } + -+ /* Post per-buffer notifications using EXISTING elements that have + /* Post per-buffer notifications using EXISTING elements that have +- cached state from the previous cycle. */ +- for (EmacsAccessibilityBuffer *elem in accessibilityElements) + cached state from the previous cycle. Validate each window + pointer before use. */ + for (EmacsAccessibilityElement *elem in accessibilityElements) -+ { + { +- struct window *w = elem.emacsWindow; +- if (w && WINDOW_LEAF_P (w)) +- [elem postAccessibilityUpdatesForWindow:w frame:emacsframe]; + if ([elem isKindOfClass:[EmacsAccessibilityBuffer class]]) + { + struct window *w = elem.emacsWindow; @@ -1949,12 +2289,14 @@ index 932d209..416e5a4 100644 + [(EmacsAccessibilityBuffer *) elem + postAccessibilityNotificationsForFrame:emacsframe]; + } -+ } -+ + } + +- /* Check for window switch (C-x o) before rebuild. */ + /* Check for window switch (C-x o). */ -+ Lisp_Object curSel = emacsframe->selected_window; -+ BOOL windowSwitched = !EQ (curSel, lastSelectedWindow); -+ if (windowSwitched) + Lisp_Object curSel = emacsframe->selected_window; + BOOL windowSwitched = !EQ (curSel, lastSelectedWindow); + if (windowSwitched) +- lastSelectedWindow = curSel; + { + lastSelectedWindow = curSel; + id focused = [self accessibilityFocusedUIElement]; @@ -1963,7 +2305,8 @@ index 932d209..416e5a4 100644 + { + NSAccessibilityPostNotification (focused, + NSAccessibilityFocusedUIElementChangedNotification); -+ NSDictionary *info = @{@"AXTextStateChangeType": @1}; ++ NSDictionary *info = @{@"AXTextStateChangeType": @(ns_ax_text_state_change_selection_move), ++ @"AXTextChangeElement": focused}; + NSAccessibilityPostNotificationWithUserInfo (focused, + NSAccessibilitySelectedTextChangedNotification, info); + } @@ -1974,9 +2317,13 @@ index 932d209..416e5a4 100644 + + accessibilityUpdating = NO; +} -+ + +- /* Now rebuild tree to pick up window configuration changes. */ +- [self rebuildAccessibilityTree]; +/* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- -+ + +- /* Post focus change AFTER rebuild so the new element exists. */ +- if (windowSwitched) + accessibilityFrame returns the VIEW's frame (standard behavior). + The cursor location is exposed through accessibilityBoundsForRange: + which AT tools query using the selectedTextRange. */ @@ -2022,7 +2369,11 @@ index 932d209..416e5a4 100644 +{ + if ([attribute isEqualToString: + NSAccessibilityBoundsForRangeParameterizedAttribute]) -+ { + { +- id focused = [self accessibilityFocusedUIElement]; +- if (focused && focused != self) +- NSAccessibilityPostNotification (focused, +- NSAccessibilityFocusedUIElementChangedNotification); + NSRange range = [(NSValue *) parameter rangeValue]; + return [NSValue valueWithRect: + [self accessibilityBoundsForRange:range]]; @@ -2033,1540 +2384,12 @@ index 932d209..416e5a4 100644 + { + NSRange range = [(NSValue *) parameter rangeValue]; + return [self accessibilityStringForRange:range]; -+ } + } + + return [super accessibilityAttributeValue:attribute forParameter:parameter]; -+} -+ -+#endif /* NS_IMPL_COCOA */ -+ - @end /* EmacsView */ - - -@@ -9941,6 +11408,14 @@ nswindow_orderedIndex_sort (id w1, id w2, void *c) - - return [super accessibilityAttributeValue:attribute]; } -+ -+- (id)accessibilityFocusedUIElement -+{ -+ EmacsView *view = (EmacsView *)[self delegate]; -+ if (view && [view respondsToSelector:@selector(accessibilityFocusedUIElement)]) -+ return [view accessibilityFocusedUIElement]; -+ return self; -+} + #endif /* NS_IMPL_COCOA */ - - /* Constrain size and placement of a frame. --- -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