From 6e907a1000a8b138976d6a906e40449fdf1a61c5 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 Subject: [PATCH 1/2] ns: announce overlay completion candidates for VoiceOver Completion frameworks such as Vertico, Ivy, and Icomplete render candidates via overlay before-string/after-string properties rather than buffer text. Without this patch, VoiceOver cannot read overlay-based completion UIs. Identify the selected candidate by scanning overlay strings for a face whose symbol name contains "current", "selected", or "selection" --- this matches vertico-current, icomplete-selected-match, ivy-current-match, company-tooltip-selection, and similar framework faces without hard-coding any specific name. Key implementation details: - The overlay detection branch runs independently (if, not else-if) of the text-change branch, because Vertico bumps both BUF_MODIFF (via text property changes in vertico--prompt-selection) and BUF_OVERLAY_MODIFF (via overlay-put) in the same command cycle. - Use BUF_CHARS_MODIFF to gate ValueChanged notifications, since text property changes bump BUF_MODIFF but not BUF_CHARS_MODIFF. - Remove BUF_OVERLAY_MODIFF from ensureTextCache validity checks to prevent a race condition where VoiceOver AX queries silently consume the overlay change before the notification dispatch runs. - Announce via AnnouncementRequested to NSApp with High priority. Do not post SelectedTextChanged (that reads the AX text at cursor position, which is the minibuffer input, not the candidate). - Zoom tracking: store the selected candidate's rect (at the text area left edge, computed from FRAME_LINE_HEIGHT) in overlayZoomRect. ns_draw_window_cursor checks overlayZoomActive and uses the stored rect instead of the text cursor rect, keeping Zoom focused on the candidate line start. The flag is cleared when the user types (BUF_CHARS_MODIFF changes) or when no candidate is found (minibuffer exit, C-g). * src/nsterm.h (EmacsView): Add overlayZoomActive, overlayZoomRect. (EmacsAccessibilityBuffer): Add cachedCharsModiff. * src/nsterm.m (ns_ax_face_is_selected): New predicate. Match "current", "selected", and "selection" in face symbol names. (ns_ax_selected_overlay_text): New function. (ns_draw_window_cursor): Use overlayZoomRect when active. (EmacsAccessibilityBuffer ensureTextCache): Remove overlay_modiff. (EmacsAccessibilityBuffer postAccessibilityNotificationsForFrame:): Independent overlay branch, BUF_CHARS_MODIFF gating, candidate announcement with overlay Zoom rect storage. --- src/nsterm.h | 3 + src/nsterm.m | 331 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 298 insertions(+), 36 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 51c30ca..5c15639 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -507,6 +507,7 @@ typedef struct ns_ax_visible_run @property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; @property (nonatomic, assign) ptrdiff_t cachedTextStart; @property (nonatomic, assign) ptrdiff_t cachedModiff; +@property (nonatomic, assign) ptrdiff_t cachedCharsModiff; @property (nonatomic, assign) ptrdiff_t cachedPoint; @property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement; @@ -591,6 +592,8 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType) BOOL accessibilityUpdating; @public /* Accessed by ns_draw_phys_cursor (C function). */ NSRect lastAccessibilityCursorRect; + BOOL overlayZoomActive; + NSRect overlayZoomRect; #endif BOOL font_panel_active; NSFont *font_panel_result; diff --git a/src/nsterm.m b/src/nsterm.m index 1780194..143e784 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -3258,7 +3258,12 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 if (UAZoomEnabled ()) { - NSRect windowRect = [view convertRect:r toView:nil]; + /* When overlay completion is active (e.g. Vertico), + focus Zoom on the selected candidate row instead + of the text cursor. */ + NSRect zoomSrc = view->overlayZoomActive + ? view->overlayZoomRect : r; + NSRect windowRect = [view convertRect:zoomSrc toView:nil]; NSRect screenRect = [[view window] convertRectToScreen:windowRect]; CGRect cgRect = NSRectToCGRect (screenRect); @@ -6915,11 +6920,156 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) are truncated for accessibility purposes. */ #define NS_AX_TEXT_CAP 100000 +/* Return true if FACE is or contains a face symbol whose name + includes "current" or "selected", indicating a highlighted + completion candidate. Works for vertico-current, + icomplete-selected-match, ivy-current-match, etc. */ +static bool +ns_ax_face_is_selected (Lisp_Object face) +{ + if (SYMBOLP (face) && !NILP (face)) + { + const char *name = SSDATA (SYMBOL_NAME (face)); + /* Substring match is intentionally broad --- it catches + vertico-current, icomplete-selected-match, ivy-current-match, + company-tooltip-selection, and similar. False positives are + harmless since this runs only on overlay strings during + completion. */ + if (strstr (name, "current") || strstr (name, "selected") + || strstr (name, "selection")) + return true; + } + if (CONSP (face)) + { + for (Lisp_Object tail = face; CONSP (tail); tail = XCDR (tail)) + if (ns_ax_face_is_selected (XCAR (tail))) + return true; + } + return false; +} + +/* Extract the currently selected candidate text from overlay display + strings. Completion frameworks render candidates as overlay + before-string/after-string and highlight the current candidate + with a face whose name contains "current" or "selected" + (e.g. vertico-current, icomplete-selected-match, ivy-current-match). + + Scan all overlays in the buffer region [BEG, END), find the line + whose face matches the selection heuristic, and return it (already + trimmed of surrounding whitespace). + + Also set *OUT_LINE_INDEX to the 0-based visual line index of the + selected candidate (for Zoom positioning), counting only non-trivial + lines. Set to -1 if not found. + + Returns nil if no selected candidate is found. */ +static NSString * +ns_ax_selected_overlay_text (struct buffer *b, + ptrdiff_t beg, ptrdiff_t end, + int *out_line_index) +{ + *out_line_index = -1; + + Lisp_Object ov_list = Foverlays_in (make_fixnum (beg), + make_fixnum (end)); + + for (Lisp_Object tail = ov_list; CONSP (tail); tail = XCDR (tail)) + { + Lisp_Object ov = XCAR (tail); + Lisp_Object strings[2]; + strings[0] = Foverlay_get (ov, intern_c_string ("before-string")); + strings[1] = Foverlay_get (ov, intern_c_string ("after-string")); + + for (int s = 0; s < 2; s++) + { + if (!STRINGP (strings[s])) + continue; + + Lisp_Object str = strings[s]; + ptrdiff_t slen = SCHARS (str); + if (slen == 0) + continue; + + /* Scan for newline positions using SDATA for efficiency. + The data pointer is used only in this loop, before any + Lisp calls (Fget_text_property etc.) that could trigger + GC and relocate string data. */ + const unsigned char *data = SDATA (str); + ptrdiff_t byte_len = SBYTES (str); + /* 512 lines is sufficient for any completion UI; + vertico-count defaults to 10. */ + ptrdiff_t line_starts[512]; + ptrdiff_t line_ends[512]; + int nlines = 0; + ptrdiff_t char_pos = 0, byte_pos = 0, lstart = 0; + + while (byte_pos < byte_len && nlines < 512) + { + if (data[byte_pos] == '\n') + { + if (char_pos > lstart) + { + line_starts[nlines] = lstart; + line_ends[nlines] = char_pos; + nlines++; + } + lstart = char_pos + 1; + } + if (STRING_MULTIBYTE (str)) + byte_pos += BYTES_BY_CHAR_HEAD (data[byte_pos]); + else + byte_pos++; + char_pos++; + } + if (char_pos > lstart && nlines < 512) + { + line_starts[nlines] = lstart; + line_ends[nlines] = char_pos; + nlines++; + } + + /* Find the line whose face indicates selection. Track + visual line index for Zoom (skip whitespace-only lines + like Vertico's leading cursor-space). */ + int candidate_idx = 0; + for (int li = 0; li < nlines; li++) + { + Lisp_Object face + = Fget_text_property (make_fixnum (line_starts[li]), + Qface, str); + if (ns_ax_face_is_selected (face)) + { + Lisp_Object line + = Fsubstring_no_properties ( + str, + make_fixnum (line_starts[li]), + make_fixnum (line_ends[li])); + NSString *text = [NSString stringWithLispString:line]; + text = [text stringByTrimmingCharactersInSet: + [NSCharacterSet + whitespaceAndNewlineCharacterSet]]; + if ([text length] > 0) + { + *out_line_index = candidate_idx; + return text; + } + } + + /* Count non-trivial lines as candidates for Zoom. */ + if (line_ends[li] - line_starts[li] > 1) + candidate_idx++; + } + } + } + + return nil; +} + + /* 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(). */ - static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) @@ -6996,7 +7146,7 @@ ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, /* Extract this visible run's text. Use Fbuffer_substring_no_properties which correctly handles the - buffer gap — raw BUF_BYTE_ADDRESS reads across the gap would + buffer gap --- raw BUF_BYTE_ADDRESS reads across the gap would include garbage bytes when the run spans the gap position. */ Lisp_Object lstr = Fbuffer_substring_no_properties ( make_fixnum (pos), make_fixnum (run_end)); @@ -7077,7 +7227,7 @@ ns_ax_frame_for_range (struct window *w, EmacsView *view, return NSZeroRect; /* charpos_start and charpos_len are already in buffer charpos - space — the caller maps AX string indices through + space --- the caller maps AX string indices through charposForAccessibilityIndex which handles invisible text. */ ptrdiff_t cp_start = charpos_start; ptrdiff_t cp_end = cp_start + charpos_len; @@ -7556,6 +7706,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; +@synthesize cachedCharsModiff; @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; @@ -7596,7 +7747,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, NSTRACE ("EmacsAccessibilityBuffer ensureTextCache"); /* This method is only called from the main thread (AX getters dispatch_sync to main first). Reads of cachedText/cachedTextModiff - below are therefore safe without @synchronized — only the + below are therefore safe without @synchronized --- only the write section at the end needs synchronization to protect against concurrent reads from AX server thread. */ eassert ([NSThread isMainThread]); @@ -7609,16 +7760,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, return; ptrdiff_t modiff = BUF_MODIFF (b); - ptrdiff_t overlay_modiff = BUF_OVERLAY_MODIFF (b); ptrdiff_t pt = BUF_PT (b); NSUInteger textLen = cachedText ? [cachedText length] : 0; - /* Track both BUF_MODIFF and BUF_OVERLAY_MODIFF. Overlay-only - changes (e.g., timer-based completion highlight move without - text edit) bump overlay_modiff but not modiff. Also detect - narrowing/widening which changes BUF_BEGV without bumping - either modiff counter. */ + /* Cache validity: track BUF_MODIFF and buffer narrowing. + Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not + included in the cached AX text (it is handled separately via + explicit announcements). Including overlay_modiff would + silently update cachedOverlayModiff and prevent the + notification dispatch from detecting overlay changes. */ if (cachedText && cachedTextModiff == modiff - && cachedOverlayModiff == overlay_modiff && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 @@ -7635,7 +7785,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, [cachedText release]; cachedText = [text retain]; cachedTextModiff = modiff; - cachedOverlayModiff = overlay_modiff; cachedTextStart = start; if (visibleRuns) @@ -7661,7 +7810,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* Binary search: runs are sorted by charpos (ascending). Find the run whose [charpos, charpos+length) range contains the target, or the nearest run after an invisible gap. O(log n) instead of - O(n) — matters for org-mode with many folded sections. */ + O(n) --- matters for org-mode with many folded sections. */ NSUInteger lo = 0, hi = visibleRunCount; while (lo < hi) { @@ -7674,7 +7823,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, else { /* Found: charpos is inside this run. Compute UTF-16 delta - directly from cachedText — no Lisp calls needed. */ + directly from cachedText --- no Lisp calls needed. */ NSUInteger chars_in = (NSUInteger)(charpos - r->charpos); if (chars_in == 0 || !cachedText) return r->ax_start; @@ -7699,10 +7848,10 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* Convert accessibility string index to buffer charpos. Safe to call from any thread: uses only cachedText (NSString) and - visibleRuns — no Lisp calls. */ + visibleRuns --- no Lisp calls. */ - (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx { - /* May be called from AX server thread — synchronize. */ + /* May be called from AX server thread --- synchronize. */ @synchronized (self) { if (visibleRunCount == 0) @@ -7736,7 +7885,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, return cp; } } - /* Past end — return last charpos. */ + /* Past end --- return last charpos. */ if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; @@ -7758,7 +7907,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, deadlocking the AX server thread. This is prevented by: 1. validWindow checks WINDOW_LIVE_P and BUFFERP before every - Lisp access — the window and buffer are verified live. + Lisp access --- the window and buffer are verified live. 2. All dispatch_sync blocks run on the main thread where no concurrent Lisp code can modify state between checks. 3. block_input prevents timer events and process output from @@ -8166,7 +8315,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, if (idx > [cachedText length]) idx = [cachedText length]; - /* Count lines by iterating lineRangeForRange — O(lines). */ + /* Count lines by iterating lineRangeForRange --- O(lines). */ NSInteger line = 0; NSUInteger scan = 0; NSUInteger len = [cachedText length]; @@ -8422,7 +8571,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* =================================================================== - EmacsAccessibilityBuffer (Notifications) — AX event dispatch + EmacsAccessibilityBuffer (Notifications) --- AX event dispatch These methods notify VoiceOver of text and selection changes. Called from the redisplay cycle (postAccessibilityUpdates). @@ -8437,7 +8586,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, if (point > self.cachedPoint && point - self.cachedPoint == 1) { - /* Single char inserted — refresh cache and grab it. */ + /* Single char inserted --- refresh cache and grab it. */ [self invalidateTextCache]; [self ensureTextCache]; if (cachedText) @@ -8456,7 +8605,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* Update cachedPoint here so the selection-move branch does NOT fire for point changes caused by edits. WebKit and Chromium never send both ValueChanged and SelectedTextChanged for the - same user action — they are mutually exclusive. */ + same user action --- they are mutually exclusive. */ self.cachedPoint = point; NSDictionary *change = @{ @@ -8789,16 +8938,126 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ + ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b); + BOOL textDidChange = NO; if (modiff != self.cachedModiff) { self.cachedModiff = modiff; - [self postTextChangedNotification:point]; + /* Only post ValueChanged when actual characters changed. + Text property changes (e.g. face updates from hl-line-mode, + vertico--prompt-selection) bump BUF_MODIFF but not + BUF_CHARS_MODIFF. Posting ValueChanged for property-only + changes causes VoiceOver to say "new line" when the diff + is non-empty due to overlay content changes. + + Use textDidChange to avoid blocking the cursor-move branch + below: property-only changes must not prevent + SelectedTextChanged from firing when point also moved + (e.g. hl-line-mode updates face properties on every cursor + movement in dired and other read-only buffers). */ + if (chars_modiff != self.cachedCharsModiff) + { + self.cachedCharsModiff = chars_modiff; + self.emacsView->overlayZoomActive = NO; + [self postTextChangedNotification:point]; + textDidChange = YES; + } + } + + + /* --- Overlay content changed (e.g. Vertico/Ivy candidate switch) --- + Check independently of the modiff branch above, because + frameworks like Vertico bump BOTH BUF_MODIFF (via text property + changes in vertico--prompt-selection) and BUF_OVERLAY_MODIFF + (via overlay-put) in the same command cycle. If this were an + else-if, the modiff branch would always win and overlay + announcements would never fire. + Do NOT invalidate the text cache --- the buffer text has not + changed, and cache invalidation causes VoiceOver to diff old + vs new AX text and announce spurious "new line". */ + if (BUF_OVERLAY_MODIFF (b) != self.cachedOverlayModiff) + { + self.cachedOverlayModiff = BUF_OVERLAY_MODIFF (b); + + int selected_line = -1; + NSString *candidate + = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b), + &selected_line); + if (candidate) + { + /* Deduplicate: only announce when the candidate changed. */ + if (![candidate isEqualToString: + self.cachedCompletionAnnouncement]) + { + self.cachedCompletionAnnouncement = candidate; + + /* Announce the candidate text directly via NSApp. + Do NOT post SelectedTextChanged --- that would cause + VoiceOver to read the AX text at the cursor position + (the minibuffer input line), not the overlay candidate. + AnnouncementRequested with High priority interrupts + any current speech and announces our text. */ + NSDictionary *annInfo = @{ + NSAccessibilityAnnouncementKey: candidate, + NSAccessibilityPriorityKey: + @(NSAccessibilityPriorityHigh) + }; + ns_ax_post_notification_with_info ( + NSApp, + NSAccessibilityAnnouncementRequestedNotification, + annInfo); + + /* --- Zoom tracking for overlay candidates --- + Store the candidate row rect so draw_window_cursor + focuses Zoom there instead of on the text cursor. + Cleared when the user types (chars_modiff change). + + Use default line height to compute the Y offset: + row 0 is the input line, overlay candidates start + from row 1. This avoids fragile glyph matrix row + index mapping which can be off when group titles + or wrapped lines shift row numbering. */ + if (selected_line >= 0) + { + struct window *w2 = [self validWindow]; + if (w2) + { + EmacsView *view = self.emacsView; + struct frame *f2 = XFRAME (w2->frame); + int line_h = FRAME_LINE_HEIGHT (f2); + int y_off = (selected_line + 1) * line_h; + + if (y_off < w2->pixel_height) + { + view->overlayZoomRect = NSMakeRect ( + WINDOW_TEXT_TO_FRAME_PIXEL_X (w2, 0), + WINDOW_TO_FRAME_PIXEL_Y (w2, y_off), + FRAME_COLUMN_WIDTH (f2), + line_h); + view->overlayZoomActive = YES; + } + } + } + } + } + else + { + /* No selected candidate --- overlay completion ended + (minibuffer exit, C-g, etc.) or overlay has no + recognizable selection face. Return Zoom to the + text cursor. */ + self.emacsView->overlayZoomActive = NO; + } } /* --- Cursor moved or selection changed --- - Use 'else if' — edits and selection moves are mutually exclusive - per the WebKit/Chromium pattern. */ - else if (point != self.cachedPoint || markActive != self.cachedMarkActive) + Skip when ValueChanged was already posted (edits and selection + moves are mutually exclusive per the WebKit/Chromium pattern). + But DO fire when only text properties changed (BUF_MODIFF bumped + without BUF_CHARS_MODIFF) --- hl-line-mode and similar packages + update face properties on every cursor movement. */ + if (!textDidChange + && (point != self.cachedPoint || markActive != self.cachedMarkActive)) { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; @@ -8966,7 +9225,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* =================================================================== - EmacsAccessibilityInteractiveSpan — helpers and implementation + EmacsAccessibilityInteractiveSpan --- helpers and implementation =================================================================== */ /* Scan visible range of window W for interactive spans. @@ -9157,7 +9416,7 @@ ns_ax_scan_interactive_spans (struct window *w, - (BOOL) isAccessibilityFocused { /* Read the cached point stored by EmacsAccessibilityBuffer on the main - thread — safe to read from any thread (plain ptrdiff_t, no Lisp calls). */ + thread --- safe to read from any thread (plain ptrdiff_t, no Lisp calls). */ EmacsAccessibilityBuffer *pb = self.parentBuffer; if (!pb) return NO; @@ -9174,7 +9433,7 @@ ns_ax_scan_interactive_spans (struct window *w, dispatch_async (dispatch_get_main_queue (), ^{ /* lwin is a Lisp_Object captured by value. This is GC-safe because Lisp_Objects are tagged integers/pointers that - remain valid across GC — GC does not relocate objects in + remain valid across GC --- GC does not relocate objects in Emacs. The WINDOW_LIVE_P check below guards against the window being deleted between capture and execution. */ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) @@ -9200,7 +9459,7 @@ ns_ax_scan_interactive_spans (struct window *w, @end -/* EmacsAccessibilityBuffer — InteractiveSpans category. +/* EmacsAccessibilityBuffer --- InteractiveSpans category. Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) @@ -11922,7 +12181,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, if (WINDOW_LEAF_P (w)) { - /* Buffer element — reuse existing if available. */ + /* Buffer element --- reuse existing if available. */ EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) @@ -11956,7 +12215,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } else { - /* Internal (combination) window — recurse into children. */ + /* Internal (combination) window --- recurse into children. */ Lisp_Object child = w->contents; while (!NILP (child)) { @@ -12068,7 +12327,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare - FRAME_ROOT_WINDOW — if it changed, the tree structure changed. */ + FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { @@ -12077,12 +12336,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } /* If tree is stale, rebuild FIRST so we don't iterate freed - window pointers. Skip notifications for this cycle — the + window pointers. Skip notifications for this cycle --- the freshly-built elements have no previous state to diff against. */ if (!accessibilityTreeValid) { [self rebuildAccessibilityTree]; - /* Invalidate span cache — window layout changed. */ + /* Invalidate span cache --- window layout changed. */ for (EmacsAccessibilityElement *elem in accessibilityElements) if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans]; -- 2.43.0