patches: review fixes — specpdl protection, overlay_modiff tracking, binary search, enum cleanup
M1: accessibilityRangeForPosition uses specpdl unwind protection for
block_input/unblock_input (consistent with all other methods).
M2: Track BUF_OVERLAY_MODIFF in ensureTextCache — overlay-only changes
(timer-based completion highlight) now invalidate the text cache.
M3: Detect narrowing/widening by comparing cachedTextStart vs BUF_BEGV.
m1: Binary search (O(log n)) for visible runs in both
accessibilityIndexForCharpos and charposForAccessibilityIndex.
m3: Add EmacsAXSpanTypeNone = -1 to enum instead of (EmacsAXSpanType)-1 cast.
m5: Add TODO comment in ns_ax_mode_line_text about non-CHAR_GLYPH limitation.
README: Remove resolved overlay_modiff limitation, document binary search
and narrowing detection, update architecture section.
This commit is contained in:
@@ -88,7 +88,7 @@ Lisp calls, read only immutable NSString and scalar cache).
|
||||
etc/NEWS | 11 +
|
||||
src/nsterm.h | 108 ++
|
||||
src/nsterm.m | 2870 +++++++++++++++++++++++++++++++++++++++++++++++---
|
||||
3 files changed, 2960 insertions(+), 149 deletions(-)
|
||||
3 files changed, 2987 insertions(+), 149 deletions(-)
|
||||
|
||||
diff --git a/etc/NEWS b/etc/NEWS
|
||||
index 7367e3cc..0e4480ad 100644
|
||||
@@ -160,6 +160,7 @@ index 7c1ee4cf..542e7d59 100644
|
||||
+}
|
||||
+@property (nonatomic, retain) NSString *cachedText;
|
||||
+@property (nonatomic, assign) ptrdiff_t cachedTextModiff;
|
||||
+@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
|
||||
+@property (nonatomic, assign) ptrdiff_t cachedTextStart;
|
||||
+@property (nonatomic, assign) ptrdiff_t cachedModiff;
|
||||
+@property (nonatomic, assign) ptrdiff_t cachedPoint;
|
||||
@@ -181,6 +182,7 @@ index 7c1ee4cf..542e7d59 100644
|
||||
+/* Span types for interactive AX child elements. */
|
||||
+typedef NS_ENUM (NSInteger, EmacsAXSpanType)
|
||||
+{
|
||||
+ EmacsAXSpanTypeNone = -1,
|
||||
+ EmacsAXSpanTypeButton = 0,
|
||||
+ EmacsAXSpanTypeLink = 1,
|
||||
+ EmacsAXSpanTypeCompletionItem = 2,
|
||||
@@ -496,6 +498,10 @@ index 932d209f..ea2de6f2 100644
|
||||
+
|
||||
+/* ---- Helper: extract mode line text from glyph rows ---- */
|
||||
+
|
||||
+/* TODO: Only CHAR_GLYPH characters (>= 32) are extracted. Image
|
||||
+ glyphs, stretch glyphs, and composed glyphs are silently skipped.
|
||||
+ Mode lines using icon fonts (e.g. doom-modeline with nerd-font)
|
||||
+ will produce incomplete accessibility text. */
|
||||
+static NSString *
|
||||
+ns_ax_mode_line_text (struct window *w)
|
||||
{
|
||||
@@ -1018,7 +1024,7 @@ index 932d209f..ea2de6f2 100644
|
||||
+ while (pos < vis_end)
|
||||
+ {
|
||||
+ Lisp_Object plist = Ftext_properties_at (make_fixnum (pos), buf_obj);
|
||||
+ EmacsAXSpanType span_type = (EmacsAXSpanType) -1;
|
||||
+ EmacsAXSpanType span_type = EmacsAXSpanTypeNone;
|
||||
+ Lisp_Object limit_prop = Qnil;
|
||||
+
|
||||
+ if (!NILP (Fplist_get (plist, Qns_ax_widget, Qnil)))
|
||||
@@ -1070,7 +1076,7 @@ index 932d209f..ea2de6f2 100644
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if ((NSInteger) span_type == -1)
|
||||
+ if (span_type == EmacsAXSpanTypeNone)
|
||||
+ {
|
||||
+ /* Skip to the next position where any interactive property
|
||||
+ changes. Try each scannable property in turn and take
|
||||
@@ -1379,6 +1385,7 @@ index 932d209f..ea2de6f2 100644
|
||||
+@implementation EmacsAccessibilityBuffer
|
||||
+@synthesize cachedText;
|
||||
+@synthesize cachedTextModiff;
|
||||
+@synthesize cachedOverlayModiff;
|
||||
+@synthesize cachedTextStart;
|
||||
+@synthesize cachedModiff;
|
||||
+@synthesize cachedPoint;
|
||||
@@ -1433,13 +1440,17 @@ index 932d209f..ea2de6f2 100644
|
||||
+ 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;
|
||||
+ /* TODO: Also track BUF_OVERLAY_MODIFF to catch overlay-only
|
||||
+ /* Track both BUF_MODIFF and BUF_OVERLAY_MODIFF. Overlay-only
|
||||
+ changes (e.g., timer-based completion highlight move without
|
||||
+ point change). Currently, overlay changes without text edits
|
||||
+ are detected only when point also moves. */
|
||||
+ text edit) bump overlay_modiff but not modiff. Also detect
|
||||
+ narrowing/widening which changes BUF_BEGV without bumping
|
||||
+ either modiff counter. */
|
||||
+ if (cachedText && cachedTextModiff == modiff
|
||||
+ && cachedOverlayModiff == overlay_modiff
|
||||
+ && cachedTextStart == BUF_BEGV (b)
|
||||
+ && pt >= cachedTextStart
|
||||
+ && (textLen == 0
|
||||
+ || [self accessibilityIndexForCharpos:pt] <= textLen))
|
||||
@@ -1455,6 +1466,7 @@ index 932d209f..ea2de6f2 100644
|
||||
+ [cachedText release];
|
||||
+ cachedText = [text retain];
|
||||
+ cachedTextModiff = modiff;
|
||||
+ cachedOverlayModiff = overlay_modiff;
|
||||
+ cachedTextStart = start;
|
||||
+
|
||||
+ if (visibleRuns)
|
||||
@@ -1474,22 +1486,31 @@ index 932d209f..ea2de6f2 100644
|
||||
+ thread invalidates the text cache concurrently. */
|
||||
+ @synchronized (self)
|
||||
+ {
|
||||
+ for (NSUInteger i = 0; i < visibleRunCount; i++)
|
||||
+ 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)
|
||||
+ {
|
||||
+ ns_ax_visible_run *r = &visibleRuns[i];
|
||||
+ if (charpos >= r->charpos && charpos < r->charpos + r->length)
|
||||
+ 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
|
||||
+ {
|
||||
+ /* Compute UTF-16 delta inside this run directly from cachedText
|
||||
+ (an NSString built on the main thread) — no Lisp calls needed. */
|
||||
+ /* Found: charpos is inside this run. Compute UTF-16 delta
|
||||
+ directly from cachedText — no Lisp calls needed. */
|
||||
+ NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
|
||||
+ if (chars_in == 0 || !cachedText)
|
||||
+ return r->ax_start;
|
||||
+ /* ax_start + UTF-16 units for the first chars_in chars of the run. */
|
||||
+ NSUInteger run_end_ax = r->ax_start + r->ax_length;
|
||||
+ NSUInteger scan = r->ax_start;
|
||||
+ /* Each visible Emacs char maps to 1 or 2 UTF-16 units.
|
||||
+ Walk the NSString using rangeOfComposedCharacterSequenceAtIndex
|
||||
+ which handles surrogates correctly. */
|
||||
+ for (NSUInteger c = 0; c < chars_in && scan < run_end_ax; c++)
|
||||
+ {
|
||||
+ NSRange seq = [cachedText
|
||||
@@ -1498,18 +1519,12 @@ index 932d209f..ea2de6f2 100644
|
||||
+ }
|
||||
+ return (scan <= run_end_ax) ? scan : run_end_ax;
|
||||
+ }
|
||||
+ /* If charpos falls in an invisible gap before the next run,
|
||||
+ map it to the start of the next visible run. */
|
||||
+ if (charpos < r->charpos)
|
||||
+ return r->ax_start;
|
||||
+ }
|
||||
+ /* Past end — return total length. */
|
||||
+ if (visibleRunCount > 0)
|
||||
+ {
|
||||
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
||||
+ return last->ax_start + last->ax_length;
|
||||
+ }
|
||||
+ return 0;
|
||||
+ /* 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 */
|
||||
+}
|
||||
+
|
||||
@@ -1521,17 +1536,25 @@ index 932d209f..ea2de6f2 100644
|
||||
+ /* May be called from AX server thread — synchronize. */
|
||||
+ @synchronized (self)
|
||||
+ {
|
||||
+ for (NSUInteger i = 0; i < visibleRunCount; i++)
|
||||
+ if (visibleRunCount == 0)
|
||||
+ return cachedTextStart;
|
||||
+
|
||||
+ /* Binary search: runs are sorted by ax_start (ascending). */
|
||||
+ NSUInteger lo = 0, hi = visibleRunCount;
|
||||
+ while (lo < hi)
|
||||
+ {
|
||||
+ ns_ax_visible_run *r = &visibleRuns[i];
|
||||
+ if (ax_idx >= r->ax_start
|
||||
+ && ax_idx < r->ax_start + r->ax_length)
|
||||
+ 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. Walk composed character
|
||||
+ sequences to count Emacs characters up to ax_idx. */
|
||||
+ if (!cachedText)
|
||||
+ return r->charpos;
|
||||
+
|
||||
+ /* Walk forward through NSString composed character sequences to
|
||||
+ count Emacs characters (= composed sequences) up to ax_idx. */
|
||||
+ NSUInteger scan = r->ax_start;
|
||||
+ ptrdiff_t cp = r->charpos;
|
||||
+ while (scan < ax_idx)
|
||||
@@ -1545,7 +1568,7 @@ index 932d209f..ea2de6f2 100644
|
||||
+ }
|
||||
+ }
|
||||
+ /* Past end — return last charpos. */
|
||||
+ if (visibleRunCount > 0)
|
||||
+ if (lo > 0)
|
||||
+ {
|
||||
+ ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
|
||||
+ return last->charpos + last->length;
|
||||
@@ -2078,7 +2101,11 @@ index 932d209f..ea2de6f2 100644
|
||||
+ return NSMakeRange (0, 0);
|
||||
+
|
||||
+ /* Block input to prevent concurrent redisplay from modifying the
|
||||
+ glyph matrix while we traverse it. */
|
||||
+ 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. */
|
||||
@@ -2101,7 +2128,7 @@ index 932d209f..ea2de6f2 100644
|
||||
+
|
||||
+ if (!hit_row)
|
||||
+ {
|
||||
+ unblock_input ();
|
||||
+ unbind_to (count, Qnil);
|
||||
+ return NSMakeRange (0, 0);
|
||||
+ }
|
||||
+
|
||||
@@ -2131,7 +2158,7 @@ index 932d209f..ea2de6f2 100644
|
||||
+ if (cachedText && ax_idx > [cachedText length])
|
||||
+ ax_idx = [cachedText length];
|
||||
+
|
||||
+ unblock_input ();
|
||||
+ unbind_to (count, Qnil);
|
||||
+ return NSMakeRange (ax_idx, 1);
|
||||
+}
|
||||
+
|
||||
|
||||
Reference in New Issue
Block a user