Files
emacs-doom/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch
Daneel 3d2fa7a54e patches: fix O(overlays) performance regression
Performance issue: editing large files (>~10KB, >2000 lines) caused
progressive slowdown regardless of VoiceOver status.

Root causes:
1. ns_zoom_find_overlay_candidate_line: called Foverlays_in on the
   entire visible buffer range on every redisplay when UAZoomEnabled().
   In files with many overlays (font-lock, hl-line, show-paren etc.)
   this was O(overlays) Lisp work per keystroke.

2. postAccessibilityNotificationsForFrame: when ns-accessibility-enabled
   is non-nil, checked BUF_OVERLAY_MODIFF every redisplay. font-lock
   bumps this on every redraw, triggering ns_ax_selected_overlay_text
   (another O(overlays) scan) for non-minibuffer windows.

Fix: Both scans now guard with MINI_WINDOW_P check. Overlay completion
frameworks (Vertico, Icomplete, Ivy) only display candidates in
minibuffer windows --- no completion framework puts selected-face
overlays in normal editing buffers. For non-minibuffer windows both
functions return immediately with zero Lisp calls.

Additionally: ns_zoom_find_child_frame_candidate is skipped when
f->child_frame_list is nil (no child frames = no Corfu popup).
2026-03-01 04:26:12 +01:00

