Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/emacs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,18 @@ pub const Env = struct {
return self.call0(sym.@"point-max");
}

pub fn markMarker(self: Env) Value {
return self.call0(sym.@"mark-marker");
}

pub fn markerPosition(self: Env, marker: Value) Value {
return self.call1(sym.@"marker-position", marker);
}

pub fn setMarker(self: Env, marker: Value, pos: Value) Value {
return self.call2(sym.@"set-marker", marker, pos);
}

pub fn deleteRegion(self: Env, start: Value, end: Value) void {
_ = self.call2(sym.@"delete-region", start, end);
}
Expand Down Expand Up @@ -291,6 +303,9 @@ pub const Sym = struct {
@"point-max": Value,
@"delete-region": Value,
@"char-before": Value,
@"mark-marker": Value,
@"marker-position": Value,
@"set-marker": Value,

// Text property names
face: Value,
Expand Down
21 changes: 21 additions & 0 deletions src/render.zig
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,27 @@ fn positionCursorByCell(env: emacs.Env, term: *Terminal, cx: u16, cy: u16) bool
/// When `force_full` is true, the viewport region is fully re-rendered
/// instead of using the incremental dirty-row path.
pub fn redraw(env: emacs.Env, term: *Terminal, force_full_arg: bool) void {
// Snapshot the buffer's mark across the destructive ops below. Both
// paths — full (eraseBuffer / deleteRegion over the viewport) and
// partial (per-row deleteRegion + insert) — move every marker in the
// buffer by standard Emacs marker rules. Point is owned by the
// renderer and is placed at the TUI cursor on exit, but mark is user
// state (C-SPC, region commands) and must survive the redraw. Other
// markers (e.g. evil's visual-beginning/end) remain the caller's
// responsibility to preserve in elisp.
const saved_mark: ?i64 = blk: {
const pos = env.markerPosition(env.markMarker());
if (!env.isNotNil(pos)) break :blk null;
break :blk env.extractInteger(pos);
};
defer {
if (saved_mark) |pos| {
const pmax = env.extractInteger(env.pointMax());
const clamped: i64 = if (pos > pmax) pmax else pos;
_ = env.setMarker(env.markMarker(), env.makeInteger(clamped));
}
}

var force_full = force_full_arg;

// Lock the libghostty viewport to the bottom. Users navigate history
Expand Down
22 changes: 22 additions & 0 deletions test/ghostel-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,28 @@ succeeds."
;; Test: erase sequences
;; -----------------------------------------------------------------------

(ert-deftest ghostel-test-redraw-preserves-mark ()
"`ghostel--redraw' must keep `mark' stable across the destructive ops.
Full redraws call `eraseBuffer' and partial redraws `deleteRegion',
either of which would snap every marker in the buffer to `point-min'."
(let ((buf (generate-new-buffer " *ghostel-test-mark*")))
(unwind-protect
(with-current-buffer buf
(let* ((term (ghostel--new 5 40 1000))
(inhibit-read-only t))
(ghostel--write-input term "line one\r\nline two\r\nline three")
(ghostel--redraw term t)
;; Anchor mark to "two" so its position sits well past point-min.
(goto-char (point-min))
(search-forward "two")
(let ((target (point)))
(set-marker (mark-marker) target)
;; Trigger a full redraw (erase-buffer path).
(ghostel--write-input term " more")
(ghostel--redraw term t)
(should (= target (marker-position (mark-marker)))))))
(kill-buffer buf))))

(ert-deftest ghostel-test-erase ()
"Test CSI erase sequences."
(let ((term (ghostel--new 25 80 1000)))
Expand Down
Loading