Skip to content

Commit 28f5071

Browse files
Cianidosdakra
authored andcommitted
Preserve per-cell face props under font-lock
The native module writes inline `face' text-properties (:foreground / :background per cell) via applyStyle in src/render.zig. If `font-lock-mode' ends up active in a ghostel buffer, JIT-lock's fontify pass calls `font-lock-unfontify-region' which strips all face props in the region — output then renders in the buffer's default fg only. `ghostel-mode' already runs `(font-lock-mode -1)' in its body, but that executes before `after-change-major-mode-hook', and configurations that force-enable font-lock later (Doom Emacs sets `font-lock-defaults = (nil t)' globally, which makes `turn-on-font-lock-if-desired' activate it even in modes that have no keywords) flip it back on. A subsequent redraw or JIT-lock pass then wipes the face props. Neutralise the unfontify pass with a buffer-local override. ghostel-mode has no font-lock keywords to fontify, so skipping unfontify has no other effect, and the face props the module writes survive even when font-lock is re-enabled by user configuration. Add regression test for face-prop preservation under font-lock Writes a coloured SGR run into a ghostel-mode buffer, renders, then force-enables `font-lock-mode' (with `font-lock-defaults' set the way Doom Emacs sets it globally) and runs `font-lock-ensure'. The face text-property applied by the native renderer must survive the fontify pass — without the buffer-local `font-lock-unfontify-region-function' override installed in `ghostel-mode', JIT-lock's unfontify strips it. ghostel-compile-view-mode: preserve face props under font-lock After ghostel-compile finalises, the buffer's major mode switches to ghostel-compile-view-mode (derived from compilation-mode). The text inherited from the ghostel run still carries the per-cell `face' text-properties the native module wrote — but compilation-mode installs font-lock keywords for error highlighting, and the default `font-lock-unfontify-region-function' strips every `face' prop on the first JIT-lock pass, wiping colour from the recorded output. Neutralise unfontify in the view mode the same way ghostel-mode does. The compilation keywords are applied once on a finalised, static buffer, so there's nothing that needs refontifying. Includes a regression test that finalises a buffer with a propertized RED segment plus an error line, runs `font-lock-ensure', and checks both that the ghostel face survived and that compilation error highlighting still applied.
1 parent 69d4b0d commit 28f5071

4 files changed

Lines changed: 91 additions & 2 deletions

File tree

evil-ghostel.el

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
(defun evil-ghostel--reset-cursor-point ()
4646
"Move Emacs point to the terminal cursor position.
4747
`ghostel--cursor-position' returns row relative to the viewport
48-
(the last `ghostel--term-rows' lines of the buffer), so the row
48+
\(the last `ghostel--term-rows' lines of the buffer), so the row
4949
must be offset by the scrollback line count. Mirrors the
5050
placement math the native module performs in `src/render.zig'."
5151
(when (and ghostel--term ghostel--term-rows)

ghostel-compile.el

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,15 @@ to a ghostel terminal."
171171
(setq-local next-error-function #'compilation-next-error-function)
172172
;; Make sure point lands at the top after a successful recompile (and
173173
;; that future input doesn't inherit ghostel's terminal-style behaviour).
174-
(setq-local window-point-insertion-type nil))
174+
(setq-local window-point-insertion-type nil)
175+
;; The buffer text inherited from the ghostel run carries per-cell `face'
176+
;; text-properties written by the native module. `compilation-mode'
177+
;; installs font-lock keywords for error highlighting, and the default
178+
;; unfontify function strips every `face' prop — wiping the colour from
179+
;; the recorded output on the first JIT-lock pass. Neutralise unfontify:
180+
;; compilation-mode's keywords are applied once via `font-lock-ensure'
181+
;; on a finalised, static buffer and don't need to be cleaned up.
182+
(setq-local font-lock-unfontify-region-function #'ignore))
175183

176184
(defun ghostel-compile--format-duration (seconds)
177185
"Format SECONDS (float) as a compilation-style duration string.

ghostel.el

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2819,6 +2819,14 @@ PROCESS is the shell process, WINDOWS is the list of windows."
28192819
"Major mode for Ghostel terminal emulator."
28202820
(buffer-disable-undo)
28212821
(font-lock-mode -1)
2822+
;; `font-lock-mode' can still be re-enabled by user configuration that
2823+
;; forces `font-lock-defaults' globally (e.g. Doom Emacs). When active,
2824+
;; JIT-lock calls `font-lock-unfontify-region' on every redraw, which
2825+
;; strips the per-cell `face' text-properties the native module writes.
2826+
;; Neutralise the unfontify pass so face props survive regardless of
2827+
;; whether font-lock ends up on. `ghostel-mode' has no keywords, so
2828+
;; skipping unfontify has no other effect.
2829+
(setq-local font-lock-unfontify-region-function #'ignore)
28222830
(setq buffer-read-only nil)
28232831
(setq-local scroll-margin 0)
28242832
(setq-local auto-hscroll-mode nil)

test/ghostel-test.el

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,54 @@ scrolling libghostty's viewport."
518518
(should-not (eq 'light (plist-get face :weight))))))) ; no :weight light
519519
(kill-buffer buf))))
520520

521+
;; -----------------------------------------------------------------------
522+
;; Test: per-cell face props survive font-lock activation
523+
;; -----------------------------------------------------------------------
524+
525+
(ert-deftest ghostel-test-face-props-survive-font-lock ()
526+
"Regression: per-cell face text-properties must survive a font-lock pass.
527+
User configs that force `font-lock-defaults' on (notably Doom Emacs,
528+
which sets `(nil t)' globally) cause `font-lock-mode' to activate in
529+
ghostel buffers despite the mode body disabling it. JIT-lock's
530+
fontify pass then calls `font-lock-unfontify-region' which, without
531+
the buffer-local override installed by `ghostel-mode', strips every
532+
`face' property the native module wrote."
533+
(let ((buf (generate-new-buffer " *ghostel-test-fl*")))
534+
(unwind-protect
535+
(with-current-buffer buf
536+
;; Activate `ghostel-mode' so the fix under test (buffer-local
537+
;; `font-lock-unfontify-region-function' override) is installed.
538+
(ghostel-mode)
539+
(let* ((term (ghostel--new 5 40 100))
540+
(inhibit-read-only t))
541+
(setq-local ghostel--term term)
542+
;; Known palette so the red SGR resolves predictably.
543+
(let ((rest (apply #'concat (make-list 14 "#000000"))))
544+
(ghostel--set-palette term
545+
(concat "#000000" "#ff0000" rest
546+
"#ffffff" "#000000")))
547+
(ghostel--write-input term "\e[31mRED\e[0m normal")
548+
(ghostel--redraw term t)
549+
(goto-char (point-min))
550+
(let ((face-before (get-text-property (point) 'face)))
551+
(should face-before)
552+
(should (plist-get face-before :foreground))
553+
;; Simulate a user config that force-enables font-lock.
554+
;; Without the buffer-local unfontify override installed
555+
;; by `ghostel-mode', the fontify pass would strip face
556+
;; props across the buffer.
557+
(setq-local font-lock-defaults '(nil t))
558+
(font-lock-mode 1)
559+
(font-lock-ensure (point-min) (point-max))
560+
;; Face property for the coloured cell must still be there.
561+
(goto-char (point-min))
562+
(let ((face-after (get-text-property (point) 'face)))
563+
(should face-after)
564+
(should (plist-get face-after :foreground))
565+
(should (equal (plist-get face-before :foreground)
566+
(plist-get face-after :foreground)))))))
567+
(kill-buffer buf))))
568+
521569
;; -----------------------------------------------------------------------
522570
;; Test: multi-byte character rendering (box drawing, Unicode)
523571
;; -----------------------------------------------------------------------
@@ -1906,6 +1954,30 @@ Downstream consumers (notably `ghostel-compile') depend on it."
19061954
(forward-char 1)))
19071955
(should found))))
19081956

1957+
(ert-deftest ghostel-test-compile-finalize-preserves-face-props ()
1958+
"Regression: per-cell `face' text-properties baked in during the ghostel
1959+
run must survive the transition to `ghostel-compile-view-mode'.
1960+
`compilation-mode' installs font-lock keywords for error highlighting,
1961+
and the default `font-lock-unfontify-region-function' strips every
1962+
`face' property — wiping the colour of the recorded output on the first
1963+
JIT-lock pass. `ghostel-compile-view-mode' installs a buffer-local
1964+
`#'ignore' override to preserve those props."
1965+
(ghostel-test--with-compile-buffer buf
1966+
(let ((inhibit-read-only t))
1967+
(setq ghostel-compile--command "make"
1968+
ghostel-compile--start-time (current-time)
1969+
ghostel-compile--scan-marker (copy-marker (point-max)))
1970+
(insert (propertize "RED" 'face '(:foreground "#ff0000")))
1971+
(insert " output\n/tmp/x.c:42:5: error: bad\n"))
1972+
(ghostel-compile--finalize buf 1 (current-time))
1973+
(font-lock-ensure (point-min) (point-max))
1974+
;; The ghostel-painted face on "RED" must still be present.
1975+
(goto-char (point-min))
1976+
(re-search-forward "RED")
1977+
(let ((face (get-text-property (match-beginning 0) 'face)))
1978+
(should face)
1979+
(should (equal "#ff0000" (plist-get face :foreground))))))
1980+
19091981
(ert-deftest ghostel-test-compile-finalize-does-not-double-count-errors ()
19101982
"Regression: parsing must not count each error twice.
19111983

@@ -5424,6 +5496,7 @@ while :; do sleep 0.1; done'\n")
54245496
ghostel-test-compile-finalize-footer-on-failure
54255497
ghostel-test-compile-finalize-trims-trailing-blank-rows
54265498
ghostel-test-compile-finalize-colors-errors
5499+
ghostel-test-compile-finalize-preserves-face-props
54275500
ghostel-test-compile-finalize-does-not-double-count-errors
54285501
ghostel-test-compile-finalize-does-not-kill-buffer
54295502
ghostel-test-compile-view-mode-n-p-navigate-without-opening

0 commit comments

Comments
 (0)