patches: fix Tab navigation in completion buffer (probe order + Tab detection)

This commit is contained in:
2026-02-27 10:10:39 +01:00
parent 1245253e15
commit 7a0e7722f7

View File

@@ -1,13 +1,13 @@
From 1caa0476b3109ad583715c2f8a90c943780ffcb9 Mon Sep 17 00:00:00 2001
From 03ad0337a0f4cf8b01261eb34068fb17cc925e96 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Fri, 27 Feb 2026 09:57:00 +0100
Date: Fri, 27 Feb 2026 10:10:36 +0100
Subject: [PATCH] ns: implement VoiceOver accessibility (AXBoundsForRange, line
nav, completions)
---
src/nsterm.h | 73 ++
src/nsterm.m | 2249 +++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 2187 insertions(+), 135 deletions(-)
src/nsterm.m | 2262 +++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 2210 insertions(+), 125 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..717a838 100644
@@ -108,7 +108,7 @@ index 7c1ee4c..717a838 100644
diff --git a/src/nsterm.m b/src/nsterm.m
index 932d209..220dccf 100644
index 932d209..0fa6b2d 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -46,6 +46,7 @@ Updated by Christian Limpach (chris@nice.ch)
@@ -169,7 +169,7 @@ index 932d209..220dccf 100644
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -6849,207 +6886,1829 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
@@ -6849,194 +6886,1849 @@ - (BOOL)fulfillService: (NSString *)name withArg: (NSString *)arg
/* ==========================================================================
@@ -508,20 +508,23 @@ index 932d209..220dccf 100644
+ ptrdiff_t best_end = 0;
+ ptrdiff_t best_dist = PTRDIFF_MAX;
+ BOOL found = NO;
- if (!font_panel_result && FRAME_FONT (emacsframe))
+ /* Fast path: look at point and immediate neighbors first. */
+ ptrdiff_t probes[3] = { point, point - 1, point + 1 };
+
+ /* Fast path: look at point and immediate neighbors first.
+ Prefer point+1 over point-1: when Tab moves to a new completion,
+ point is at the START of the new entry while point-1 is still
+ inside the previous entry's overlay. Forward probe finds the
+ correct new entry; backward probe finds the wrong old one. */
+ ptrdiff_t probes[3] = { point, point + 1, point - 1 };
+ for (int i = 0; i < 3 && !found; i++)
{
- font_panel_result
- = macfont_get_nsctfont (FRAME_FONT (emacsframe));
+ {
+ ptrdiff_t p = probes[i];
+ if (p < begv || p > zv)
+ continue;
- if (font_panel_result)
- [font_panel_result retain];
- if (!font_panel_result && FRAME_FONT (emacsframe))
- {
- font_panel_result
- = macfont_get_nsctfont (FRAME_FONT (emacsframe));
+ Lisp_Object overlays = Foverlays_at (make_fixnum (p), Qnil);
+ Lisp_Object tail;
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail))
@@ -543,9 +546,10 @@ index 932d209..220dccf 100644
+ found = YES;
+ break;
+ }
}
+ }
- [NSApp stop: self];
- if (font_panel_result)
- [font_panel_result retain];
+ if (!found)
+ {
+ for (ptrdiff_t scan = begv; scan < zv; scan++)
@@ -582,8 +586,9 @@ index 932d209..220dccf 100644
+ }
+ }
+ }
+ }
+
}
- [NSApp stop: self];
+ if (!found)
+ return NO;
+
@@ -605,7 +610,19 @@ index 932d209..220dccf 100644
- [font_panel_result release];
- font_panel_result = nil;
+ if (!FIXNUMP (ev))
+ {
+ /* Handle symbol events: backtab (S-Tab = previous completion). */
+ if (SYMBOLP (ev) && !NILP (ev))
+ {
+ if (EQ (ev, intern ("backtab")))
+ {
+ if (which)
+ *which = -1;
+ return true;
+ }
+ }
+ return false;
+ }
- [NSApp stop: self];
+ EMACS_INT c = XFIXNUM (ev);
@@ -621,6 +638,12 @@ index 932d209..220dccf 100644
+ *which = -1;
+ return true;
+ }
+ if (c == 9) /* Tab — next completion/link */
+ {
+ if (which)
+ *which = 1;
+ return true;
+ }
+ return false;
}
-#endif
@@ -674,10 +697,10 @@ index 932d209..220dccf 100644
+ struct buffer *oldb = current_buffer;
+ if (b != current_buffer)
+ set_buffer_internal_1 (b);
- font_panel_active = YES;
- timeout = make_timespec (0, 100000000);
+ /* Prefer canonical completion candidate string from text property. */
+
+ /* Prefer canonical completion candidate string from text property.
+ Try both completion--string (new API, set by minibuffer.el) and
+ completion (older API used by some modes). */
+ ptrdiff_t probes[2] = { start, end - 1 };
+ for (int i = 0; i < 2 && !text; i++)
+ {
@@ -687,6 +710,25 @@ index 932d209..220dccf 100644
+ Qnil);
+ if (STRINGP (cstr))
+ text = [NSString stringWithLispString:cstr];
+ if (!text)
+ {
+ /* Fallback: 'completion property used by display-completion-list. */
+ cstr = Fget_char_property (make_fixnum (p),
+ intern ("completion"),
+ Qnil);
+ if (STRINGP (cstr))
+ text = [NSString stringWithLispString:cstr];
+ }
+ }
- font_panel_active = YES;
- timeout = make_timespec (0, 100000000);
+ if (!text)
+ {
+ NSUInteger ax_s = [elem accessibilityIndexForCharpos:start];
+ NSUInteger ax_e = [elem accessibilityIndexForCharpos:end];
+ if (ax_e > ax_s && ax_e <= [cachedText length])
+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)];
+ }
- block_input ();
@@ -699,23 +741,11 @@ index 932d209..220dccf 100644
- )
- ns_select_1 (0, NULL, NULL, NULL, &timeout, NULL, YES);
- unblock_input ();
+ if (!text)
+ {
+ NSUInteger ax_s = [elem accessibilityIndexForCharpos:start];
+ NSUInteger ax_e = [elem accessibilityIndexForCharpos:end];
+ if (ax_e > ax_s && ax_e <= [cachedText length])
+ text = [cachedText substringWithRange:NSMakeRange (ax_s, ax_e - ax_s)];
+ }
- if (font_panel_result)
- [font_panel_result autorelease];
+ if (b != oldb)
+ set_buffer_internal_1 (oldb);
-#ifdef NS_IMPL_COCOA
- if (!canceled)
- font_panel_result = nil;
-#endif
- if (font_panel_result)
- [font_panel_result autorelease];
+ if (text)
+ {
+ text = [text stringByTrimmingCharactersInSet:
@@ -724,48 +754,45 @@ index 932d209..220dccf 100644
+ text = nil;
+ }
- result = font_panel_result;
-#ifdef NS_IMPL_COCOA
- if (!canceled)
- font_panel_result = nil;
-#endif
+ return text;
+}
- result = font_panel_result;
- font_panel_result = nil;
- [[fm fontPanel: YES] setIsVisible: NO];
- font_panel_active = NO;
+@implementation EmacsAccessibilityElement
- if (result)
- return ns_font_desc_to_font_spec ([result fontDescriptor],
- result);
+@implementation EmacsAccessibilityElement
- return Qnil;
+- (instancetype)init
+{
+ self = [super init];
+ if (self)
+ self.lispWindow = Qnil;
+ return self;
}
+}
-- (BOOL)acceptsFirstResponder
- return Qnil;
+/* Return the associated Emacs window if it is still live, else NULL.
+ Use this instead of storing a raw struct window * which can become a
+ dangling pointer after delete-window or kill-buffer. */
+- (struct window *)validWindow
{
- NSTRACE ("[EmacsView acceptsFirstResponder]");
- return YES;
+{
+ if (NILP (self.lispWindow) || !WINDOW_LIVE_P (self.lispWindow))
+ return NULL;
+ return XWINDOW (self.lispWindow);
}
-/* Tell NS we want to accept clicks that activate the window */
-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
-- (BOOL)acceptsFirstResponder
+- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)ew height:(int)eh
{
- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld",
- [theEvent type], [theEvent clickCount]);
- return ns_click_through;
+{
+ EmacsView *view = self.emacsView;
+ if (!view || ![view window])
+ return NSZeroRect;
@@ -773,36 +800,32 @@ index 932d209..220dccf 100644
+ NSRect r = NSMakeRect (x, y, ew, eh);
+ NSRect winRect = [view convertRect:r toView:nil];
+ return [[view window] convertRectToScreen:winRect];
+}
+
+- (BOOL)isAccessibilityElement
{
- NSTRACE ("[EmacsView acceptsFirstResponder]");
return YES;
}
-/* Tell NS we want to accept clicks that activate the window */
-- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
+/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */
+
+- (id)accessibilityParent
{
- NSTRACE_MSG ("First mouse event: type=%ld, clickCount=%ld",
- [theEvent type], [theEvent clickCount]);
- return ns_click_through;
+ return NSAccessibilityUnignoredAncestor (self.emacsView);
}
-- (void)resetCursorRects
+
+- (BOOL)isAccessibilityElement
+- (id)accessibilityWindow
{
- NSRect visible = [self visibleRect];
- NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe);
- NSTRACE ("[EmacsView resetCursorRects]");
+ return YES;
+}
- if (currentCursor == nil)
- currentCursor = [NSCursor arrowCursor];
+/* ---- Hierarchy plumbing (required for VoiceOver to find us) ---- */
- if (!NSIsEmptyRect (visible))
- [self addCursorRect: visible cursor: currentCursor];
+- (id)accessibilityParent
+{
+ return NSAccessibilityUnignoredAncestor (self.emacsView);
+}
-#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300
-#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
- if ([currentCursor respondsToSelector: @selector(setOnMouseEntered:)])
-#endif
- [currentCursor setOnMouseEntered: YES];
-#endif
+- (id)accessibilityWindow
+{
+ return [self.emacsView window];
+}
+
@@ -2118,23 +2141,10 @@ index 932d209..220dccf 100644
+ NSRect visible = [self visibleRect];
+ NSCursor *currentCursor = FRAME_POINTER_TYPE (emacsframe);
+ NSTRACE ("[EmacsView resetCursorRects]");
+
+ if (currentCursor == nil)
+ currentCursor = [NSCursor arrowCursor];
+
+ if (!NSIsEmptyRect (visible))
+ [self addCursorRect: visible cursor: currentCursor];
+
+#if defined (NS_IMPL_GNUSTEP) || MAC_OS_X_VERSION_MIN_REQUIRED < 101300
+#if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
+ if ([currentCursor respondsToSelector: @selector(setOnMouseEntered:)])
+#endif
+ [currentCursor setOnMouseEntered: YES];
+#endif
}
@@ -8237,6 +9896,28 @@ - (void)windowDidBecomeKey /* for direct calls */
if (currentCursor == nil)
currentCursor = [NSCursor arrowCursor];
@@ -8237,6 +9929,28 @@ - (void)windowDidBecomeKey /* for direct calls */
XSETFRAME (event.frame_or_window, emacsframe);
kbd_buffer_store_event (&event);
ns_send_appdefined (-1); // Kick main loop
@@ -2163,7 +2173,7 @@ index 932d209..220dccf 100644
}
@@ -9474,6 +11155,304 @@ - (int) fullscreenState
@@ -9474,6 +11188,304 @@ - (int) fullscreenState
return fs_state;
}