From b87fb2b1824761fe3d91a27afe966eada39c1c45 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Mon, 2 Mar 2026 18:39:46 +0100 Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver Completion frameworks such as Vertico, Ivy, and Icomplete render candidates via overlay before-string/after-string properties. Without this change VoiceOver cannot read overlay-based completion UIs. * src/nsterm.m (ns_ax_selected_overlay_text): New function; scan overlay strings in the window for a line with a selected face; return its text. (accessibilityStringForRange:, accessibilityAttributedStringForRange:) (accessibilityRangeForLine:): New NSAccessibility protocol methods. Moved here from planned patch 0008 to keep the AX protocol interface complete before notification logic uses it. (ensureTextCache): Switch cache-validity counter from BUF_CHARS_MODIFF to BUF_MODIFF. Fold/unfold commands (org-mode, outline-mode, hideshow-mode) change the 'invisible text property via `put-text-property', which bumps BUF_MODIFF but not BUF_CHARS_MODIFF. Using BUF_CHARS_MODIFF would serve stale AX text across fold/unfold. The rebuild is O(visible-buffer-text) but ensureTextCache is called exclusively from AX getters at human interaction speed, never from the redisplay notification path; font-lock passes cause zero rebuild cost. (postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF changes independently of text changes. Use BUF_CHARS_MODIFF to gate ValueChanged. Do not call ensureTextCache from the cursor-moved branch: the granularity detection uses cachedText directly (falling back to granularity_unknown when the cache is absent), so font-lock passes cannot trigger O(buffer-size) rebuilds via the notification path. --- src/nsterm.h | 1 + src/nsterm.m | 384 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 306 insertions(+), 79 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 4bf79a9adb..72ca210bb0 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -510,6 +510,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; diff --git a/src/nsterm.m b/src/nsterm.m index e4e43dd7a3..c9fe93a57b 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -7263,11 +7263,154 @@ Accessibility virtual elements (macOS / Cocoa only) /* ---- Helper: extract buffer text for accessibility ---- */ +/* 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, Qbefore_string); + strings[1] = Foverlay_get (ov, Qafter_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) @@ -7343,7 +7486,7 @@ Accessibility virtual elements (macOS / Cocoa only) /* 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)); @@ -7424,7 +7567,7 @@ Mode lines using icon fonts (e.g. nerd-font icons) 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; @@ -7606,31 +7749,7 @@ already on the main queue (e.g., inside postAccessibilityUpdates freeing the main queue for VoiceOver's dispatch_sync calls. */ /* Return true if FACE (a symbol or list of symbols) looks like a - "selected item" 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: this runs only on overlay/child-frame - strings during completion, never in a hot redisplay path. */ -static bool -ns_ax_face_is_selected (Lisp_Object face) -{ - if (SYMBOLP (face) && !NILP (face)) - { - const char *name = SSDATA (SYMBOL_NAME (face)); - 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; -} - -static inline void + static inline void ns_ax_post_notification (id element, NSAccessibilityNotificationName name) { @@ -7924,6 +8043,7 @@ @implementation EmacsAccessibilityBuffer @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; +@synthesize cachedCharsModiff; @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; @@ -8021,7 +8141,7 @@ - (void)ensureTextCache 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]); @@ -8033,25 +8153,38 @@ - (void)ensureTextCache if (!b) return; - /* Use BUF_CHARS_MODIFF, not BUF_MODIFF, for cache validity. - BUF_MODIFF is bumped by every text-property change, including - font-lock face applications on every redisplay. AX text contains - only characters, not face data, so property-only changes do not - affect the cached value. Rebuilding the full buffer text on - each font-lock pass is O(buffer-size) per redisplay --- this - causes progressive slowdown when scrolling through large files. - BUF_CHARS_MODIFF is bumped only on actual character insertions - and deletions, matching the semantic of "did the text change". - This is the pattern used by WebKit and NSTextView. - Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not - included in the cached AX text (it is handled separately via - explicit announcements in postAccessibilityNotificationsForFrame). - Including overlay_modiff would silently update cachedOverlayModiff - and prevent the notification dispatch from detecting changes. */ - ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b); + /* Use BUF_MODIFF, not BUF_CHARS_MODIFF, for cache validity. + + Fold/unfold commands (org-mode, outline-mode, hideshow-mode) change + text visibility by modifying the 'invisible text property via + `put-text-property' or `add-text-properties'. These bump BUF_MODIFF + but NOT BUF_CHARS_MODIFF, because no characters are inserted or + deleted. Using only BUF_CHARS_MODIFF would serve stale AX text + across fold/unfold: VoiceOver would continue reading hidden content + as if it were visible, or miss newly revealed content entirely. + + BUF_MODIFF is bumped by all buffer modifications including + text-property changes (e.g. font-lock face assignments). The + per-rebuild cost is O(visible-buffer-text), but `ensureTextCache' + is called exclusively from AX getters (accessibilityValue, + accessibilitySelectedTextRange, etc.) which run at human interaction + speed --- not from the redisplay notification path. Font-lock + passes do not call this method, so the rebuild cost per font-lock + cycle is zero. The redisplay notification path (postAccessibility- + NotificationsForFrame:) uses cachedText directly without calling + ensureTextCache; granularity detection falls back gracefully when + the cache is absent. + + Do NOT use BUF_OVERLAY_MODIFF alone: org-mode >= 29 (org-fold-core) + uses text properties, not overlays, for folding, so + BUF_OVERLAY_MODIFF would miss those changes. Additionally, modes + like hl-line-mode bump BUF_OVERLAY_MODIFF on every + post-command-hook, yielding the same per-keystroke rebuild cost as + BUF_MODIFF, with none of its correctness guarantee. */ + ptrdiff_t modiff = BUF_MODIFF (b); ptrdiff_t pt = BUF_PT (b); NSUInteger textLen = cachedText ? [cachedText length] : 0; - if (cachedText && cachedTextModiff == chars_modiff + if (cachedText && cachedTextModiff == modiff && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 @@ -8067,7 +8200,7 @@ included in the cached AX text (it is handled separately via { [cachedText release]; cachedText = [text retain]; - cachedTextModiff = chars_modiff; + cachedTextModiff = modiff; cachedTextStart = start; if (visibleRuns) @@ -8079,9 +8212,9 @@ included in the cached AX text (it is handled separately via Walk the cached text once, recording the start offset of each line. Uses NSString lineRangeForRange: --- O(N) in the total text --- but this loop runs only on cache rebuild, which is - gated on BUF_CHARS_MODIFF: actual character insertions or - deletions. Font-lock (text property changes) does not trigger - a rebuild, so the hot path (cursor movement, redisplay) never + gated on BUF_MODIFF changes. Rebuilds happen when any buffer + modification occurs (including fold/unfold), ensuring the line + index always matches the currently visible text. enters this code. */ if (lineStartOffsets) xfree (lineStartOffsets); @@ -8136,7 +8269,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos /* 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) { @@ -8185,10 +8318,10 @@ by run length (visible window), not total buffer size. */ /* 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) @@ -8230,7 +8363,7 @@ the slow path (composed character sequence walk), which is return cp; } } - /* Past end — return last charpos. */ + /* Past end --- return last charpos. */ if (lo > 0) { ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1]; @@ -8252,7 +8385,7 @@ the slow path (composed character sequence walk), which is 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 @@ -8597,26 +8730,26 @@ - (NSInteger)accessibilityInsertionPointLineNumber return [self lineForAXIndex:point_idx]; } -- (NSRange)accessibilityRangeForLine:(NSInteger)line +- (NSString *)accessibilityStringForRange:(NSRange)range { if (![NSThread isMainThread]) { - __block NSRange result; + __block NSString *result; dispatch_sync (dispatch_get_main_queue (), ^{ - result = [self accessibilityRangeForLine:line]; + result = [self accessibilityStringForRange:range]; }); return result; } [self ensureTextCache]; - if (!cachedText || line < 0) - return NSMakeRange (NSNotFound, 0); - - NSUInteger len = [cachedText length]; - if (len == 0) - return (line == 0) ? NSMakeRange (0, 0) - : NSMakeRange (NSNotFound, 0); + if (!cachedText || range.location + range.length > [cachedText length]) + return @""; + return [cachedText substringWithRange:range]; +} - return [self rangeForLine:(NSUInteger)line textLength:len]; +- (NSAttributedString *)accessibilityAttributedStringForRange:(NSRange)range +{ + NSString *str = [self accessibilityStringForRange:range]; + return [[[NSAttributedString alloc] initWithString:str] autorelease]; } - (NSInteger)accessibilityLineForIndex:(NSInteger)index @@ -8638,6 +8771,29 @@ - (NSInteger)accessibilityLineForIndex:(NSInteger)index idx = [cachedText length]; return [self lineForAXIndex:idx]; + +} + +- (NSRange)accessibilityRangeForLine:(NSInteger)line +{ + if (![NSThread isMainThread]) + { + __block NSRange result; + dispatch_sync (dispatch_get_main_queue (), ^{ + result = [self accessibilityRangeForLine:line]; + }); + return result; + } + [self ensureTextCache]; + if (!cachedText || line < 0) + return NSMakeRange (NSNotFound, 0); + + NSUInteger len = [cachedText length]; + if (len == 0) + return (line == 0) ? NSMakeRange (0, 0) + : NSMakeRange (NSNotFound, 0); + + return [self rangeForLine:(NSUInteger)line textLength:len]; } - (NSRange)accessibilityRangeForIndex:(NSInteger)index @@ -8840,7 +8996,7 @@ - (NSRect)accessibilityFrame /* =================================================================== - 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). @@ -8855,7 +9011,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point 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) @@ -8874,7 +9030,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point /* 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 = @{ @@ -9268,16 +9424,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ + ptrdiff_t chars_modiff = BUF_CHARS_MODIFF (b); 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 + 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. */ + if (chars_modiff != self.cachedCharsModiff) + { + self.cachedCharsModiff = chars_modiff; + [self postTextChangedNotification:point]; + } + } + + + /* --- 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); + + /* Overlay completion candidates (Vertico, Icomplete, Ivy) are + displayed in the minibuffer. In normal editing buffers, + font-lock and other modes change BUF_OVERLAY_MODIFF on + every redisplay, triggering O(overlays) work per keystroke. + Restrict the scan to minibuffer windows. */ + if (MINI_WINDOW_P (w)) + { + 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); + } + } + } } /* --- 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) + Independent check from the overlay branch above. */ + if (point != self.cachedPoint || markActive != self.cachedMarkActive) { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; @@ -9295,8 +9515,14 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP); /* --- Granularity detection --- */ + /* Use cached text as-is; do NOT call ensureTextCache here. + ensureTextCache is O(visible-buffer-text) and must not run on + every redisplay cycle. Using stale cached text for granularity + classification is safe: the worst case is an incorrect + granularity hint (defaulting to unknown), which causes VoiceOver + to make its own determination. Fresh text is always available + to VoiceOver via the AX getter path (accessibilityValue etc.). */ NSInteger granularity = ns_ax_text_selection_granularity_unknown; - [self ensureTextCache]; if (cachedText && oldPoint > 0) { NSUInteger tlen = [cachedText length]; @@ -12457,7 +12683,7 @@ - (int) fullscreenState 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) @@ -12491,7 +12717,7 @@ - (int) fullscreenState } else { - /* Internal (combination) window — recurse into children. */ + /* Internal (combination) window --- recurse into children. */ Lisp_Object child = w->contents; while (!NILP (child)) { @@ -12603,7 +12829,7 @@ - (void)postAccessibilityUpdates 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)) { @@ -12612,12 +12838,12 @@ - (void)postAccessibilityUpdates } /* 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