17 Commits

Author SHA1 Message Date
ad868e0fab patches: fix 0002 bloated commit message (15MB git log artifact)
The original commit a4adced9b5 had its commit message accidentally
polluted with the entire Emacs git log (~15MB).  The cherry-pick script
copied this bloat into the new 0002 patch.  Regenerated 0002 with a
clean ChangeLog-only commit message (1385 bytes).
2026-03-04 18:18:10 +01:00
2ab4468ca0 patches: address review B1-B4 and N1,N3
B4: Shorten all subject lines to <=50 chars (preference from CONTRIBUTE).
B3: Fix intermediate BUF_CHARS_MODIFF state in 0002: use BUF_MODIFF
    from the start, eliminating the wrong-then-corrected pattern across
    patches 0002+0007.
B2: Wrap long NEWS line in Zoom entry (was 80 chars, now <=79).
B1: Long block_input comment already fixed by 0004 in both branches;
    confirmed no change needed in final state.
N1: Fix DEFVAR doc in 0001 to reflect auto-detection at startup.
N3: Convert VoiceOver NEWS bullet list to prose paragraphs.
N2: Skip (existing file uses 80-char separators throughout; changing
    only new ones would be inconsistent).
2026-03-04 15:28:09 +01:00
Martin Sukany
61f629350c fix: removed position stuff from agenda movement - caused VoiceOver issues 2026-03-04 14:22:16 +01:00
f78da08e6b patches: fix 0008 hunk headers and context mismatches
Two applicability fixes:
- Last hunk (@@ -12823): header said old=14 new=71 but actual content
  (before the -- 2.43.0 footer) is old=11 new=69.  Correct the counts.
- Hunk @@ -9243: context showed block_input before specpdl_ref count2,
  but patch 0003 introduces them with specpdl_ref first.  Swap context
  lines to match actual post-0003 state.

Verified: git am applies all 9 patches cleanly on Emacs f8d9ecb.
2026-03-04 14:13:55 +01:00
d5b5d5301d patches: fix 0004 hunk context for accessibilityRangeForPosition:
Patch 0002 introduces accessibilityRangeForPosition: with the correct
block_input-before-record_unwind ordering.  Patch 0004 was trying to
fix the ordering again (expecting wrong-order context), causing git am
to fail at 0005 with 'patch does not apply'.

Fix: remove the ordering swap from the 0004 hunk; retain only the
comment improvement.  The context now correctly reflects the state
after 0002 applies (block_input followed by record_unwind_protect_void).
2026-03-04 14:05:44 +01:00
fe0a0181d3 patches: fix if(candidate) block indentation in 0008 (I1)
The if(candidate) block in postAccessibilityNotificationsForFrame:
had its opening brace at col 8 (tab) while the if itself is at
col 10 (tab+2), violating GNU coding style.  Two consecutive
closing braces at col 8 made nesting ambiguous on inspection.

Correct indentation (all +4 columns throughout the block):
  - if(candidate) brace: tab+4 = col 12
  - Body (/* Deduplicate */, if(![...]): tab+6 = col 14
  - Inner if(![...]) brace: 2 tabs = col 16
  - Inner if body: tab+tab+2 = col 18
  - Inner closing brace: 2 tabs = col 16
  - if(candidate) closing brace: tab+4 = col 12

New indentation exactly matches the original deleted code (same
method, before our refactor), confirming it follows the pre-existing
style in this file.
2026-03-04 13:53:05 +01:00
6dd3c9bfe4 patches: fix if(candidate) block indentation in 0008
The if(candidate) block's braces and body were not consistently
indented relative to the enclosing if(MINI_WINDOW_P) block.
Change context lines to deletion+addition pairs to correct:
- Opening brace: tab -> tab+4 (col 8 -> col 12)
- Body comment and inner if: tab+2 -> tab+6 (col 10 -> col 14)
- Inner if brace: tab+4 -> tab+6 (col 12 -> col 14)
- Inner and outer closing braces: tab+4->tab+6, tab->tab+4

This is a pre-existing indentation inconsistency that violates
GNU coding style; the if(candidate) check was at col 10 but its
brace was at col 8.
2026-03-04 13:50:45 +01:00
5f18d727e1 patches: fix em-dash and SPECPDL_INDEX ordering
Replace all Unicode em-dashes (---) with ASCII triple-dash (---)
across 0001-0007 (39 occurrences). GNU Emacs source files must be
ASCII; em-dash in comments would cause encoding issues on some systems.

Fix SPECPDL_INDEX() call order in postCompletionAnnouncementForBuffer:
(0003): specpdl_ref must be taken BEFORE block_input() so the snapshot
captures the pre-block_input specpdl depth. This matches the pattern
established in patches 0001 and 0002.
2026-03-04 13:38:54 +01:00
2b7c7abb48 patches: fix if-block indentation in 0008 (B5 fix cleanup)
The B5 goto-replacement introduced indentation inconsistency:
opening/closing braces were at tab+2, body at 6 spaces. The original
code (ns_ax_face_is_selected deletion) used tab for braces and tab+2
for body. Align the new if-block to match existing style:
- Opening brace: tab (was tab+2)
- Body lines (int selected_line, NSString *candidate, if(candidate)):
  tab+2 (was 6 spaces after partial fix)
- Continuation of NSString declaration: tab+4 (was tab)
- Closing brace: tab (was tab+2)
2026-03-04 13:37:39 +01:00
f51da9f8d4 patches: fix I2 body indent and I3 childFrameLastCandidate type
I2: Re-indent body of MINI_WINDOW_P if-block in 0008 from 6 spaces to
8 spaces, consistent with GNU style (if at 6 spaces, body at 8).

I3: Change childFrameLastCandidate from char* to NSString*.
Replaces xstrdup/xfree/strcmp with ObjC retain/release/isEqualToString:,
which is consistent with surrounding ObjC code style and avoids
heap allocation with xmalloc.
2026-03-04 13:35:54 +01:00
9ad01c03fd patches: re-fix B6 nlines < 128 -> 512 (lost in revert)
Fix was accidentally reverted when reverting the broken B5 goto fix.
Both line_starts[512]/line_ends[512] arrays and the main while loop
guard use 512; the trailing-line check must also use 512 to avoid
silently dropping the last line in buffers with 128-511 lines.
2026-03-04 13:32:24 +01:00
c418db05dc patches: fix all round-1 blockers (B1 B3 B5 B6)
B1 (0007): Fix dangling comment - change context line to deletion so
the opening comment of the removed ns_ax_face_is_selected function
is properly deleted, not left as an unclosed comment fragment that
would cause a compilation failure.

B3 (0002): Fix block_input/record_unwind_protect_void ordering in
two places in accessibilityRangeForPosition: and related method.
Correct order: block_input() BEFORE record_unwind_protect_void(),
so the unwind handler cannot call unblock_input without a matching
block_input even if specpdl_push fails.

B5 (0008): Replace goto skip_overlay_scan with proper if-block.
The goto skipped over ObjC variable declarations which is poor style
and would be flagged by Emacs maintainers.  Restructure as
if (MINI_WINDOW_P (w) && !didTextChange) { ... }.

B6 (0008): Fix nlines < 128 to nlines < 512 in ns_ax_selected_child_
frame_text.  Arrays line_starts[] and line_ends[] are declared with
size 512; the trailing-line guard used 128, silently dropping the
last line for buffers with 128-511 lines.
2026-03-04 13:29:55 +01:00
ea9d231177 Revert "patches: fix B5 - replace goto skip_overlay_scan with if-block"
This reverts commit a69aec96d8.
2026-03-04 13:26:20 +01:00
a69aec96d8 patches: fix B5 - replace goto skip_overlay_scan with if-block 2026-03-04 13:21:49 +01:00
97c14a3bd9 patches: fix all review issues from Opus core maintainer review
BLOCKERS fixed:
- #1: add missing skip_overlay_scan: label (goto without target)
- #2: move accessibilityLineForIndex: to patch 0002 (used in 0002/0003,
  was defined in 0007 — forward reference crash risk)
- #3: move BOOL singleLineMove to patch 0008 (declared in 0007, unused
  until 0008 — -Werror build failure)

WARNINGS fixed:
- #1: block_input ordering at 5 sites (record_unwind_protect_void must
  come AFTER block_input to avoid unmatched unblock_input on error)
- #2: deduplicate ns_ax_face_is_selected (move to patch 0001, remove
  from patch 0007 where it was duplicated)
- #4: improve childFrameLastBuffer comment (document rename edge case)
- #5: complete ChangeLog for patch 0007 (add 3 missing method entries)
- #6: document lastSelectedWindow/lastRootWindow GC safety in nsterm.h

MINOR fixed:
- #2: wrap ns_zoom_track_completion call in MAC_OS_X_VERSION >= 101000
  guard (consistent with the function definition)
- #3: @cindex Zoom verified present in VoiceOver section (already OK)
- #4: raise line_starts/line_ends bound from 128 to 512 (consistent
  with ns_ax_selected_overlay_text)
- #5: add echo_area_buffer[] lifetime comment referencing xdisp.c

