6 Commits

Author SHA1 Message Date
abc518a60a patches: fix duplicate EmacsAccessibilityBuffer(InteractiveSpans)
Stub @implementation added in 0001 was never removed when 0004
added the full implementation, causing Clang to error:
  reimplementation of category 'InteractiveSpans'

Remove the stub block in 0004 (interactive span elements for Tab).
2026-03-04 19:42:01 +01:00
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
11 changed files with 2469 additions and 566 deletions

View File

@@ -423,29 +423,6 @@ Skip for beamer exports — beamer uses adjustbox on plain tabular."
;;; ORG MODE — CUSTOM BEHAVIOR ;;; 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, ;; Also trigger on post-command-hook in agenda buffers (catches Evil j/k,
;; super-agenda navigation, and any other motion commands) ;; super-agenda navigation, and any other motion commands)
(add-hook 'org-agenda-mode-hook (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,6 +1,6 @@
From fcc1826baee5b424d5fdc176239c5675aee6159b Mon Sep 17 00:00:00 2001 From 2bce9ba4ad500eabad619e684ba319b58f9b1fca Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 22:39:35 +0100 Date: Wed, 4 Mar 2026 15:23:53 +0100
Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking
Inform macOS Zoom of the text cursor position so the zoomed viewport Inform macOS Zoom of the text cursor position so the zoomed viewport
@@ -28,8 +28,8 @@ to the selected completion candidate after normal cursor tracking.
--- ---
etc/NEWS | 11 ++ etc/NEWS | 11 ++
src/nsterm.h | 6 + src/nsterm.h | 6 +
src/nsterm.m | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 366 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 371 insertions(+) 3 files changed, 383 insertions(+)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index 7367e3ccbd..4c149e41d6 100644 index 7367e3ccbd..4c149e41d6 100644
@@ -71,7 +71,7 @@ index 7c1ee4cf53..ea6e7ba4f5 100644
} }
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209f56..88c9251c18 100644 index 932d209f56..6333a7253a 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -71,6 +71,11 @@ Updated by Christian Limpach (chris@nice.ch) @@ -71,6 +71,11 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -86,7 +86,7 @@ index 932d209f56..88c9251c18 100644
#endif #endif
static EmacsMenu *dockMenu; static EmacsMenu *dockMenu;
@@ -1081,6 +1086,281 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) @@ -1081,6 +1086,293 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
} }
@@ -127,10 +127,9 @@ index 932d209f56..88c9251c18 100644
+/* Identify faces that mark a selected completion candidate. +/* Identify faces that mark a selected completion candidate.
+ Matches vertico-current, corfu-current, icomplete-selected-match, + Matches vertico-current, corfu-current, icomplete-selected-match,
+ ivy-current-match, etc. by checking the face symbol name. + ivy-current-match, etc. by checking the face symbol name.
+ Defined here so the Zoom patch compiles independently of the + Shared by both Zoom cursor tracking and VoiceOver accessibility. */
+ VoiceOver patches. */
+static bool +static bool
+ns_zoom_face_is_selected (Lisp_Object face) +ns_face_name_matches_selected_p (Lisp_Object face)
+{ +{
+ if (SYMBOLP (face)) + if (SYMBOLP (face))
+ { + {
@@ -143,7 +142,7 @@ index 932d209f56..88c9251c18 100644
+ { + {
+ Lisp_Object tail; + Lisp_Object tail;
+ for (tail = face; CONSP (tail); tail = XCDR (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 true;
+ } + }
+ return false; + return false;
@@ -163,6 +162,13 @@ index 932d209f56..88c9251c18 100644
+ if (!MINI_WINDOW_P (w)) + if (!MINI_WINDOW_P (w))
+ return -1; + 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); + struct buffer *b = XBUFFER (w->contents);
+ ptrdiff_t beg = marker_position (w->start); + ptrdiff_t beg = marker_position (w->start);
+ ptrdiff_t end = BUF_ZV (b); + ptrdiff_t end = BUF_ZV (b);
@@ -195,8 +201,11 @@ index 932d209f56..88c9251c18 100644
+ Lisp_Object face + Lisp_Object face
+ = Fget_text_property (make_fixnum (line_start), + = Fget_text_property (make_fixnum (line_start),
+ Qface, str); + Qface, str);
+ if (ns_zoom_face_is_selected (face)) + if (ns_face_name_matches_selected_p (face))
+ {
+ unbind_to (count, Qnil);
+ return line; + return line;
+ }
+ line++; + line++;
+ line_start = i + 1; + line_start = i + 1;
+ } + }
@@ -207,6 +216,7 @@ index 932d209f56..88c9251c18 100644
+ } + }
+ } + }
+ } + }
+ unbind_to (count, Qnil);
+ return -1; + return -1;
+} +}
+ +
@@ -242,7 +252,9 @@ index 932d209f56..88c9251c18 100644
+ ptrdiff_t zv = BUF_ZV (b); + ptrdiff_t zv = BUF_ZV (b);
+ int line = 0; + int line = 0;
+ +
+ block_input ();
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_protect_void (unblock_input);
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
@@ -252,7 +264,7 @@ index 932d209f56..88c9251c18 100644
+ Lisp_Object face + Lisp_Object face
+ = Fget_char_property (make_fixnum (pos), Qface, + = Fget_char_property (make_fixnum (pos), Qface,
+ cw->contents); + cw->contents);
+ if (ns_zoom_face_is_selected (face)) + if (ns_face_name_matches_selected_p (face))
+ { + {
+ unbind_to (count, Qnil); + unbind_to (count, Qnil);
+ *child_frame = cf; + *child_frame = cf;
@@ -368,7 +380,7 @@ index 932d209f56..88c9251c18 100644
static void static void
ns_update_end (struct frame *f) 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 (); unblock_input ();
ns_updating_frame = NULL; ns_updating_frame = NULL;
@@ -410,7 +422,7 @@ index 932d209f56..88c9251c18 100644
} }
static void 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. */ /* Prevent the cursor from being drawn outside the text area. */
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));

View File

