The Qnil initialization was in patch 0000 (Zoom) but the ivar
declaration is in patch 0008 (child frame tracking). Moved the
init to patch 0008 so each patch compiles independently.
Three fixes:
1. Patch 0000 now compiles standalone: replaced forward declaration
of ns_ax_face_is_selected (defined in VoiceOver patches) with
self-contained ns_zoom_face_is_selected in the Zoom patch.
2. ns_accessibility_enabled defaults to nil: eliminates ALL VoiceOver
overhead (text cache rebuild, AX notifications, Mach IPC to AX
server) when VoiceOver is not in use. Zero per-redisplay cost.
Enable with (setq ns-accessibility-enabled t).
3. UAZoomEnabled() cached for 1s + ns_zoom_track_completion rate-
limited to 2Hz: eliminates 150-600µs/frame of IPC overhead.
Root cause (per Opus analysis): UAZoomEnabled() is a synchronous
Mach IPC roundtrip to macOS Accessibility server, called 3x per
redisplay cycle. At 60fps = 180 IPC roundtrips/second blocking the
main thread. Combined with Emacs's inherent O(position) redisplay
cost, this compounded into progressive choppy behavior.
Fix 1: ns_zoom_enabled_p() caches UAZoomEnabled() for 1 second.
Fix 2: ns_zoom_track_completion() rate-limited to 2 Hz.
Also includes BUF_CHARS_MODIFF fix (patch 0009) for VoiceOver cache.
BUF_CHARS_MODIFF fix — the core performance regression:
ensureTextCache checked BUF_MODIFF which font-lock bumps on every
redisplay. Each cursor movement in a large file triggered full buffer
rebuild. Now uses BUF_CHARS_MODIFF (changes only on char insert/delete).
Performance issue: editing large files (>~10KB, >2000 lines) caused
progressive slowdown regardless of VoiceOver status.
Root causes:
1. ns_zoom_find_overlay_candidate_line: called Foverlays_in on the
entire visible buffer range on every redisplay when UAZoomEnabled().
In files with many overlays (font-lock, hl-line, show-paren etc.)
this was O(overlays) Lisp work per keystroke.
2. postAccessibilityNotificationsForFrame: when ns-accessibility-enabled
is non-nil, checked BUF_OVERLAY_MODIFF every redisplay. font-lock
bumps this on every redraw, triggering ns_ax_selected_overlay_text
(another O(overlays) scan) for non-minibuffer windows.
Fix: Both scans now guard with MINI_WINDOW_P check. Overlay completion
frameworks (Vertico, Icomplete, Ivy) only display candidates in
minibuffer windows --- no completion framework puts selected-face
overlays in normal editing buffers. For non-minibuffer windows both
functions return immediately with zero Lisp calls.
Additionally: ns_zoom_find_child_frame_candidate is skipped when
f->child_frame_list is nil (no child frames = no Corfu popup).
Zoom patch 0000 now tracks completion candidates:
- Overlay: Vertico, Icomplete, Ivy (face heuristic on before-string)
- Child frame: Corfu, Company-box (scan buffer text for selected face)
Also fixes duplicate lastCursorRect ivar when applied with VoiceOver.
Zoom (0000) declares lastCursorRect @public in EmacsView.
VoiceOver (0005) was re-declaring it, causing 'duplicate member'
compiler error when both applied together. Removed the duplicate.
VoiceOver patches 0001-0008 now apply cleanly on top of Zoom patch
0000. The full set (git am patches/000*.patch) works without
conflicts. Patch 0005 (integration) merges Zoom fallback and
VoiceOver postAccessibilityUpdates in ns_update_end.
The ivar was declared in patch 0001 but first used in patch 0005,
creating dead code in intermediate commits 0001-0004. Now each
commit only introduces declarations that are immediately used.
Fixes from Opus maintainer review:
1. [BLOCKER] Zoom code completely removed from ALL intermediate patches
(0005-0007 no longer have UAZoom/overlayZoom at any commit point)
2. [BLOCKER] Unified cursor rect ivar: lastCursorRect (was split
between lastZoomCursorRect and lastAccessibilityCursorRect)
3. [HIGH] Child frame static vars moved to EmacsView ivars
(childFrameLastCandidate/Buffer/Modiff — no cross-frame interference)
4. [HIGH] intern_c_string replaced with Qbefore_string/Qafter_string
5. [MEDIUM] Zoom fallback gated by zoomCursorUpdated flag (no double call)
Major changes:
1. Zoom separated into standalone patch 0000
- UAZoomChangeFocus in ns_draw_window_cursor
- Fallback in ns_update_end for window-switch tracking
- No overlayZoomActive (source of split/switch/move bug)
2. VoiceOver patches 0001-0008 are now Zoom-free
- All UAZoom*, overlayZoom*, kUAZoomFocus references removed
- lastAccessibilityCursorRect kept for VoiceOver bounds queries
- Commit messages cleaned of Zoom references
3. README.txt and TESTING.txt rewritten for new structure
Addresses reviewer (Stéphane Marks) feedback:
- Keep Zoom patch separate from VoiceOver work
- Design discussion needed for non-Zoom patches
- Performance: ns-accessibility-enabled=nil for zero overhead
BLOCKER fixes:
1. Remove duplicate ns_ax_face_is_selected, ns_ax_selected_overlay_text,
ns_ax_selected_child_frame_text definitions from patch 0002
(now defined only in 0007/0008 where they belong)
2. Fix idx → point_idx in accessibilityInsertionPointLineNumber (0002)
3. Remove stale 100K cap reference from documentation (0006)
Architecture fix:
- ns_ax_selected_child_frame_text moved from 0007 to 0008
(where it logically belongs)
Verified: all 8 patches apply cleanly on fresh emacs HEAD.
- 0001: remove NS_AX_TEXT_CAP (100K char cap), add lineStartOffsets/
lineCount ivars and method declarations to nsterm.h
- 0002: add lineForAXIndex:/rangeForLine: O(log L) helpers, build line
index in ensureTextCache, replace O(L) line scanning in
accessibilityInsertionPointLineNumber/accessibilityLineForIndex/
accessibilityRangeForLine, free index in invalidateTextCache/dealloc
- 0009 deleted (folded into 0001+0002)
- README.txt: remove NS_AX_TEXT_CAP references, update known
limitations, stress test threshold 50K lines
Split VoiceOver accessibility into 4 logical patches:
0001: Base classes + text extraction (+753)
0002: Buffer/ModeLine/InteractiveSpan implementations (+1716)
0003: EmacsView integration + cursor tracking (+395)
0004: Documentation with known limitations (+75)
Each patch is self-contained: 0001 adds infrastructure that compiles
but doesn't change behavior. 0002 adds protocol implementations.
0003 wires everything into EmacsView. 0004 documents for users.
All patches verified: apply cleanly to current Emacs master,
final state identical to original monolithic patch.