Skip to content

Preserve mark across native redraws#163

Merged
dakra merged 1 commit intodakra:mainfrom
Cianidos:native-preserve-mark-across-redraw
Apr 21, 2026
Merged

Preserve mark across native redraws#163
dakra merged 1 commit intodakra:mainfrom
Cianidos:native-preserve-mark-across-redraw

Conversation

@Cianidos
Copy link
Copy Markdown
Contributor

Problem

`ghostel--redraw' moves every marker in the buffer:

  • Full path (redraw' in src/render.zig:862') calls env.eraseBuffer()' on resize/rotation and env.deleteRegion(viewport_start, pointMax)' in the non-partial branch — every marker snaps to the deletion start (point-min' for eraseBuffer').
  • Partial path (DIRTY_PARTIAL') calls env.deleteRegion' + `env.insert' per dirty row. Markers drift asymmetrically by insertion-type, random-walking away from the user's intended position.

point' is the renderer's cursor during the redraw pass and the TUI cursor after; that's the contract native exposes. mark' has no such role — it's user state (C-SPC', M-w', any region command) — and currently gets collapsed to `point-min' after any redraw, so copying a region in a vanilla ghostel buffer is impossible without rereading state in elisp.

Change

Snapshot (marker-position (mark-marker))' at the top of redraw' and restore it on exit via Zig defer'. Clamped to point-max' in case the buffer shrank. No-op when mark was never set in this buffer (`marker-position' returns nil).

Adds mark-marker' / marker-position' / set-marker' to the symbol cache and three thin Env' helpers in `src/emacs.zig'. Overhead is three funcalls per redraw tick.

What this does not cover

  • `point' — load-bearing for the renderer, must not be preserved at this layer.
  • Package-specific markers (e.g. evil-visual-beginning', evil-visual-end', isearch markers) — the native module has no knowledge of them; their owners should wrap ghostel--redraw' with their own save/restore. evil-ghostel' will follow up for the evil-specific markers.

Tests

One ERT case (`ghostel-test-redraw-preserves-mark') sets mark inside the viewport, triggers a force-full redraw, verifies mark position unchanged. Passes alongside the existing 152 tests against the built module.

Cianidos added a commit to Cianidos/ghostel that referenced this pull request Apr 21, 2026
`evil-ghostel--around-redraw' used to save and restore only `point',
and only in non-insert/emacs states.  Native `ghostel--redraw'
rewrites the viewport region on every call, moving every marker in
the buffer — `evil-visual-beginning' and `evil-visual-end' drift
asymmetrically by insertion-type, so `v' in a buffer with a
streaming TUI (Claude Code, watch, streaming logs) shows a
multi-row selection the moment `v' is pressed: mark had drifted
backwards and `evil-visual-end' ratcheted forward while the user
was idle in the buffer.

Extend the advice to save and restore:

  - `point' in non-terminal states (unchanged).
  - `evil-visual-beginning' and `evil-visual-end' in `visual' state.

`mark' is preserved by `ghostel--redraw' itself (see PR dakra#163 — the
native-side fix lives in `src/render.zig', where all users benefit
regardless of whether evil-ghostel is loaded).  This PR layers on
top of that — it can be merged after or before dakra#163; if merged
first, `mark' stays drifting until the native patch lands.

Tests: three mock-based cases covering point preservation in
normal, point follow-through in emacs, and visual-marker
preservation in visual.
Full and partial redraw paths both destroy markers:

  - Full: `env.eraseBuffer()' snaps every marker to `point-min'.
  - Partial: `env.deleteRegion' + `env.insert' per dirty row drift
    markers asymmetrically by insertion-type, random-walking them
    away from the user's intended position.

Point is owned by the renderer (placed at the TUI cursor on exit),
but `mark' is user state — `C-SPC' in an emacs-state buffer, any
region command in normal-state — and must survive.

Snapshot `(mark-marker)` position at the top of `redraw' and
restore it on exit via `defer'.  Clamp to `point-max' in case the
buffer shrank.  No change when the mark was never set in the buffer
(`marker-position' returns nil).

Other markers (e.g. evil's `evil-visual-beginning' /
`evil-visual-end') stay the caller's concern — a package can wrap
`ghostel--redraw' with its own save/restore for state the native
module cannot know about.

Adds `mark-marker' / `marker-position' / `set-marker' to the
symbol cache and thin `Env' helpers.  One ERT case verifying mark
survives a full-redraw cycle.
@dakra dakra force-pushed the native-preserve-mark-across-redraw branch from 88af410 to 4816ece Compare April 21, 2026 08:04
@dakra
Copy link
Copy Markdown
Owner

dakra commented Apr 21, 2026

as always, thanks 🙏

dakra pushed a commit to Cianidos/ghostel that referenced this pull request Apr 21, 2026
`evil-ghostel--around-redraw' used to save and restore only `point',
and only in non-insert/emacs states.  Native `ghostel--redraw'
rewrites the viewport region on every call, moving every marker in
the buffer — `evil-visual-beginning' and `evil-visual-end' drift
asymmetrically by insertion-type, so `v' in a buffer with a
streaming TUI (Claude Code, watch, streaming logs) shows a
multi-row selection the moment `v' is pressed: mark had drifted
backwards and `evil-visual-end' ratcheted forward while the user
was idle in the buffer.

Extend the advice to save and restore:

  - `point' in non-terminal states (unchanged).
  - `evil-visual-beginning' and `evil-visual-end' in `visual' state.

`mark' is preserved by `ghostel--redraw' itself (see PR dakra#163 — the
native-side fix lives in `src/render.zig', where all users benefit
regardless of whether evil-ghostel is loaded).  This PR layers on
top of that — it can be merged after or before dakra#163; if merged
first, `mark' stays drifting until the native patch lands.

Tests: three mock-based cases covering point preservation in
normal, point follow-through in emacs, and visual-marker
preservation in visual.
@Cianidos
Copy link
Copy Markdown
Contributor Author

I’m glad I could help.
I've been waiting for someone to integrate ghostty into emacs, and now I really want to use it. And here it is

dakra pushed a commit to Cianidos/ghostel that referenced this pull request Apr 21, 2026
`evil-ghostel--around-redraw' used to save and restore only `point',
and only in non-insert/emacs states.  Native `ghostel--redraw'
rewrites the viewport region on every call, moving every marker in
the buffer — `evil-visual-beginning' and `evil-visual-end' drift
asymmetrically by insertion-type, so `v' in a buffer with a
streaming TUI (Claude Code, watch, streaming logs) shows a
multi-row selection the moment `v' is pressed: mark had drifted
backwards and `evil-visual-end' ratcheted forward while the user
was idle in the buffer.

Extend the advice to save and restore:

  - `point' in non-terminal states (unchanged).
  - `evil-visual-beginning' and `evil-visual-end' in `visual' state.

`mark' is preserved by `ghostel--redraw' itself (see PR dakra#163 — the
native-side fix lives in `src/render.zig', where all users benefit
regardless of whether evil-ghostel is loaded).  This PR layers on
top of that — it can be merged after or before dakra#163; if merged
first, `mark' stays drifting until the native patch lands.

Tests: three mock-based cases covering point preservation in
normal, point follow-through in emacs, and visual-marker
preservation in visual.
@dakra dakra merged commit 4816ece into dakra:main Apr 21, 2026
18 checks passed
@dakra
Copy link
Copy Markdown
Owner

dakra commented Apr 21, 2026

@Cianidos I had a look at your emacs config and saw add-advice for title tracking (and I saw that in other integrations already too).
So I changed it now that you can customize. See: #167

Also if you see something where you think a setting / hook etc would be handy, don't hesitate to create an issue.
Ghostel is still very young so there is room for improvements :)

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.

2 participants