Skip to content

Never scroll the live cursor out of view when anchoring#548

Open
dakra wants to merge 1 commit into
mainfrom
fix/minibuffer-leave-resize-race
Open

Never scroll the live cursor out of view when anchoring#548
dakra wants to merge 1 commit into
mainfrom
fix/minibuffer-leave-resize-race

Conversation

@dakra

@dakra dakra commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Problem

On a fresh terminal (cursor near the top, trailing blank grid rows), any window-shrinking minibuffer (vertico/fido M-x, find-file, read-string, …) misbehaved twice:

  • While the minibuffer was open, the window scrolled to the blank tail of the grid, hiding the prompt and recent output.
  • On exit, the buffer spuriously flipped into copy mode; the "Copy mode" message stomped the echo area (masking e.g. the major-mode guard error from f00d735) and the next keystroke was swallowed by the exit binding.

Root cause: ghostel--adjust-size defers the terminal resize during a minibuffer but still re-anchors. Bottom-aligning point-max in the shrunken window pushed the cursor row above window-start; redisplay then clamped window-point off the cursor — a pure point move no marker can track — and the deferred minibuffer-exit check misread it as user navigation.

Fix

Fix the anchor rather than the check (redisplay's clamp is indistinguishable from a user jump):

  • ghostel--anchor-window computes one (START . VSCROLL) from the pixel (GUI) or line-count (TTY) path, clamps START to the live cursor's line, and zeroes the vscroll whenever the cursor sits on the top line. One-directional: a byte-for-byte no-op whenever the cursor is at or below the bottom-aligned start (normal streaming, TUIs, filled scrollback).
  • ghostel--window-anchored-p also counts a window as anchored when window-start sits exactly on the (capped) cursor's line, so a clamped window keeps auto-following. Stateless — recomputed from live data on every call, nothing to record or go stale.
  • ghostel-semi-char-mode / ghostel-char-mode run goto-char before the forced anchor, so the anchor's point placement (the live cursor) wins and exiting copy mode cannot leave point off the cursor.
  • ghostel-maybe-leave-input compares point against the capped cursor position, so a stale cursor beyond point-max cannot fake a jump.

Deliberate navigation (e.g. goto-char after read-string, leaving isearch away from the prompt) still enters copy mode; line mode and the ghostel-inhibit-anchor-functions veto (evil-ghostel) are untouched.

Tests

Five new tests in test/ghostel-modes-test.el (clamp, no-op at bottom, nil/stale cursor, anchored-p recognition, end-to-end no-flip regression), sharing a fixture macro the existing force-anchor test was ported onto.

Verification

  • make -j8 all green; evil-ghostel 124/124.
  • Live matrix (GUI + terminal Emacs, fido-vertical): M-x text-mode shows the guard error with no copy flip; M-x ignore stays semi-char with the prompt visible and window-vscroll 0 while the minibuffer is open; copy-mode exit lands point on the cursor; filled-scrollback, isearch, and auto-follow paths unchanged.
  • Negative control: the same live harness against pre-fix HEAD reproduces the flip in both the pixel and line-count anchor paths.

On a fresh terminal (cursor near the top, trailing blank grid rows), a
window-shrinking minibuffer deferred the terminal resize but still
re-anchored: bottom-aligning point-max pushed the cursor row above
window-start, redisplay clamped window-point off the cursor, and the
minibuffer-exit check misread the pure point move as user navigation,
flipping the buffer into copy mode and stomping the echo area.

Fix the anchor instead of the check (no marker can distinguish
redisplay's point clamp from a user jump):

- ghostel--anchor-window computes one (START . VSCROLL) from the pixel
  or line-count path, clamps START to the cursor's line, and zeroes the
  vscroll whenever the cursor sits on the top line.  A no-op whenever
  the cursor is at or below the bottom-aligned start.

- ghostel--window-anchored-p also counts a window as anchored when
  window-start sits exactly on the (capped) cursor's line, so a clamped
  window keeps auto-following.  The check is stateless, recomputed from
  live data on every call.

- ghostel-semi-char-mode/ghostel-char-mode run goto-char before the
  forced anchor, so the anchor's point placement (the live cursor)
  wins and exiting copy mode cannot leave point off the cursor.

- ghostel-maybe-leave-input compares point against the capped cursor
  position, so a stale cursor beyond point-max cannot fake a jump.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant