Skip to content

Commit 8e3135f

Browse files
emil-edakra
authored andcommitted
Refactor native rendering for correctness and robustness
Replaces the old viewport-based render algorithm with one that parks the terminal at `max_offset - 1` after each pass. This lets us: - Track scrollback correctly even when the libghostty scrollback cap is reached, by detecting eviction from the parked offset rather than scanning for specific escape sequences. - Identify scrollback-clear (CSI 3J et al.) reliably via the `offset+len==total` snap-back signal — no per-byte VT scanning. - Use libghostty dirty flags directly to identify stale lines, fixing a bug where promoted scrollback rows could carry outdated content. - Evict old scrollback rows from the Emacs buffer in lockstep with libghostty when its ring buffer wraps. OSC 8 hyperlink handling switches from storing URI strings on every run to a lazy lookup via a `help-echo` function that calls `ghostel--native-uri-at`. The Emacs buffer now always carries a trailing newline so line-math is uniform across the codebase (callers like evil-ghostel--reset-cursor-point that used `line-number-at-pos` on `point-max` are switched to `count-lines`, which is off-by-one-safe regardless of the trailing newline). Cursor rendering is split out of the main render path. Tests for `ghostel--cursor-on-empty-row-p` and `ghostel--cursor-pending-wrap-p` (and the anchor-window clamp tests that depended on them) are dropped — those helpers were removed along with the old viewport algorithm. Docstrings and the file-level render.zig comment block are refreshed to describe the new algorithm; checkdoc imperative-form fixes for two new OSC 8 helpers; the unused `exact-point` argument to `ghostel--anchor-window` is dropped along with a stale TODO in fnUriAt. Bug fixes folded in: * fnCursorPosition / fnDebugState / fnDebugFeed: the viewport- restore defer sat inside `if (term.getScrollbar()) |sb| { defer { ... } }`, so the defer fired the moment the if-block ended — before the SCROLL_BOTTOM call below it. Each call left the viewport parked at the bottom (offset+len==total), which the next redraw mistook for a libghostty scrollback-clear and treated as a full erase + rebuild signal. With evil-ghostel this fired on every state transition once scrollback existed, causing a buffer wipe + rebuild on every i/Esc. Hoist the defer to function scope via a captured optional offset. * fnUriAt: the heap-fallback `defer std.heap.c_allocator.free(buf)` was scoped to the inner alloc block, freeing the buffer before `makeString` read through the `heap_uri` alias. Hoist the free defer to function scope and add a SUCCESS check on the result so error returns from either `ghostty_grid_ref_hyperlink_uri` call no longer stringify uninitialised data. Tighten input bounds checks. * Untabify whitespace cleanup across affected files. Adds a regression test that anchors a marker in scrollback, calls `ghostel--cursor-position`, redraws, and asserts the marker is preserved — a spurious eraseBuffer collapses all markers to point-min.
1 parent 90f1f71 commit 8e3135f

7 files changed

Lines changed: 708 additions & 1214 deletions

File tree

extensions/evil-ghostel/evil-ghostel.el

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ placement math the native module performs in `src/render.zig'."
8585
(when (and ghostel--term ghostel--term-rows)
8686
(let ((pos (ghostel--cursor-position ghostel--term)))
8787
(when pos
88-
(let ((scrollback (max 0 (- (line-number-at-pos (point-max))
88+
(let ((scrollback (max 0 (- (count-lines (point-min) (point-max))
8989
ghostel--term-rows))))
9090
(goto-char (point-min))
9191
(forward-line (+ scrollback (cdr pos)))

lisp/ghostel.el

Lines changed: 32 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -567,8 +567,6 @@ Bump this only when the Elisp code requires a newer native module
567567
;; Declare native module functions for the byte compiler
568568

569569
(declare-function ghostel--cursor-position "ghostel-module")
570-
(declare-function ghostel--cursor-pending-wrap-p "ghostel-module")
571-
(declare-function ghostel--cursor-on-empty-row-p "ghostel-module")
572570
(declare-function ghostel--encode-key "ghostel-module")
573571
(declare-function ghostel--focus-event "ghostel-module")
574572
(declare-function ghostel--mode-enabled "ghostel-module")
@@ -578,11 +576,11 @@ Bump this only when the Elisp code requires a newer native module
578576
(declare-function ghostel--mouse-event "ghostel-module")
579577
(declare-function ghostel--new "ghostel-module")
580578
(declare-function ghostel--redraw "ghostel-module" (term &optional full))
581-
(declare-function ghostel--scroll-bottom "ghostel-module")
582579
(declare-function ghostel--set-default-colors "ghostel-module")
583580
(declare-function ghostel--set-palette "ghostel-module")
584581
(declare-function ghostel--set-size "ghostel-module")
585582
(declare-function ghostel--write-input "ghostel-module")
583+
(declare-function ghostel--native-uri-at "ghostel-module")
586584

587585

588586
;;; Automatic download and compilation of native module
@@ -1325,7 +1323,6 @@ when `ghostel-scroll-on-input' is nil. Call from any path where
13251323
the user's action implies \"show me the prompt\" — typed input,
13261324
paste, yank, drop."
13271325
(when (and ghostel-scroll-on-input ghostel--term)
1328-
(ghostel--scroll-bottom ghostel--term)
13291326
(setq ghostel--snap-requested t)
13301327
(setq ghostel--force-next-redraw t)))
13311328

