patches: fix Zoom, j/k line-read, fold/unfold, C-n/C-p (regression fixes)

Four regressions introduced during review-based refactoring:

1. ZOOM FOCUS JUMPING (P0008 fix in P0000 scope):
   ns_accessibility_enabled guard was added to ns_zoom_track_completion,
   ns_update_end fallback, and ns_draw_window_cursor.  Zoom works
   independently of VoiceOver; ns_accessibility_enabled is only set when
   a screen reader (VoiceOver) activates the AT layer.  Users who use
   Zoom without VoiceOver got no cursor tracking at all.
   Fix: remove ns_accessibility_enabled from all three Zoom call sites;
   guard only with ns_zoom_enabled_p() as in the original.

2. j/k (ANY SINGLE-STEP LINE COMMAND) READS ONLY FIRST WORD:
   The code only treated C-n/C-p (isCtrlNP) as sequential line moves.
   All other line-movement commands (evil j/k, outline-next-heading,
   org-next-visible-heading, etc.) were classified as 'discontiguous'
   jumps, causing VoiceOver to re-anchor and read only a word.
   Fix: detect single-step moves structurally via NSString line-range
   adjacency (NSMaxRange(oldLine) == newLine.location for forward,
   NSMaxRange(newLine) == oldLine.location for backward).  Any command
   that moves exactly one line is sequential --- no command-name
   whitelisting needed, no package-specific code.

3. ORG FOLD/UNFOLD NOT REFRESHING VOICEOVER (P0007):
   BUF_CHARS_MODIFF misses text-property changes such as 'invisible
   used by org-fold-core (org >= 29), outline-mode, hideshow-mode.
   Fix: use BUF_MODIFF; cost is acceptable (rebuild only on VoiceOver
   queries at human interaction speed, not at redisplay speed).

4. C-n/C-p DROPPED LINE-READ (P0005):
   FocusedUIElementChanged posted for ALL emacsMovedCursor moves raced
   with AXSelectedTextChanged(granularity=line) and caused VoiceOver
   to drop the line-read.  Fix: skip FocusedUIElementChanged for
   sequential moves (isCtrlNP or singleLineMove).
This commit is contained in:
2026-03-02 21:10:12 +01:00
parent a5ff8d391b
commit d6fc21f975
9 changed files with 110 additions and 18 deletions

View File

@@ -1,4 +1,4 @@
From 2d2b7eb2b3039f3e581460c874a5ece52ebfdb9a Mon Sep 17 00:00:00 2001 From 95f998ecdda8342da491c57cd62c610cd3c15a3e 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: Sat, 28 Feb 2026 22:39:35 +0100
Subject: [PATCH 0/8] ns: integrate with macOS Zoom for cursor tracking Subject: [PATCH 0/8] ns: integrate with macOS Zoom for cursor tracking

View File

@@ -1,4 +1,4 @@
From 01f32063667eb000b95b1514b0f78056aaa53c28 Mon Sep 17 00:00:00 2001 From 1465c5f3ad2ddec5a89329de34d871207626b078 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: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction Subject: [PATCH 1/8] ns: add accessibility base classes and text extraction

View File

