Skip to content

Anchor window-point clamp via pos-visible-in-window-p#158

Closed
emil-e wants to merge 1 commit intodakra:mainfrom
emil-e:fix/anchor-window-pos-visible
Closed

Anchor window-point clamp via pos-visible-in-window-p#158
emil-e wants to merge 1 commit intodakra:mainfrom
emil-e:fix/anchor-window-pos-visible

Conversation

@emil-e
Copy link
Copy Markdown
Contributor

@emil-e emil-e commented Apr 20, 2026

Fixes #157.

Summary

See #157 for reproduction, evidence (focus-change/anchor-window log on Claude Code as the example TUI), and case-by-case coverage table.

Test plan

Marked draft pending the regression test and a maintainer sanity check on pos-visible-in-window-p performance inside the redraw path (called once per anchored window per redraw — likely fine but worth confirming against any existing benchmarks).

The pending-wrap-narrowed clamp from ad8536e fixes dakra#146 but reopens
a variant of dakra#138 for TUIs that move the cursor to the bottom row
via CUP rather than by writing-then-wrapping.  In that case
pt = point-max and the cursor sits on the last viewport row, but
ghostel--cursor-pending-wrap-p returns nil, so the clamp does not
fire and Emacs scrolls window-start by one row to make pt visible
— fighting the viewport pin.

The real precondition for the bug is "Emacs considers pt = point-max
off-screen given the just-pinned window-start", which is exactly
what pos-visible-in-window-p answers.  Replace the pending-wrap
predicate with that direct check.  pending-wrap becomes a special
case of "pt off-screen", so the dakra#138 fix is preserved; mid-window
typing at a shell prompt continues to render the block cursor after
the last character (dakra#146).
@emil-e
Copy link
Copy Markdown
Contributor Author

emil-e commented Apr 20, 2026

Closing — the proposed approach (replace pending-wrap predicate with pos-visible-in-window-p) is unsound. pos-visible-in-window-p reflects the previous redisplay rather than the just-pinned window-start, and manual count-screen-lines substitutes have edge-case failures in both directions. See #157 (comment) for the full analysis and a suggested terminal-side direction.

@emil-e emil-e closed this Apr 20, 2026
dakra added a commit that referenced this pull request Apr 20, 2026
ad8536e narrowed the `ghostel--anchor-window' clamp to fire only when
libghostty reports pending-wrap, which fixed the #146 regression but
reopened a variant of #138 for TUIs that move their cursor to the
bottom of the screen via absolute positioning (CUP) rather than via
writing-then-wrapping.  In that case `pt' equals `point-max' and the
cursor sits on the last viewport row, but pending-wrap is nil — so the
clamp doesn't fire and Emacs shifts `window-start' by one row to make
`pt' "visible," fighting the viewport pin.

Expose a second terminal-side predicate `ghostel--cursor-on-empty-row-p'
backed by a new `render.isRowEmptyAt' helper, and widen the clamp guard
to `(or pending-wrap cursor-on-empty-row)'.  The predicate returns t iff
the row containing the cursor has no written cells and no cells with
non-default styling — exactly the condition under which `buildRowContent'
produces `byte_len == 0'.  Wide-spacer-tail cells are skipped to mirror
`buildRowContent' (defensive — in practice spacer tails always follow a
wide grapheme).

Source `cy' from `RS_DATA_CURSOR_VIEWPORT_Y' gated by `...HAS_VALUE' so
the coordinate space matches the viewport row iterator that
`isRowEmptyAt' walks, and the predicate returns nil when the cursor
isn't visible in the current viewport.  Caller owns the
`ghostty_render_state_update' refresh so it only happens once per call.

An earlier draft (#158) tried `pos-visible-in-window-p' but it reflects
the previous redisplay rather than the just-pinned `window-start', and
breaks in batch.  The terminal-side predicate answers the real
question — is there anything on this row to anchor `pt' to? — without
consulting Emacs redisplay state.

Bump module version to 0.16.3 (new exported function).

Test changes:
- `ghostel-test-cursor-on-empty-row-p' covers the predicate across
  fresh-terminal / post-write / post-CRLF / post-CUP cursor positions.
- `ghostel-test-anchor-window-clamps-on-empty-row' is the #157
  regression test: feeds "foo\r\nbar\r\n" to park the cursor on an
  empty last row, asserts PT lands at `point-max', pending-wrap is nil,
  empty-row is t, and the clamp fires.
- `ghostel-test-anchor-window-no-clamp-on-populated-last-row'
  complements #146: cursor at `point-max' on a last row that does have
  content (e.g. a shell prompt) must NOT be clamped regardless of which
  predicate is consulted.

Closes #157.
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.

ghostel--anchor-window: viewport scrolls on focus-out when TUI parks cursor at bottom-left (pt = point-max, no pending-wrap)

1 participant