Two bugs introduced during rebase/amend: 1. Stray 'unbind_to (count, Qnil)' in ns_focus (P0000): A hunk was misplaced into ns_focus where 'count' is not declared. The comment and unbind_to belonged at the end of ns_zoom_track_completion, which already has a correct unbind_to. Remove the duplicate from ns_focus. 2. 'voiceoverSetPoint = NO' in EmacsView::initFrameFromEmacs: (P0008): voiceoverSetPoint is a BOOL ivar of EmacsAXBuffer, not EmacsView. Setting it in EmacsView's init method causes 'undeclared identifier'. ObjC BOOL ivars zero-initialize to NO automatically. Remove the line. voiceoverSetPoint is consumed/set in EmacsAXBuffer methods only.
1158 lines
35 KiB
Diff
1158 lines
35 KiB
Diff
From 60f0e223190b158679322411b4186b7a378114e7 Mon Sep 17 00:00:00 2001
|
|
From: Martin Sukany <martin@sukany.cz>
|
|
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
|
Subject: [PATCH 2/8] ns: implement buffer accessibility element (core
|
|
protocol)
|
|
|
|
Implement the NSAccessibility text protocol for Emacs buffer windows.
|
|
|
|
* src/nsterm.m (ns_ax_find_completion_overlay_range): New function.
|
|
(ns_ax_event_is_line_nav_key, ns_ax_completion_text_for_span): New
|
|
functions.
|
|
(EmacsAccessibilityBuffer): Implement core NSAccessibility protocol.
|
|
(ensureTextCache): Validity gated on BUF_CHARS_MODIFF, not BUF_MODIFF,
|
|
to avoid O(buffer-size) rebuilds on every font-lock pass. Add
|
|
explanatory comment on why lineRangeForRange: in the lineStartOffsets
|
|
loop is safe: it runs only on actual character modifications.
|
|
(accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs
|
|
(ax_length == length); fall back to sequence walk for multi-byte runs.
|
|
(charposForAccessibilityIndex:): Symmetric O(1) fast path.
|
|
(accessibilitySelectedTextRange, accessibilityLineForIndex:)
|
|
(accessibilityIndexForLine:, accessibilityRangeForIndex:)
|
|
(accessibilityStringForRange:, accessibilityFrameForRange:)
|
|
(accessibilityRangeForPosition:, setAccessibilitySelectedTextRange:)
|
|
(setAccessibilityFocused:): Implement NSAccessibility protocol methods.
|
|
---
|
|
src/nsterm.m | 1115 ++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
1 file changed, 1115 insertions(+)
|
|
|
|
diff --git a/src/nsterm.m b/src/nsterm.m
|
|
index 9d36de66f9..6256dbc22e 100644
|
|
--- a/src/nsterm.m
|
|
+++ b/src/nsterm.m
|
|
@@ -7625,6 +7625,1121 @@ - (id)accessibilityTopLevelUIElement
|
|
|
|
@end
|
|
|
|
+
|
|
+
|
|
+
|
|
+
|
|
+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 = Qns_ax_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.
|
|
+ Prefer point+1 over point-1: when Tab moves to a new completion,
|
|
+ point is at the START of the new entry while point-1 is still
|
|
+ inside the previous entry's overlay. Forward probe finds the
|
|
+ correct new entry; backward probe finds the wrong old one. */
|
|
+ ptrdiff_t probes[3] = { point, point + 1, point - 1 };
|
|
+ for (int i = 0; i < 3 && !found; i++)
|
|
+ {
|
|
+ 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))
|
|
+ {
|
|
+ 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 (!found)
|
|
+ {
|
|
+ /* Bulk query: get all overlays in the buffer at once.
|
|
+ Avoids the previous O(n) per-character Foverlays_at loop. */
|
|
+ Lisp_Object all = Foverlays_in (make_fixnum (begv),
|
|
+ make_fixnum (zv));
|
|
+ Lisp_Object tail;
|
|
+ for (tail = all; 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)
|
|
+ {
|
|
+ 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;
|
|
+}
|
|
+
|
|
+/* Detect line-level navigation commands. Inspects Vthis_command
|
|
+ (the command symbol being executed) rather than raw key codes so
|
|
+ that remapped bindings (e.g., C-j -> next-line) are recognized.
|
|
+ Falls back to last_command_event for Tab/backtab which are not
|
|
+ bound to a single canonical command symbol. */
|
|
+static bool
|
|
+ns_ax_event_is_line_nav_key (int *which)
|
|
+{
|
|
+ /* 1. Check Vthis_command for known navigation command symbols.
|
|
+ All symbols are registered via DEFSYM in syms_of_nsterm to avoid
|
|
+ per-call obarray lookups in this hot path (runs every cursor move). */
|
|
+ if (SYMBOLP (Vthis_command) && !NILP (Vthis_command))
|
|
+ {
|
|
+ Lisp_Object cmd = Vthis_command;
|
|
+ /* Forward line commands. */
|
|
+ if (EQ (cmd, Qns_ax_next_line)
|
|
+ || EQ (cmd, Qns_ax_dired_next_line))
|
|
+ {
|
|
+ if (which) *which = 1;
|
|
+ return true;
|
|
+ }
|
|
+ /* Backward line commands. */
|
|
+ if (EQ (cmd, Qns_ax_previous_line)
|
|
+ || EQ (cmd, Qns_ax_dired_previous_line))
|
|
+ {
|
|
+ if (which) *which = -1;
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* 2. Fallback: check raw key events for Tab/backtab. */
|
|
+ Lisp_Object ev = last_command_event;
|
|
+ if (CONSP (ev))
|
|
+ ev = EVENT_HEAD (ev);
|
|
+
|
|
+ if (SYMBOLP (ev) && EQ (ev, Qns_ax_backtab))
|
|
+ {
|
|
+ if (which) *which = -1;
|
|
+ return true;
|
|
+ }
|
|
+ if (FIXNUMP (ev) && XFIXNUM (ev) == 9) /* Tab */
|
|
+ {
|
|
+ if (which) *which = 1;
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+}
|
|
+
|
|
+
|
|
+
|
|
+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;
|
|
+ specpdl_ref count = SPECPDL_INDEX ();
|
|
+ record_unwind_current_buffer ();
|
|
+ /* Block input to prevent concurrent redisplay from modifying buffer
|
|
+ state while we read text properties. Unwind-protected so
|
|
+ block_input is always matched by unblock_input on signal. */
|
|
+ record_unwind_protect_void (unblock_input);
|
|
+ block_input ();
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ /* Prefer canonical completion candidate string from text property.
|
|
+ Try both completion--string (new API, set by minibuffer.el) and
|
|
+ completion (older API used by some modes). */
|
|
+ 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),
|
|
+ Qns_ax_completion__string,
|
|
+ Qnil);
|
|
+ if (STRINGP (cstr))
|
|
+ text = [NSString stringWithLispString:cstr];
|
|
+ if (!text)
|
|
+ {
|
|
+ /* Fallback: 'completion property used by display-completion-list. */
|
|
+ cstr = Fget_char_property (make_fixnum (p),
|
|
+ Qns_ax_completion,
|
|
+ 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)];
|
|
+ }
|
|
+
|
|
+ unbind_to (count, Qnil);
|
|
+
|
|
+ if (text)
|
|
+ {
|
|
+ text = [text stringByTrimmingCharactersInSet:
|
|
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
+ if ([text length] == 0)
|
|
+ text = nil;
|
|
+ }
|
|
+
|
|
+ return text;
|
|
+}
|
|
+
|
|
+@implementation EmacsAccessibilityBuffer
|
|
+@synthesize cachedText;
|
|
+@synthesize cachedTextModiff;
|
|
+@synthesize cachedOverlayModiff;
|
|
+@synthesize cachedTextStart;
|
|
+@synthesize cachedModiff;
|
|
+@synthesize cachedPoint;
|
|
+@synthesize cachedMarkActive;
|
|
+@synthesize cachedCompletionAnnouncement;
|
|
+@synthesize cachedCompletionOverlayStart;
|
|
+@synthesize cachedCompletionOverlayEnd;
|
|
+@synthesize cachedCompletionPoint;
|
|
+
|
|
+- (void)dealloc
|
|
+{
|
|
+ [cachedText release];
|
|
+ [cachedCompletionAnnouncement release];
|
|
+ [cachedInteractiveSpans release];
|
|
+ if (visibleRuns)
|
|
+ xfree (visibleRuns);
|
|
+ if (lineStartOffsets)
|
|
+ xfree (lineStartOffsets);
|
|
+ [super dealloc];
|
|
+}
|
|
+
|
|
+/* ---- Text cache ---- */
|
|
+
|
|
+- (void)invalidateTextCache
|
|
+{
|
|
+ @synchronized (self)
|
|
+ {
|
|
+ [cachedText release];
|
|
+ cachedText = nil;
|
|
+ if (visibleRuns)
|
|
+ {
|
|
+ xfree (visibleRuns);
|
|
+ visibleRuns = NULL;
|
|
+ }
|
|
+ visibleRunCount = 0;
|
|
+ if (lineStartOffsets)
|
|
+ {
|
|
+ xfree (lineStartOffsets);
|
|
+ lineStartOffsets = NULL;
|
|
+ }
|
|
+ lineCount = 0;
|
|
+ }
|
|
+ [self invalidateInteractiveSpans];
|
|
+}
|
|
+
|
|
+/* ---- Line index helpers ---- */
|
|
+
|
|
+/* Return the line number for AX string index IDX using the
|
|
+ precomputed lineStartOffsets array. Binary search: O(log L)
|
|
+ where L is the number of lines in the cached text.
|
|
+
|
|
+ lineStartOffsets[i] holds the AX string index where line i
|
|
+ begins. Built once per cache rebuild in ensureTextCache. */
|
|
+- (NSInteger)lineForAXIndex:(NSUInteger)idx
|
|
+{
|
|
+ @synchronized (self)
|
|
+ {
|
|
+ if (!lineStartOffsets || lineCount == 0)
|
|
+ return 0;
|
|
+
|
|
+ /* Binary search for the largest line whose start offset <= idx. */
|
|
+ NSUInteger lo = 0, hi = lineCount;
|
|
+ while (lo < hi)
|
|
+ {
|
|
+ NSUInteger mid = lo + (hi - lo) / 2;
|
|
+ if (lineStartOffsets[mid] <= idx)
|
|
+ lo = mid + 1;
|
|
+ else
|
|
+ hi = mid;
|
|
+ }
|
|
+ return (NSInteger) (lo > 0 ? lo - 1 : 0);
|
|
+ }
|
|
+}
|
|
+
|
|
+/* Return the AX string range for LINE using the precomputed
|
|
+ lineStartOffsets array. O(1) lookup. */
|
|
+- (NSRange)rangeForLine:(NSUInteger)line textLength:(NSUInteger)tlen
|
|
+{
|
|
+ @synchronized (self)
|
|
+ {
|
|
+ if (!lineStartOffsets || lineCount == 0)
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+
|
|
+ if (line >= lineCount)
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+
|
|
+ NSUInteger start = lineStartOffsets[line];
|
|
+ NSUInteger end = (line + 1 < lineCount)
|
|
+ ? lineStartOffsets[line + 1]
|
|
+ : tlen;
|
|
+ return NSMakeRange (start, end - start);
|
|
+ }
|
|
+}
|
|
+
|
|
+- (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
|
|
+ write section at the end needs synchronization to protect
|
|
+ against concurrent reads from AX server thread. */
|
|
+ eassert ([NSThread isMainThread]);
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ 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);
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ NSUInteger textLen = cachedText ? [cachedText length] : 0;
|
|
+ if (cachedText && cachedTextModiff == chars_modiff
|
|
+ && cachedTextStart == BUF_BEGV (b)
|
|
+ && pt >= cachedTextStart
|
|
+ && (textLen == 0
|
|
+ || [self accessibilityIndexForCharpos:pt] <= textLen))
|
|
+ return;
|
|
+
|
|
+ ptrdiff_t start;
|
|
+ ns_ax_visible_run *runs = NULL;
|
|
+ NSUInteger nruns = 0;
|
|
+ NSString *text = ns_ax_buffer_text (w, &start, &runs, &nruns);
|
|
+
|
|
+ @synchronized (self)
|
|
+ {
|
|
+ [cachedText release];
|
|
+ cachedText = [text retain];
|
|
+ cachedTextModiff = chars_modiff;
|
|
+ cachedTextStart = start;
|
|
+
|
|
+ if (visibleRuns)
|
|
+ xfree (visibleRuns);
|
|
+ visibleRuns = runs;
|
|
+ visibleRunCount = nruns;
|
|
+
|
|
+ /* Build line-start index for O(log L) line queries.
|
|
+ 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
|
|
+ enters this code. */
|
|
+ if (lineStartOffsets)
|
|
+ xfree (lineStartOffsets);
|
|
+ lineStartOffsets = NULL;
|
|
+ lineCount = 0;
|
|
+
|
|
+ NSUInteger tlen = [cachedText length];
|
|
+ if (tlen > 0)
|
|
+ {
|
|
+ NSUInteger cap = 256;
|
|
+ lineStartOffsets = xmalloc (cap * sizeof (NSUInteger));
|
|
+ lineStartOffsets[0] = 0;
|
|
+ lineCount = 1;
|
|
+ NSUInteger pos = 0;
|
|
+ while (pos < tlen)
|
|
+ {
|
|
+ NSRange lr = [cachedText lineRangeForRange:
|
|
+ NSMakeRange (pos, 0)];
|
|
+ NSUInteger next = NSMaxRange (lr);
|
|
+ if (next <= pos)
|
|
+ break; /* safety */
|
|
+ if (next < tlen)
|
|
+ {
|
|
+ if (lineCount >= cap)
|
|
+ {
|
|
+ cap *= 2;
|
|
+ lineStartOffsets = xrealloc (lineStartOffsets,
|
|
+ cap * sizeof (NSUInteger));
|
|
+ }
|
|
+ lineStartOffsets[lineCount] = next;
|
|
+ lineCount++;
|
|
+ }
|
|
+ pos = next;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+/* ---- Index mapping ---- */
|
|
+
|
|
+/* Convert buffer charpos to accessibility string index. */
|
|
+- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
|
|
+{
|
|
+ /* This method may be called from the AX server thread.
|
|
+ Synchronize on self to prevent use-after-free if the main
|
|
+ thread invalidates the text cache concurrently. */
|
|
+ @synchronized (self)
|
|
+ {
|
|
+ if (visibleRunCount == 0)
|
|
+ return 0;
|
|
+
|
|
+ /* 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. */
|
|
+ NSUInteger lo = 0, hi = visibleRunCount;
|
|
+ while (lo < hi)
|
|
+ {
|
|
+ NSUInteger mid = lo + (hi - lo) / 2;
|
|
+ ns_ax_visible_run *r = &visibleRuns[mid];
|
|
+ if (charpos < r->charpos)
|
|
+ hi = mid;
|
|
+ else if (charpos >= r->charpos + r->length)
|
|
+ lo = mid + 1;
|
|
+ else
|
|
+ {
|
|
+ /* Found: charpos is inside this run. Compute UTF-16 delta.
|
|
+ Fast path for pure-ASCII runs (ax_length == length): every
|
|
+ Emacs charpos maps to exactly one UTF-16 code unit, so the
|
|
+ conversion is O(1). This matters because ensureTextCache
|
|
+ calls this method on every redisplay frame to validate the
|
|
+ cache --- a O(cursor_position) loop here means O(position)
|
|
+ cost per frame even when the buffer is unchanged.
|
|
+ Multi-byte runs fall through to the sequence walk, bounded
|
|
+ by run length (visible window), not total buffer size. */
|
|
+ NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
|
|
+ if (chars_in == 0)
|
|
+ return r->ax_start;
|
|
+ if (r->ax_length == (NSUInteger) r->length)
|
|
+ return r->ax_start + chars_in;
|
|
+ if (!cachedText)
|
|
+ return r->ax_start;
|
|
+ NSUInteger run_end_ax = r->ax_start + r->ax_length;
|
|
+ NSUInteger scan = r->ax_start;
|
|
+ for (NSUInteger c = 0; c < chars_in && scan < run_end_ax; c++)
|
|
+ {
|
|
+ NSRange seq = [cachedText
|
|
+ rangeOfComposedCharacterSequenceAtIndex:scan];
|
|
+ scan = NSMaxRange (seq);
|
|
+ }
|
|
+ return (scan <= run_end_ax) ? scan : run_end_ax;
|
|
+ }
|
|
+ }
|
|
+ /* charpos falls in an invisible gap or past the end. */
|
|
+ if (lo < visibleRunCount)
|
|
+ return visibleRuns[lo].ax_start;
|
|
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
|
+ return last->ax_start + last->ax_length;
|
|
+ } /* @synchronized */
|
|
+}
|
|
+
|
|
+/* Convert accessibility string index to buffer charpos.
|
|
+ Safe to call from any thread: uses only cachedText (NSString) and
|
|
+ visibleRuns — no Lisp calls. */
|
|
+- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx
|
|
+{
|
|
+ /* May be called from AX server thread — synchronize. */
|
|
+ @synchronized (self)
|
|
+ {
|
|
+ if (visibleRunCount == 0)
|
|
+ return cachedTextStart;
|
|
+
|
|
+ /* Binary search: runs are sorted by ax_start (ascending). */
|
|
+ NSUInteger lo = 0, hi = visibleRunCount;
|
|
+ while (lo < hi)
|
|
+ {
|
|
+ NSUInteger mid = lo + (hi - lo) / 2;
|
|
+ ns_ax_visible_run *r = &visibleRuns[mid];
|
|
+ if (ax_idx < r->ax_start)
|
|
+ hi = mid;
|
|
+ else if (ax_idx >= r->ax_start + r->ax_length)
|
|
+ lo = mid + 1;
|
|
+ else
|
|
+ {
|
|
+ /* Found: ax_idx is inside this run.
|
|
+ Fast path for pure-ASCII runs: ax_length == length means
|
|
+ every Emacs charpos maps to exactly one AX string index.
|
|
+ The conversion is then O(1) instead of O(cursor_position).
|
|
+ Buffers with emoji, CJK, or other non-BMP characters use
|
|
+ the slow path (composed character sequence walk), which is
|
|
+ bounded by run length, not total buffer size. */
|
|
+ if (r->ax_length == (NSUInteger) r->length)
|
|
+ return r->charpos + (ptrdiff_t) (ax_idx - r->ax_start);
|
|
+
|
|
+ if (!cachedText)
|
|
+ return r->charpos;
|
|
+ NSUInteger scan = r->ax_start;
|
|
+ ptrdiff_t cp = r->charpos;
|
|
+ while (scan < ax_idx)
|
|
+ {
|
|
+ NSRange seq = [cachedText
|
|
+ rangeOfComposedCharacterSequenceAtIndex:scan];
|
|
+ scan = NSMaxRange (seq);
|
|
+ cp++;
|
|
+ }
|
|
+ return cp;
|
|
+ }
|
|
+ }
|
|
+ /* Past end — return last charpos. */
|
|
+ if (lo > 0)
|
|
+ {
|
|
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
|
+ return last->charpos + last->length;
|
|
+ }
|
|
+ return cachedTextStart;
|
|
+ } /* @synchronized */
|
|
+}
|
|
+
|
|
+/* --- Threading and signal safety ---
|
|
+
|
|
+ AX getter methods may be called from the VoiceOver server thread.
|
|
+ All methods that access Lisp objects or buffer state dispatch_sync
|
|
+ to the main thread where Emacs state is consistent.
|
|
+
|
|
+ Longjmp safety: Lisp functions called inside dispatch_sync blocks
|
|
+ (Fget_char_property, Fbuffer_substring_no_properties, etc.) could
|
|
+ theoretically signal and longjmp through the dispatch_sync frame,
|
|
+ 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.
|
|
+ 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
|
|
+ running between precondition checks and Lisp calls.
|
|
+ 4. Buffer positions are clamped to BUF_BEGV/BUF_ZV before
|
|
+ use, preventing out-of-range signals.
|
|
+ 5. specpdl unwind protection ensures block_input is always
|
|
+ matched by unblock_input, even on longjmp.
|
|
+
|
|
+ This matches the safety model of existing nsterm.m F-function
|
|
+ calls (24 direct calls, none wrapped in internal_condition_case).
|
|
+
|
|
+ Known gap: if the Emacs window tree is modified between redisplay
|
|
+ cycles in a way that invalidates validWindow's cached result,
|
|
+ a stale dereference could occur. In practice this does not happen
|
|
+ because window tree modifications go through the event loop which
|
|
+ we are blocking via dispatch_sync. */
|
|
+
|
|
+/* ---- NSAccessibility protocol ---- */
|
|
+
|
|
+- (NSAccessibilityRole)accessibilityRole
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSAccessibilityRole result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityRole];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (w && MINI_WINDOW_P (w))
|
|
+ return NSAccessibilityTextFieldRole;
|
|
+ return NSAccessibilityTextAreaRole;
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilityPlaceholderValue
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSString *result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityPlaceholderValue];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w || !MINI_WINDOW_P (w))
|
|
+ return nil;
|
|
+ Lisp_Object prompt = Fminibuffer_prompt ();
|
|
+ if (STRINGP (prompt))
|
|
+ return [NSString stringWithLispString: prompt];
|
|
+ return nil;
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilityRoleDescription
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSString *result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityRoleDescription];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (w && MINI_WINDOW_P (w))
|
|
+ return @"minibuffer";
|
|
+ return @"editor";
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilityLabel
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSString *result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityLabel];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (w && WINDOW_LEAF_P (w))
|
|
+ {
|
|
+ if (MINI_WINDOW_P (w))
|
|
+ return @"Minibuffer";
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (b)
|
|
+ {
|
|
+ Lisp_Object name = BVAR (b, name);
|
|
+ if (STRINGP (name))
|
|
+ return [NSString stringWithLispString:name];
|
|
+ }
|
|
+ }
|
|
+ return @"buffer";
|
|
+}
|
|
+
|
|
+- (BOOL)isAccessibilityFocused
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block BOOL result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self isAccessibilityFocused];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ 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)accessibilityValue
|
|
+{
|
|
+ /* AX getters can be called from any thread by the AT subsystem.
|
|
+ Dispatch to main thread where Emacs buffer state is consistent. */
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block id result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityValue];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ [self ensureTextCache];
|
|
+ return cachedText ? cachedText : @"";
|
|
+}
|
|
+
|
|
+- (NSInteger)accessibilityNumberOfCharacters
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSInteger result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityNumberOfCharacters];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ [self ensureTextCache];
|
|
+ return cachedText ? [cachedText length] : 0;
|
|
+}
|
|
+
|
|
+- (NSString *)accessibilitySelectedText
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSString *result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilitySelectedText];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return @"";
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b || NILP (BVAR (b, mark_active)))
|
|
+ return @"";
|
|
+
|
|
+ NSRange sel = [self accessibilitySelectedTextRange];
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || sel.location == NSNotFound
|
|
+ || sel.location + sel.length > [cachedText length])
|
|
+ return @"";
|
|
+ return [cachedText substringWithRange:sel];
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilitySelectedTextRange
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSRange result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilitySelectedTextRange];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ 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);
|
|
+
|
|
+ [self ensureTextCache];
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:pt];
|
|
+
|
|
+ if (NILP (BVAR (b, mark_active)))
|
|
+ return NSMakeRange (point_idx, 0);
|
|
+
|
|
+ ptrdiff_t mark_pos = marker_position (BVAR (b, mark));
|
|
+ NSUInteger mark_idx = [self accessibilityIndexForCharpos:mark_pos];
|
|
+ NSUInteger start_idx = MIN (point_idx, mark_idx);
|
|
+ NSUInteger end_idx = MAX (point_idx, mark_idx);
|
|
+ return NSMakeRange (start_idx, end_idx - start_idx);
|
|
+}
|
|
+
|
|
+- (void)setAccessibilitySelectedTextRange:(NSRange)range
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ dispatch_async (dispatch_get_main_queue (), ^{
|
|
+ [self setAccessibilitySelectedTextRange:range];
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return;
|
|
+
|
|
+ [self ensureTextCache];
|
|
+
|
|
+ specpdl_ref count = SPECPDL_INDEX ();
|
|
+ record_unwind_current_buffer ();
|
|
+ /* Ensure block_input is always matched by unblock_input even if
|
|
+ Fset_marker or another Lisp call signals (longjmp). */
|
|
+ record_unwind_protect_void (unblock_input);
|
|
+ block_input ();
|
|
+
|
|
+ /* Convert accessibility index to buffer charpos via mapping. */
|
|
+ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location];
|
|
+
|
|
+ /* Clamp to buffer bounds. */
|
|
+ if (charpos < BUF_BEGV (b))
|
|
+ charpos = BUF_BEGV (b);
|
|
+ if (charpos > BUF_ZV (b))
|
|
+ charpos = BUF_ZV (b);
|
|
+
|
|
+ /* Move point directly in the buffer. */
|
|
+ if (b != current_buffer)
|
|
+ set_buffer_internal_1 (b);
|
|
+
|
|
+ SET_PT_BOTH (charpos, CHAR_TO_BYTE (charpos));
|
|
+
|
|
+ /* Always deactivate mark: VoiceOver range.length is an internal
|
|
+ word boundary hint, not a text selection. Activating the mark
|
|
+ makes accessibilitySelectedTextRange return a non-zero length,
|
|
+ which confuses VoiceOver into positioning its browse cursor at
|
|
+ the END of the selection instead of the start. */
|
|
+ bset_mark_active (b, Qnil);
|
|
+ unbind_to (count, Qnil);
|
|
+
|
|
+ /* 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
|
|
+{
|
|
+ if (!flag)
|
|
+ return;
|
|
+
|
|
+ /* VoiceOver may call this from the AX server thread.
|
|
+ All Lisp reads, block_input, and AppKit calls require main. */
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ dispatch_async (dispatch_get_main_queue (), ^{
|
|
+ [self setAccessibilityFocused:flag];
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return;
|
|
+
|
|
+ EmacsView *view = self.emacsView;
|
|
+ if (!view || !view->emacsframe)
|
|
+ return;
|
|
+
|
|
+ /* Use specpdl unwind protection for block_input safety. */
|
|
+ specpdl_ref count = SPECPDL_INDEX ();
|
|
+ record_unwind_protect_void (unblock_input);
|
|
+ block_input ();
|
|
+
|
|
+ /* Select the Emacs window so keyboard focus follows VoiceOver. */
|
|
+ struct frame *f = view->emacsframe;
|
|
+ if (w != XWINDOW (f->selected_window))
|
|
+ Fselect_window (self.lispWindow, Qnil);
|
|
+
|
|
+ /* Raise the frame's NS window to ensure keyboard focus. */
|
|
+ NSWindow *nswin = [view window];
|
|
+ if (nswin && ![nswin isKeyWindow])
|
|
+ [nswin makeKeyAndOrderFront:nil];
|
|
+
|
|
+ unbind_to (count, Qnil);
|
|
+
|
|
+ /* Post SelectedTextChanged so VoiceOver reads the current line
|
|
+ upon entering text interaction mode.
|
|
+ WebKit AXObjectCacheMac fallback enum: SelectionMove = 2. */
|
|
+ NSDictionary *info = @{
|
|
+ @"AXTextStateChangeType":
|
|
+ @(ns_ax_text_state_change_selection_move),
|
|
+ @"AXTextChangeElement": self
|
|
+ };
|
|
+ ns_ax_post_notification_with_info (
|
|
+ self, NSAccessibilitySelectedTextChangedNotification, info);
|
|
+}
|
|
+
|
|
+- (NSInteger)accessibilityInsertionPointLineNumber
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSInteger result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityInsertionPointLineNumber];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w || !WINDOW_LEAF_P (w))
|
|
+ return 0;
|
|
+
|
|
+ struct buffer *b = XBUFFER (w->contents);
|
|
+ if (!b)
|
|
+ return 0;
|
|
+
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText)
|
|
+ return 0;
|
|
+
|
|
+ ptrdiff_t pt = BUF_PT (b);
|
|
+ NSUInteger point_idx = [self accessibilityIndexForCharpos:pt];
|
|
+ if (point_idx > [cachedText length])
|
|
+ point_idx = [cachedText length];
|
|
+
|
|
+ return [self lineForAXIndex:point_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
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSRange result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityRangeForIndex:index];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ [self ensureTextCache];
|
|
+ if (!cachedText || index < 0
|
|
+ || (NSUInteger) index >= [cachedText length])
|
|
+ return NSMakeRange (NSNotFound, 0);
|
|
+ return [cachedText
|
|
+ rangeOfComposedCharacterSequenceAtIndex:(NSUInteger)index];
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityStyleRangeForIndex:(NSInteger)index
|
|
+{
|
|
+ /* Return the range of the current line. A more accurate
|
|
+ implementation would return face/font property boundaries,
|
|
+ but line granularity is acceptable for VoiceOver. */
|
|
+ NSInteger line = [self accessibilityLineForIndex:index];
|
|
+ return [self accessibilityRangeForLine:line];
|
|
+}
|
|
+
|
|
+- (NSRect)accessibilityFrameForRange:(NSRange)range
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSRect result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityFrameForRange:range];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ EmacsView *view = self.emacsView;
|
|
+ if (!w || !view)
|
|
+ return NSZeroRect;
|
|
+ /* Convert ax-index range to charpos range for glyph lookup. */
|
|
+ [self ensureTextCache];
|
|
+ ptrdiff_t cp_start = [self charposForAccessibilityIndex:range.location];
|
|
+ ptrdiff_t cp_end = [self charposForAccessibilityIndex:
|
|
+ range.location + range.length];
|
|
+ return ns_ax_frame_for_range (w, view, cp_start,
|
|
+ cp_end - cp_start);
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSRange result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result
|
|
+ = [self accessibilityRangeForPosition:screenPoint];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ /* Hit test: convert screen point to buffer character index. */
|
|
+ struct window *w = [self validWindow];
|
|
+ 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];
|
|
+
|
|
+ /* 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);
|
|
+
|
|
+ /* Block input to prevent concurrent redisplay from modifying the
|
|
+ glyph matrix while we traverse it. Use specpdl unwind protection
|
|
+ so block_input is always matched by unblock_input, even if
|
|
+ ensureTextCache triggers a Lisp signal (longjmp). */
|
|
+ specpdl_ref count = SPECPDL_INDEX ();
|
|
+ record_unwind_protect_void (unblock_input);
|
|
+ block_input ();
|
|
+
|
|
+ /* 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;
|
|
+ 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));
|
|
+ if ((int) viewPoint.y >= row_top
|
|
+ && (int) viewPoint.y < row_top + row->visible_height)
|
|
+ {
|
|
+ hit_row = row;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!hit_row)
|
|
+ {
|
|
+ unbind_to (count, Qnil);
|
|
+ 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 (x >= glyph_x && x < glyph_x + glyph->pixel_width)
|
|
+ {
|
|
+ best_charpos = glyph->charpos;
|
|
+ break;
|
|
+ }
|
|
+ best_charpos = glyph->charpos;
|
|
+ }
|
|
+ glyph_x += glyph->pixel_width;
|
|
+ }
|
|
+
|
|
+ /* 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];
|
|
+
|
|
+ unbind_to (count, Qnil);
|
|
+ return NSMakeRange (ax_idx, 1);
|
|
+}
|
|
+
|
|
+- (NSRange)accessibilityVisibleCharacterRange
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSRange result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityVisibleCharacterRange];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ /* 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
|
|
+{
|
|
+ if (![NSThread isMainThread])
|
|
+ {
|
|
+ __block NSRect result;
|
|
+ dispatch_sync (dispatch_get_main_queue (), ^{
|
|
+ result = [self accessibilityFrame];
|
|
+ });
|
|
+ return result;
|
|
+ }
|
|
+ struct window *w = [self validWindow];
|
|
+ if (!w)
|
|
+ return NSZeroRect;
|
|
+
|
|
+ /* Subtract mode line height so the buffer element does not overlap it. */
|
|
+ int text_h = w->pixel_height;
|
|
+ if (w->current_matrix)
|
|
+ {
|
|
+ for (int i = w->current_matrix->nrows - 1; i >= 0; i--)
|
|
+ {
|
|
+ struct glyph_row *row = w->current_matrix->rows + i;
|
|
+ if (row->enabled_p && row->mode_line_p)
|
|
+ {
|
|
+ text_h -= row->visible_height;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return [self screenRectFromEmacsX:w->pixel_left
|
|
+ y:w->pixel_top
|
|
+ width:w->pixel_width
|
|
+ height:text_h];
|
|
+}
|
|
+
|
|
+/* ---- Notification dispatch (helper methods) ---- */
|
|
+
|
|
+/* Post NSAccessibilityValueChangedNotification for a text edit.
|
|
+ Called when BUF_MODIFF changes between redisplay cycles. */
|
|
+
|
|
+@end
|
|
+
|
|
#endif /* NS_IMPL_COCOA */
|
|
|
|
|
|
--
|
|
2.43.0
|
|
|