All 9 patches apply cleanly (git apply verified sequentially).
2026-03-03 20:43:56 +01:00
f42e799991 patches: fix Blocker #2 - remove ensureTextCache from notification path
BUF_MODIFF was needed for fold/unfold correctness (org-mode), but
ensureTextCache was called from postAccessibilityNotificationsForFrame:
on every cursor move, causing O(buffer-size) rebuild on every font-lock
pass.

Fix: remove [self ensureTextCache] from the granularity-detection branch
of the notification path.  The granularity detection now uses cachedText
directly, falling back to granularity_unknown when absent (safe: VoiceOver
makes its own determination).  ensureTextCache is called exclusively from
AX getters (human interaction speed).  Font-lock passes no longer trigger
any cache rebuild.

The BUF_MODIFF validity in ensureTextCache is retained (correct for
fold/unfold). Comment updated to accurately describe the calling pattern.

All 9 patches apply cleanly on fresh base (git apply verified).
2026-03-03 18:26:17 +01:00
70f0cb9a86 patches: fix all safe pre-submission issues
- Blocker #1: add BOOL emacsMovedCursor = YES in patch 0005 so series
  compiles standalone; patch 0008 replaces it with !voiceoverSetPoint
- Blocker #3: change all Daneel authorship to Martin Sukany
- Zoom gate: remove ns_accessibility_enabled guards from Zoom code
  paths (0005 no longer adds them; 0008 retains the clarifying comment)
- eassert: remove redundant BUFFER_LIVE_P eassert with contradictory
  comment in patch 0008
- macos.texi: integrate orphan 'Block-style cursors' paragraph as
  @item in the Known Limitations list
- cindex: restore @cindex Zoom, cursor tracking (macOS) removed in 0008
- ChangeLog 0002: list only functions actually added in that patch
- ChangeLog 0008: accurate description (remove wrong BUF_CHARS_MODIFF
  claim for ensureTextCache; ns_ax_buffer_text block_input was in 0001)
- git apply --check: all 9 patches apply cleanly on fresh base

Remaining open issue: BUF_MODIFF regression in patch 0007 (ensureTextCache
O(N) rebuild per font-lock pass) requires design decision before
submission.
2026-03-03 17:50:18 +01:00
11 changed files with 2657 additions and 550 deletions

View File

@@ -423,29 +423,6 @@ Skip for beamer exports — beamer uses adjustbox on plain tabular."
;;; ORG MODE — CUSTOM BEHAVIOR
;;; ============================================================
;; Agenda: position cursor at task name (after TODO keyword and priority)
(defun my/org-agenda-all-keywords ()
"Return list of all org todo keyword strings (without shortcut suffixes)."
(let (result)
(dolist (seq org-todo-keywords result)
(dolist (kw (cdr seq))
(unless (equal kw "|")
(push (replace-regexp-in-string "(.*" "" kw) result))))))
(defun my/org-agenda-goto-task-name (&rest _)
"Move cursor to the task name on the current org-agenda line."
(when (get-text-property (line-beginning-position) 'org-hd-marker)
(beginning-of-line)
(let* ((eol (line-end-position))
(kw-re (regexp-opt (my/org-agenda-all-keywords) 'words)))
(when (re-search-forward kw-re eol t)
(skip-chars-forward " \t")
(when (looking-at "\\[#.\\][ \t]+")
(goto-char (match-end 0)))))))
(advice-add 'org-agenda-next-line :after #'my/org-agenda-goto-task-name)
(advice-add 'org-agenda-previous-line :after #'my/org-agenda-goto-task-name)
;; Also trigger on post-command-hook in agenda buffers (catches Evil j/k,
;; super-agenda navigation, and any other motion commands)
(add-hook 'org-agenda-mode-hook

2127
flycheck_config.el Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
From 0470786c91eb4a464d8580387b83a4a8d4e4f8eb Mon Sep 17 00:00:00 2001
From 2bce9ba4ad500eabad619e684ba319b58f9b1fca Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 22:39:35 +0100
Subject: [PATCH] ns: integrate with macOS Zoom for cursor tracking
Date: Wed, 4 Mar 2026 15:23:53 +0100
Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking
Inform macOS Zoom of the text cursor position so the zoomed viewport
follows keyboard focus in Emacs. Also track completion candidates so
@@ -28,8 +28,8 @@ to the selected completion candidate after normal cursor tracking.
---
etc/NEWS | 11 ++
src/nsterm.h | 6 +
src/nsterm.m | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 371 insertions(+)
src/nsterm.m | 366 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 383 insertions(+)
diff --git a/etc/NEWS b/etc/NEWS
index 7367e3ccbd..4c149e41d6 100644
@@ -71,7 +71,7 @@ index 7c1ee4cf53..ea6e7ba4f5 100644
}
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209f56..88c9251c18 100644
index 932d209f56..6333a7253a 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -71,6 +71,11 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -86,7 +86,7 @@ index 932d209f56..88c9251c18 100644
#endif
static EmacsMenu *dockMenu;
@@ -1081,6 +1086,284 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
@@ -1081,6 +1086,293 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
}
@@ -103,9 +103,9 @@ index 932d209f56..88c9251c18 100644
+
+/* Cached wrapper around ns_zoom_enabled_p ().
+ ns_zoom_enabled_p () performs a synchronous Mach IPC roundtrip to the
+ macOS Accessibility server (~50-200 uss per call). With call sites
+ macOS Accessibility server (~50-200 µs per call). With call sites
+ in ns_draw_window_cursor, ns_update_end, and ns_zoom_track_completion,
+ the overhead accumulates to ~150-600 uss per redisplay cycle. Zoom
+ the overhead accumulates to ~150-600 µs per redisplay cycle. Zoom
+ state changes only on explicit user action in System Settings, so a
+ 1-second TTL is safe and indistinguishable from querying every frame.
+ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */
@@ -127,13 +127,9 @@ index 932d209f56..88c9251c18 100644
+/* Identify faces that mark a selected completion candidate.
+ Matches vertico-current, corfu-current, icomplete-selected-match,
+ ivy-current-match, etc. by checking the face symbol name.
+ Defined here so the Zoom patch compiles independently of the
+ VoiceOver patches. */
+ Shared by both Zoom cursor tracking and VoiceOver accessibility. */
+static bool
+/* Note: ns_ax_face_is_selected in the VoiceOver series has identical
+ logic. The duplication is intentional: both series are
+ independent and must compile standalone. */
+ns_zoom_face_is_selected (Lisp_Object face)
+ns_face_name_matches_selected_p (Lisp_Object face)
+{
+ if (SYMBOLP (face))
+ {
@@ -146,7 +142,7 @@ index 932d209f56..88c9251c18 100644
+ {
+ Lisp_Object tail;
+ for (tail = face; CONSP (tail); tail = XCDR (tail))
+ if (ns_zoom_face_is_selected (XCAR (tail)))
+ if (ns_face_name_matches_selected_p (XCAR (tail)))
+ return true;
+ }
+ return false;
@@ -166,6 +162,13 @@ index 932d209f56..88c9251c18 100644
+ if (!MINI_WINDOW_P (w))
+ return -1;
+
+ /* block_input must come before record_unwind_protect_void (unblock_input)
+ so that the unwind handler is never invoked without a matching
+ block_input, even if Foverlays_in or Foverlay_get signals. */
+ specpdl_ref count = SPECPDL_INDEX ();
+ block_input ();
+ record_unwind_protect_void (unblock_input);
+
+ struct buffer *b = XBUFFER (w->contents);
+ ptrdiff_t beg = marker_position (w->start);
+ ptrdiff_t end = BUF_ZV (b);
@@ -198,8 +201,11 @@ index 932d209f56..88c9251c18 100644
+ Lisp_Object face
+ = Fget_text_property (make_fixnum (line_start),
+ Qface, str);
+ if (ns_zoom_face_is_selected (face))
+ return line;
+ if (ns_face_name_matches_selected_p (face))
+ {
+ unbind_to (count, Qnil);
+ return line;
+ }
+ line++;
+ line_start = i + 1;
+ }
@@ -210,6 +216,7 @@ index 932d209f56..88c9251c18 100644
+ }
+ }
+ }
+ unbind_to (count, Qnil);
+ return -1;
+}
+
@@ -245,7 +252,9 @@ index 932d209f56..88c9251c18 100644
+ ptrdiff_t zv = BUF_ZV (b);
+ int line = 0;
+
+ block_input ();
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_protect_void (unblock_input);
+ record_unwind_current_buffer ();
+ set_buffer_internal_1 (b);
+
@@ -255,7 +264,7 @@ index 932d209f56..88c9251c18 100644
+ Lisp_Object face
+ = Fget_char_property (make_fixnum (pos), Qface,
+ cw->contents);
+ if (ns_zoom_face_is_selected (face))
+ if (ns_face_name_matches_selected_p (face))
+ {
+ unbind_to (count, Qnil);
+ *child_frame = cf;
@@ -371,7 +380,7 @@ index 932d209f56..88c9251c18 100644
static void
ns_update_end (struct frame *f)
/* --------------------------------------------------------------------------
@@ -1104,6 +1384,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
@@ -1104,6 +1396,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
unblock_input ();
ns_updating_frame = NULL;
@@ -413,7 +422,7 @@ index 932d209f56..88c9251c18 100644
}
static void
@@ -3232,6 +3547,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
@@ -3232,6 +3559,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
/* Prevent the cursor from being drawn outside the text area. */
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));

