ns: track completion candidates for macOS Zoom
When a completion framework displays candidates (overlay-based or child-frame popup), override Zoom focus to the selected candidate so the zoomed viewport follows the user's selection rather than the text cursor. * src/nsterm.m (NS_ZOOM_MAX_COMPLETION_CHARS): New macro. (ns_face_name_matches_selected_p_1): New static helper; recursive face name match with depth-limit guard. (ns_face_name_matches_selected_p): New static predicate; matches 'current', 'selected', 'selection' in face names. (ns_zoom_find_overlay_candidate_line): New static function; scans minibuffer overlays for selected completion candidate. (ns_zoom_find_child_frame_candidate): New static function; scans child frame buffers for selected candidate. (ns_zoom_track_completion): New static function; overrides Zoom focus to selected completion candidate. (ns_update_end): Call ns_zoom_track_completion. * etc/NEWS: Document completion tracking for Zoom.
This commit is contained in:
8
etc/NEWS
8
etc/NEWS
@@ -4679,11 +4679,3 @@ GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
Local variables:
|
||||
coding: utf-8
|
||||
mode: outline
|
||||
mode: emacs-news
|
||||
paragraph-separate: "[ ]"
|
||||
end:
|
||||
|
||||
304
src/nsterm.m
304
src/nsterm.m
@@ -1205,6 +1205,304 @@ ns_UAZoomChangeFocus (EmacsView *view, BOOL force)
|
||||
else
|
||||
ns_update_was_UAZoomEnabled = NO;
|
||||
}
|
||||
|
||||
/* Maximum buffer size (in characters) for a window that we consider
|
||||
a candidate for a completion popup. Completion popups are small;
|
||||
if the buffer is larger than this, it is not a popup and we skip it
|
||||
to avoid O(buffer-size) work per redisplay cycle. */
|
||||
#define NS_ZOOM_MAX_COMPLETION_CHARS 10000
|
||||
|
||||
/* Identify faces that mark a selected completion candidate.
|
||||
Matches face names containing "current", "selected", or "selection",
|
||||
as used by common completion frameworks.
|
||||
Used by Zoom cursor tracking to identify the selected candidate. */
|
||||
/* Depth limit for CONSP recursion to guard against malformed
|
||||
circular or deeply nested face specs. */
|
||||
#define NS_FACE_NAME_MATCH_DEPTH_LIMIT 10
|
||||
|
||||
static bool
|
||||
ns_face_name_matches_selected_p_1 (Lisp_Object face, int depth)
|
||||
{
|
||||
if (depth > NS_FACE_NAME_MATCH_DEPTH_LIMIT)
|
||||
return false;
|
||||
if (SYMBOLP (face))
|
||||
{
|
||||
const char *name = SSDATA (SYMBOL_NAME (face));
|
||||
return (strstr (name, "current") != NULL
|
||||
|| strstr (name, "selected") != NULL
|
||||
|| strstr (name, "selection") != NULL);
|
||||
}
|
||||
if (CONSP (face))
|
||||
{
|
||||
Lisp_Object tail;
|
||||
for (tail = face; CONSP (tail); tail = XCDR (tail))
|
||||
if (ns_face_name_matches_selected_p_1 (XCAR (tail), depth + 1))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
ns_face_name_matches_selected_p (Lisp_Object face)
|
||||
{
|
||||
return ns_face_name_matches_selected_p_1 (face, 0);
|
||||
}
|
||||
|
||||
/* 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 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;
|
||||
|
||||
/* 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);
|
||||
|
||||
if (!BUFFERP (w->contents))
|
||||
{
|
||||
unbind_to (count, Qnil);
|
||||
return -1;
|
||||
}
|
||||
struct buffer *b = XBUFFER (w->contents);
|
||||
/* Foverlays_in operates on current_buffer, so switch if needed. */
|
||||
record_unwind_current_buffer ();
|
||||
if (b != current_buffer)
|
||||
set_buffer_internal_1 (b);
|
||||
|
||||
/* Guard against dead markers: w->start may have buffer == NULL
|
||||
after aggressive window manipulation (e.g. org-agenda refresh).
|
||||
marker_position on a dead marker signals, which longjmps through
|
||||
block_input and can cause abort on unblock_input underflow. */
|
||||
if (!MARKERP (w->start) || !XMARKER (w->start)->buffer)
|
||||
{
|
||||
unbind_to (count, Qnil);
|
||||
return -1;
|
||||
}
|
||||
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);
|
||||
if (!OVERLAYP (ov))
|
||||
continue;
|
||||
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.
|
||||
Use byte-level iteration to correctly handle multibyte
|
||||
strings (SREF uses byte indices, not character indices). */
|
||||
const unsigned char *data = SDATA (str);
|
||||
ptrdiff_t nbytes = SBYTES (str);
|
||||
int line = 0;
|
||||
ptrdiff_t char_pos = 0, byte_pos = 0, line_start_char = 0;
|
||||
|
||||
while (byte_pos < nbytes)
|
||||
{
|
||||
if (data[byte_pos] == '\n')
|
||||
{
|
||||
if (char_pos > line_start_char)
|
||||
{
|
||||
/* Check the face at line_start_char. */
|
||||
Lisp_Object face
|
||||
= Fget_text_property (make_fixnum (line_start_char),
|
||||
Qface, str);
|
||||
if (ns_face_name_matches_selected_p (face))
|
||||
{
|
||||
unbind_to (count, Qnil);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
line++;
|
||||
line_start_char = char_pos + 1;
|
||||
}
|
||||
if (STRING_MULTIBYTE (str))
|
||||
byte_pos += BYTES_BY_CHAR_HEAD (data[byte_pos]);
|
||||
else
|
||||
byte_pos++;
|
||||
char_pos++;
|
||||
}
|
||||
/* Check last line (no trailing newline). */
|
||||
if (char_pos > line_start_char)
|
||||
{
|
||||
Lisp_Object face
|
||||
= Fget_text_property (make_fixnum (line_start_char),
|
||||
Qface, str);
|
||||
if (ns_face_name_matches_selected_p (face))
|
||||
{
|
||||
unbind_to (count, Qnil);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
unbind_to (count, Qnil);
|
||||
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 frame, tail;
|
||||
|
||||
FOR_EACH_FRAME (tail, frame)
|
||||
{
|
||||
struct frame *cf = XFRAME (frame);
|
||||
if (!FRAME_NS_P (cf) || !FRAME_LIVE_P (cf))
|
||||
continue;
|
||||
if (FRAME_PARENT_FRAME (cf) != f)
|
||||
continue;
|
||||
/* Small buffer = likely completion popup. Guard against
|
||||
partially initialized frames where selected_window or its
|
||||
buffer may not yet be live. */
|
||||
if (!WINDOWP (cf->selected_window))
|
||||
continue;
|
||||
struct window *cw = XWINDOW (cf->selected_window);
|
||||
if (!BUFFERP (cw->contents))
|
||||
continue;
|
||||
struct buffer *b = XBUFFER (cw->contents);
|
||||
if (BUF_ZV (b) - BUF_BEGV (b) > NS_ZOOM_MAX_COMPLETION_CHARS)
|
||||
continue;
|
||||
|
||||
ptrdiff_t beg = BUF_BEGV (b);
|
||||
ptrdiff_t zv = BUF_ZV (b);
|
||||
int line = 0;
|
||||
|
||||
specpdl_ref count = SPECPDL_INDEX ();
|
||||
block_input ();
|
||||
record_unwind_protect_void (unblock_input);
|
||||
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_face_name_matches_selected_p (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 (!ns_ua_zoom_enabled_p ())
|
||||
return;
|
||||
if (!WINDOWP (f->selected_window))
|
||||
return;
|
||||
/* Child frame completion popups have no children to scan;
|
||||
their parent frame's ns_update_end will scan them via
|
||||
FOR_EACH_FRAME. Return early to avoid a redundant O(frames)
|
||||
scan on every child-frame redisplay cycle. */
|
||||
if (FRAME_PARENT_FRAME (f))
|
||||
return;
|
||||
|
||||
specpdl_ref count = SPECPDL_INDEX ();
|
||||
block_input ();
|
||||
record_unwind_protect_void (unblock_input);
|
||||
record_unwind_current_buffer ();
|
||||
|
||||
struct window *w = XWINDOW (f->selected_window);
|
||||
int line_h = FRAME_LINE_HEIGHT (f);
|
||||
|
||||
/* 1. Check overlay-based completion candidates. */
|
||||
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. This assumes the input line occupies the
|
||||
first row of the minibuffer window. */
|
||||
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 = ns_cg_rect_flip_y (NSRectToCGRect (screenRect));
|
||||
|
||||
UAZoomChangeFocus (&cgRect, &cgRect,
|
||||
kUAZoomFocusTypeInsertionPoint);
|
||||
unbind_to (count, Qnil);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. Check child frame completions. */
|
||||
struct frame *cf = NULL;
|
||||
int 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 = ns_cg_rect_flip_y (NSRectToCGRect (screenRect));
|
||||
|
||||
UAZoomChangeFocus (&cgRect, &cgRect,
|
||||
kUAZoomFocusTypeInsertionPoint);
|
||||
}
|
||||
unbind_to (count, Qnil);
|
||||
}
|
||||
|
||||
#endif /* NS_IMPL_COCOA */
|
||||
|
||||
static void
|
||||
@@ -1230,6 +1528,12 @@ ns_update_end (struct frame *f)
|
||||
|
||||
#ifdef NS_IMPL_COCOA
|
||||
ns_UAZoomChangeFocus (view, false);
|
||||
|
||||
/* 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
|
||||
|
||||
unblock_input ();
|
||||
|
||||
Reference in New Issue
Block a user