Skip to content

Commit 4816ece

Browse files
Cianidosdakra
authored andcommitted
Preserve `mark' across native redraws
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.
1 parent b955dbb commit 4816ece

3 files changed

Lines changed: 58 additions & 0 deletions

File tree

src/emacs.zig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,18 @@ pub const Env = struct {
228228
return self.call0(sym.@"point-max");
229229
}
230230

231+
pub fn markMarker(self: Env) Value {
232+
return self.call0(sym.@"mark-marker");
233+
}
234+
235+
pub fn markerPosition(self: Env, marker: Value) Value {
236+
return self.call1(sym.@"marker-position", marker);
237+
}
238+
239+
pub fn setMarker(self: Env, marker: Value, pos: Value) Value {
240+
return self.call2(sym.@"set-marker", marker, pos);
241+
}
242+
231243
pub fn deleteRegion(self: Env, start: Value, end: Value) void {
232244
_ = self.call2(sym.@"delete-region", start, end);
233245
}
@@ -291,6 +303,9 @@ pub const Sym = struct {
291303
@"point-max": Value,
292304
@"delete-region": Value,
293305
@"char-before": Value,
306+
@"mark-marker": Value,
307+
@"marker-position": Value,
308+
@"set-marker": Value,
294309

295310
// Text property names
296311
face: Value,

src/render.zig

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,27 @@ fn positionCursorByCell(env: emacs.Env, term: *Terminal, cx: u16, cy: u16) bool
899899
/// When `force_full` is true, the viewport region is fully re-rendered
900900
/// instead of using the incremental dirty-row path.
901901
pub fn redraw(env: emacs.Env, term: *Terminal, force_full_arg: bool) void {
902+
// Snapshot the buffer's mark across the destructive ops below. Both
903+
// paths — full (eraseBuffer / deleteRegion over the viewport) and
904+
// partial (per-row deleteRegion + insert) — move every marker in the
905+
// buffer by standard Emacs marker rules. Point is owned by the
906+
// renderer and is placed at the TUI cursor on exit, but mark is user
907+
// state (C-SPC, region commands) and must survive the redraw. Other
908+
// markers (e.g. evil's visual-beginning/end) remain the caller's
909+
// responsibility to preserve in elisp.
910+
const saved_mark: ?i64 = blk: {
911+
const pos = env.markerPosition(env.markMarker());
912+
if (!env.isNotNil(pos)) break :blk null;
913+
break :blk env.extractInteger(pos);
914+
};
915+
defer {
916+
if (saved_mark) |pos| {
917+
const pmax = env.extractInteger(env.pointMax());
918+
const clamped: i64 = if (pos > pmax) pmax else pos;
919+
_ = env.setMarker(env.markMarker(), env.makeInteger(clamped));
920+
}
921+
}
922+
902923
var force_full = force_full_arg;
903924

904925
// Lock the libghostty viewport to the bottom. Users navigate history

test/ghostel-test.el

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,28 @@ succeeds."
161161
;; Test: erase sequences
162162
;; -----------------------------------------------------------------------
163163

164+
(ert-deftest ghostel-test-redraw-preserves-mark ()
165+
"`ghostel--redraw' must keep `mark' stable across the destructive ops.
166+
Full redraws call `eraseBuffer' and partial redraws `deleteRegion',
167+
either of which would snap every marker in the buffer to `point-min'."
168+
(let ((buf (generate-new-buffer " *ghostel-test-mark*")))
169+
(unwind-protect
170+
(with-current-buffer buf
171+
(let* ((term (ghostel--new 5 40 1000))
172+
(inhibit-read-only t))
173+
(ghostel--write-input term "line one\r\nline two\r\nline three")
174+
(ghostel--redraw term t)
175+
;; Anchor mark to "two" so its position sits well past point-min.
176+
(goto-char (point-min))
177+
(search-forward "two")
178+
(let ((target (point)))
179+
(set-marker (mark-marker) target)
180+
;; Trigger a full redraw (erase-buffer path).
181+
(ghostel--write-input term " more")
182+
(ghostel--redraw term t)
183+
(should (= target (marker-position (mark-marker)))))))
184+
(kill-buffer buf))))
185+
164186
(ert-deftest ghostel-test-erase ()
165187
"Test CSI erase sequences."
166188
(let ((term (ghostel--new 25 80 1000)))

0 commit comments

Comments
 (0)