View File

@@ -1,7 +1,7 @@
From 6c075e29fccc7dc6e9df693c4123ce0001a2dbfc Mon Sep 17 00:00:00 2001
From 573beced02b3f9b70ba82694d8e4790cfeee9563 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction
Date: Wed, 4 Mar 2026 15:23:53 +0100
Subject: [PATCH 2/9] ns: add accessibility base classes and helpers
Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa)
port. No existing code paths are modified.
@@ -29,11 +29,11 @@ ns-accessibility-enabled with corrected doc: initial value is nil,
set non-nil automatically when an AT is detected at startup.
---
src/nsterm.h | 131 +++++++++++++++
src/nsterm.m | 454 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 585 insertions(+)
src/nsterm.m | 466 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 597 insertions(+)
diff --git a/src/nsterm.h b/src/nsterm.h
index ea6e7ba4f5..5746e9e9bd 100644
index ea6e7ba4f5..d9ae6efc2e 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -453,6 +453,124 @@ enum ns_return_frame_mode
@@ -44,7 +44,7 @@ index ea6e7ba4f5..5746e9e9bd 100644
+
+ Accessibility virtual elements (macOS / Cocoa only)
+
+ ========================================================================= */
+ ========================================================================== */
+
+#ifdef NS_IMPL_COCOA
+@class EmacsView;
@@ -124,7 +124,7 @@ index ea6e7ba4f5..5746e9e9bd 100644
+@end
+
+/* Span types for interactive AX child elements. */
+typedef NS_ENUM (NSInteger, EmacsAXSpanType)
+typedef NS_ENUM(NSInteger, EmacsAXSpanType)
+{
+ EmacsAXSpanTypeNone = -1,
+ EmacsAXSpanTypeButton = 0,
@@ -189,7 +189,7 @@ index ea6e7ba4f5..5746e9e9bd 100644
diff --git a/src/nsterm.m b/src/nsterm.m
index 88c9251c18..9d36de66f9 100644
index 6333a7253a..9c53001e37 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -200,7 +200,7 @@ index 88c9251c18..9d36de66f9 100644
#include "systime.h"
#include "character.h"
#include "xwidget.h"
@@ -7201,6 +7202,436 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
@@ -7213,6 +7214,443 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
}
#endif
@@ -208,7 +208,7 @@ index 88c9251c18..9d36de66f9 100644
+
+ Accessibility virtual elements (macOS / Cocoa only)
+
+ ========================================================================= */
+ ========================================================================== */
+
+#ifdef NS_IMPL_COCOA
+
@@ -249,12 +249,8 @@ index 88c9251c18..9d36de66f9 100644
+
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ /* block_input must precede record_unwind_protect_void (unblock_input):
+ if anything between SPECPDL_INDEX and block_input were to throw,
+ the unwind handler would call unblock_input without a matching
+ block_input, corrupting the input-blocking reference count. */
+ block_input ();
+ record_unwind_protect_void (unblock_input);
+ block_input ();
+ if (b != current_buffer)
+ set_buffer_internal_1 (b);
+
@@ -518,7 +514,7 @@ index 88c9251c18..9d36de66f9 100644
+}
+
+/* Build label for span [START, END) in BUF_OBJ.
+ Priority: completion--string -> buffer text -> help-echo. */
+ Priority: completion--string buffer text help-echo. */
+static NSString *
+ns_ax_get_span_label (ptrdiff_t start, ptrdiff_t end,
+ Lisp_Object buf_obj)
@@ -631,22 +627,33 @@ index 88c9251c18..9d36de66f9 100644
+
+@end
+
+/* Stub implementation of InteractiveSpans category.
+ The full implementation is added in a later patch. */
+@implementation EmacsAccessibilityBuffer (InteractiveSpans)
+
+- (void)invalidateInteractiveSpans
+{
+ /* Stub: full implementation added in patch 0004. */
+}
+
+@end
+
+#endif /* NS_IMPL_COCOA */
+
+
/* ==========================================================================
EmacsView implementation
@@ -11657,6 +12084,24 @@ Convert an X font name (XLFD) to an NS font name.
@@ -11669,6 +12107,24 @@ Convert an X font name (XLFD) to an NS font name.
DEFSYM (Qns_drag_operation_generic, "ns-drag-operation-generic");
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
+ /* Accessibility: line navigation command symbols for
+ ns_ax_event_is_line_nav_key (hot path, avoid intern per call). */
+ DEFSYM (Qns_ax_next_line, "next-line");
+ DEFSYM (Qns_ax_previous_line, "previous-line");
+ DEFSYM (Qns_ax_dired_next_line, "dired-next-line");
+ DEFSYM (Qns_ax_dired_previous_line, "dired-previous-line");
+ DEFSYM (Qnext_line, "next-line");
+ DEFSYM (Qprevious_line, "previous-line");
+ DEFSYM (Qdired_next_line, "dired-next-line");
+ DEFSYM (Qdired_previous_line, "dired-previous-line");
+
+ /* Accessibility span scanning symbols. */
+ DEFSYM (Qns_ax_widget, "widget");
@@ -662,7 +669,7 @@ index 88c9251c18..9d36de66f9 100644
Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
@@ -11805,6 +12250,15 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
@@ -11817,6 +12273,16 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
This variable is ignored on Mac OS X < 10.7 and GNUstep. */);
ns_use_srgb_colorspace = YES;
@@ -672,7 +679,8 @@ index 88c9251c18..9d36de66f9 100644
+When nil, the accessibility virtual element tree is not built and no
+notifications are posted, eliminating the associated overhead.
+Requires the Cocoa (NS) build on macOS; ignored on GNUstep.
+Default is nil. Set to t to enable VoiceOver support. */);
+The initial value is nil. Emacs sets this automatically at startup
+when macOS Zoom is active or any assistive technology is connected. */);
+ ns_accessibility_enabled = NO;
+
DEFVAR_BOOL ("ns-use-mwheel-acceleration",

View File

@@ -1,8 +1,7 @@
From 705bba0809db081f538c124aa900bc911de9b0ff Mon Sep 17 00:00:00 2001
From 64859d37421bdaabe2ec416285b6f1847da0737c 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)
Date: Wed, 4 Mar 2026 15:23:54 +0100
Subject: [PATCH 3/9] ns: implement buffer accessibility element
Implement the NSAccessibility text protocol for Emacs buffer windows.
@@ -10,34 +9,35 @@ Implement the NSAccessibility text protocol for Emacs buffer windows.
(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.
(ensureTextCache): Validity gated on BUF_MODIFF to catch fold/unfold
commands (org-mode, outline-mode, hideshow-mode) that change the
'invisible text property without modifying character content.
BUF_CHARS_MODIFF would serve stale AX text after org-cycle or similar.
ensureTextCache is called only from AX getters at human interaction
speed, not from the redisplay notification path.
(accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs.
(charposForAccessibilityIndex:): Symmetric O(1) fast path.
(accessibilitySelectedTextRange, accessibilityLineForIndex:)
(accessibilityIndexForLine:, accessibilityRangeForIndex:)
(accessibilityStringForRange:, accessibilityFrameForRange:)
(accessibilityRangeForPosition:, setAccessibilitySelectedTextRange:)
(accessibilityRole, accessibilityLabel, accessibilityValue)
(accessibilityNumberOfCharacters, accessibilitySelectedText)
(accessibilitySelectedTextRange, accessibilityInsertionPointLineNumber)
(accessibilityLineForIndex:, accessibilityRangeForLine:)
(accessibilityRangeForIndex:, accessibilityStyleRangeForIndex:)
(accessibilityFrameForRange:, accessibilityRangeForPosition:)
(accessibilityVisibleCharacterRange, accessibilityFrame)
(setAccessibilitySelectedTextRange:)
(setAccessibilityFocused:): Implement NSAccessibility protocol methods.
---
src/nsterm.m | 1115 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1115 insertions(+)
src/nsterm.m | 1135 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 1133 insertions(+), 2 deletions(-)
diff --git a/src/nsterm.m b/src/nsterm.m
index 9d36de66f9..6256dbc22e 100644
index 9c53001e37..e4b3fb17a0 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7625,6 +7625,1121 @@ - (id)accessibilityTopLevelUIElement
@@ -7648,6 +7648,1137 @@ - (void)invalidateInteractiveSpans
@end
+
+
+
+
+static BOOL
+ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
+ ptrdiff_t *out_start,
@@ -149,15 +149,15 @@ index 9d36de66f9..6256dbc22e 100644
+ {
+ Lisp_Object cmd = Vthis_command;
+ /* Forward line commands. */
+ if (EQ (cmd, Qns_ax_next_line)
+ || EQ (cmd, Qns_ax_dired_next_line))
+ if (EQ (cmd, Qnext_line)
+ || EQ (cmd, Qdired_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 (EQ (cmd, Qprevious_line)
+ || EQ (cmd, Qdired_previous_line))
+ {
+ if (which) *which = -1;
+ return true;
@@ -364,25 +364,25 @@ index 9d36de66f9..6256dbc22e 100644
+ 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.
+ /* 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. Using BUF_CHARS_MODIFF would serve stale
+ AX text across fold/unfold, causing VoiceOver to read the wrong
+ content after an org-cycle or similar command.
+ ensureTextCache is called exclusively from AX getters at human
+ interaction speed (never from the redisplay notification path), so
+ font-lock passes cause zero rebuild cost via the notification path.
+ 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 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
@@ -398,7 +398,7 @@ index 9d36de66f9..6256dbc22e 100644
+ {
+ [cachedText release];
+ cachedText = [text retain];
+ cachedTextModiff = chars_modiff;
+ cachedTextModiff = modiff;
+ cachedTextStart = start;
+
+ if (visibleRuns)
@@ -410,10 +410,9 @@ index 9d36de66f9..6256dbc22e 100644
+ 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. */
+ gated on BUF_MODIFF. Font-lock passes trigger a rebuild only
+ when called from AX getters (human interaction speed), never
+ from the notification path. */
+ if (lineStartOffsets)
+ xfree (lineStartOffsets);
+ lineStartOffsets = NULL;
@@ -951,6 +950,27 @@ index 9d36de66f9..6256dbc22e 100644
+ return [self rangeForLine:(NSUInteger)line textLength:len];
+}
+
+- (NSInteger)accessibilityLineForIndex:(NSInteger)index
+{
+ if (![NSThread isMainThread])
+ {
+ __block NSInteger result;
+ dispatch_sync (dispatch_get_main_queue (), ^{
+ result = [self accessibilityLineForIndex:index];
+ });
+ return result;
+ }
+ [self ensureTextCache];
+ if (!cachedText || index < 0)
+ return 0;
+
+ NSUInteger idx = (NSUInteger) index;
+ if (idx > [cachedText length])
+ idx = [cachedText length];
+
+ return [self lineForAXIndex:idx];
+}
+
+- (NSRange)accessibilityRangeForIndex:(NSInteger)index
+{
+ if (![NSThread isMainThread])
@@ -1033,9 +1053,9 @@ index 9d36de66f9..6256dbc22e 100644
+ 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). */
+ block_input ();
+ 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;
@@ -1152,6 +1172,22 @@ index 9d36de66f9..6256dbc22e 100644
#endif /* NS_IMPL_COCOA */
@@ -8918,13 +10049,13 @@ - (NSSize)windowWillResize: (NSWindow *)sender toSize: (NSSize)frameSize
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);
--
2.43.0

View File

@@ -1,8 +1,7 @@
From 94d2aa2736ec116e2965d02241ad8b20d8daf4bc Mon Sep 17 00:00:00 2001
From 11aa323081fd9117381413b6d8e477659d6afc29 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line
element
Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 4/9] ns: add AX notifications and mode-line element
Add VoiceOver notification dispatch and mode-line readout.
@@ -26,17 +25,17 @@ mode line.
1 file changed, 606 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m
index 6256dbc22e..9e0e317237 100644
index e4b3fb17a0..84a32a05cb 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -8740,6 +8740,612 @@ - (NSRect)accessibilityFrame
@@ -8779,6 +8779,612 @@ - (NSRect)accessibilityFrame
@end
+
+
+/* ===================================================================
+ 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).
@@ -51,7 +50,7 @@ index 6256dbc22e..9e0e317237 100644
+ 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)
@@ -70,7 +69,7 @@ index 6256dbc22e..9e0e317237 100644
+ /* 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 = @{
@@ -471,7 +470,7 @@ index 6256dbc22e..9e0e317237 100644
+ }
+
+ /* --- 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)
+ {

View File

@@ -1,7 +1,7 @@
From 3206d93511fe9337c4ca683a5dc1e6885ed9985c Mon Sep 17 00:00:00 2001
From e276b4cc0cda1504373656f664c39af7631208ad Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation
Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 5/9] ns: add interactive span elements for Tab
* src/nsterm.m (ns_ax_scan_interactive_spans): New function; scans the
visible portion of a buffer for interactive text properties
@@ -14,14 +14,14 @@ elements with an AXPress action that sends a synthetic TAB keystroke.
(accessibilityChildrenInNavigationOrder): Return cached span array,
rebuilding lazily when interactiveSpansDirty is set.
---
src/nsterm.m | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 292 insertions(+)
src/nsterm.m | 293 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 293 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m
index 9e0e317237..8aa5b6ac1b 100644
index 84a32a05cb..c713943b30 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -9346,6 +9346,298 @@ - (NSRect)accessibilityFrame
@@ -9385,6 +9385,299 @@ - (NSRect)accessibilityFrame
@end
@@ -76,6 +76,7 @@ index 9e0e317237..8aa5b6ac1b 100644
+ EmacsAXSpanType span_type = EmacsAXSpanTypeNone;
+ Lisp_Object limit_prop = Qnil;
+
+ /* Fplist_get third arg Qnil: use `eq' predicate (the default). */
+ if (!NILP (Fplist_get (plist, Qns_ax_widget, Qnil)))
+ {
+ span_type = EmacsAXSpanTypeWidget;

View File

@@ -1,7 +1,7 @@
From f2e97ea6ba4ffc1c73e625f9d61636b7261cbecf Mon Sep 17 00:00:00 2001
From 6a4b906f183f4472b3df458cdfbeb7d9dfef95f4 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay
Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 6/9] ns: wire accessibility into EmacsView and redisplay
Wire the accessibility element tree into EmacsView and hook it into
the redisplay cycle.
@@ -23,8 +23,8 @@ com.apple.accessibility.api distributed notification.
(accessibilityAttributeValue:forParameter:): New methods.
---
etc/NEWS | 13 ++
src/nsterm.m | 474 +++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 475 insertions(+), 12 deletions(-)
src/nsterm.m | 475 +++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 478 insertions(+), 10 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS
index 4c149e41d6..7f917f93b2 100644
@@ -51,48 +51,31 @@ index 4c149e41d6..7f917f93b2 100644
** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.m b/src/nsterm.m
index 8aa5b6ac1b..32eb04acef 100644
index c713943b30..b66f350486 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1275,7 +1275,7 @@ If a completion candidate is selected (overlay or child frame),
static void
ns_zoom_track_completion (struct frame *f, EmacsView *view)
{
- if (!ns_zoom_enabled_p ())
+ if (!ns_accessibility_enabled || !ns_zoom_enabled_p ())
return;
if (!WINDOWP (f->selected_window))
return;
@@ -1393,7 +1393,8 @@ so the visual offset is (ov_line + 1) * line_h from
@@ -1405,7 +1405,8 @@ so the visual offset is (ov_line + 1) * line_h from
(zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (view && !view->zoomCursorUpdated && ns_zoom_enabled_p ()
+ if (ns_accessibility_enabled && view && !view->zoomCursorUpdated
+ if (view && !view->zoomCursorUpdated
+ && ns_zoom_enabled_p ()
&& !NSIsEmptyRect (view->lastCursorRect))
{
NSRect r = view->lastCursorRect;
@@ -1420,6 +1421,9 @@ so the visual offset is (ov_line + 1) * line_h from
@@ -1432,6 +1433,10 @@ so the visual offset is (ov_line + 1) * line_h from
if (view)
ns_zoom_track_completion (f, view);
#endif /* NS_IMPL_COCOA */
+
+ /* Post accessibility notifications after each redisplay cycle. */
+ [view postAccessibilityUpdates];
+ if (view)
+ [view postAccessibilityUpdates];
}
static void
@@ -3567,7 +3571,7 @@ EmacsView pixels (AppKit, flipped, top-left origin)
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_zoom_enabled_p ())
+ if (ns_accessibility_enabled && ns_zoom_enabled_p ())
{
NSRect windowRect = [view convertRect:r toView:nil];
NSRect screenRect
@@ -6723,9 +6727,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification
@@ -6735,9 +6740,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification
}
#endif
@@ -149,28 +132,24 @@ index 8aa5b6ac1b..32eb04acef 100644
- (void)antialiasThresholdDidChange:(NSNotification *)notification
{
#ifdef NS_IMPL_COCOA
@@ -7628,7 +7679,6 @@ - (id)accessibilityTopLevelUIElement
-
static BOOL
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
ptrdiff_t *out_start,
@@ -8741,7 +8791,6 @@ - (NSRect)accessibilityFrame
@@ -8780,7 +8832,6 @@ - (NSRect)accessibilityFrame
@end
-
/* ===================================================================
EmacsAccessibilityBuffer (Notifications) --- AX event dispatch
EmacsAccessibilityBuffer (Notifications) AX event dispatch
@@ -9235,6 +9284,50 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
@@ -9274,6 +9325,54 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
granularity = ns_ax_text_selection_granularity_line;
}
+ /* Treat all moves as Emacs-initiated until voiceoverSetPoint
+ tracking is introduced (subsequent patch). */
+ BOOL emacsMovedCursor = YES;
+
+ /* Programmatic jumps that cross a line boundary (]], [[, M-<,
+ xref, imenu, ...) are discontiguous: the cursor teleported to an
+ xref, imenu, ) are discontiguous: the cursor teleported to an
+ arbitrary position, not one sequential step forward/backward.
+ Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver
+ to re-anchor its rotor browse cursor at the new
@@ -216,7 +195,7 @@ index 8aa5b6ac1b..32eb04acef 100644
/* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused])
[self postFocusedCursorNotification:point
@@ -9347,7 +9440,6 @@ - (NSRect)accessibilityFrame
@@ -9386,7 +9485,6 @@ - (NSRect)accessibilityFrame
@end
@@ -224,7 +203,7 @@ index 8aa5b6ac1b..32eb04acef 100644
/* ===================================================================
EmacsAccessibilityInteractiveSpan --- helpers and implementation
=================================================================== */
@@ -9683,6 +9775,7 @@ - (void)dealloc
@@ -9723,6 +9821,7 @@ - (void)dealloc
[layer release];
#endif
@@ -232,7 +211,7 @@ index 8aa5b6ac1b..32eb04acef 100644
[[self menu] release];
[super dealloc];
}
@@ -11031,6 +11124,32 @@ - (void)windowDidBecomeKey /* for direct calls */
@@ -11071,6 +11170,32 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -265,7 +244,7 @@ index 8aa5b6ac1b..32eb04acef 100644
}
@@ -12268,6 +12387,332 @@ - (int) fullscreenState
@@ -12308,6 +12433,332 @@ - (int) fullscreenState
return fs_state;
}
@@ -285,7 +264,7 @@ index 8aa5b6ac1b..32eb04acef 100644
+
+ 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)
@@ -319,7 +298,7 @@ index 8aa5b6ac1b..32eb04acef 100644
+ }
+ else
+ {
+ /* Internal (combination) window --- recurse into children. */
+ /* Internal (combination) window recurse into children. */
+ Lisp_Object child = w->contents;
+ while (!NILP (child))
+ {
@@ -431,7 +410,7 @@ index 8aa5b6ac1b..32eb04acef 100644
+ 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))
+ {
@@ -440,12 +419,12 @@ index 8aa5b6ac1b..32eb04acef 100644
+ }
+
+ /* 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];
@@ -598,7 +577,7 @@ index 8aa5b6ac1b..32eb04acef 100644
@end /* EmacsView */
@@ -14264,12 +14709,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
@@ -14304,13 +14755,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
ns_use_srgb_colorspace = YES;
DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
@@ -607,7 +586,8 @@ index 8aa5b6ac1b..32eb04acef 100644
-When nil, the accessibility virtual element tree is not built and no
-notifications are posted, eliminating the associated overhead.
-Requires the Cocoa (NS) build on macOS; ignored on GNUstep.
-Default is nil. Set to t to enable VoiceOver support. */);
-The initial value is nil. Emacs sets this automatically at startup
-when macOS Zoom is active or any assistive technology is connected. */);
+ doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support.
+Emacs sets this automatically at startup when macOS Zoom is active or
+any assistive technology (VoiceOver, Switch Control, etc.) is connected,