407 lines
13 KiB
Diff
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
From 19ab5ca41d38959b8f92ae2e1b7f724972515c3d Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 22:39:35 +0100
Subject: [PATCH 1/9] ns: integrate with macOS Zoom for cursor tracking
Inform macOS Zoom of the text cursor position so the zoomed viewport
follows keyboard focus in Emacs.
Basic cursor tracking:
* src/nsterm.h (EmacsView): Add lastCursorRect, zoomCursorUpdated.
* src/nsterm.m (ns_draw_window_cursor): Store cursor rect in
lastCursorRect; call UAZoomChangeFocus with CG-space coordinates
when UAZoomEnabled returns true. Set zoomCursorUpdated flag.
(ns_update_end): Call UAZoomChangeFocus as fallback when cursor
was not physically redrawn (e.g. after C-x o window switch).
Gated by zoomCursorUpdated to avoid double calls.
Completion candidate tracking:
* src/nsterm.m (ns_zoom_face_is_selected): New predicate.
Match 'current', 'selected', and 'selection' in face symbol
names to identify the highlighted completion candidate.
(ns_zoom_find_overlay_candidate_line): Scan overlay
before-string/after-string for the selected candidate line.
Handles Vertico, Icomplete, Ivy, and similar overlay frameworks.
(ns_zoom_find_child_frame_candidate): Scan child frame buffer
text for the selected candidate. Handles Corfu, Company-box,
and similar child frame frameworks.
(ns_zoom_track_completion): Called from ns_update_end after
cursor tracking. Overrides Zoom focus to the selected
completion candidate when one is found.
Coordinate conversion: EmacsView pixels (AppKit, flipped) ->
NSWindow -> NSScreen -> CGRect with y-flip for CoreGraphics
top-left origin.
Tested on macOS 14 with Zoom enabled: cursor tracking works across
window splits, switches (C-x o), and completion frameworks.
---
etc/NEWS | 11 ++
src/nsterm.h | 6 ++
src/nsterm.m | 289 +++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 306 insertions(+)
diff --git a/etc/NEWS b/etc/NEWS
index ef36df5..80661a9 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -82,6 +82,17 @@ other directory on your system. You can also invoke the
* Changes in Emacs 31.1
++++
+** The macOS NS port now integrates with macOS Zoom.
+When macOS Zoom is enabled (System Settings, Accessibility, Zoom,
+Follow keyboard focus), Emacs informs Zoom of the text cursor position
+after every cursor redraw via 'UAZoomChangeFocus'. The zoomed viewport
+automatically tracks the insertion point across window splits and
+switches. Completion frameworks (Vertico, Icomplete, Ivy for overlay
+candidates; Corfu, Company-box for child frame popups) are also
+tracked: Zoom follows the selected candidate rather than the text
+cursor during completion.
+
+++
** 'line-spacing' now supports specifying spacing above the line.
Previously, only spacing below the line could be specified. The user
diff --git a/src/nsterm.h b/src/nsterm.h
index 7c1ee4c..ea6e7ba 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -484,6 +484,12 @@ enum ns_return_frame_mode
@public
struct frame *emacsframe;
int scrollbarsNeedingUpdate;
+#ifdef NS_IMPL_COCOA
+ /* Cached cursor rect for macOS Zoom integration. Set by
+ ns_draw_window_cursor, used by ns_update_end fallback. */
+ NSRect lastCursorRect;
+ BOOL zoomCursorUpdated;
+#endif
NSRect ns_userRect;
}
diff --git a/src/nsterm.m b/src/nsterm.m
index 74e4ad5..eee4cd9 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1081,6 +1081,218 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
}
+
+#ifdef NS_IMPL_COCOA
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+
+/* Forward declaration --- defined in the VoiceOver section below.
+ Identifies faces like vertico-current, icomplete-selected-match,
+ ivy-current-match, corfu-current that mark the selected candidate. */
+static bool ns_ax_face_is_selected (Lisp_Object face);
+
+/* Scan overlay before-string / after-string properties in the
+ selected window for a completion candidate with a "selected"
+ face. Return the 0-based visual line index of the selected
+ candidate, or -1 if none found. */
+static int
+ns_zoom_find_overlay_candidate_line (struct window *w)
+{
+ /* Overlay completion frameworks (Vertico, Icomplete, Ivy) place
+ candidates as overlay strings in the minibuffer only. Scanning
+ overlays in large normal buffers causes O(overlays) work per
+ redisplay --- return immediately for non-minibuffer windows. */
+ if (!MINI_WINDOW_P (w))
+ return -1;
+
+ struct buffer *b = XBUFFER (w->contents);
+ ptrdiff_t beg = marker_position (w->start);
+ ptrdiff_t end = BUF_ZV (b);
+ Lisp_Object overlays = Foverlays_in (make_fixnum (beg),
+ make_fixnum (end));
+ Lisp_Object tail;
+
+ for (tail = overlays; CONSP (tail); tail = XCDR (tail))
+ {
+ Lisp_Object ov = XCAR (tail);
+ Lisp_Object str = Foverlay_get (ov, Qbefore_string);
+
+ if (NILP (str))
+ str = Foverlay_get (ov, Qafter_string);
+ if (!STRINGP (str) || SCHARS (str) < 2)
+ continue;
+
+ /* Walk the string line by line, checking faces. */
+ ptrdiff_t len = SCHARS (str);
+ int line = 0;
+ ptrdiff_t line_start = 0;
+
+ for (ptrdiff_t i = 0; i <= len; i++)
+ {
+ bool at_newline = (i == len
+ || SREF (str, i) == '\n');
+ if (at_newline && i > line_start)
+ {
+ /* Check the face at line_start. */
+ Lisp_Object face
+ = Fget_text_property (make_fixnum (line_start),
+ Qface, str);
+ if (ns_ax_face_is_selected (face))
+ return line;
+ line++;
+ line_start = i + 1;
+ }
+ else if (at_newline)
+ {
+ line++;
+ line_start = i + 1;
+ }
+ }
+ }
+ return -1;
+}
+
+/* Scan child frames for a completion popup with a selected
+ candidate. Return the 0-based line index, or -1 if none.
+ Set *CHILD_FRAME to the child frame if found. */
+static int
+ns_zoom_find_child_frame_candidate (struct frame *f,
+ struct frame **child_frame)
+{
+ Lisp_Object frames, tail;
+
+ FOR_EACH_FRAME (tail, frames)
+ {
+ struct frame *cf = XFRAME (frames);
+ if (!FRAME_NS_P (cf) || !FRAME_LIVE_P (cf))
+ continue;
+ if (FRAME_PARENT_FRAME (cf) != f)
+ continue;
+ /* Small buffer = likely completion popup. */
+ struct window *cw = XWINDOW (cf->selected_window);
+ struct buffer *b = XBUFFER (cw->contents);
+ if (BUF_ZV (b) - BUF_BEGV (b) > 10000)
+ continue;
+
+ ptrdiff_t beg = BUF_BEGV (b);
+ ptrdiff_t zv = BUF_ZV (b);
+ int line = 0;
+
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+ set_buffer_internal_1 (b);
+
+ ptrdiff_t pos = beg;
+ while (pos < zv)
+ {
+ Lisp_Object face
+ = Fget_char_property (make_fixnum (pos), Qface,
+ cw->contents);
+ if (ns_ax_face_is_selected (face))
+ {
+ unbind_to (count, Qnil);
+ *child_frame = cf;
+ return line;
+ }
+ /* Advance to next line. */
+ ptrdiff_t next = find_newline (pos, -1, zv, -1,
+ 1, NULL, NULL, false);
+ if (next <= pos)
+ break;
+ pos = next;
+ line++;
+ }
+ unbind_to (count, Qnil);
+ }
+ return -1;
+}
+
+/* Update Zoom focus based on completion candidates.
+ Called from ns_update_end after normal cursor tracking.
+ If a completion candidate is selected (overlay or child frame),
+ move Zoom to that candidate instead of the text cursor. */
+static void
+ns_zoom_track_completion (struct frame *f, EmacsView *view)
+{
+ if (!UAZoomEnabled ())
+ return;
+ if (!WINDOWP (f->selected_window))
+ return;
+
+ specpdl_ref count = SPECPDL_INDEX ();
+ record_unwind_current_buffer ();
+
+ struct window *w = XWINDOW (f->selected_window);
+ int line_h = FRAME_LINE_HEIGHT (f);
+
+ /* 1. Check overlay completions (Vertico, Icomplete, Ivy). */
+ int ov_line = ns_zoom_find_overlay_candidate_line (w);
+ if (ov_line >= 0)
+ {
+ /* Overlay candidates typically start after the input line,
+ so the visual offset is (ov_line + 1) * line_h from
+ the window top. */
+ int y_off = (ov_line + 1) * line_h;
+ if (y_off < w->pixel_height)
+ {
+ NSRect r = NSMakeRect (
+ WINDOW_TEXT_TO_FRAME_PIXEL_X (w, 0),
+ WINDOW_TO_FRAME_PIXEL_Y (w, y_off),
+ FRAME_COLUMN_WIDTH (f),
+ line_h);
+
+ NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect
+ = [[view window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ unbind_to (count, Qnil);
+ return;
+ }
+ }
+
+ /* 2. Check child frame completions (Corfu, Company-box). */
+ struct frame *cf = NULL;
+ int cf_line = -1;
+ if (!NILP (f->child_frame_list))
+ cf_line = ns_zoom_find_child_frame_candidate (f, &cf);
+ if (cf_line >= 0 && cf)
+ {
+ EmacsView *cv = FRAME_NS_VIEW (cf);
+ struct window *cw
+ = XWINDOW (cf->selected_window);
+ int cf_line_h = FRAME_LINE_HEIGHT (cf);
+ int y_off = cf_line * cf_line_h;
+
+ NSRect r = NSMakeRect (
+ WINDOW_TEXT_TO_FRAME_PIXEL_X (cw, 0),
+ WINDOW_TO_FRAME_PIXEL_Y (cw, y_off),
+ FRAME_COLUMN_WIDTH (cf),
+ cf_line_h);
+
+ NSRect windowRect = [cv convertRect:r toView:nil];
+ NSRect screenRect
+ = [[cv window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ }
+}
+
+#endif /* MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 */
+#endif /* NS_IMPL_COCOA */
+
static void
ns_update_end (struct frame *f)
/* --------------------------------------------------------------------------
@@ -1104,6 +1316,41 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
unblock_input ();
ns_updating_frame = NULL;
+
+#ifdef NS_IMPL_COCOA
+ /* Zoom fallback: ensure Zoom tracks the cursor after window
+ switches (C-x o) where the physical cursor may not be redrawn.
+ Only fires when ns_draw_window_cursor did NOT run in this cycle
+ (zoomCursorUpdated is NO). */
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ if (view && !view->zoomCursorUpdated && UAZoomEnabled ()
+ && !NSIsEmptyRect (view->lastCursorRect))
+ {
+ NSRect r = view->lastCursorRect;
+ NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect
+ = [[view window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ }
+ if (view)
+ view->zoomCursorUpdated = NO;
+#endif
+
+ /* Track completion candidates for Zoom (overlay and child frame).
+ Runs after cursor tracking so the selected candidate overrides
+ the default cursor position. */
+ if (view)
+ ns_zoom_track_completion (f, view);
+#endif /* NS_IMPL_COCOA */
}
static void
@@ -3232,6 +3479,45 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
/* Prevent the cursor from being drawn outside the text area. */
r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA));
+#ifdef NS_IMPL_COCOA
+ /* Zoom integration: inform macOS Zoom of the cursor position.
+ Zoom (System Settings -> Accessibility -> Zoom) tracks a focus
+ element to keep the zoomed viewport centered on the cursor.
+
+ Coordinate conversion:
+ EmacsView pixels (AppKit, flipped, top-left origin)
+ -> NSWindow (convertRect:toView:nil)
+ -> NSScreen (convertRectToScreen:)
+ -> CGRect with y-flip for CoreGraphics top-left origin. */
+ {
+ EmacsView *view = FRAME_NS_VIEW (f);
+ if (view && on_p && active_p)
+ {
+ view->lastCursorRect = r;
+ view->zoomCursorUpdated = YES;
+
+#if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \
+ && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
+ if (UAZoomEnabled ())
+ {
+ NSRect windowRect = [view convertRect:r toView:nil];
+ NSRect screenRect
+ = [[view window] convertRectToScreen:windowRect];
+ CGRect cgRect = NSRectToCGRect (screenRect);
+
+ CGFloat primaryH
+ = [[[NSScreen screens] firstObject] frame].size.height;
+ cgRect.origin.y
+ = primaryH - cgRect.origin.y - cgRect.size.height;
+
+ UAZoomChangeFocus (&cgRect, &cgRect,
+ kUAZoomFocusTypeInsertionPoint);
+ }
+#endif
+ }
+ }
+#endif /* NS_IMPL_COCOA */
+
ns_focus (f, NULL, 0);
NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
@@ -8321,6 +8607,9 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
windowClosing = NO;
processingCompose = NO;
+#ifdef NS_IMPL_COCOA
+ childFrameLastBuffer = Qnil;
+#endif
scrollbarsNeedingUpdate = 0;
fs_state = FULLSCREEN_NONE;
fs_before_fs = next_maximized = -1;
--
2.43.0