diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index 230c859..91436ef 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,4 +1,4 @@ -From 403a1c4664e7491c20eac86c143898bc366a57bc Mon Sep 17 00:00:00 2001 +From 3616ffd4289b0a73da93d83fa8dfc7be9d6554b9 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 Subject: [PATCH] ns: announce overlay completion candidates for VoiceOver @@ -32,10 +32,11 @@ Key implementation details: position, which is the minibuffer input, not the candidate). - Zoom tracking: store the selected candidate's glyph row rect in - overlayZoomRect. draw_window_cursor checks overlayZoomActive and - uses the stored rect instead of the text cursor rect, keeping + overlayZoomRect. ns_draw_window_cursor checks overlayZoomActive + and uses the stored rect instead of the text cursor rect, keeping Zoom focused on the candidate. The flag is cleared when the user - types (BUF_CHARS_MODIFF changes) to return Zoom to the cursor. + 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. @@ -48,8 +49,8 @@ Independent overlay branch, BUF_CHARS_MODIFF gating, candidate announcement with overlay Zoom rect storage. --- src/nsterm.h | 3 + - src/nsterm.m | 252 ++++++++++++++++++++++++++++++++++++++++++++++++--- - 2 files changed, 244 insertions(+), 11 deletions(-) + src/nsterm.m | 314 +++++++++++++++++++++++++++++++++++++++++++++------ + 2 files changed, 281 insertions(+), 36 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 51c30ca..5c15639 100644 @@ -73,7 +74,7 @@ index 51c30ca..5c15639 100644 BOOL font_panel_active; NSFont *font_panel_result; diff --git a/src/nsterm.m b/src/nsterm.m -index 1780194..5c3758a 100644 +index 1780194..31b2c33 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, @@ -90,7 +91,7 @@ index 1780194..5c3758a 100644 NSRect screenRect = [[view window] convertRectToScreen:windowRect]; CGRect cgRect = NSRectToCGRect (screenRect); -@@ -6915,11 +6920,145 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) +@@ -6915,11 +6920,153 @@ ns_create_font_panel_buttons (id target, SEL select, SEL cancel_action) are truncated for accessibility purposes. */ #define NS_AX_TEXT_CAP 100000 @@ -104,6 +105,9 @@ index 1780194..5c3758a 100644 + if (SYMBOLP (face) && !NILP (face)) + { + const char *name = SSDATA (SYMBOL_NAME (face)); ++ /* Substring match is intentionally broad; false positives ++ are harmless since this runs only on overlay strings in ++ the minibuffer during completion. */ + if (strstr (name, "current") || strstr (name, "selected")) + return true; + } @@ -158,9 +162,14 @@ index 1780194..5c3758a 100644 + if (slen == 0) + continue; + -+ /* Scan for newline positions using SDATA for efficiency. */ ++ /* 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; @@ -237,7 +246,25 @@ index 1780194..5c3758a 100644 static NSString * ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_visible_run **out_runs, NSUInteger *out_nruns) -@@ -7556,6 +7695,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -6996,7 +7143,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 +7224,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 +7703,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, @synthesize cachedOverlayModiff; @synthesize cachedTextStart; @synthesize cachedModiff; @@ -245,7 +272,16 @@ index 1780194..5c3758a 100644 @synthesize cachedPoint; @synthesize cachedMarkActive; @synthesize cachedCompletionAnnouncement; -@@ -7609,16 +7749,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7596,7 +7744,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 +7757,15 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, return; ptrdiff_t modiff = BUF_MODIFF (b); @@ -268,7 +304,7 @@ index 1780194..5c3758a 100644 && cachedTextStart == BUF_BEGV (b) && pt >= cachedTextStart && (textLen == 0 -@@ -7635,7 +7774,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7635,7 +7782,6 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, [cachedText release]; cachedText = [text retain]; cachedTextModiff = modiff; @@ -276,7 +312,92 @@ index 1780194..5c3758a 100644 cachedTextStart = start; if (visibleRuns) -@@ -8789,10 +8927,102 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -7661,7 +7807,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 +7820,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 +7845,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 +7882,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 +7904,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 +8312,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 +8568,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 +8583,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 +8602,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,14 +8935,110 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -375,11 +496,114 @@ index 1780194..5c3758a 100644 + } + 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 ++ Use 'else if' --- edits and selection moves are mutually exclusive + per the WebKit/Chromium pattern. */ + else if (point != self.cachedPoint || markActive != self.cachedMarkActive) + { +@@ -8966,7 +9208,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 +9399,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 +9416,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 +9442,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) +@@ -10520,13 +10762,13 @@ ns_in_echo_area (void) + if (old_title == 0) + { + char *t = strdup ([[[self window] title] UTF8String]); +- char *pos = strstr (t, " — "); ++ char *pos = strstr (t, " --- "); + if (pos) + *pos = '\0'; + old_title = t; + } + size_title = xmalloc (strlen (old_title) + 40); +- esprintf (size_title, "%s — (%d × %d)", old_title, cols, rows); ++ esprintf (size_title, "%s --- (%d × %d)", old_title, cols, rows); + [window setTitle: [NSString stringWithUTF8String: size_title]]; + [window display]; + xfree (size_title); +@@ -11922,7 +12164,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 +12198,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 +12310,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 +12319,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