View File

@@ -1,8 +1,7 @@
From 4310549aa1e486dba054948a2937bb8bb236bb27 Mon Sep 17 00:00:00 2001
From 9845dd8500f80118b3c9bb5bdf13fdbbb897d550 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS
appendix
Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 7/9] doc: add VoiceOver section to macOS appendix
* doc/emacs/macos.texi (VoiceOver Accessibility): New node between
'Mac / GNUstep Events' and 'GNUstep Support'. Document screen reader
@@ -11,12 +10,12 @@ enabled, and known limitations. Use @xref for cross-reference at
sentence start. Correct description of ns-accessibility-enabled
default: initial value is nil, set automatically at startup.
---
doc/emacs/macos.texi | 76 ++++++++++++++++++++++++++++++++++++++++++++
doc/emacs/macos.texi | 77 ++++++++++++++++++++++++++++++++++++++++++++
src/nsterm.m | 10 ++++--
2 files changed, 83 insertions(+), 3 deletions(-)
2 files changed, 84 insertions(+), 3 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 6bd334f48e..8d4a7825d8 100644
index 6bd334f48e..72ac3a9aa9 100644
--- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi
@@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future.
@@ -27,7 +26,7 @@ index 6bd334f48e..8d4a7825d8 100644
* GNUstep Support:: Details on status of GNUstep support.
@end menu
@@ -272,6 +273,82 @@
@@ -272,6 +273,82 @@ and return the result as a string. You can also use the Lisp function
services and receive the results back. Note that you may need to
restart Emacs to access newly-available services.
@@ -97,24 +96,24 @@ index 6bd334f48e..8d4a7825d8 100644
+Right-to-left (bidi) text is exposed correctly as buffer content,
+but @code{accessibilityRangeForPosition} hit-testing assumes
+left-to-right glyph layout.
+@item
+Block-style cursors are handled correctly: character navigation
+announces the character at the cursor position, not the character
+before it.
+@end itemize
+
+ This support is available only on the Cocoa build. GNUstep has a
+different accessibility model and is not yet supported.
+
+Block-style cursors are handled
+correctly: character navigation announces the character at the cursor
+position, not the character before it.
+
+
@node GNUstep Support
@section GNUstep Support
diff --git a/src/nsterm.m b/src/nsterm.m
index 32eb04acef..8e5cc7e1d7 100644
index b66f350486..4261886974 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -14710,9 +14710,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
@@ -14756,9 +14756,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support.