@@ -1570,7 +1567,6 @@ pasted using bracketed paste."
15701567
(ghostel--flush-pending-output)
15711568
;; CSI H = home, CSI 2 J = erase screen, CSI 3 J = erase scrollback.
15721569
(ghostel--write-input ghostel--term "\e[H\e[2J\e[3J")
1573-
(ghostel--scroll-bottom ghostel--term)
15741570
(setq ghostel--force-next-redraw t)
15751571
;; Scrollback is gone; any recorded scroll position no longer
15761572
;; refers to real content. Reset so the next redraw anchors
@@ -1851,6 +1847,30 @@ stripped so the copied text matches the original terminal content."
18511847
map)
18521848
"Keymap for clickable hyperlinks in ghostel buffers.")
18531849

1850+
(defun ghostel--native-link-help-echo (window _ pos)
1851+
"help-echo handler for OSC8 hyperlinks. Retrieves native URI from libghostty."
1852+
(with-current-buffer (window-buffer window)
1853+
(ghostel--native-uri-at-pos pos)))
1854+
1855+
(defun ghostel--native-uri-at-pos (pos)
1856+
"Return the native OSC8 hyperlink URI at POS."
1857+
(save-excursion
1858+
(goto-char pos)
1859+
(let* ((line (line-number-at-pos nil t))
1860+
(total (line-number-at-pos (point-max) t))
1861+
(row-from-bottom (- total line))
1862+
(col (current-column)))
1863+
(ghostel--native-uri-at ghostel--term row-from-bottom col))))
1864+
1865+
(defun ghostel--uri-at-pos (pos)
1866+
"Return the URI at POS.
1867+
If the `help-echo' property is a string, return it; otherwise fetch
1868+
the native OSC8 URI at that position."
1869+
(let ((help-echo (get-text-property pos 'help-echo)))
1870+
(if (stringp help-echo)
1871+
help-echo
1872+
(ghostel--native-uri-at-pos pos))))
1873+
18541874
(defun ghostel--open-link (url)
18551875
"Open URL, dispatching by scheme.
18561876
file:// URIs open in Emacs; http(s) and other schemes use `browse-url'.
@@ -1879,13 +1899,12 @@ a line suffix opens at the start of the file or directory."
18791899
(defun ghostel-open-link-at-click (event)
18801900
"Open the hyperlink at the mouse click EVENT position."
18811901
(interactive "e")
1882-
(ghostel--open-link
1883-
(get-text-property (posn-point (event-start event)) 'help-echo)))
1902+
(ghostel--open-link (ghostel--uri-at-pos (posn-point (event-start event)))))
18841903

18851904
(defun ghostel-open-link-at-point ()
18861905
"Open the hyperlink at point."
18871906
(interactive)
1888-
(ghostel--open-link (get-text-property (point) 'help-echo)))
1907+
(ghostel--open-link (ghostel--uri-at-pos (point))))
18891908

18901909
(defun ghostel--find-next-link (from)
18911910
"Return start position of the first hyperlink after FROM, or nil.
@@ -3156,14 +3175,7 @@ position COL columns into the first matched line."
31563175
(when (> tr 0)
31573176
(save-excursion
31583177
(goto-char (point-max))
3159-
;; Partial redraws can leave a trailing \n after the last row.
3160-
;; Step past it so `forward-line' counts only content rows, not
3161-
;; the phantom empty line that would otherwise push the viewport
3162-
;; one line too deep and clip the bottom row.
3163-
(when (and (not (bobp))
3164-
(eq (char-before) ?\n))
3165-
(forward-char -1))
3166-
(forward-line (- (1- tr)))
3178+
(forward-line (- tr))
31673179
(line-beginning-position)))))
31683180

31693181
(defun ghostel--active-preedit-overlay ()
@@ -3310,47 +3322,13 @@ No-op when `ghostel--snap-requested' (user input overrides)."
33103322
(eq (window-buffer win) buffer))
33113323
(ghostel--reconcile-saved-position win entry))))))
33123324

3313-
(defun ghostel--anchor-window (win vs pt &optional exact-point)
3325+
(defun ghostel--anchor-window (win vs pt)
33143326
"Pin WIN to viewport-start VS and sync its point to PT.
33153327
Also resets pixel vscroll (pixel-scroll-precision-mode may leave a
3316-
partial offset that would clip the top line after a redraw).
3317-
3318-
Two terminal-side configurations land PT at `point-max' on the last
3319-
visible row in a position Emacs redisplay treats as off-screen — which
3320-
makes `scroll-conservatively' shift `window-start' up by one row,
3321-
fighting the viewport pin and hiding the block cursor:
3322-
3323-
1. Pending-wrap: the last printed character filled the rightmost
3324-
column and the next print will soft-wrap (issue #138).
3325-
3326-
2. CUP park onto an empty trailing row, no pending-wrap: the TUI
3327-
moved the cursor via absolute positioning to a row that has no
3328-
written cells, so the row renders to an empty buffer line and PT
3329-
lands at `point-max' (issue #157).
3330-
3331-
Clamp `window-point' back by one in either case so it sits inside the
3332-
viewport; buffer-point is unaffected and subsequent redraws recapture
3333-
the real cursor. We must NOT clamp for a plain shell prompt where the
3334-
cursor is legitimately at `point-max' after typing — doing so would
3335-
draw the block cursor on the last character instead of after it
3336-
\(issue #146).
3337-
3338-
When EXACT-POINT is non-nil, set `window-point' to PT exactly. This is
3339-
used while a GUI input method is displaying preedit text: the candidate
3340-
window is anchored to point, and moving it by one character is visible
3341-
as a jump."
3328+
partial offset that would clip the top line after a redraw)."
33423329
(set-window-start win vs t)
33433330
(set-window-vscroll win 0 t)
3344-
(set-window-point win (if (and (not exact-point)
3345-
(= pt (point-max))
3346-
(> pt (point-min))
3347-
ghostel--term
3348-
(or (ghostel--cursor-pending-wrap-p
3349-
ghostel--term)
3350-
(ghostel--cursor-on-empty-row-p
3351-
ghostel--term)))
3352-
(1- pt)
3353-
pt)))
3331+
(set-window-point win pt))
33543332

33553333
(defun ghostel--restore-scrollback-window (win state)
33563334
"Restore WIN to ws/wp recorded in STATE and push STATE to scroll-positions.
@@ -3423,8 +3401,7 @@ the candidate window does not jump while text is streaming in."
34233401
(eq win preedit-window))))
34243402
(ghostel--anchor-window
34253403
win vs
3426-
(if preedit-win-p preedit-point pt)
3427-
preedit-win-p))
3404+
(if preedit-win-p preedit-point pt)))
34283405
(ghostel--restore-scrollback-window
34293406
win (assq win non-anchored-states))))
34303407
(when preedit-point

src/emacs.zig

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ pub const Env = struct {
224224
return self.call0(sym.@"line-end-position");
225225
}
226226

227+
pub fn lineBeginningPosition2(self: Env) Value {
228+
return self.call1(sym.@"line-beginning-position", self.makeInteger(2));
229+
}
230+
231+
pub fn pointMin(self: Env) Value {
232+
return self.call0(sym.@"point-min");
233+
}
234+
227235
pub fn pointMax(self: Env) Value {
228236
return self.call0(sym.@"point-max");
229237
}
@@ -300,12 +308,15 @@ pub const Sym = struct {
300308
@"move-to-column": Value,
301309
@"erase-buffer": Value,
302310
@"line-end-position": Value,
311+
@"line-beginning-position": Value,
312+
@"point-min": Value,
303313
@"point-max": Value,
304314
@"delete-region": Value,
305315
@"char-before": Value,
306316
@"mark-marker": Value,
307317
@"marker-position": Value,
308318
@"set-marker": Value,
319+
ding: Value,
309320

310321
// Text property names
311322
face: Value,
@@ -330,7 +341,8 @@ pub const Sym = struct {
330341
@"ghostel--flush-output": Value,
331342
@"ghostel--set-title": Value,
332343
@"ghostel--debug-log-vt": Value,
333-
ding: Value,
344+
@"ghostel--native-uri-at": Value,
345+
@"ghostel--native-link-help-echo": Value,
334346
};
335347

336348
pub var sym: Sym = undefined;

0 commit comments

Comments
 (0)