@@ -1,7 +1,7 @@
From 29546d323559dbbefd846f7b2720285ff90368c8 Mon Sep 17 00:00:00 2001 From 573beced02b3f9b70ba82694d8e4790cfeee9563 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Wed, 4 Mar 2026 15:23:53 +0100
Subject: [PATCH 2/9] ns: add accessibility base classes and text extraction Subject: [PATCH 2/9] ns: add accessibility base classes and helpers
Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa) Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa)
port. No existing code paths are modified. port. No existing code paths are modified.
@@ -28,12 +28,12 @@ rect via glyph matrix.
ns-accessibility-enabled with corrected doc: initial value is nil, ns-accessibility-enabled with corrected doc: initial value is nil,
set non-nil automatically when an AT is detected at startup. set non-nil automatically when an AT is detected at startup.
--- ---
src/nsterm.h | 131 ++++++++++++++ src/nsterm.h | 131 +++++++++++++++
src/nsterm.m | 482 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 466 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 613 insertions(+) 2 files changed, 597 insertions(+)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index ea6e7ba4f5..f245675513 100644 index ea6e7ba4f5..d9ae6efc2e 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -453,6 +453,124 @@ enum ns_return_frame_mode @@ -453,6 +453,124 @@ enum ns_return_frame_mode
@@ -189,7 +189,7 @@ index ea6e7ba4f5..f245675513 100644
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 88c9251c18..3b923ee5fa 100644 index 6333a7253a..9c53001e37 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch) @@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -200,7 +200,7 @@ index 88c9251c18..3b923ee5fa 100644
#include "systime.h" #include "systime.h"
#include "character.h" #include "character.h"
#include "xwidget.h" #include "xwidget.h"
@@ -7201,6 +7202,460 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg @@ -7213,6 +7214,443 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
} }
#endif #endif
@@ -249,11 +249,8 @@ index 88c9251c18..3b923ee5fa 100644
+ +
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ /* block_input must come before record_unwind_protect_void (unblock_input):
+ if specpdl_push were to fail after registration, the unwind handler
+ would call unblock_input without a matching block_input. */
+ block_input ();
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ block_input ();
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
@@ -556,31 +553,6 @@ index 88c9251c18..3b923ee5fa 100644
+ Deferring via dispatch_async lets the current method return first, + Deferring via dispatch_async lets the current method return first,
+ freeing the main queue for VoiceOver's dispatch_sync calls. */ + freeing the main queue for VoiceOver's dispatch_sync calls. */
+ +
+/* Return true if FACE (a symbol or list of symbols) looks like a
+ "selected item" 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: this runs only on overlay/child-frame
+ strings during completion, never in a hot redisplay path. */
+static bool
+ns_ax_face_is_selected (Lisp_Object face)
+{
+ if (SYMBOLP (face) && !NILP (face))
+ {
+ const char *name = SSDATA (SYMBOL_NAME (face));
+ 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;
+}
+
+static inline void +static inline void
+ns_ax_post_notification (id element, +ns_ax_post_notification (id element,
+ NSAccessibilityNotificationName name) + NSAccessibilityNotificationName name)
@@ -655,22 +627,33 @@ index 88c9251c18..3b923ee5fa 100644
+ +
+@end +@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 */ +#endif /* NS_IMPL_COCOA */
+ +
+ +
/* ========================================================================== /* ==========================================================================
EmacsView implementation EmacsView implementation
@@ -11657,6 +12112,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_drag_operation_generic, "ns-drag-operation-generic");
DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion"); DEFSYM (Qns_handle_drag_motion, "ns-handle-drag-motion");
+ /* Accessibility: line navigation command symbols for + /* Accessibility: line navigation command symbols for
+ ns_ax_event_is_line_nav_key (hot path, avoid intern per call). */ + ns_ax_event_is_line_nav_key (hot path, avoid intern per call). */
+ DEFSYM (Qns_ax_next_line, "next-line"); + DEFSYM (Qnext_line, "next-line");
+ DEFSYM (Qns_ax_previous_line, "previous-line"); + DEFSYM (Qprevious_line, "previous-line");
+ DEFSYM (Qns_ax_dired_next_line, "dired-next-line"); + DEFSYM (Qdired_next_line, "dired-next-line");
+ DEFSYM (Qns_ax_dired_previous_line, "dired-previous-line"); + DEFSYM (Qdired_previous_line, "dired-previous-line");
+ +
+ /* Accessibility span scanning symbols. */ + /* Accessibility span scanning symbols. */
+ DEFSYM (Qns_ax_widget, "widget"); + DEFSYM (Qns_ax_widget, "widget");
@@ -686,7 +669,7 @@ index 88c9251c18..3b923ee5fa 100644
Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier));
Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier)); Fput (Qhyper, Qmodifier_value, make_fixnum (hyper_modifier));
Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier));
@@ -11805,6 +12278,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. */); This variable is ignored on Mac OS X < 10.7 and GNUstep. */);
ns_use_srgb_colorspace = YES; ns_use_srgb_colorspace = YES;
@@ -696,7 +679,8 @@ index 88c9251c18..3b923ee5fa 100644
+When nil, the accessibility virtual element tree is not built and no +When nil, the accessibility virtual element tree is not built and no
+notifications are posted, eliminating the associated overhead. +notifications are posted, eliminating the associated overhead.
+Requires the Cocoa (NS) build on macOS; ignored on GNUstep. +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; + ns_accessibility_enabled = NO;
+ +
DEFVAR_BOOL ("ns-use-mwheel-acceleration", DEFVAR_BOOL ("ns-use-mwheel-acceleration",

View File

@@ -1,8 +1,7 @@
From f587654717e7a3d3121e4871f04ffbf4e0d5e9be Mon Sep 17 00:00:00 2001 From 64859d37421bdaabe2ec416285b6f1847da0737c Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Wed, 4 Mar 2026 15:23:54 +0100
Subject: [PATCH 3/9] ns: implement buffer accessibility element (core Subject: [PATCH 3/9] ns: implement buffer accessibility element
protocol)
Implement the NSAccessibility text protocol for Emacs buffer windows. Implement the NSAccessibility text protocol for Emacs buffer windows.
@@ -10,40 +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 (ns_ax_event_is_line_nav_key, ns_ax_completion_text_for_span): New
functions. functions.
(EmacsAccessibilityBuffer): Implement core NSAccessibility protocol. (EmacsAccessibilityBuffer): Implement core NSAccessibility protocol.
(ensureTextCache): Validity gated on BUF_CHARS_MODIFF, not BUF_MODIFF, (ensureTextCache): Validity gated on BUF_MODIFF to catch fold/unfold
to avoid O(buffer-size) rebuilds on every font-lock pass. Add commands (org-mode, outline-mode, hideshow-mode) that change the
explanatory comment on why lineRangeForRange: in the lineStartOffsets 'invisible text property without modifying character content.
loop is safe: it runs only on actual character modifications. BUF_CHARS_MODIFF would serve stale AX text after org-cycle or similar.
(accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs ensureTextCache is called only from AX getters at human interaction
(ax_length == length); fall back to sequence walk for multi-byte runs. speed, not from the redisplay notification path.
(accessibilityIndexForCharpos:): O(1) fast path for pure-ASCII runs.
(charposForAccessibilityIndex:): Symmetric O(1) fast path. (charposForAccessibilityIndex:): Symmetric O(1) fast path.
(accessibilityRole, accessibilityLabel, accessibilityValue) (accessibilityRole, accessibilityLabel, accessibilityValue)
(accessibilityNumberOfCharacters, accessibilitySelectedText) (accessibilityNumberOfCharacters, accessibilitySelectedText)
(accessibilitySelectedTextRange, accessibilityInsertionPointLineNumber) (accessibilitySelectedTextRange, accessibilityInsertionPointLineNumber)
(accessibilityLineForIndex:): New method; return the line number for an (accessibilityLineForIndex:, accessibilityRangeForLine:)
AX character index; defined here so patches 0003+ can call it without (accessibilityRangeForIndex:, accessibilityStyleRangeForIndex:)
forward reference. (accessibilityFrameForRange:, accessibilityRangeForPosition:)
(accessibilityRangeForLine:, accessibilityRangeForIndex:) (accessibilityVisibleCharacterRange, accessibilityFrame)
(accessibilityStyleRangeForIndex:, accessibilityFrameForRange:) (setAccessibilitySelectedTextRange:)
(accessibilityRangeForPosition:, accessibilityVisibleCharacterRange)
(accessibilityFrame, setAccessibilitySelectedTextRange:)
(setAccessibilityFocused:): Implement NSAccessibility protocol methods. (setAccessibilityFocused:): Implement NSAccessibility protocol methods.
--- ---
src/nsterm.m | 1135 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 1135 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 1135 insertions(+) 1 file changed, 1133 insertions(+), 2 deletions(-)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 3b923ee5fa..41c6b8dc14 100644 index 9c53001e37..e4b3fb17a0 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7653,6 +7653,1141 @@ - (id)accessibilityTopLevelUIElement @@ -7648,6 +7648,1137 @@ - (void)invalidateInteractiveSpans
@end @end
+
+
+
+
+static BOOL +static BOOL
+ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point, +ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
+ ptrdiff_t *out_start, + ptrdiff_t *out_start,
@@ -155,15 +149,15 @@ index 3b923ee5fa..41c6b8dc14 100644
+ { + {
+ Lisp_Object cmd = Vthis_command; + Lisp_Object cmd = Vthis_command;
+ /* Forward line commands. */ + /* Forward line commands. */
+ if (EQ (cmd, Qns_ax_next_line) + if (EQ (cmd, Qnext_line)
+ || EQ (cmd, Qns_ax_dired_next_line)) + || EQ (cmd, Qdired_next_line))
+ { + {
+ if (which) *which = 1; + if (which) *which = 1;
+ return true; + return true;
+ } + }
+ /* Backward line commands. */ + /* Backward line commands. */
+ if (EQ (cmd, Qns_ax_previous_line) + if (EQ (cmd, Qprevious_line)
+ || EQ (cmd, Qns_ax_dired_previous_line)) + || EQ (cmd, Qdired_previous_line))
+ { + {
+ if (which) *which = -1; + if (which) *which = -1;
+ return true; + return true;
@@ -206,8 +200,8 @@ index 3b923ee5fa..41c6b8dc14 100644
+ /* Block input to prevent concurrent redisplay from modifying buffer + /* Block input to prevent concurrent redisplay from modifying buffer
+ state while we read text properties. Unwind-protected so + state while we read text properties. Unwind-protected so
+ block_input is always matched by unblock_input on signal. */ + block_input is always matched by unblock_input on signal. */
+ block_input ();
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ block_input ();
+ if (b != current_buffer) + if (b != current_buffer)
+ set_buffer_internal_1 (b); + set_buffer_internal_1 (b);
+ +
@@ -370,25 +364,25 @@ index 3b923ee5fa..41c6b8dc14 100644
+ if (!b) + if (!b)
+ return; + return;
+ +
+ /* Use BUF_CHARS_MODIFF, not BUF_MODIFF, for cache validity. + /* Use BUF_MODIFF, not BUF_CHARS_MODIFF, for cache validity.
+ BUF_MODIFF is bumped by every text-property change, including + Fold/unfold commands (org-mode, outline-mode, hideshow-mode) change
+ font-lock face applications on every redisplay. AX text contains + text visibility by modifying the 'invisible text property via
+ only characters, not face data, so property-only changes do not + put-text-property or add-text-properties. These bump BUF_MODIFF
+ affect the cached value. Rebuilding the full buffer text on + but not BUF_CHARS_MODIFF. Using BUF_CHARS_MODIFF would serve stale
+ each font-lock pass is O(buffer-size) per redisplay --- this + AX text across fold/unfold, causing VoiceOver to read the wrong
+ causes progressive slowdown when scrolling through large files. + content after an org-cycle or similar command.
+ BUF_CHARS_MODIFF is bumped only on actual character insertions + ensureTextCache is called exclusively from AX getters at human
+ and deletions, matching the semantic of "did the text change". + interaction speed (never from the redisplay notification path), so
+ This is the pattern used by WebKit and NSTextView. + font-lock passes cause zero rebuild cost via the notification path.
+ Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not + Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
+ included in the cached AX text (it is handled separately via + included in the cached AX text (it is handled separately via
+ explicit announcements in postAccessibilityNotificationsForFrame). + explicit announcements in postAccessibilityNotificationsForFrame).
+ Including overlay_modiff would silently update cachedOverlayModiff + Including overlay_modiff would silently update cachedOverlayModiff
+ and prevent the notification dispatch from detecting changes. */ + 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); + ptrdiff_t pt = BUF_PT (b);
+ NSUInteger textLen = cachedText ? [cachedText length] : 0; + NSUInteger textLen = cachedText ? [cachedText length] : 0;
+ if (cachedText && cachedTextModiff == chars_modiff + if (cachedText && cachedTextModiff == modiff
+ && cachedTextStart == BUF_BEGV (b) + && cachedTextStart == BUF_BEGV (b)
+ && pt >= cachedTextStart + && pt >= cachedTextStart
+ && (textLen == 0 + && (textLen == 0
@@ -404,7 +398,7 @@ index 3b923ee5fa..41c6b8dc14 100644
+ { + {
+ [cachedText release]; + [cachedText release];
+ cachedText = [text retain]; + cachedText = [text retain];
+ cachedTextModiff = chars_modiff; + cachedTextModiff = modiff;
+ cachedTextStart = start; + cachedTextStart = start;
+ +
+ if (visibleRuns) + if (visibleRuns)
@@ -416,10 +410,9 @@ index 3b923ee5fa..41c6b8dc14 100644
+ Walk the cached text once, recording the start offset of each + Walk the cached text once, recording the start offset of each
+ line. Uses NSString lineRangeForRange: --- O(N) in the total + line. Uses NSString lineRangeForRange: --- O(N) in the total
+ text --- but this loop runs only on cache rebuild, which is + text --- but this loop runs only on cache rebuild, which is
+ gated on BUF_CHARS_MODIFF: actual character insertions or + gated on BUF_MODIFF. Font-lock passes trigger a rebuild only
+ deletions. Font-lock (text property changes) does not trigger + when called from AX getters (human interaction speed), never
+ a rebuild, so the hot path (cursor movement, redisplay) never + from the notification path. */
+ enters this code. */
+ if (lineStartOffsets) + if (lineStartOffsets)
+ xfree (lineStartOffsets); + xfree (lineStartOffsets);
+ lineStartOffsets = NULL; + lineStartOffsets = NULL;
@@ -819,9 +812,10 @@ index 3b923ee5fa..41c6b8dc14 100644
+ +
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ /* block_input must come before record_unwind_protect_void (unblock_input). */ + /* Ensure block_input is always matched by unblock_input even if
+ block_input (); + Fset_marker or another Lisp call signals (longjmp). */
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ block_input ();
+ +
+ /* Convert accessibility index to buffer charpos via mapping. */ + /* Convert accessibility index to buffer charpos via mapping. */
+ ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location]; + ptrdiff_t charpos = [self charposForAccessibilityIndex:range.location];
@@ -875,10 +869,10 @@ index 3b923ee5fa..41c6b8dc14 100644
+ if (!view || !view->emacsframe) + if (!view || !view->emacsframe)
+ return; + return;
+ +
+ /* block_input must come before record_unwind_protect_void (unblock_input). */ + /* Use specpdl unwind protection for block_input safety. */
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ block_input ();
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ block_input ();
+ +
+ /* Select the Emacs window so keyboard focus follows VoiceOver. */ + /* Select the Emacs window so keyboard focus follows VoiceOver. */
+ struct frame *f = view->emacsframe; + struct frame *f = view->emacsframe;
@@ -1059,8 +1053,8 @@ index 3b923ee5fa..41c6b8dc14 100644
+ glyph matrix while we traverse it. Use specpdl unwind protection + glyph matrix while we traverse it. Use specpdl unwind protection
+ so block_input is always matched by unblock_input, even if + so block_input is always matched by unblock_input, even if
+ ensureTextCache triggers a Lisp signal (longjmp). */ + ensureTextCache triggers a Lisp signal (longjmp). */
+ specpdl_ref count = SPECPDL_INDEX ();
+ block_input (); + block_input ();
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ +
+ /* Find the glyph row at this y coordinate. */ + /* Find the glyph row at this y coordinate. */
@@ -1178,6 +1172,22 @@ index 3b923ee5fa..41c6b8dc14 100644
#endif /* NS_IMPL_COCOA */ #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 2.43.0

View File

@@ -1,8 +1,7 @@
From d8a98fc40d8285c19e0a73a7e8a53778926b9836 Mon Sep 17 00:00:00 2001 From 11aa323081fd9117381413b6d8e477659d6afc29 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 4/9] ns: add buffer notification dispatch and mode-line Subject: [PATCH 4/9] ns: add AX notifications and mode-line element
element
Add VoiceOver notification dispatch and mode-line readout. Add VoiceOver notification dispatch and mode-line readout.
@@ -26,17 +25,17 @@ mode line.
1 file changed, 606 insertions(+) 1 file changed, 606 insertions(+)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 41c6b8dc14..16343f978a 100644 index e4b3fb17a0..84a32a05cb 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -8788,6 +8788,612 @@ - (NSRect)accessibilityFrame @@ -8779,6 +8779,612 @@ - (NSRect)accessibilityFrame
@end @end
+ +
+ +
+/* =================================================================== +/* ===================================================================
+ EmacsAccessibilityBuffer (Notifications) --- AX event dispatch + EmacsAccessibilityBuffer (Notifications) AX event dispatch
+ +
+ These methods notify VoiceOver of text and selection changes. + These methods notify VoiceOver of text and selection changes.
+ Called from the redisplay cycle (postAccessibilityUpdates). + Called from the redisplay cycle (postAccessibilityUpdates).
@@ -51,7 +50,7 @@ index 41c6b8dc14..16343f978a 100644
+ if (point > self.cachedPoint + if (point > self.cachedPoint
+ && point - self.cachedPoint == 1) + && point - self.cachedPoint == 1)
+ { + {
+ /* Single char inserted --- refresh cache and grab it. */ + /* Single char inserted refresh cache and grab it. */
+ [self invalidateTextCache]; + [self invalidateTextCache];
+ [self ensureTextCache]; + [self ensureTextCache];
+ if (cachedText) + if (cachedText)
@@ -70,7 +69,7 @@ index 41c6b8dc14..16343f978a 100644
+ /* Update cachedPoint here so the selection-move branch does NOT + /* Update cachedPoint here so the selection-move branch does NOT
+ fire for point changes caused by edits. WebKit and Chromium + fire for point changes caused by edits. WebKit and Chromium
+ never send both ValueChanged and SelectedTextChanged for the + 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; + self.cachedPoint = point;
+ +
+ NSDictionary *change = @{ + NSDictionary *change = @{
@@ -281,8 +280,8 @@ index 41c6b8dc14..16343f978a 100644
+ ptrdiff_t currentOverlayStart = 0; + ptrdiff_t currentOverlayStart = 0;
+ ptrdiff_t currentOverlayEnd = 0; + ptrdiff_t currentOverlayEnd = 0;
+ +
+ specpdl_ref count2 = SPECPDL_INDEX ();
+ block_input (); + block_input ();
+ specpdl_ref count2 = SPECPDL_INDEX ();
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ if (b != current_buffer) + if (b != current_buffer)
@@ -471,7 +470,7 @@ index 41c6b8dc14..16343f978a 100644
+ } + }
+ +
+ /* --- Cursor moved or selection changed --- + /* --- 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. */ + per the WebKit/Chromium pattern. */
+ else if (point != self.cachedPoint || markActive != self.cachedMarkActive) + else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ { + {

View File

@@ -1,7 +1,7 @@
From 9c233aa400c2769e1621ec37f326d1e24c0da2df Mon Sep 17 00:00:00 2001 From 9d99df6f95ac2011a1a9f3de448f2f0ec4c27145 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 5/9] ns: add interactive span elements for Tab navigation Subject: [PATCH 5/9] ns: add interactive span elements for Tab
* src/nsterm.m (ns_ax_scan_interactive_spans): New function; scans the * src/nsterm.m (ns_ax_scan_interactive_spans): New function; scans the
visible portion of a buffer for interactive text properties visible portion of a buffer for interactive text properties
@@ -14,31 +14,32 @@ elements with an AXPress action that sends a synthetic TAB keystroke.
(accessibilityChildrenInNavigationOrder): Return cached span array, (accessibilityChildrenInNavigationOrder): Return cached span array,
rebuilding lazily when interactiveSpansDirty is set. rebuilding lazily when interactiveSpansDirty is set.
--- ---
src/nsterm.m | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/nsterm.m | 304 +++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 298 insertions(+), 4 deletions(-) 1 file changed, 293 insertions(+), 11 deletions(-)
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 16343f978a..f5e5cea074 100644 index 84a32a05cb..b327102521 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -8669,12 +8669,12 @@ - (NSRange)accessibilityRangeForPosition:(NSPoint)screenPoint @@ -7637,17 +7637,6 @@ - (id)accessibilityTopLevelUIElement
return NSMakeRange (0, 0);
/* Block input to prevent concurrent redisplay from modifying the @end
- 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). */
+ glyph matrix while we traverse it. block_input must come before
+ record_unwind_protect_void (unblock_input) so that the unwind
+ handler is never called without a matching block_input. */
specpdl_ref count = SPECPDL_INDEX ();
- record_unwind_protect_void (unblock_input);
block_input ();
+ record_unwind_protect_void (unblock_input);
/* Find the glyph row at this y coordinate. */ -/* Stub implementation of InteractiveSpans category.
struct glyph_matrix *matrix = w->current_matrix; - The full implementation is added in a later patch. */
@@ -9394,6 +9394,300 @@ - (NSRect)accessibilityFrame -@implementation EmacsAccessibilityBuffer (InteractiveSpans)
-
-- (void)invalidateInteractiveSpans
-{
- /* Stub: full implementation added in patch 0004. */
-}
-
-@end
-
static BOOL
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
ptrdiff_t *out_start,
@@ -9385,6 +9374,299 @@ - (NSRect)accessibilityFrame
@end @end
@@ -265,12 +266,11 @@ index 16343f978a..f5e5cea074 100644
+ window being deleted between capture and execution. */ + window being deleted between capture and execution. */
+ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) + if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin)))
+ return; + return;
+ /* block_input must come before record_unwind_protect_void (unblock_input) + /* Use specpdl unwind protection so that block_input is always
+ so the unwind handler is never invoked without a matching block_input, + matched by unblock_input, even if Fselect_window signals. */
+ even if Fselect_window signals (longjmp). */
+ specpdl_ref count = SPECPDL_INDEX (); + specpdl_ref count = SPECPDL_INDEX ();
+ block_input ();
+ record_unwind_protect_void (unblock_input); + record_unwind_protect_void (unblock_input);
+ block_input ();
+ record_unwind_current_buffer (); + record_unwind_current_buffer ();
+ Fselect_window (lwin, Qnil); + Fselect_window (lwin, Qnil);
+ struct window *w = XWINDOW (lwin); + struct window *w = XWINDOW (lwin);

View File

@@ -1,7 +1,7 @@
From 411c0c3f06ad4c2d5aae2b17b809e8899ea892ba Mon Sep 17 00:00:00 2001 From cba3eda4d3c50dcd77c73353c6a8d2713bfcb8ae Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 6/9] ns: integrate accessibility with EmacsView and redisplay Subject: [PATCH 6/9] ns: wire accessibility into EmacsView and redisplay
Wire the accessibility element tree into EmacsView and hook it into Wire the accessibility element tree into EmacsView and hook it into
the redisplay cycle. the redisplay cycle.
@@ -23,9 +23,8 @@ com.apple.accessibility.api distributed notification.
(accessibilityAttributeValue:forParameter:): New methods. (accessibilityAttributeValue:forParameter:): New methods.
--- ---
etc/NEWS | 13 ++ etc/NEWS | 13 ++
src/nsterm.h | 7 +- src/nsterm.m | 475 +++++++++++++++++++++++++++++++++++++++++++++++++--
src/nsterm.m | 474 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 478 insertions(+), 10 deletions(-)
3 files changed, 483 insertions(+), 11 deletions(-)
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index 4c149e41d6..7f917f93b2 100644 index 4c149e41d6..7f917f93b2 100644
@@ -51,29 +50,11 @@ index 4c149e41d6..7f917f93b2 100644
--- ---
** Re-introduced dictation, lost in Emacs v30 (macOS). ** Re-introduced dictation, lost in Emacs v30 (macOS).
We lost macOS dictation in v30 when migrating to NSTextInputClient. We lost macOS dictation in v30 when migrating to NSTextInputClient.
diff --git a/src/nsterm.h b/src/nsterm.h
index f245675513..4bf79a9adb 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -590,7 +590,12 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
char *old_title;
BOOL maximizing_resize;
NSMutableArray *accessibilityElements;
- /* See GC safety comment on EmacsAccessibilityElement.lispWindow. */
+ /* Lisp_Object ivars not visible to GC. Both objects are always
+ reachable via the frame's live window tree, so GC cannot collect
+ them. After a window-tree rebuild (delete-window, split-window)
+ a stale EQ match would merely skip a focus notification --- the
+ worst case is one spurious VoiceOver focus event per rebuild.
+ No staticpro() needed: the window tree holds a strong reference. */
Lisp_Object lastSelectedWindow;
Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid;
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index f5e5cea074..c3cd83b774 100644 index b327102521..e003bca5bd 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -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). */ (zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ #if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
@@ -83,17 +64,18 @@ index f5e5cea074..c3cd83b774 100644
&& !NSIsEmptyRect (view->lastCursorRect)) && !NSIsEmptyRect (view->lastCursorRect))
{ {
NSRect r = 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) if (view)
ns_zoom_track_completion (f, view); ns_zoom_track_completion (f, view);
#endif /* NS_IMPL_COCOA */ #endif /* NS_IMPL_COCOA */
+ +
+ /* Post accessibility notifications after each redisplay cycle. */ + /* Post accessibility notifications after each redisplay cycle. */
+ if (view)
+ [view postAccessibilityUpdates]; + [view postAccessibilityUpdates];
} }
static void static void
@@ -6723,9 +6727,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification @@ -6735,9 +6740,56 @@ - (void)applicationDidFinishLaunching: (NSNotification *)notification
} }
#endif #endif
@@ -150,23 +132,15 @@ index f5e5cea074..c3cd83b774 100644
- (void)antialiasThresholdDidChange:(NSNotification *)notification - (void)antialiasThresholdDidChange:(NSNotification *)notification
{ {
#ifdef NS_IMPL_COCOA #ifdef NS_IMPL_COCOA
@@ -7656,7 +7707,6 @@ - (id)accessibilityTopLevelUIElement @@ -8769,7 +8821,6 @@ - (NSRect)accessibilityFrame
-
static BOOL
ns_ax_find_completion_overlay_range (struct buffer *b, ptrdiff_t point,
ptrdiff_t *out_start,
@@ -8789,7 +8839,6 @@ - (NSRect)accessibilityFrame
@end @end
- -
/* =================================================================== /* ===================================================================
EmacsAccessibilityBuffer (Notifications) --- AX event dispatch EmacsAccessibilityBuffer (Notifications) AX event dispatch
@@ -9283,6 +9332,54 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9263,6 +9314,54 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
granularity = ns_ax_text_selection_granularity_line; granularity = ns_ax_text_selection_granularity_line;
} }
@@ -221,7 +195,7 @@ index f5e5cea074..c3cd83b774 100644
/* Post notifications for focused and non-focused elements. */ /* Post notifications for focused and non-focused elements. */
if ([self isAccessibilityFocused]) if ([self isAccessibilityFocused])
[self postFocusedCursorNotification:point [self postFocusedCursorNotification:point
@@ -9395,7 +9492,6 @@ - (NSRect)accessibilityFrame @@ -9375,7 +9474,6 @@ - (NSRect)accessibilityFrame
@end @end
@@ -229,7 +203,7 @@ index f5e5cea074..c3cd83b774 100644
/* =================================================================== /* ===================================================================
EmacsAccessibilityInteractiveSpan --- helpers and implementation EmacsAccessibilityInteractiveSpan --- helpers and implementation
=================================================================== */ =================================================================== */
@@ -9733,6 +9829,7 @@ - (void)dealloc @@ -9712,6 +9810,7 @@ - (void)dealloc
[layer release]; [layer release];
#endif #endif
@@ -237,7 +211,7 @@ index f5e5cea074..c3cd83b774 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11081,6 +11178,32 @@ - (void)windowDidBecomeKey /* for direct calls */ @@ -11060,6 +11159,32 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe); XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event); kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop ns_send_appdefined (-1); // Kick main loop
@@ -270,7 +244,7 @@ index f5e5cea074..c3cd83b774 100644
} }
@@ -12318,6 +12441,332 @@ - (int) fullscreenState @@ -12297,6 +12422,332 @@ - (int) fullscreenState
return fs_state; return fs_state;
} }
@@ -290,7 +264,7 @@ index f5e5cea074..c3cd83b774 100644
+ +
+ if (WINDOW_LEAF_P (w)) + if (WINDOW_LEAF_P (w))
+ { + {
+ /* Buffer element --- reuse existing if available. */ + /* Buffer element reuse existing if available. */
+ EmacsAccessibilityBuffer *elem + EmacsAccessibilityBuffer *elem
+ = [existing objectForKey:[NSValue valueWithPointer:w]]; + = [existing objectForKey:[NSValue valueWithPointer:w]];
+ if (!elem) + if (!elem)
@@ -324,7 +298,7 @@ index f5e5cea074..c3cd83b774 100644
+ } + }
+ else + else
+ { + {
+ /* Internal (combination) window --- recurse into children. */ + /* Internal (combination) window recurse into children. */
+ Lisp_Object child = w->contents; + Lisp_Object child = w->contents;
+ while (!NILP (child)) + while (!NILP (child))
+ { + {
@@ -436,7 +410,7 @@ index f5e5cea074..c3cd83b774 100644
+ accessibilityUpdating = YES; + accessibilityUpdating = YES;
+ +
+ /* Detect window tree change (split, delete, new buffer). Compare + /* 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); + Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
+ if (!EQ (curRoot, lastRootWindow)) + if (!EQ (curRoot, lastRootWindow))
+ { + {
@@ -445,12 +419,12 @@ index f5e5cea074..c3cd83b774 100644
+ } + }
+ +
+ /* If tree is stale, rebuild FIRST so we don't iterate freed + /* 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. */ + freshly-built elements have no previous state to diff against. */
+ if (!accessibilityTreeValid) + if (!accessibilityTreeValid)
+ { + {
+ [self rebuildAccessibilityTree]; + [self rebuildAccessibilityTree];
+ /* Invalidate span cache --- window layout changed. */ + /* Invalidate span cache window layout changed. */
+ for (EmacsAccessibilityElement *elem in accessibilityElements) + for (EmacsAccessibilityElement *elem in accessibilityElements)
+ if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) + if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]])
+ [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans]; + [(EmacsAccessibilityBuffer *) elem invalidateInteractiveSpans];
@@ -603,7 +577,7 @@ index f5e5cea074..c3cd83b774 100644
@end /* EmacsView */ @end /* EmacsView */
@@ -14314,12 +14763,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with @@ -14293,13 +14744,17 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
ns_use_srgb_colorspace = YES; ns_use_srgb_colorspace = YES;
DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
@@ -612,7 +586,8 @@ index f5e5cea074..c3cd83b774 100644
-When nil, the accessibility virtual element tree is not built and no -When nil, the accessibility virtual element tree is not built and no
-notifications are posted, eliminating the associated overhead. -notifications are posted, eliminating the associated overhead.
-Requires the Cocoa (NS) build on macOS; ignored on GNUstep. -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. + doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support.
+Emacs sets this automatically at startup when macOS Zoom is active or +Emacs sets this automatically at startup when macOS Zoom is active or
+any assistive technology (VoiceOver, Switch Control, etc.) is connected, +any assistive technology (VoiceOver, Switch Control, etc.) is connected,

View File

@@ -1,8 +1,7 @@
From 274c545be1a3af3c7e6f416ac3a22e3b98626b0b Mon Sep 17 00:00:00 2001 From a1ea69133e5a5c04076901dce47cd54683c4ca2e Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 12:58:11 +0100 Date: Wed, 4 Mar 2026 15:23:55 +0100
Subject: [PATCH 7/9] doc: add VoiceOver accessibility section to macOS Subject: [PATCH 7/9] doc: add VoiceOver section to macOS appendix
appendix
* doc/emacs/macos.texi (VoiceOver Accessibility): New node between * doc/emacs/macos.texi (VoiceOver Accessibility): New node between
'Mac / GNUstep Events' and 'GNUstep Support'. Document screen reader 'Mac / GNUstep Events' and 'GNUstep Support'. Document screen reader
@@ -111,10 +110,10 @@ index 6bd334f48e..72ac3a9aa9 100644
@section GNUstep Support @section GNUstep Support
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index c3cd83b774..e4e43dd7a3 100644 index e003bca5bd..705c3ece06 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -14764,9 +14764,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with @@ -14745,9 +14745,13 @@ Nil means use fullscreen the old (< 10.7) way. The old way works better with
DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled, DEFVAR_BOOL ("ns-accessibility-enabled", ns_accessibility_enabled,
doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support. doc: /* Non-nil enables Zoom cursor tracking and VoiceOver support.

View File

@@ -1,19 +1,16 @@
From b87fb2b1824761fe3d91a27afe966eada39c1c45 Mon Sep 17 00:00:00 2001 From b6bc1d102334e32dbc3e284d9e65b0f304c3e694 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Mon, 2 Mar 2026 18:39:46 +0100 Date: Wed, 4 Mar 2026 15:23:56 +0100
Subject: [PATCH 8/9] ns: announce overlay completion candidates for VoiceOver Subject: [PATCH 8/9] ns: announce overlay completions to VoiceOver
Completion frameworks such as Vertico, Ivy, and Icomplete render Completion frameworks such as Vertico, Ivy, and Icomplete render
candidates via overlay before-string/after-string properties. Without candidates via overlay before-string/after-string properties. Without
this change VoiceOver cannot read overlay-based completion UIs. this change VoiceOver cannot read overlay-based completion UIs.
* src/nsterm.m (ns_ax_selected_overlay_text): New function; scan * src/nsterm.m (ns_ax_face_is_selected): New static function; matches
overlay strings in the window for a line with a selected face; return 'current', 'selected', 'selection' in face symbol names.
its text. (ns_ax_selected_overlay_text): New function; scan overlay strings in
(accessibilityStringForRange:, accessibilityAttributedStringForRange:) the window for a line with a selected face; return its text.
(accessibilityRangeForLine:): New NSAccessibility protocol methods.
Moved here from planned patch 0008 to keep the AX protocol interface
complete before notification logic uses it.
(ensureTextCache): Switch cache-validity counter from BUF_CHARS_MODIFF (ensureTextCache): Switch cache-validity counter from BUF_CHARS_MODIFF
to BUF_MODIFF. Fold/unfold commands (org-mode, outline-mode, to BUF_MODIFF. Fold/unfold commands (org-mode, outline-mode,
hideshow-mode) change the 'invisible text property via hideshow-mode) change the 'invisible text property via
@@ -30,11 +27,11 @@ granularity_unknown when the cache is absent), so font-lock passes
cannot trigger O(buffer-size) rebuilds via the notification path. cannot trigger O(buffer-size) rebuilds via the notification path.
--- ---
src/nsterm.h | 1 + src/nsterm.h | 1 +
src/nsterm.m | 384 ++++++++++++++++++++++++++++++++++++++++----------- src/nsterm.m | 301 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 306 insertions(+), 79 deletions(-) 2 files changed, 262 insertions(+), 40 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 4bf79a9adb..72ca210bb0 100644 index d9ae6efc2e..ff81675bb5 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -510,6 +510,7 @@ typedef struct ns_ax_visible_run @@ -510,6 +510,7 @@ typedef struct ns_ax_visible_run
@@ -46,41 +43,13 @@ index 4bf79a9adb..72ca210bb0 100644
@property (nonatomic, assign) BOOL cachedMarkActive; @property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement; @property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index e4e43dd7a3..c9fe93a57b 100644 index 705c3ece06..209b8a0a1d 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7263,11 +7263,154 @@ Accessibility virtual elements (macOS / Cocoa only) @@ -7276,11 +7276,126 @@ Accessibility virtual elements (macOS / Cocoa only)
/* ---- Helper: extract buffer text for accessibility ---- */ /* ---- 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
+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 +/* Extract the currently selected candidate text from overlay display
+ strings. Completion frameworks render candidates as overlay + strings. Completion frameworks render candidates as overlay
+ before-string/after-string and highlight the current candidate + before-string/after-string and highlight the current candidate
@@ -170,7 +139,7 @@ index e4e43dd7a3..c9fe93a57b 100644
+ Lisp_Object face + Lisp_Object face
+ = Fget_text_property (make_fixnum (line_starts[li]), + = Fget_text_property (make_fixnum (line_starts[li]),
+ Qface, str); + Qface, str);
+ if (ns_ax_face_is_selected (face)) + if (ns_face_name_matches_selected_p (face))
+ { + {
+ Lisp_Object line + Lisp_Object line
+ = Fsubstring_no_properties ( + = Fsubstring_no_properties (
@@ -205,57 +174,7 @@ index e4e43dd7a3..c9fe93a57b 100644
static NSString * static NSString *
ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start, ns_ax_buffer_text (struct window *w, ptrdiff_t *out_start,
ns_ax_visible_run **out_runs, NSUInteger *out_nruns) ns_ax_visible_run **out_runs, NSUInteger *out_nruns)
@@ -7343,7 +7486,7 @@ Accessibility virtual elements (macOS / Cocoa only) @@ -7906,6 +8021,7 @@ @implementation EmacsAccessibilityBuffer
/* 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));
@@ -7424,7 +7567,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;
@@ -7606,31 +7749,6 @@ already on the main queue (e.g., inside postAccessibilityUpdates
freeing the main queue for VoiceOver's dispatch_sync calls. */
-/* Return true if FACE (a symbol or list of symbols) looks like a
- "selected item" 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: this runs only on overlay/child-frame
- strings during completion, never in a hot redisplay path. */
-static bool
-ns_ax_face_is_selected (Lisp_Object face)
-{
- if (SYMBOLP (face) && !NILP (face))
- {
- const char *name = SSDATA (SYMBOL_NAME (face));
- 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;
-}
-
static inline void
ns_ax_post_notification (id element,
NSAccessibilityNotificationName name)
{
@@ -7924,6 +8043,7 @@ @implementation EmacsAccessibilityBuffer
@synthesize cachedOverlayModiff; @synthesize cachedOverlayModiff;
@synthesize cachedTextStart; @synthesize cachedTextStart;
@synthesize cachedModiff; @synthesize cachedModiff;
@@ -263,39 +182,25 @@ index e4e43dd7a3..c9fe93a57b 100644
@synthesize cachedPoint; @synthesize cachedPoint;
@synthesize cachedMarkActive; @synthesize cachedMarkActive;
@synthesize cachedCompletionAnnouncement; @synthesize cachedCompletionAnnouncement;
@@ -8021,7 +8141,7 @@ - (void)ensureTextCache @@ -8016,20 +8132,33 @@ - (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]);
@@ -8033,25 +8153,38 @@ - (void)ensureTextCache
if (!b)
return; return;
- /* Use BUF_CHARS_MODIFF, not BUF_MODIFF, for cache validity. /* Use BUF_MODIFF, not BUF_CHARS_MODIFF, for cache validity.
- BUF_MODIFF is bumped by every text-property change, including +
- font-lock face applications on every redisplay. AX text contains Fold/unfold commands (org-mode, outline-mode, hideshow-mode) change
- only characters, not face data, so property-only changes do not text visibility by modifying the 'invisible text property via
- affect the cached value. Rebuilding the full buffer text on - put-text-property or add-text-properties. These bump BUF_MODIFF
- each font-lock pass is O(buffer-size) per redisplay --- this - but not BUF_CHARS_MODIFF. Using BUF_CHARS_MODIFF would serve stale
- causes progressive slowdown when scrolling through large files. - AX text across fold/unfold, causing VoiceOver to read the wrong
- BUF_CHARS_MODIFF is bumped only on actual character insertions - content after an org-cycle or similar command.
- and deletions, matching the semantic of "did the text change". - ensureTextCache is called exclusively from AX getters at human
- This is the pattern used by WebKit and NSTextView. - 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 - Do NOT track BUF_OVERLAY_MODIFF here --- overlay text is not
- included in the cached AX text (it is handled separately via - included in the cached AX text (it is handled separately via
- explicit announcements in postAccessibilityNotificationsForFrame). - explicit announcements in postAccessibilityNotificationsForFrame).
- Including overlay_modiff would silently update cachedOverlayModiff - Including overlay_modiff would silently update cachedOverlayModiff
- and prevent the notification dispatch from detecting changes. */ - 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 + `put-text-property' or `add-text-properties'. These bump BUF_MODIFF
+ but NOT BUF_CHARS_MODIFF, because no characters are inserted or + but NOT BUF_CHARS_MODIFF, because no characters are inserted or
+ deleted. Using only BUF_CHARS_MODIFF would serve stale AX text + deleted. Using only BUF_CHARS_MODIFF would serve stale AX text
@@ -320,77 +225,22 @@ index e4e43dd7a3..c9fe93a57b 100644
+ like hl-line-mode bump BUF_OVERLAY_MODIFF on every + like hl-line-mode bump BUF_OVERLAY_MODIFF on every
+ post-command-hook, yielding the same per-keystroke rebuild cost as + post-command-hook, yielding the same per-keystroke rebuild cost as
+ BUF_MODIFF, with none of its correctness guarantee. */ + 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); ptrdiff_t pt = BUF_PT (b);
NSUInteger textLen = cachedText ? [cachedText length] : 0; NSUInteger textLen = cachedText ? [cachedText length] : 0;
- if (cachedText && cachedTextModiff == chars_modiff @@ -8061,9 +8190,8 @@ included in the cached AX text (it is handled separately via
+ if (cachedText && cachedTextModiff == modiff
&& cachedTextStart == BUF_BEGV (b)
&& pt >= cachedTextStart
&& (textLen == 0
@@ -8067,7 +8200,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)
@@ -8079,9 +8212,9 @@ included in the cached AX text (it is handled separately via
Walk the cached text once, recording the start offset of each Walk the cached text once, recording the start offset of each
line. Uses NSString lineRangeForRange: --- O(N) in the total line. Uses NSString lineRangeForRange: --- O(N) in the total
text --- but this loop runs only on cache rebuild, which is text --- but this loop runs only on cache rebuild, which is
- gated on BUF_CHARS_MODIFF: actual character insertions or - gated on BUF_MODIFF. Font-lock passes trigger a rebuild only
- deletions. Font-lock (text property changes) does not trigger - when called from AX getters (human interaction speed), never
- a rebuild, so the hot path (cursor movement, redisplay) never - from the notification path. */
+ gated on BUF_MODIFF changes. Rebuilds happen when any buffer + gated on BUF_MODIFF changes, ensuring the line index always
+ modification occurs (including fold/unfold), ensuring the line + matches the currently visible text (including after fold/unfold). */
+ index always matches the currently visible text.
enters this code. */
if (lineStartOffsets) if (lineStartOffsets)
xfree (lineStartOffsets); xfree (lineStartOffsets);
@@ -8136,7 +8269,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos lineStartOffsets = NULL;
/* Binary search: runs are sorted by charpos (ascending). Find the @@ -8579,26 +8707,26 @@ - (NSInteger)accessibilityInsertionPointLineNumber
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)
{
@@ -8185,10 +8318,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)
@@ -8230,7 +8363,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];
@@ -8252,7 +8385,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
@@ -8597,26 +8730,26 @@ - (NSInteger)accessibilityInsertionPointLineNumber
return [self lineForAXIndex:point_idx]; return [self lineForAXIndex:point_idx];
} }
@@ -428,7 +278,7 @@ index e4e43dd7a3..c9fe93a57b 100644
} }
- (NSInteger)accessibilityLineForIndex:(NSInteger)index - (NSInteger)accessibilityLineForIndex:(NSInteger)index
@@ -8638,6 +8771,29 @@ - (NSInteger)accessibilityLineForIndex:(NSInteger)index @@ -8620,6 +8748,29 @@ - (NSInteger)accessibilityLineForIndex:(NSInteger)index
idx = [cachedText length]; idx = [cachedText length];
return [self lineForAXIndex:idx]; return [self lineForAXIndex:idx];
@@ -458,34 +308,34 @@ index e4e43dd7a3..c9fe93a57b 100644
} }
- (NSRange)accessibilityRangeForIndex:(NSInteger)index - (NSRange)accessibilityRangeForIndex:(NSInteger)index
@@ -8840,7 +8996,7 @@ - (NSRect)accessibilityFrame @@ -8822,7 +8973,7 @@ - (NSRect)accessibilityFrame
/* =================================================================== /* ===================================================================
- EmacsAccessibilityBuffer (Notifications) --- AX event dispatch - EmacsAccessibilityBuffer (Notifications) AX event dispatch
+ EmacsAccessibilityBuffer (Notifications) --- AX event dispatch + EmacsAccessibilityBuffer (Notifications) --- AX event dispatch
These methods notify VoiceOver of text and selection changes. These methods notify VoiceOver of text and selection changes.
Called from the redisplay cycle (postAccessibilityUpdates). Called from the redisplay cycle (postAccessibilityUpdates).
@@ -8855,7 +9011,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point @@ -8837,7 +8988,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
if (point > self.cachedPoint if (point > self.cachedPoint
&& point - self.cachedPoint == 1) && 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. */ + /* Single char inserted --- refresh cache and grab it. */
[self invalidateTextCache]; [self invalidateTextCache];
[self ensureTextCache]; [self ensureTextCache];
if (cachedText) if (cachedText)
@@ -8874,7 +9030,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point @@ -8856,7 +9007,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
/* Update cachedPoint here so the selection-move branch does NOT /* Update cachedPoint here so the selection-move branch does NOT
fire for point changes caused by edits. WebKit and Chromium fire for point changes caused by edits. WebKit and Chromium
never send both ValueChanged and SelectedTextChanged for the 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. */ + same user action --- they are mutually exclusive. */
self.cachedPoint = point; self.cachedPoint = point;
NSDictionary *change = @{ NSDictionary *change = @{
@@ -9268,16 +9424,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9250,16 +9401,80 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active)); BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */ /* --- Text changed (edit) --- */
@@ -562,7 +412,7 @@ index e4e43dd7a3..c9fe93a57b 100644
} }
/* --- Cursor moved or selection changed --- /* --- 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. */ - per the WebKit/Chromium pattern. */
- else if (point != self.cachedPoint || markActive != self.cachedMarkActive) - else if (point != self.cachedPoint || markActive != self.cachedMarkActive)
+ Independent check from the overlay branch above. */ + Independent check from the overlay branch above. */
@@ -570,7 +420,7 @@ index e4e43dd7a3..c9fe93a57b 100644
{ {
ptrdiff_t oldPoint = self.cachedPoint; ptrdiff_t oldPoint = self.cachedPoint;
BOOL oldMarkActive = self.cachedMarkActive; BOOL oldMarkActive = self.cachedMarkActive;
@@ -9295,8 +9515,14 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9277,8 +9492,14 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP); bool isCtrlNP = ns_ax_event_is_line_nav_key (&ctrlNP);
/* --- Granularity detection --- */ /* --- Granularity detection --- */
@@ -586,44 +436,44 @@ index e4e43dd7a3..c9fe93a57b 100644
if (cachedText && oldPoint > 0) if (cachedText && oldPoint > 0)
{ {
NSUInteger tlen = [cachedText length]; NSUInteger tlen = [cachedText length];
@@ -12457,7 +12683,7 @@ - (int) fullscreenState @@ -12438,7 +12659,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w)) if (WINDOW_LEAF_P (w))
{ {
- /* Buffer element --- reuse existing if available. */ - /* Buffer element reuse existing if available. */
+ /* Buffer element --- reuse existing if available. */ + /* Buffer element --- reuse existing if available. */
EmacsAccessibilityBuffer *elem EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]]; = [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem) if (!elem)
@@ -12491,7 +12717,7 @@ - (int) fullscreenState @@ -12472,7 +12693,7 @@ - (int) fullscreenState
} }
else else
{ {
- /* Internal (combination) window --- recurse into children. */ - /* Internal (combination) window recurse into children. */
+ /* Internal (combination) window --- recurse into children. */ + /* Internal (combination) window --- recurse into children. */
Lisp_Object child = w->contents; Lisp_Object child = w->contents;
while (!NILP (child)) while (!NILP (child))
{ {
@@ -12603,7 +12829,7 @@ - (void)postAccessibilityUpdates @@ -12584,7 +12805,7 @@ - (void)postAccessibilityUpdates
accessibilityUpdating = YES; accessibilityUpdating = YES;
/* Detect window tree change (split, delete, new buffer). Compare /* 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. */ + FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow)) if (!EQ (curRoot, lastRootWindow))
{ {
@@ -12612,12 +12838,12 @@ - (void)postAccessibilityUpdates @@ -12593,12 +12814,12 @@ - (void)postAccessibilityUpdates
} }
/* If tree is stale, rebuild FIRST so we don't iterate freed /* 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 + window pointers. Skip notifications for this cycle --- the
freshly-built elements have no previous state to diff against. */ freshly-built elements have no previous state to diff against. */
if (!accessibilityTreeValid) if (!accessibilityTreeValid)
{ {
[self rebuildAccessibilityTree]; [self rebuildAccessibilityTree];
- /* Invalidate span cache --- window layout changed. */ - /* Invalidate span cache window layout changed. */
+ /* Invalidate span cache --- window layout changed. */ + /* Invalidate span cache --- window layout changed. */
for (EmacsAccessibilityElement *elem in accessibilityElements) for (EmacsAccessibilityElement *elem in accessibilityElements)
if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]]) if ([elem isKindOfClass: [EmacsAccessibilityBuffer class]])

View File

@@ -1,8 +1,7 @@
From 5bef7fa553d0dfd9ab933d341a8115d42e026b42 Mon Sep 17 00:00:00 2001 From a6b1def9a83e23cc0e3325d795ac22b46eb16a5d Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Mon, 2 Mar 2026 18:49:13 +0100 Date: Wed, 4 Mar 2026 15:23:56 +0100
Subject: [PATCH 9/9] ns: announce child frame completion candidates for Subject: [PATCH 9/9] ns: announce child frame completions to VoiceOver
VoiceOver
Child frame popups (Corfu, Company-mode child frames) render completion Child frame popups (Corfu, Company-mode child frames) render completion
candidates in a separate frame whose buffer is not accessible via the candidates in a separate frame whose buffer is not accessible via the
@@ -35,10 +34,10 @@ echo area announcements and VoiceOver rotor cursor synchronization.
Remove Zoom section (covered by patch 0000). Fix dangling paragraph. Remove Zoom section (covered by patch 0000). Fix dangling paragraph.
--- ---
doc/emacs/macos.texi | 13 +- doc/emacs/macos.texi | 13 +-
etc/NEWS | 25 +- etc/NEWS | 22 +-
src/nsterm.h | 21 ++ src/nsterm.h | 21 ++
src/nsterm.m | 577 +++++++++++++++++++++++++++++++++++++------ src/nsterm.m | 562 +++++++++++++++++++++++++++++++++++++------
4 files changed, 541 insertions(+), 95 deletions(-) 4 files changed, 528 insertions(+), 90 deletions(-)
diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi diff --git a/doc/emacs/macos.texi b/doc/emacs/macos.texi
index 72ac3a9aa9..cf5ed0ff28 100644 index 72ac3a9aa9..cf5ed0ff28 100644
@@ -65,24 +64,20 @@ index 72ac3a9aa9..cf5ed0ff28 100644
@vindex ns-accessibility-enabled @vindex ns-accessibility-enabled
To disable the accessibility interface entirely (for instance, to To disable the accessibility interface entirely (for instance, to
diff --git a/etc/NEWS b/etc/NEWS diff --git a/etc/NEWS b/etc/NEWS
index 7f917f93b2..bbec21b635 100644 index 7f917f93b2..c6bb4cc5ad 100644
--- a/etc/NEWS --- a/etc/NEWS
+++ b/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 Follow keyboard focus), Emacs informs Zoom of the text cursor position
after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport
automatically tracks the insertion point across window splits and automatically tracks the insertion point across window splits and
-switches. Completion frameworks (Vertico, Icomplete, Ivy for overlay -switches. Completion frameworks (Vertico, Icomplete, Ivy for overlay
-candidates; Corfu, Company-box for child frame popups) are also -candidates; Corfu, Company-box for child frame popups) are also
-tracked: Zoom follows the selected candidate rather than the text +switches. Overlay-based and child-frame completion frameworks are also
-cursor during completion. tracked: Zoom follows the selected candidate rather than the text
+switches. Overlay-based completion frameworks and child-frame popup completions cursor during completion.
+are also tracked: Zoom follows the selected candidate rather than the
+text cursor during completion.
+++ @@ -4385,16 +4384,19 @@ allowing Emacs users access to speech recognition utilities.
** 'line-spacing' now supports specifying spacing above the line.
@@ -4385,16 +4384,20 @@ allowing Emacs users access to speech recognition utilities.
Note: Accepting this permission allows the use of system APIs, which may Note: Accepting this permission allows the use of system APIs, which may
send user data to Apple's speech recognition servers. send user data to Apple's speech recognition servers.
@@ -90,28 +85,28 @@ index 7f917f93b2..bbec21b635 100644
++++ ++++
** VoiceOver accessibility support on macOS. ** VoiceOver accessibility support on macOS.
Emacs now exposes buffer content, cursor position, and interactive 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 -includes AXBoundsForRange for macOS Zoom cursor tracking, line and
-word navigation announcements, Tab-navigable interactive spans -word navigation announcements, Tab-navigable interactive spans
-(buttons, links, completion candidates), and completion announcements -(buttons, links, completion candidates), and completion announcements
-for the *Completions* buffer. The implementation uses a virtual -for the *Completions* buffer. The implementation uses a virtual
-accessibility tree with per-window elements, hybrid SelectedTextChanged -accessibility tree with per-window elements, hybrid SelectedTextChanged
-and AnnouncementRequested notifications, and thread-safe text caching. -and AnnouncementRequested notifications, and thread-safe text caching.
+includes: +elements to the macOS accessibility subsystem (VoiceOver). Standard
+- Line and word navigation announcements via standard movement keys. +navigation keys produce speech feedback: arrow keys read characters and
+- Echo area messages (e.g., "Wrote file", "Git finished") announced +lines, 'M-f'/'M-b' announce words, and shift-modified movement reports
+ automatically as they appear, without user interaction. +selected text. Echo area messages (e.g., "Wrote file", "Git finished")
+- VoiceOver rotor cursor synchronization after large programmatic +are announced automatically without user interaction. The VoiceOver
+ jumps (]], M-<, xref, imenu, etc.). +rotor cursor stays synchronized after large programmatic jumps such as
+- Tab-navigable interactive spans (buttons, links, completion +xref, imenu, or Org heading navigation. Pressing 'TAB' navigates
+ candidates) within a buffer. +interactive spans (buttons, links, completion candidates) within a
+- Completion announcements for the *Completions* buffer, overlay +buffer. Completion frameworks that render via overlays or child frames
+ completion UIs, and child-frame completion popup UIs. +(Vertico, Ivy, Corfu, etc.) announce the selected candidate.
Set 'ns-accessibility-enabled' to nil to disable the accessibility Set 'ns-accessibility-enabled' to nil to disable the accessibility
interface and eliminate the associated overhead. interface and eliminate the associated overhead.
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 72ca210bb0..1c79c8aced 100644 index ff81675bb5..9ee6c86f18 100644
--- a/src/nsterm.h --- a/src/nsterm.h
+++ b/src/nsterm.h +++ b/src/nsterm.h
@@ -504,9 +504,20 @@ typedef struct ns_ax_visible_run @@ -504,9 +504,20 @@ typedef struct ns_ax_visible_run
@@ -135,12 +130,12 @@ index 72ca210bb0..1c79c8aced 100644
@property (nonatomic, assign) ptrdiff_t cachedOverlayModiff; @property (nonatomic, assign) ptrdiff_t cachedOverlayModiff;
@property (nonatomic, assign) ptrdiff_t cachedTextStart; @property (nonatomic, assign) ptrdiff_t cachedTextStart;
@property (nonatomic, assign) ptrdiff_t cachedModiff; @property (nonatomic, assign) ptrdiff_t cachedModiff;
@@ -601,6 +612,14 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType) @@ -596,6 +607,14 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
Lisp_Object lastRootWindow; Lisp_Object lastRootWindow;
BOOL accessibilityTreeValid; BOOL accessibilityTreeValid;
BOOL accessibilityUpdating; BOOL accessibilityUpdating;
+ BOOL childFrameCompletionActive; + BOOL childFrameCompletionActive;
+ NSString *childFrameLastCandidate; + char *childFrameLastCandidate;
+ Lisp_Object childFrameLastBuffer; + Lisp_Object childFrameLastBuffer;
+ EMACS_INT childFrameLastModiff; + EMACS_INT childFrameLastModiff;
+ /* Last BUF_CHARS_MODIFF seen for echo_area_buffer[0]. Used by + /* Last BUF_CHARS_MODIFF seen for echo_area_buffer[0]. Used by
@@ -150,7 +145,7 @@ index 72ca210bb0..1c79c8aced 100644
#endif #endif
BOOL font_panel_active; BOOL font_panel_active;
NSFont *font_panel_result; NSFont *font_panel_result;
@@ -670,6 +689,8 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType) @@ -665,6 +684,8 @@ typedef NS_ENUM(NSInteger, EmacsAXSpanType)
- (void)rebuildAccessibilityTree; - (void)rebuildAccessibilityTree;
- (void)invalidateAccessibilityTree; - (void)invalidateAccessibilityTree;
- (void)postAccessibilityUpdates; - (void)postAccessibilityUpdates;
@@ -160,10 +155,10 @@ index 72ca210bb0..1c79c8aced 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index c9fe93a57b..f7574efb39 100644 index 209b8a0a1d..13696786ab 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1275,6 +1275,12 @@ 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 static void
ns_zoom_track_completion (struct frame *f, EmacsView *view) ns_zoom_track_completion (struct frame *f, EmacsView *view)
{ {
@@ -176,23 +171,7 @@ index c9fe93a57b..f7574efb39 100644
if (!ns_zoom_enabled_p ()) if (!ns_zoom_enabled_p ())
return; return;
if (!WINDOWP (f->selected_window)) if (!WINDOWP (f->selected_window))
@@ -1417,9 +1423,14 @@ so the visual offset is (ov_line + 1) * line_h from @@ -7392,6 +7398,117 @@ visual line index for Zoom (skip whitespace-only lines
/* Track completion candidates for Zoom (overlay and child frame).
Runs after cursor tracking so the selected candidate overrides
- the default cursor position. */
+ the default cursor position. Guard with the same version check
+ as ns_zoom_track_completion's callee (UAZoomChangeFocus requires
+ macOS 10.10+). */
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
if (view)
ns_zoom_track_completion (f, view);
+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
#endif /* NS_IMPL_COCOA */
/* Post accessibility notifications after each redisplay cycle. */
@@ -7407,6 +7418,119 @@ visual line index for Zoom (skip whitespace-only lines
return nil; return nil;
} }
@@ -239,13 +218,11 @@ index c9fe93a57b..f7574efb39 100644
+ The data pointer is used only in this loop, before Lisp calls. */ + The data pointer is used only in this loop, before Lisp calls. */
+ const unsigned char *data = SDATA (str); + const unsigned char *data = SDATA (str);
+ ptrdiff_t byte_len = SBYTES (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 + The caller rejects buffers larger than 10,000 characters
+ (BUF_ZV(b) - BUF_BEGV(b) > 10000 guard in announceChildFrameCompletion), + (BUF_ZV(b) - BUF_BEGV(b) > 10000 guard in announceChildFrameCompletion),
+ so at most ~10,000 / 1-byte-per-line = 10,000 lines could appear, + so the worst case is ~10 KB / 1 byte per line < 512. If a future
+ but completion popups are typically < 512 lines. Use 512 to match + caller removes that guard, lines beyond 512 are silently skipped; */
+ the bound in ns_ax_selected_overlay_text; lines beyond 512 are
+ silently skipped. */
+ ptrdiff_t line_starts[512]; + ptrdiff_t line_starts[512];
+ ptrdiff_t line_ends[512]; + ptrdiff_t line_ends[512];
+ int nlines = 0; + int nlines = 0;
@@ -285,7 +262,7 @@ index c9fe93a57b..f7574efb39 100644
+ Lisp_Object face + Lisp_Object face
+ = Fget_char_property (make_fixnum (buf_pos), Qface, buf_obj); + = 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 + Lisp_Object line
+ = Fsubstring_no_properties (str, + = Fsubstring_no_properties (str,
@@ -312,24 +289,22 @@ index c9fe93a57b..f7574efb39 100644
/* Build accessibility text for window W, skipping invisible text. /* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos. Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -7440,11 +7564,12 @@ visual line index for Zoom (skip whitespace-only lines @@ -7425,9 +7542,13 @@ visual line index for Zoom (skip whitespace-only lines
return @""; return @"";
specpdl_ref count = SPECPDL_INDEX (); specpdl_ref count = SPECPDL_INDEX ();
- record_unwind_current_buffer ();
- /* block_input must come before record_unwind_protect_void (unblock_input):
- if specpdl_push were to fail after registration, the unwind handler
- would call unblock_input without a matching block_input. */
+ /* block_input must precede record_unwind_protect_void (unblock_input): + /* block_input must precede record_unwind_protect_void (unblock_input):
+ if anything between SPECPDL_INDEX and block_input were to throw, + if anything between SPECPDL_INDEX and block_input were to throw,
+ the unwind handler would call unblock_input without a matching + the unwind handler would call unblock_input without a matching
+ block_input, corrupting the input-blocking reference count. */ + block_input, corrupting the input-blocking reference count. */
block_input (); + block_input ();
+ record_unwind_current_buffer (); record_unwind_current_buffer ();
record_unwind_protect_void (unblock_input); record_unwind_protect_void (unblock_input);
- block_input ();
if (b != current_buffer) if (b != current_buffer)
set_buffer_internal_1 (b); set_buffer_internal_1 (b);
@@ -8613,6 +8738,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
@@ -8589,6 +8710,11 @@ - (void)setAccessibilitySelectedTextRange:(NSRange)range
[self ensureTextCache]; [self ensureTextCache];
@@ -340,8 +315,8 @@ index c9fe93a57b..f7574efb39 100644
+ +
specpdl_ref count = SPECPDL_INDEX (); specpdl_ref count = SPECPDL_INDEX ();
record_unwind_current_buffer (); record_unwind_current_buffer ();
/* block_input must come before record_unwind_protect_void (unblock_input). */ /* Ensure block_input is always matched by unblock_input even if
@@ -9060,20 +9190,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point @@ -9037,20 +9163,38 @@ - (void)postFocusedCursorNotification:(ptrdiff_t)point
&& granularity && granularity
== ns_ax_text_selection_granularity_character); == ns_ax_text_selection_granularity_character);
@@ -390,7 +365,7 @@ index c9fe93a57b..f7574efb39 100644
ns_ax_post_notification_with_info ( ns_ax_post_notification_with_info (
self, self,
NSAccessibilitySelectedTextChangedNotification, NSAccessibilitySelectedTextChangedNotification,
@@ -9173,12 +9321,17 @@ user expectation ("w" jumps to next word and reads it). */ @@ -9150,12 +9294,17 @@ user expectation ("w" jumps to next word and reads it). */
} }
} }
@@ -413,7 +388,7 @@ index c9fe93a57b..f7574efb39 100644
if (cachedText if (cachedText
&& granularity == ns_ax_text_selection_granularity_line) && granularity == ns_ax_text_selection_granularity_line)
{ {
@@ -9243,6 +9396,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b @@ -9220,6 +9369,11 @@ - (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
block_input (); block_input ();
specpdl_ref count2 = SPECPDL_INDEX (); specpdl_ref count2 = SPECPDL_INDEX ();
@@ -425,7 +400,7 @@ index c9fe93a57b..f7574efb39 100644
record_unwind_protect_void (unblock_input); record_unwind_protect_void (unblock_input);
record_unwind_current_buffer (); record_unwind_current_buffer ();
if (b != current_buffer) if (b != current_buffer)
@@ -9419,12 +9577,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f @@ -9396,12 +9550,29 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
if (!b) if (!b)
return; return;
@@ -455,7 +430,7 @@ index c9fe93a57b..f7574efb39 100644
if (modiff != self.cachedModiff) if (modiff != self.cachedModiff)
{ {
self.cachedModiff = modiff; self.cachedModiff = modiff;
@@ -9438,6 +9613,7 @@ Text property changes (e.g. face updates from @@ -9415,6 +9586,7 @@ Text property changes (e.g. face updates from
{ {
self.cachedCharsModiff = chars_modiff; self.cachedCharsModiff = chars_modiff;
[self postTextChangedNotification:point]; [self postTextChangedNotification:point];
@@ -463,7 +438,7 @@ index c9fe93a57b..f7574efb39 100644
} }
} }
@@ -9460,41 +9636,49 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9437,41 +9609,49 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
displayed in the minibuffer. In normal editing buffers, displayed in the minibuffer. In normal editing buffers,
font-lock and other modes change BUF_OVERLAY_MODIFF on font-lock and other modes change BUF_OVERLAY_MODIFF on
every redisplay, triggering O(overlays) work per keystroke. every redisplay, triggering O(overlays) work per keystroke.
@@ -477,16 +452,15 @@ index c9fe93a57b..f7574efb39 100644
+ echo produced by postTextChangedNotification, making typed + echo produced by postTextChangedNotification, making typed
+ characters inaudible. VoiceOver should read the overlay + characters inaudible. VoiceOver should read the overlay
+ candidate only when the user navigates (C-n/C-p), not types. */ + candidate only when the user navigates (C-n/C-p), not types. */
+ if (MINI_WINDOW_P (w) && !didTextChange) + if (!MINI_WINDOW_P (w) || didTextChange)
+ { + goto skip_overlay_scan;
+ +
+ int selected_line = -1; + int selected_line = -1;
+ NSString *candidate + NSString *candidate
+ = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b), + = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
+ &selected_line); + &selected_line);
+ if (candidate) + if (candidate)
- { {
+ {
- int selected_line = -1; - int selected_line = -1;
- NSString *candidate - NSString *candidate
- = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b), - = ns_ax_selected_overlay_text (b, BUF_BEGV (b), BUF_ZV (b),
@@ -495,8 +469,7 @@ index c9fe93a57b..f7574efb39 100644
+ /* Deduplicate: only announce when the candidate changed. */ + /* Deduplicate: only announce when the candidate changed. */
+ if (![candidate isEqualToString: + if (![candidate isEqualToString:
+ self.cachedCompletionAnnouncement]) + self.cachedCompletionAnnouncement])
- { {
+ {
- /* Deduplicate: only announce when the candidate changed. */ - /* Deduplicate: only announce when the candidate changed. */
- if (![candidate isEqualToString: - if (![candidate isEqualToString:
- self.cachedCompletionAnnouncement]) - self.cachedCompletionAnnouncement])
@@ -536,17 +509,15 @@ index c9fe93a57b..f7574efb39 100644
+ NSApp, + NSApp,
+ NSAccessibilityAnnouncementRequestedNotification, + NSAccessibilityAnnouncementRequestedNotification,
+ annInfo); + annInfo);
- } }
+ } }
- }
+ }
+ }
} }
+ skip_overlay_scan:
/* --- Cursor moved or selection changed --- /* --- Cursor moved or selection changed ---
Independent check from the overlay branch above. */ Independent check from the overlay branch above. */
if (point != self.cachedPoint || markActive != self.cachedMarkActive) if (point != self.cachedPoint || markActive != self.cachedMarkActive)
@@ -9504,7 +9688,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property @@ -9481,7 +9661,18 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
self.cachedPoint = point; self.cachedPoint = point;
self.cachedMarkActive = markActive; self.cachedMarkActive = markActive;
@@ -566,7 +537,7 @@ index c9fe93a57b..f7574efb39 100644
NSInteger direction = ns_ax_text_selection_direction_discontiguous; NSInteger direction = ns_ax_text_selection_direction_discontiguous;
if (point > oldPoint) if (point > oldPoint)
direction = ns_ax_text_selection_direction_next; direction = ns_ax_text_selection_direction_next;
@@ -9523,6 +9718,7 @@ granularity hint (defaulting to unknown), which causes VoiceOver @@ -9500,6 +9691,7 @@ granularity hint (defaulting to unknown), which causes VoiceOver
to make its own determination. Fresh text is always available to make its own determination. Fresh text is always available
to VoiceOver via the AX getter path (accessibilityValue etc.). */ to VoiceOver via the AX getter path (accessibilityValue etc.). */
NSInteger granularity = ns_ax_text_selection_granularity_unknown; NSInteger granularity = ns_ax_text_selection_granularity_unknown;
@@ -574,7 +545,7 @@ index c9fe93a57b..f7574efb39 100644
if (cachedText && oldPoint > 0) if (cachedText && oldPoint > 0)
{ {
NSUInteger tlen = [cachedText length]; NSUInteger tlen = [cachedText length];
@@ -9536,7 +9732,18 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */ @@ -9513,7 +9705,18 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */
NSRange newLine = [cachedText lineRangeForRange: NSRange newLine = [cachedText lineRangeForRange:
NSMakeRange (newIdx, 0)]; NSMakeRange (newIdx, 0)];
if (oldLine.location != newLine.location) if (oldLine.location != newLine.location)
@@ -594,7 +565,7 @@ index c9fe93a57b..f7574efb39 100644
else else
{ {
NSUInteger dist = (newIdx > oldIdx NSUInteger dist = (newIdx > oldIdx
@@ -9558,38 +9765,23 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */ @@ -9535,38 +9738,23 @@ to VoiceOver via the AX getter path (accessibilityValue etc.). */
granularity = ns_ax_text_selection_granularity_line; granularity = ns_ax_text_selection_granularity_line;
} }
@@ -646,7 +617,7 @@ index c9fe93a57b..f7574efb39 100644
{ {
NSWindow *win = [self.emacsView window]; NSWindow *win = [self.emacsView window];
if (win) if (win)
@@ -9748,6 +9940,13 @@ - (NSRect)accessibilityFrame @@ -9725,6 +9913,13 @@ - (NSRect)accessibilityFrame
if (vis_start >= vis_end) if (vis_start >= vis_end)
return @[]; return @[];
@@ -660,17 +631,18 @@ index c9fe93a57b..f7574efb39 100644
block_input (); block_input ();
specpdl_ref blk_count = SPECPDL_INDEX (); specpdl_ref blk_count = SPECPDL_INDEX ();
record_unwind_protect_void (unblock_input); record_unwind_protect_void (unblock_input);
@@ -10056,6 +10255,9 @@ - (void)dealloc @@ -10032,6 +10227,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
+#ifdef NS_IMPL_COCOA +#ifdef NS_IMPL_COCOA
+ [childFrameLastCandidate release]; + if (childFrameLastCandidate)
+ xfree (childFrameLastCandidate);
+#endif +#endif
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11505,6 +11708,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -11481,6 +11680,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;
@@ -680,7 +652,7 @@ index c9fe93a57b..f7574efb39 100644
scrollbarsNeedingUpdate = 0; scrollbarsNeedingUpdate = 0;
fs_state = FULLSCREEN_NONE; fs_state = FULLSCREEN_NONE;
fs_before_fs = next_maximized = -1; fs_before_fs = next_maximized = -1;
@@ -12813,6 +13019,158 @@ - (id)accessibilityFocusedUIElement @@ -12789,6 +12991,156 @@ - (id)accessibilityFocusedUIElement
The existing elements carry cached state (modiff, point) from the The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */ elements with current values, making change detection impossible. */
@@ -700,10 +672,7 @@ index c9fe93a57b..f7574efb39 100644
+ Reads echo_area_buffer[0] directly because with_echo_area_buffer() + Reads echo_area_buffer[0] directly because with_echo_area_buffer()
+ sets current_buffer via set_buffer_internal_1() but does NOT call + sets current_buffer via set_buffer_internal_1() but does NOT call
+ Fset_window_buffer(), so the minibuffer window's contents pointer + Fset_window_buffer(), so the minibuffer window's contents pointer
+ still points to the inactive " *Minibuf-0*" buffer. + still points to the inactive " *Minibuf-0*" buffer. */
+ echo_area_buffer[] is maintained by setup_echo_area_for_printing()
+ and clear_message() in xdisp.c; its lifetime is the process lifetime
+ and it is valid whenever BUFFERP (echo_area_buffer[0]) is true. */
+- (void)postEchoAreaAnnouncementIfNeeded +- (void)postEchoAreaAnnouncementIfNeeded
+{ +{
+ if (minibuf_level != 0) + if (minibuf_level != 0)
@@ -768,19 +737,19 @@ index c9fe93a57b..f7574efb39 100644
+ if (!BUFFER_LIVE_P (b)) + if (!BUFFER_LIVE_P (b))
+ return; + return;
+ EMACS_INT modiff = BUF_MODIFF (b); + EMACS_INT modiff = BUF_MODIFF (b);
+ /* Compare buffer identity via the buffer name symbol. Interned + /* Compare buffer identity via the buffer name symbol, which is always
+ symbols (obarray) are GC-reachable without staticpro(), avoiding + GC-reachable through the obarray. Storing the name avoids keeping
+ a direct struct buffer pointer in a non-GC-visible ObjC ivar. + a direct buffer pointer in a non-GC-visible ObjC ivar: if the buffer
+ Caveat: if the buffer is renamed (rename-buffer), the stored + were killed and GC swept, a stale make_lisp_ptr value could collide
+ symbol no longer matches the new name and the equality check + with a newly-allocated buffer at the same address. */
+ returns nil, causing one redundant re-scan. This is harmless ---
+ completion popups (Corfu, Company) are never renamed during a
+ completion session. Using a sequence number would avoid the
+ rename edge case but would require another ivar; the name symbol
+ is a pragmatic, GC-safe approximation. */
+ if (EQ (childFrameLastBuffer, BVAR (b, name)) + if (EQ (childFrameLastBuffer, BVAR (b, name))
+ && modiff == childFrameLastModiff) + && modiff == childFrameLastModiff)
+ return; + return;
+ /* Store the buffer name symbol (an interned Lisp_Object from
+ obarray) rather than a raw pointer to struct buffer.
+ Interned symbols are reachable from obarray and will not be
+ garbage-collected, so no staticpro() registration is needed
+ for this ivar. */
+ childFrameLastBuffer = BVAR (b, name); + childFrameLastBuffer = BVAR (b, name);
+ childFrameLastModiff = modiff; + childFrameLastModiff = modiff;
+ +
@@ -807,10 +776,11 @@ index c9fe93a57b..f7574efb39 100644
+ return; + return;
+ +
+ /* Deduplicate --- avoid re-announcing the same candidate. */ + /* Deduplicate --- avoid re-announcing the same candidate. */
+ if ([candidate isEqualToString:childFrameLastCandidate]) + const char *cstr = [candidate UTF8String];
+ if (childFrameLastCandidate && strcmp (cstr, childFrameLastCandidate) == 0)
+ return; + return;
+ [childFrameLastCandidate release]; + xfree (childFrameLastCandidate);
+ childFrameLastCandidate = [candidate copy]; + childFrameLastCandidate = xstrdup (cstr);
+ +
+ NSDictionary *annInfo = @{ + NSDictionary *annInfo = @{
+ NSAccessibilityAnnouncementKey: candidate, + NSAccessibilityAnnouncementKey: candidate,
@@ -839,7 +809,7 @@ index c9fe93a57b..f7574efb39 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12823,14 +13182,71 @@ - (void)postAccessibilityUpdates @@ -12799,11 +13151,69 @@ - (void)postAccessibilityUpdates
/* Re-entrance guard: VoiceOver callbacks during notification posting /* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us can trigger redisplay, which calls ns_update_end, which calls us