View File

@@ -1,28 +1,37 @@
From affdaf60d28ad4d9836c6505216e19599b31c437 Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz>
Date: Mon, 2 Mar 2026 18:39:46 +0100
Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver
From 87d1a86ca4130475a908e41928f4ee802c861372 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Wed, 4 Mar 2026 15:23:56 +0100
Subject: [PATCH 8/9] ns: announce overlay completions to VoiceOver
Overlay-based completion UIs (Vertico, Ivy, Icomplete) render candidates
via overlay before-string/after-string properties. Without this change
VoiceOver cannot read overlay-based completion UIs.
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_face_is_selected): New static function; matches
'current', 'selected', 'selection' in face symbol names.
(ns_ax_selected_overlay_text): New function; scan overlay strings in
the window for a line with a selected face; return its text.
(EmacsAccessibilityBuffer(Notifications)
postAccessibilityNotificationsForFrame:): Handle BUF_OVERLAY_MODIFF
(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; keep overlay_modiff out of ensureTextCache to prevent a
race where an AX query consumes the change before notification.
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 | 348 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 307 insertions(+), 42 deletions(-)
src/nsterm.m | 301 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 262 insertions(+), 40 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index 5746e9e9bd..21a93bc799 100644
index d9ae6efc2e..ff81675bb5 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -510,6 +510,7 @@ typedef struct ns_ax_visible_run
@@ -34,44 +43,13 @@ index 5746e9e9bd..21a93bc799 100644
@property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
diff --git a/src/nsterm.m b/src/nsterm.m
index 8e5cc7e1d7..8ef344d9fe 100644
index 4261886974..90c59c47b4 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7263,11 +7263,157 @@ Accessibility virtual elements (macOS / Cocoa only)
@@ -7276,11 +7276,126 @@ 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
+/* Note: ns_zoom_face_is_selected in the Zoom series has identical
+ logic. The duplication is intentional: both series are
+ independent and must compile standalone. */
+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
@@ -161,7 +139,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
+ Lisp_Object face
+ = Fget_text_property (make_fixnum (line_starts[li]),
+ Qface, str);
+ if (ns_ax_face_is_selected (face))
+ if (ns_face_name_matches_selected_p (face))
+ {
+ Lisp_Object line
+ = Fsubstring_no_properties (
@@ -196,25 +174,7 @@ index 8e5cc7e1d7..8ef344d9fe 100644
static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -7340,7 +7483,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));
@@ -7421,7 +7564,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;
@@ -7896,6 +8039,7 @@ @implementation EmacsAccessibilityBuffer
@@ -7917,6 +8032,7 @@ @implementation EmacsAccessibilityBuffer
@synthesize cachedOverlayModiff;
@synthesize cachedTextStart;
@synthesize cachedModiff;
@@ -222,39 +182,25 @@ index 8e5cc7e1d7..8ef344d9fe 100644
@synthesize cachedPoint;
@synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement;
@@ -7993,7 +8137,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]);
@@ -8005,25 +8149,34 @@ - (void)ensureTextCache
if (!b)
@@ -8027,20 +8143,33 @@ - (void)ensureTextCache
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.
/* 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. Using BUF_CHARS_MODIFF would serve stale
- AX text across fold/unfold, causing VoiceOver to read the wrong
- content after an org-cycle or similar command.
- ensureTextCache is called exclusively from AX getters at human
- interaction speed (never from the redisplay notification path), so
- font-lock passes cause zero rebuild cost via the notification path.
- 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
@@ -262,12 +208,16 @@ index 8e5cc7e1d7..8ef344d9fe 100644
+ 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), causing a
+ full text-cache rebuild on each redisplay cycle. This is acceptable
+ because `ensureTextCache' is only called when VoiceOver queries
+ accessibilityValue or related AX properties --- which happens at
+ human interaction speed, not at redisplay speed. The per-rebuild
+ cost is O(visible-buffer-text).
+ 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
@@ -275,155 +225,117 @@ index 8e5cc7e1d7..8ef344d9fe 100644
+ 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 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
@@ -8039,7 +8192,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)
@@ -8051,9 +8204,9 @@ included in the cached AX text (it is handled separately via
@@ -8072,9 +8201,8 @@ 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. */
- gated on BUF_MODIFF. Font-lock passes trigger a rebuild only
- when called from AX getters (human interaction speed), never
- from the notification path. */
+ gated on BUF_MODIFF changes, ensuring the line index always
+ matches the currently visible text (including after fold/unfold). */
if (lineStartOffsets)
xfree (lineStartOffsets);
@@ -8108,7 +8261,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)
{
@@ -8157,10 +8310,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)
@@ -8202,7 +8355,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];
@@ -8224,7 +8377,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
@@ -8570,6 +8723,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
lineStartOffsets = NULL;
@@ -8590,26 +8718,26 @@ - (NSInteger)accessibilityInsertionPointLineNumber
return [self lineForAXIndex:point_idx];
}
-- (NSRange)accessibilityRangeForLine:(NSInteger)line
+- (NSString *)accessibilityStringForRange:(NSRange)range
+{
+ if (![NSThread isMainThread])
+ {
{
if (![NSThread isMainThread])
{
- __block NSRange result;
+ __block NSString *result;
+ dispatch_sync (dispatch_get_main_queue (), ^{
dispatch_sync (dispatch_get_main_queue (), ^{
- result = [self accessibilityRangeForLine:line];
+ result = [self accessibilityStringForRange:range];
+ });
+ return result;
+ }
+ [self ensureTextCache];
});
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
@@ -8631,6 +8759,29 @@ - (NSInteger)accessibilityLineForIndex:(NSInteger)index
idx = [cachedText length];
return [self lineForAXIndex:idx];
+
+}
+
+- (NSInteger)accessibilityLineForIndex:(NSInteger)index
+- (NSRange)accessibilityRangeForLine:(NSInteger)line
+{
+ if (![NSThread isMainThread])
+ {
+ __block NSInteger result;
+ __block NSRange result;
+ dispatch_sync (dispatch_get_main_queue (), ^{
+ result = [self accessibilityLineForIndex:index];
+ result = [self accessibilityRangeForLine:line];
+ });
+ return result;
+ }
+ [self ensureTextCache];
+ if (!cachedText || index < 0)
+ return 0;
+ if (!cachedText || line < 0)
+ return NSMakeRange (NSNotFound, 0);
+
+ NSUInteger idx = (NSUInteger) index;
+ if (idx > [cachedText length])
+ idx = [cachedText length];
+ NSUInteger len = [cachedText length];
+ if (len == 0)
+ return (line == 0) ? NSMakeRange (0, 0)
+ : NSMakeRange (NSNotFound, 0);
+
+ return [self lineForAXIndex:idx];
+
+}
+
- (NSRange)accessibilityRangeForLine:(NSInteger)line
{
if (![NSThread isMainThread])
@@ -8792,7 +8989,7 @@ - (NSRect)accessibilityFrame
+ return [self rangeForLine:(NSUInteger)line textLength:len];
}
- (NSRange)accessibilityRangeForIndex:(NSInteger)index
@@ -8833,7 +8984,7 @@ - (NSRect)accessibilityFrame
/* ===================================================================
- EmacsAccessibilityBuffer (Notifications) --- AX event dispatch
- 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).
@@ -8807,7 +9004,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8848,7 +8999,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. */
+ /* Single char inserted --- refresh cache and grab it. */
[self invalidateTextCache];
[self ensureTextCache];
if (cachedText)
@@ -8826,7 +9023,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8867,7 +9018,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. */
+ same user action --- they are mutually exclusive. */
self.cachedPoint = point;
NSDictionary *change = @{
@@ -9220,16 +9417,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
@@ -9261,16 +9412,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */
@@ -465,90 +377,103 @@ index 8e5cc7e1d7..8ef344d9fe 100644
+ 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))
+ goto skip_overlay_scan;
+
+ int selected_line = -1;
+ NSString *candidate
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line);
+ if (candidate)
+ if (MINI_WINDOW_P (w))
+ {
+ /* Deduplicate: only announce when the candidate changed. */
+ if (![candidate isEqualToString:
+ self.cachedCompletionAnnouncement])
+ int selected_line = -1;
+ NSString *candidate
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line);
+ if (candidate)
+ {
+ 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);
+ /* 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);
+ }
+ }
+ }
}
+ skip_overlay_scan:;
/* --- 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)
+ Independent check: the goto above may jump here from the overlay
+ branch, so this must be a standalone if, not else-if. */
+ Independent check from the overlay branch above. */
+ if (point != self.cachedPoint || markActive != self.cachedMarkActive)
{
ptrdiff_t oldPoint = self.cachedPoint;
BOOL oldMarkActive = self.cachedMarkActive;
@@ -12403,7 +12667,7 @@ - (int) fullscreenState
@@ -9288,8 +9503,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];
@@ -12449,7 +12670,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w))
{
- /* Buffer element --- reuse existing if available. */
- /* Buffer element reuse existing if available. */
+ /* Buffer element --- reuse existing if available. */
EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem)
@@ -12437,7 +12701,7 @@ - (int) fullscreenState
@@ -12483,7 +12704,7 @@ - (int) fullscreenState
}
else
{
- /* Internal (combination) window --- recurse into children. */
- /* Internal (combination) window recurse into children. */
+ /* Internal (combination) window --- recurse into children. */
Lisp_Object child = w->contents;
while (!NILP (child))
{
@@ -12549,7 +12813,7 @@ - (void)postAccessibilityUpdates
@@ -12595,7 +12816,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. */
+ FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow))
{
@@ -12558,12 +12822,12 @@ - (void)postAccessibilityUpdates
@@ -12604,12 +12825,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
+ 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. */
+ /* Invalidate span cache --- window layout changed. */
for (EmacsAccessibilityElement *elem in accessibilityElements)
if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]])