@@ -1,4 +1,4 @@
From 4f4fa019e14e1cd9283b09b9b7cf20e772edc809 Mon Sep 17 00:00:00 2001 From d8fb143c071a55e9463282b8017643a3b1159dde 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: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 2/8] ns: implement buffer accessibility element (core Subject: [PATCH 2/8] ns: implement buffer accessibility element (core

View File

@@ -1,4 +1,4 @@
From 4a77386e8210b2ad8fe1c9a0145cbde96d282c7c Mon Sep 17 00:00:00 2001 From a4f0ba874e18fc5b59ab92f38122461223e916f6 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: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line Subject: [PATCH 3/8] ns: add buffer notification dispatch and mode-line

View File

@@ -1,4 +1,4 @@
From f6a4baf7e19aa2b95becff1dc8be4c2fdc85a0d5 Mon Sep 17 00:00:00 2001 From de1a3e8c2cbc4e90d3d9f24378fa00716e1ee175 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: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation Subject: [PATCH 4/8] ns: add interactive span elements for Tab navigation

View File

@@ -1,4 +1,4 @@
From 825798ddb922cb24cf3db72d76c6ea4e29596844 Mon Sep 17 00:00:00 2001 From da937d7af5f787c2bf594457ec494a838bd64afc 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: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay Subject: [PATCH 5/8] ns: integrate accessibility with EmacsView and redisplay

View File

@@ -1,4 +1,4 @@
From b0a0cf378168cd15d2af663100dae87ded394801 Mon Sep 17 00:00:00 2001 From 4f7f340b5e3e2068b9d20e9fe81f6d0cc20a1340 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: Sat, 28 Feb 2026 12:58:11 +0100
Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS Subject: [PATCH 6/8] doc: add VoiceOver accessibility section to macOS

View File

@@ -1,4 +1,4 @@
From 1a3dea1876ecde2e625d3a2b8f41d688568ecbf8 Mon Sep 17 00:00:00 2001 From b90379446166af5f851f6994aff68e242b8d2dca Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz> From: Daneel <daneel@sukany.cz>
Date: Mon, 2 Mar 2026 18:39:46 +0100 Date: Mon, 2 Mar 2026 18:39:46 +0100
Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver

View File

@@ -1,4 +1,4 @@
From f26d6d8d2a9030af3296902b4e3db79fef3ed760 Mon Sep 17 00:00:00 2001 From 614cc39167cfb89a0ee7731b6b8c012ff838f8dc Mon Sep 17 00:00:00 2001
From: Daneel <daneel@sukany.cz> From: Daneel <daneel@sukany.cz>
Date: Mon, 2 Mar 2026 18:49:13 +0100 Date: Mon, 2 Mar 2026 18:49:13 +0100
Subject: [PATCH 8/8] ns: announce child frame completion candidates for Subject: [PATCH 8/8] ns: announce child frame completion candidates for
@@ -33,8 +33,8 @@ area announcements.
doc/emacs/macos.texi | 14 +- doc/emacs/macos.texi | 14 +-
etc/NEWS | 18 +- etc/NEWS | 18 +-
src/nsterm.h | 20 ++ src/nsterm.h | 20 ++
src/nsterm.m | 436 ++++++++++++++++++++++++++++++++++++++++--- src/nsterm.m | 496 ++++++++++++++++++++++++++++++++++++++-----
4 files changed, 446 insertions(+), 42 deletions(-) 4 files changed, 479 insertions(+), 69 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 8d4a7825d8..03a657f970 100644 index 8d4a7825d8..03a657f970 100644
@@ -149,7 +149,7 @@ index 21a93bc799..bdd40b8eb7 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index 8f744d1bf3..b434a7fd41 100644 index 8f744d1bf3..3533eca3bc 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -1126,24 +1126,19 @@ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */ @@ -1126,24 +1126,19 @@ Uses CFAbsoluteTimeGetCurrent() (~5 ns, a VDSO read) for timing. */
@@ -187,6 +187,33 @@ index 8f744d1bf3..b434a7fd41 100644
} }
/* Scan overlay before-string / after-string properties in the /* Scan overlay before-string / after-string properties in the
@@ -1275,7 +1270,7 @@ If a completion candidate is selected (overlay or child frame),
static void
ns_zoom_track_completion (struct frame *f, EmacsView *view)
{
- if (!ns_accessibility_enabled || !ns_zoom_enabled_p ())
+ if (!ns_zoom_enabled_p ())
return;
if (!WINDOWP (f->selected_window))
return;
@@ -1393,7 +1388,7 @@ so the visual offset is (ov_line + 1) * line_h from
(zoomCursorUpdated is NO). */
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && view && !view->zoomCursorUpdated
+ if (view && !view->zoomCursorUpdated
&& ns_zoom_enabled_p ()
&& !NSIsEmptyRect (view->lastCursorRect))
{
@@ -3571,7 +3566,7 @@ EmacsView pixels (AppKit, flipped, top-left origin)
#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
- if (ns_accessibility_enabled && ns_zoom_enabled_p ())
+ if (ns_zoom_enabled_p ())
{
NSRect windowRect = [view convertRect:r toView:nil];
NSRect screenRect
@@ -7407,6 +7402,112 @@ visual line index for Zoom (skip whitespace-only lines @@ -7407,6 +7402,112 @@ visual line index for Zoom (skip whitespace-only lines
return nil; return nil;
@@ -453,7 +480,72 @@ index 8f744d1bf3..b434a7fd41 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;
@@ -9734,6 +9894,13 @@ - (NSRect)accessibilityFrame @@ -9548,34 +9708,40 @@ frameworks like Vertico bump BOTH BUF_MODIFF (via text property
granularity = ns_ax_text_selection_granularity_line;
}
- /* Programmatic jumps that cross a line boundary (]], [[, M-<,
- xref, imenu, …) are discontiguous: the cursor teleported to an
- arbitrary position, not one sequential step forward/backward.
- Reporting AXTextSelectionDirectionDiscontiguous causes VoiceOver
- to re-anchor its rotor browse cursor at the new
- accessibilitySelectedTextRange rather than advancing linearly
- from its previous internal position. */
- if (!isCtrlNP && granularity == ns_ax_text_selection_granularity_line)
+ /* Detect whether this is a single adjacent-line move.
+ Any command that steps exactly one line forward or backward
+ --- including C-n/C-p, evil j/k, outline-next-heading, etc. ---
+ is treated as sequential so VoiceOver reads the full destination
+ line. Multi-line teleports (]], M-<, xref, imenu, isearch, ...) are
+ discontiguous so VoiceOver re-anchors its browse cursor.
+ Adjacency is detected structurally (NSString line ranges), not
+ by command name, so no package-specific code is needed. */
+ BOOL singleLineMove = NO;
+ if (!isCtrlNP
+ && granularity == ns_ax_text_selection_granularity_line
+ && cachedText)
+ {
+ BOOL adjFwd = (newLine.location == NSMaxRange (oldLine));
+ BOOL adjBwd = (NSMaxRange (newLine) == oldLine.location);
+ singleLineMove = adjFwd || adjBwd;
+ }
+
+ /* Multi-line teleports are discontiguous; single adjacent-line
+ steps stay sequential. */
+ if (!isCtrlNP && !singleLineMove
+ && granularity == ns_ax_text_selection_granularity_line)
direction = ns_ax_text_selection_direction_discontiguous;
- /* If Emacs moved the cursor (not VoiceOver), force discontiguous
- so VoiceOver re-anchors its browse cursor to the current
- accessibilitySelectedTextRange. This covers all Emacs-initiated
- moves: editing commands, ELisp, isearch, etc.
- Exception: C-n/C-p (isCtrlNP) already uses next/previous with
- line granularity; those are already sequential and VoiceOver
- handles them correctly. */
- if (emacsMovedCursor && !isCtrlNP)
+ /* Emacs-initiated teleports need re-anchor; sequential steps
+ (C-n/C-p or any adjacent-line command) do not. */
+ if (emacsMovedCursor && !isCtrlNP && !singleLineMove)
direction = ns_ax_text_selection_direction_discontiguous;
- /* Re-anchor VoiceOver's browse cursor for discontiguous (teleport)
- moves only. For sequential C-n/C-p (isCtrlNP), posting
- FocusedUIElementChanged on the window races with the
- AXSelectedTextChanged(granularity=line) notification and
- causes VoiceOver to drop the line-read speech. Sequential
- moves are already handled correctly by AXSelectedTextChanged
- with direction=next/previous + granularity=line. */
- if (emacsMovedCursor && !isCtrlNP && [self isAccessibilityFocused])
+ /* FocusedUIElementChanged only for teleports: posting it for
+ sequential moves races with AXSelectedTextChanged(granularity=line)
+ and causes VoiceOver to drop the line-read speech. */
+ if (emacsMovedCursor && !isCtrlNP && !singleLineMove
+ && [self isAccessibilityFocused])
{
NSWindow *win = [self.emacsView window];
if (win)
@@ -9734,6 +9900,13 @@ - (NSRect)accessibilityFrame
if (vis_start >= vis_end) if (vis_start >= vis_end)
return @[]; return @[];
@@ -467,7 +559,7 @@ index 8f744d1bf3..b434a7fd41 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);
@@ -9858,6 +10025,7 @@ than O(chars). Fall back to pos+1 as safety net. */ @@ -9858,6 +10031,7 @@ than O(chars). Fall back to pos+1 as safety net. */
pos = span_end; pos = span_end;
} }
@@ -475,7 +567,7 @@ index 8f744d1bf3..b434a7fd41 100644
return [[spans copy] autorelease]; return [[spans copy] autorelease];
} }
@@ -10039,6 +10207,10 @@ - (void)dealloc @@ -10039,6 +10213,10 @@ - (void)dealloc
#endif #endif
[accessibilityElements release]; [accessibilityElements release];
@@ -486,7 +578,7 @@ index 8f744d1bf3..b434a7fd41 100644
[[self menu] release]; [[self menu] release];
[super dealloc]; [super dealloc];
} }
@@ -11488,6 +11660,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f @@ -11488,6 +11666,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO; windowClosing = NO;
processingCompose = NO; processingCompose = NO;
@@ -496,7 +588,7 @@ index 8f744d1bf3..b434a7fd41 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;
@@ -12796,6 +12971,154 @@ - (id)accessibilityFocusedUIElement @@ -12796,6 +12977,154 @@ - (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. */
@@ -651,7 +743,7 @@ index 8f744d1bf3..b434a7fd41 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12806,11 +13129,64 @@ - (void)postAccessibilityUpdates @@ -12806,11 +13135,64 @@ - (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