View File

@@ -1,54 +1,49 @@
From 1e3d3919fd41e4480a02190fb89bee1ef8107d62 Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz>
Date: Mon, 2 Mar 2026 18:49:13 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for
VoiceOver
From 40650cf6d2272083b62c9a60b3ef61908c3b0092 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Wed, 4 Mar 2026 15:23:56 +0100
Subject: [PATCH 9/9] ns: announce child frame completions to VoiceOver
Child frame popups (Corfu, Company-mode) render completion candidates in
a separate frame whose buffer is not accessible via the minibuffer
overlay path. This patch scans child frame buffers for selected
candidates and announces them via VoiceOver.
Child frame popups (Corfu, Company-mode child frames) render completion
candidates in a separate frame whose buffer is not accessible via the
minibuffer overlay path. This patch scans child frame buffers for
selected candidates and announces them via VoiceOver.
* src/nsterm.h (EmacsView): Add childFrameLastBuffer, childFrameLastModiff,
childFrameLastCandidate, childFrameCompletionActive, lastEchoCharsModiff
ivars; remove cachedOverlayModiffForText (unused after BUF_OVERLAY_MODIFF
removed from ensureTextCache to prevent hl-line-mode O(N) rebuilds).
Initialize voiceoverSetPoint, childFrameLastBuffer in initFrameFromEmacs:.
ivars. Initialize childFrameLastBuffer to Qnil in initFrameFromEmacs:.
(EmacsAccessibilityBuffer): Add voiceoverSetPoint ivar.
* src/nsterm.m (ns_ax_buffer_text): Add block_input protection for
Lisp calls; use record_unwind_protect_void to guarantee unblock_input.
(ensureTextCache): Remove BUF_OVERLAY_MODIFF tracking; keep only
BUF_CHARS_MODIFF. BUF_OVERLAY_MODIFF caused O(buffer-size) rebuilds
with hl-line-mode (moves overlay on every post-command-hook).
* src/nsterm.m (ns_ax_selected_child_frame_text): New function; scans
child frame buffer text for the selected completion candidate.
(announceChildFrameCompletion): New method; scans child frame buffers
for selected completion candidates. Store childFrameLastBuffer as
BVAR(b, name) (buffer name symbol, GC-reachable via obarray) rather
than make_lisp_ptr to avoid dangling pointer after buffer kill.
than a raw buffer pointer to avoid a dangling pointer after buffer kill.
(postEchoAreaAnnouncementIfNeeded): New method; announces echo area
changes for commands like C-g.
changes (e.g., "Wrote file", "Quit") for commands that produce output
while the minibuffer is inactive.
(postAccessibilityNotificationsForFrame:): Drive child frame and echo
area announcements.
* doc/emacs/macos.texi: Fix dangling semicolon in GNUstep paragraph.
area announcements. Add voiceoverSetPoint flag and singleLineMove
adjacency detection to distinguish VoiceOver-initiated cursor moves
from Emacs-initiated moves; sequential adjacent-line moves use
next/previous direction, teleports use discontiguous. Add didTextChange
guard to suppress overlay completion announcements while the user types.
(setAccessibilitySelectedTextRange:): Set voiceoverSetPoint so that the
subsequent notification cycle uses sequential direction.
* doc/emacs/macos.texi (VoiceOver Accessibility): Update to document
echo area announcements and VoiceOver rotor cursor synchronization.
Remove Zoom section (covered by patch 0000). Fix dangling paragraph.
---
doc/emacs/macos.texi | 14 +-
etc/NEWS | 25 ++-
doc/emacs/macos.texi | 13 +-
etc/NEWS | 22 +-
src/nsterm.h | 21 ++
src/nsterm.m | 514 +++++++++++++++++++++++++++++++++++++++----
4 files changed, 510 insertions(+), 64 deletions(-)
src/nsterm.m | 562 +++++++++++++++++++++++++++++++++++++------
4 files changed, 528 insertions(+), 90 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 8d4a7825d8..03a657f970 100644
index 72ac3a9aa9..cf5ed0ff28 100644
--- a/doc/emacs/macos.texi
+++ b/doc/emacs/macos.texi
@@ -278,7 +278,6 @@ restart Emacs to access newly-available services.
@cindex VoiceOver
@cindex accessibility (macOS)
@cindex screen reader (macOS)
-@cindex Zoom, cursor tracking (macOS)
When built with the Cocoa interface on macOS, Emacs exposes buffer
content, cursor position, mode lines, and interactive elements to the
@@ -309,10 +308,15 @@ Shift-modified movement announces selected or deselected text.
@@ -309,10 +309,15 @@ Shift-modified movement announces selected or deselected text.
The @file{*Completions*} buffer announces each completion candidate
as you navigate, even while keyboard focus remains in the minibuffer.
@@ -69,24 +64,20 @@ index 8d4a7825d8..03a657f970 100644
@vindex ns-accessibility-enabled
To disable the accessibility interface entirely (for instance, to
diff --git a/etc/NEWS b/etc/NEWS
index 7f917f93b2..bbec21b635 100644
index 7f917f93b2..c6bb4cc5ad 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -88,10 +88,9 @@ When macOS Zoom is enabled (System Settings, Accessibility, Zoom,
@@ -88,8 +88,7 @@ When macOS Zoom is enabled (System Settings, Accessibility, Zoom,
Follow keyboard focus), Emacs informs Zoom of the text cursor position
after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport
automatically tracks the insertion point across window splits and
-switches. Completion frameworks (Vertico, Icomplete, Ivy for overlay
-candidates; Corfu, Company-box for child frame popups) are also
-tracked: Zoom follows the selected candidate rather than the text
-cursor during completion.
+switches. Overlay-based completion frameworks and child-frame popup completions
+are also tracked: Zoom follows the selected candidate rather than the
+text cursor during completion.
+switches. Overlay-based and child-frame completion frameworks are also
tracked: Zoom follows the selected candidate rather than the text
cursor during completion.
+++
** 'line-spacing' now supports specifying spacing above the line.
@@ -4385,16 +4384,20 @@ allowing Emacs users access to speech recognition utilities.
@@ -4385,16 +4384,19 @@ allowing Emacs users access to speech recognition utilities.
Note: Accepting this permission allows the use of system APIs, which may
send user data to Apple's speech recognition servers.
@@ -94,28 +85,28 @@ index 7f917f93b2..bbec21b635 100644
++++
** VoiceOver accessibility support on macOS.
Emacs now exposes buffer content, cursor position, and interactive
elements to the macOS accessibility subsystem (VoiceOver). This
-elements to the macOS accessibility subsystem (VoiceOver). This
-includes AXBoundsForRange for macOS Zoom cursor tracking, line and
-word navigation announcements, Tab-navigable interactive spans
-(buttons, links, completion candidates), and completion announcements
-for the *Completions* buffer. The implementation uses a virtual
-accessibility tree with per-window elements, hybrid SelectedTextChanged
-and AnnouncementRequested notifications, and thread-safe text caching.
+includes:
+- Line and word navigation announcements via standard movement keys.
+- Echo area messages (e.g., "Wrote file", "Git finished") announced
+ automatically as they appear, without user interaction.
+- VoiceOver rotor cursor synchronization after large programmatic
+ jumps (]], M-<, xref, imenu, etc.).
+- Tab-navigable interactive spans (buttons, links, completion
+ candidates) within a buffer.
+- Completion announcements for the *Completions* buffer, overlay
+ completion UIs, and child-frame completion popup UIs.
+elements to the macOS accessibility subsystem (VoiceOver). Standard
+navigation keys produce speech feedback: arrow keys read characters and
+lines, 'M-f'/'M-b' announce words, and shift-modified movement reports
+selected text. Echo area messages (e.g., "Wrote file", "Git finished")
+are announced automatically without user interaction. The VoiceOver
+rotor cursor stays synchronized after large programmatic jumps such as
+xref, imenu, or Org heading navigation. Pressing 'TAB' navigates
+interactive spans (buttons, links, completion candidates) within a
+buffer. Completion frameworks that render via overlays or child frames
+(Vertico, Ivy, Corfu, etc.) announce the selected candidate.
Set 'ns-accessibility-enabled' to nil to disable the accessibility
interface and eliminate the associated overhead.
diff --git a/src/nsterm.h b/src/nsterm.h
index 21a93bc799..b5c9f84499 100644
index ff81675bb5..9ee6c86f18 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -504,9 +504,20 @@ typedef struct ns_ax_visible_run
@@ -139,7 +130,7 @@ index 21a93bc799..b5c9f84499 100644
@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) ptrdiff_t cachedModiff;
@@ -596,6 +607,14 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
@@ -596,6 +607,14 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid;
BOOL accessibilityUpdating;
@@ -154,7 +145,7 @@ index 21a93bc799..b5c9f84499 100644
#endif
BOOL font_panel_active;
NSFont *font_panel_result;
@@ -665,6 +684,8 @@ typedef NS_ENUM (NSInteger, EmacsAXSpanType)
@@ -665,6 +684,8 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates;
@@ -164,43 +155,23 @@ index 21a93bc799..b5c9f84499 100644
@end
diff --git a/src/nsterm.m b/src/nsterm.m
index 8ef344d9fe..1acb64630a 100644
index 90c59c47b4..79a4fa371d 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1275,7 +1275,13 @@ If a completion candidate is selected (overlay or child frame),
@@ -1287,6 +1287,12 @@ If a completion candidate is selected (overlay or child frame),
static void
ns_zoom_track_completion (struct frame *f, EmacsView *view)
{
- if (!ns_accessibility_enabled || !ns_zoom_enabled_p ())
+ /* Zoom cursor tracking is controlled exclusively by
+ ns_zoom_enabled_p (). We do NOT gate on ns_accessibility_enabled:
+ users can run Zoom without VoiceOver, and those users should still
+ get completion-candidate tracking. ns_accessibility_enabled is
+ only set when a screen reader (VoiceOver or similar) activates the
+ AX layer; it has no bearing on the Zoom feature. */
+ if (!ns_zoom_enabled_p ())
if (!ns_zoom_enabled_p ())
return;
if (!WINDOWP (f->selected_window))
return;
@@ -1393,7 +1399,7 @@ so the visual offset is (ov_line + 1) * line_h from
(zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && view && !view->zoomCursorUpdated
+ if (view && !view->zoomCursorUpdated
&& ns_zoom_enabled_p ()
&& !NSIsEmptyRect (view->lastCursorRect))
{
@@ -3571,7 +3577,7 @@ EmacsView pixels (AppKit, flipped, top-left origin)
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && ns_zoom_enabled_p ())
+ if (ns_zoom_enabled_p ())
{
NSRect windowRect = [view convertRect:r toView:nil];
NSRect screenRect
@@ -7407,6 +7413,117 @@ visual line index for Zoom (skip whitespace-only lines
@@ -7392,6 +7398,117 @@ visual line index for Zoom (skip whitespace-only lines
return nil;
}
@@ -247,17 +218,17 @@ index 8ef344d9fe..1acb64630a 100644
+ The data pointer is used only in this loop, before Lisp calls. */
+ const unsigned char *data = SDATA (str);
+ ptrdiff_t byte_len = SBYTES (str);
+ /* 128 lines is a safe upper bound for a completion child frame.
+ /* 512 lines is a safe upper bound for a completion child frame.
+ The caller rejects buffers larger than 10,000 characters
+ (BUF_ZV(b) - BUF_BEGV(b) > 10000 guard in announceChildFrameCompletion),
+ so the worst case is ~10 KB / 1 byte per line < 128. If a future
+ caller removes that guard, lines beyond 128 are silently skipped; */
+ ptrdiff_t line_starts[128];
+ ptrdiff_t line_ends[128];
+ so the worst case is ~10 KB / 1 byte per line < 512. If a future
+ caller removes that guard, lines beyond 512 are silently skipped; */
+ 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 < 128)
+ while (byte_pos < byte_len && nlines < 512)
+ {
+ if (data[byte_pos] == '\n')
+ {
@@ -275,7 +246,7 @@ index 8ef344d9fe..1acb64630a 100644
+ byte_pos++;
+ char_pos++;
+ }
+ if (char_pos > lstart && nlines < 128)
+ if (char_pos > lstart && nlines < 512)
+ {
+ line_starts[nlines] = lstart;
+ line_ends[nlines] = char_pos;
@@ -291,7 +262,7 @@ index 8ef344d9fe..1acb64630a 100644
+ Lisp_Object face
+ = Fget_char_property (make_fixnum (buf_pos), Qface, buf_obj);
+
+ if (ns_ax_face_is_selected (face))
+ if (ns_face_name_matches_selected_p (face))
+ {
+ Lisp_Object line
+ = Fsubstring_no_properties (str,
@@ -318,7 +289,22 @@ index 8ef344d9fe..1acb64630a 100644
/* 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
@@ -8605,6 +8726,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
@@ -7425,9 +7542,13 @@ visual line index for Zoom (skip whitespace-only lines
return @"";
specpdl_ref count = SPECPDL_INDEX ();
+ /* block_input must precede record_unwind_protect_void (unblock_input):
+ if anything between SPECPDL_INDEX and block_input were to throw,
+ the unwind handler would call unblock_input without a matching
+ block_input, corrupting the input-blocking reference count. */
+ block_input ();
record_unwind_current_buffer ();
record_unwind_protect_void (unblock_input);
- block_input ();
if (b != current_buffer)
set_buffer_internal_1 (b);
@@ -8600,6 +8721,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
[self ensureTextCache];
@@ -330,7 +316,7 @@ index 8ef344d9fe..1acb64630a 100644
specpdl_ref count = SPECPDL_INDEX ();
record_unwind_current_buffer ();
/* Ensure block_input is always matched by unblock_input even if
@@ -9053,20 +9179,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
@@ -9048,20 +9174,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
&& granularity
== ns_ax_text_selection_granularity_character);
@@ -379,7 +365,7 @@ index 8ef344d9fe..1acb64630a 100644
ns_ax_post_notification_with_info (
self,
NSAccessibilitySelectedTextChangedNotification,
@@ -9166,12 +9310,17 @@ user expectation ("w" jumps to next word and reads it). */
@@ -9161,12 +9305,17 @@ user expectation ("w" jumps to next word and reads it). */
}
}
@@ -392,7 +378,7 @@ index 8ef344d9fe..1acb64630a 100644
+ - C-n/C-p: SelectedTextChanged carries granularity=line, but
+ VoiceOver processes those keystrokes specially and may not
+ produce speech; the explicit announcement is the reliable path.
+ - Discontiguous jumps (]], M-<, xref, imenu, ...): granularity=line
+ - Discontiguous jumps (]], M-<, xref, imenu, ): granularity=line
+ in the notification is omitted (see above) so VoiceOver will
+ not announce automatically; this explicit announcement fills
+ the gap.
@@ -402,7 +388,7 @@ index 8ef344d9fe..1acb64630a 100644
if (cachedText
&& granularity == ns_ax_text_selection_granularity_line)
{
@@ -9236,6 +9385,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
@@ -9231,6 +9380,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
block_input ();
specpdl_ref count2 = SPECPDL_INDEX ();
@@ -414,7 +400,7 @@ index 8ef344d9fe..1acb64630a 100644
record_unwind_protect_void (unblock_input);
record_unwind_current_buffer ();
if (b != current_buffer)
@@ -9412,12 +9566,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
@@ -9407,12 +9561,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
if (!b)
return;
@@ -444,7 +430,7 @@ index 8ef344d9fe..1acb64630a 100644
if (modiff != self.cachedModiff)
{
self.cachedModiff = modiff;
@@ -9431,6 +9602,7 @@ Text property changes (e.g. face updates from
@@ -9426,6 +9597,7 @@ Text property changes (e.g. face updates from
{
self.cachedCharsModiff = chars_modiff;
[self postTextChangedNotification:point];
@@ -452,12 +438,12 @@ index 8ef344d9fe..1acb64630a 100644
}
}
@@ -9453,8 +9625,15 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
@@ -9448,41 +9620,49 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
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))
- if (MINI_WINDOW_P (w))
+ Restrict the scan to minibuffer windows.
+ Skip overlay announcements when the user just typed a character
+ (didTextChange). Completion frameworks update their overlay
@@ -467,10 +453,71 @@ index 8ef344d9fe..1acb64630a 100644
+ characters inaudible. VoiceOver should read the overlay
+ candidate only when the user navigates (C-n/C-p), not types. */
+ if (!MINI_WINDOW_P (w) || didTextChange)
goto skip_overlay_scan;
+ goto skip_overlay_scan;
+
+ int selected_line = -1;
+ NSString *candidate
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line);
+ if (candidate)
{
- 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])
{
- /* 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);
- }
+ 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);
}
}
}
int selected_line = -1;
@@ -9500,7 +9679,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
+ skip_overlay_scan:
/* --- Cursor moved or selection changed ---
Independent check from the overlay branch above. */
if (point != self.cachedPoint || markActive != self.cachedMarkActive)
@@ -9492,7 +9672,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
self.cachedPoint = point;
self.cachedMarkActive = markActive;
@@ -490,15 +537,15 @@ index 8ef344d9fe..1acb64630a 100644
NSInteger direction = ns_ax_text_selection_direction_discontiguous;
if (point > oldPoint)
direction = ns_ax_text_selection_direction_next;
@@ -9512,6 +9702,7 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
/* --- Granularity detection --- */
@@ -9511,6 +9702,7 @@ 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;
+ BOOL singleLineMove = NO;
[self ensureTextCache];
if (cachedText && oldPoint > 0)
{
@@ -9526,7 +9717,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
NSUInteger tlen = [cachedText length];
@@ -9524,7 +9716,18 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */
NSRange newLine = [cachedText lineRangeForRange:
NSMakeRange (newIdx, 0)];
if (oldLine.location != newLine.location)
@@ -518,12 +565,16 @@ index 8ef344d9fe..1acb64630a 100644
else
{
NSUInteger dist = (newIdx > oldIdx
@@ -9548,34 +9750,23 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
@@ -9546,38 +9749,23 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */
granularity = ns_ax_text_selection_granularity_line;
}
- /* Treat all moves as Emacs-initiated until voiceoverSetPoint
- tracking is introduced (subsequent patch). */
- BOOL emacsMovedCursor = YES;
-
- /* Programmatic jumps that cross a line boundary (]], [[, M-<,
- xref, imenu, ...) are discontiguous: the cursor teleported to an
- xref, imenu, ) are discontiguous: the cursor teleported to an
- arbitrary position, not one sequential step forward/backward.
- Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver
- to re-anchor its rotor browse cursor at the new
@@ -566,7 +617,7 @@ index 8ef344d9fe..1acb64630a 100644
{
NSWindow *win = [self.emacsView window];
if (win)
@@ -9734,6 +9925,13 @@ - (NSRect)accessibilityFrame
@@ -9736,6 +9924,13 @@ - (NSRect)accessibilityFrame
if (vis_start >= vis_end)
return @[];
@@ -580,7 +631,7 @@ index 8ef344d9fe..1acb64630a 100644
block_input ();
specpdl_ref blk_count = SPECPDL_INDEX ();
record_unwind_protect_void (unblock_input);
@@ -10040,6 +10238,10 @@ - (void)dealloc
@@ -10043,6 +10238,10 @@ - (void)dealloc
#endif
[accessibilityElements release];
@@ -591,7 +642,7 @@ index 8ef344d9fe..1acb64630a 100644
[[self menu] release];
[super dealloc];
}
@@ -11489,6 +11691,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
@@ -11492,6 +11691,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO;
processingCompose = NO;
@@ -601,7 +652,7 @@ index 8ef344d9fe..1acb64630a 100644
scrollbarsNeedingUpdate = 0;
fs_state = FULLSCREEN_NONE;
fs_before_fs = next_maximized = -1;
@@ -12797,6 +13002,161 @@ - (id)accessibilityFocusedUIElement
@@ -12800,6 +13002,156 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */
@@ -702,11 +753,6 @@ index 8ef344d9fe..1acb64630a 100644
+ childFrameLastBuffer = BVAR (b, name);
+ childFrameLastModiff = modiff;
+
+ /* BUFFER_LIVE_P(b) is already checked at entry (line above the
+ EQ comparison). No code between that check and here can kill
+ the buffer, so this second check is redundant. */
+ eassert (BUFFER_LIVE_P (b));
+
+ /* Skip buffers larger than a typical completion popup.
+ This avoids scanning eldoc, which-key, or other child
+ frame buffers that are not completion UIs. */
@@ -763,7 +809,7 @@ index 8ef344d9fe..1acb64630a 100644
- (void)postAccessibilityUpdates
{
NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12807,11 +13167,69 @@ - (void)postAccessibilityUpdates
@@ -12810,11 +13162,69 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us