The colour goes on the language, and the program is left plain

This commit is contained in:
Joseph Ferano 2026-09-21 19:06:16 +07:00
parent d9bb882bbe
commit 612411021c
4 changed files with 138 additions and 74 deletions

View File

@ -203,7 +203,14 @@ reason and is the odd one — it is legal only as the last item of a `def' or a
(1 font-lock-keyword-face) (1 font-lock-keyword-face)
(2 font-lock-function-name-face nil t)) (2 font-lock-function-name-face nil t))
(,(concat "(" (regexp-opt flan--special t) "\\_>") 1 font-lock-keyword-face) (,(concat "(" (regexp-opt flan--special t) "\\_>") 1 font-lock-keyword-face)
(,(concat "(" (regexp-opt flan--builtins t) "\\_>") 1 font-lock-builtin-face) ;; The builtins are deliberately not drawn. clojure-mode leaves `map',
;; `filter' and the rest of the core library plain and spends
;; `font-lock-builtin-face' only on the earmuffed dynamic variables, and a
;; buffer reads better for it: `at', `push' and `println' are the ordinary
;; vocabulary of a program, and colouring the ordinary leaves nothing for
;; the unusual. `flan--builtins' is still the list completion and the
;; dynamic rules check against, so nothing else has to know this.
;;
;; Ahead of the qualified-name rule below, which would otherwise take ;; Ahead of the qualified-name rule below, which would otherwise take
;; `context/' in `context/allocator' for a package alias. It is not one: ;; `context/' in `context/allocator' for a package alias. It is not one:
;; there is no package called `context', and the slash is part of the name. ;; there is no package called `context', and the slash is part of the name.
@ -213,12 +220,11 @@ reason and is the odd one — it is legal only as the last item of a `def' or a
;; constant rather than as a string. `:where', the one key a `defn''s ;; constant rather than as a string. `:where', the one key a `defn''s
;; constraint map accepts, is covered by the same rule. ;; constraint map accepts, is covered by the same rule.
("\\_<:\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face) ("\\_<:\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
;; A field. The label in a struct literal — `{.x 1.0}' — and the accessor ;; A field — the label in `{.x 1.0}' and the accessor in `(.x v)' — is left
;; `(.x v)' are the same name and are drawn the same way. Without this ;; plain, for the reason the builtins are: it is part of reading the
;; rule every field label in the corpus is unfontified, which is what the ;; program, not part of reading the language. clojure-mode draws Java
;; colon-to-dot change left behind: the keyword rule above used to cover ;; interop the same way.
;; them and no longer does, because the colon belongs to keywords now. ;;
("\\_<\\.\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
;; The package half of a qualified name — the `rl/' of `rl/draw-text'. ;; The package half of a qualified name — the `rl/' of `rl/draw-text'.
;; Drawn as a type the way clojure-mode draws a namespace, so the eye can ;; Drawn as a type the way clojure-mode draws a namespace, so the eye can
;; split the package from the name without reading either. The leading ;; split the package from the name without reading either. The leading
@ -243,9 +249,10 @@ reason and is the odd one — it is legal only as the last item of a `def' or a
. font-lock-type-face) . font-lock-type-face)
;; A type variable, `$t', which is what a generic `defn' names its ;; A type variable, `$t', which is what a generic `defn' names its
;; parameter types with and what `{:where (ordered? $t)}' constrains. ;; parameter types with and what `{:where (ordered? $t)}' constrains.
("\\_<\\$\\(?:\\sw\\|\\s_\\)*" . font-lock-type-face) ("\\_<\\$\\(?:\\sw\\|\\s_\\)*" . font-lock-type-face))
("\\_<\\(?:0x[0-9a-fA-F]+\\|-?[0-9]+\\(?:\\.[0-9]+\\)?\\)\\_>" ;; A number is not drawn. clojure-mode has no rule for one either: a
. font-lock-constant-face)) ;; literal is already unmistakable by shape, and a colour on every integer
;; in a program full of indices is noise.
"Font lock for `flan-mode'. "Font lock for `flan-mode'.
Every rule here is about the language itself, so a file that has never been Every rule here is about the language itself, so a file that has never been
near a running program is drawn completely. What a *particular program* near a running program is drawn completely. What a *particular program*

View File

@ -1798,21 +1798,28 @@ other, and KIND is what tells it apart where that matters.")
;; buffer when the cache is dropped, so a file opened with no session looks ;; buffer when the cache is dropped, so a file opened with no session looks
;; exactly as it did before any of this existed. ;; exactly as it did before any of this existed.
(defcustom flan-font-lock-dynamically t (defcustom flan-font-lock-dynamically '(macro)
"Whether to colour names by what the running program says they are. "Which kinds of name to colour by what the running program says they are.
With a session connected, a macro the program defines is drawn as a macro, a A list of kinds drawn from `flan--dynamic-faces': `macro', `fn', `var',
function as a function, a global as a global and a struct, union, data type, `const', `struct', `data', `union', `enum', `alias', `extern', `builtin'.
enum or alias as a type. Names the program has never heard of are left t draws all of them and nil draws none.
alone, which is what makes a typo visible.
The default is macros alone, which is the one kind a reader cannot work out
from the call itself — a macro does not evaluate its arguments, so the shape
of the call is its own and worth marking. A function drawn as a function
says only that the name exists, which most names in a program do, and the
buffer reads better for leaving them plain. cider-mode's default is the same
shape and for the same reason.
This never changes how the language itself is drawn: `if' and `push' and This never changes how the language itself is drawn: `if' and `push' and
`i64' keep the colours `flan-mode' gives them whatever the program defines. `i64' keep the colours `flan-mode' gives them whatever the program defines.
Set to nil to leave every name to `flan-mode''s own rules. Nothing else Nothing else changes with this setting — completion, eldoc and
changes — completion, eldoc and \\[xref-find-definitions] read the same \\[xref-find-definitions] read the same answer and go on working."
answer and go on working." :type '(choice (const :tag "Every kind" t)
:type 'boolean (const :tag "None" nil)
(repeat :tag "These kinds" symbol))
:group 'flan :group 'flan
:set (lambda (sym val) :set (lambda (sym val)
(set-default sym val) (set-default sym val)
@ -1871,6 +1878,14 @@ false where an undrawn one says only that this editor is older.")
"Name to face, built from `flan--defs'. "Name to face, built from `flan--defs'.
Rebuilt when the cache is, and read once per symbol per redisplay.") Rebuilt when the cache is, and read once per symbol per redisplay.")
(defun flan--dynamic-kind-wanted-p (kind)
"Non-nil when KIND, a string from the `defs' op, is one to draw.
The setting is written with symbols because that is what a user types; the
wire answers with strings."
(cond ((eq flan-font-lock-dynamically t) t)
((null flan-font-lock-dynamically) nil)
(t (and (memq (intern kind) flan-font-lock-dynamically) t))))
(defvar flan--dynamic-face nil (defvar flan--dynamic-face nil
"The face `flan--dynamic-match' found, read by the font-lock rule after it.") "The face `flan--dynamic-match' found, read by the font-lock rule after it.")
@ -1884,7 +1899,8 @@ Rebuilt when the cache is, and read once per symbol per redisplay.")
;; `draw'. ;; `draw'.
(tails (make-hash-table :test #'equal))) (tails (make-hash-table :test #'equal)))
(dolist (d flan--defs) (dolist (d flan--defs)
(let ((face (cdr (assoc (nth 1 d) flan--dynamic-faces)))) (let ((face (and (flan--dynamic-kind-wanted-p (nth 1 d))
(cdr (assoc (nth 1 d) flan--dynamic-faces)))))
(when face (when face
(puthash (car d) face tbl) (puthash (car d) face tbl)
(let ((slash (string-search "/" (car d)))) (let ((slash (string-search "/" (car d))))
@ -1934,7 +1950,12 @@ Leaves its face in `flan--dynamic-face' for the rule that calls this."
(defun flan--dynamic-wanted-p () (defun flan--dynamic-wanted-p ()
"Non-nil when there is something to draw and permission to draw it." "Non-nil when there is something to draw and permission to draw it."
(and flan-font-lock-dynamically flan--defs t)) ;; The table, not the setting: a setting naming only kinds this program has
;; none of draws nothing, and installing a rule that can never match is a
;; cost for no picture.
(and flan-font-lock-dynamically flan--defs
(> (hash-table-count flan--dynamic-table) 0)
t))
(defun flan--dynamic-install () (defun flan--dynamic-install ()
"Add or remove the dynamic rules in the current buffer, and redraw it. "Add or remove the dynamic rules in the current buffer, and redraw it.

View File

@ -329,8 +329,9 @@
;;; Font lock ;;; Font lock
;; The colon-to-dot change left every field label in the corpus unfontified: ;; What is drawn is the language; what is drawn plain is the program. A field
;; the keyword rule used to cover them, and the colon belongs to keywords now. ;; label, a builtin and a number are all part of reading a program and none of
;; them is coloured, which is how clojure-mode treats the same three.
(defun test-flan-mode--face-at (text needle) (defun test-flan-mode--face-at (text needle)
"The face on the first character of NEEDLE in TEXT, under `flan-mode'." "The face on the first character of NEEDLE in TEXT, under `flan-mode'."
@ -344,14 +345,22 @@
(message "\n-- font lock") (message "\n-- font lock")
;; The keyword rule is what is left of the constants: an enum member resolves
;; against a type and is worth marking.
(test-flan--check "an enum member is a constant"
(eq (test-flan-mode--face-at
"(rl/mouse-button-down? :mouse-left)" ":mouse-left")
'font-lock-constant-face))
(dolist (case '(("(rl/Vector2 {.x 1.0 .y 2.0})" ".x" "a struct literal's field label") (dolist (case '(("(rl/Vector2 {.x 1.0 .y 2.0})" ".x" "a struct literal's field label")
("(rl/Vector2 {.x 1.0 .y 2.0})" ".y" "and the second one") ("(rl/Vector2 {.x 1.0 .y 2.0})" ".y" "and the second one")
("(set total (.bytes c))" ".bytes" "a field accessor") ("(set total (.bytes c))" ".bytes" "a field accessor")
;; The keyword rule is still there: an enum member is one. ("(push v 42)" "42" "a number")
("(rl/mouse-button-down? :mouse-left)" ":mouse-left" "an enum member"))) ("(set mask 0xFF)" "0xFF" "a hex number")
(test-flan--check (format "%s is a constant" (nth 2 case)) ("(push v x)" "push" "a builtin")
(eq (test-flan-mode--face-at (nth 0 case) (nth 1 case)) ("(println (length xs))" "length" "and another")))
'font-lock-constant-face))) (test-flan--check (format "%s is plain" (nth 2 case))
(null (test-flan-mode--face-at (nth 0 case) (nth 1 case)))))
;; A dot inside a number is not a label, and neither is the dot in a name that ;; A dot inside a number is not a label, and neither is the dot in a name that
;; has one in the middle. ;; has one in the middle.
@ -407,15 +416,10 @@
font-lock-keyword-face "array-fill") font-lock-keyword-face "array-fill")
("(array-gen [n] f)" "array-gen" ("(array-gen [n] f)" "array-gen"
font-lock-keyword-face "array-gen") font-lock-keyword-face "array-gen")
;; A builtin is a call, not a form the parser knows, and it is ;; A builtin is a call and not a form the parser knows, and it
;; drawn as one. ;; is left plain — the rows for that are above, with the field
("(push v 1)" "push" font-lock-builtin-face "a builtin") ;; labels and the numbers.
("(slice xs 0 4)" "slice" font-lock-builtin-face "and another") ;;
("(bytes-view b)" "bytes-view" font-lock-builtin-face
"bytes-view")
("(length xs)" "length" font-lock-builtin-face "length")
("(class-of x)" "class-of" font-lock-builtin-face "class-of")
("(filled xs 0)" "filled" font-lock-builtin-face "filled")
;; Types. ;; Types.
("(defn f [x dyn] dyn x)" "dyn" font-lock-type-face "dyn") ("(defn f [x dyn] dyn x)" "dyn" font-lock-type-face "dyn")
("(def v (Vec u8))" "Vec" font-lock-type-face "Vec") ("(def v (Vec u8))" "Vec" font-lock-type-face "Vec")
@ -508,23 +512,47 @@
(message "\n-- font lock from the program") (message "\n-- font lock from the program")
(dolist (case '(("(settle 1 2)" "settle" flan-function-face "a function") ;; `flan-font-lock-dynamically' is a list of kinds and defaults to macros
("(set ticks 0)" "ticks" flan-global-face "a global") ;; alone, so a macro is drawn and everything else the program defines is not.
("(print gravity)" "gravity" flan-global-face "a constant") ;; A macro is the one kind a reader cannot work out from the call, because its
("(with-retry (go))" "with-retry" flan-macro-face "a macro") ;; arguments are not evaluated; a function drawn as a function says only that
("(def p (Vec Pixel))" "Pixel" flan-type-face "a struct") ;; the name exists. cider-mode's default is the same shape.
("(defn f [s Shape] () 1)" "Shape" flan-type-face "a data type") (test-flan--check
("(def k Key)" "Key" flan-type-face "an enum"))) "a macro the program defines is drawn as one"
(test-flan--check (format "%s the program defines is drawn as one" (nth 3 case)) (eq (test-flan-mode--dyn-face "(with-retry (go))" "with-retry")
(eq (test-flan-mode--dyn-face (nth 0 case) (nth 1 case)) 'flan-macro-face))
(nth 2 case))))
(dolist (case '(("(settle 1 2)" "settle" "a function")
("(set ticks 0)" "ticks" "a global")
("(print gravity)" "gravity" "a constant")
("(def p (Vec Pixel))" "Pixel" "a struct")
("(defn f [s Shape] () 1)" "Shape" "a data type")
("(def k Key)" "Key" "an enum")))
(test-flan--check
(format "%s the program defines is left plain" (nth 2 case))
(null (test-flan-mode--dyn-face (nth 0 case) (nth 1 case)))))
;; Asked for by name, the other kinds still draw: the default is a default and
;; not a removal.
(test-flan--check
"a function is drawn when the setting asks for one"
(let ((flan-font-lock-dynamically '(macro fn)))
(eq (test-flan-mode--dyn-face "(settle 1 2)" "settle")
'flan-function-face)))
(test-flan--check
"and t still draws every kind"
(let ((flan-font-lock-dynamically t))
(eq (test-flan-mode--dyn-face "(set ticks 0)" "ticks")
'flan-global-face)))
;; A macro and a function must not look alike — the whole point — so this is ;; A macro and a function must not look alike — the whole point — so this is
;; asserted as a difference and not only as two faces. ;; asserted as a difference and not only as two faces.
(test-flan--check (test-flan--check
"a macro and a function are not drawn the same" "a macro and a function are not drawn the same"
(let ((flan-font-lock-dynamically '(macro fn)))
(not (eq (test-flan-mode--dyn-face "(with-retry (go))" "with-retry") (not (eq (test-flan-mode--dyn-face "(with-retry (go))" "with-retry")
(test-flan-mode--dyn-face "(settle 1 2)" "settle")))) (test-flan-mode--dyn-face "(settle 1 2)" "settle")))))
;; A macro is compiled, so it has a body to disassemble and to lower — and the ;; A macro is compiled, so it has a body to disassemble and to lower — and the
;; lists that offer "a thing with a body" used to filter on kind `fn' alone. ;; lists that offer "a thing with a body" used to filter on kind `fn' alone.
@ -546,8 +574,9 @@
;; the program can speak for and the case half is left alone. ;; the program can speak for and the case half is left alone.
(test-flan--check (test-flan--check
"a constructor's type half is drawn" "a constructor's type half is drawn"
(let ((flan-font-lock-dynamically t))
(eq (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" "Shape") (eq (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" "Shape")
'flan-type-face)) 'flan-type-face)))
(test-flan--check (test-flan--check
"and its case half is not" "and its case half is not"
@ -561,28 +590,27 @@
"and so is a kind this editor does not know" "and so is a kind this editor does not know"
(null (test-flan-mode--dyn-face "(somenewthing)" "somenewthing"))) (null (test-flan-mode--dyn-face "(somenewthing)" "somenewthing")))
;; The static rules win, and they win in both directions: a program function ;; The static rules win. `length' is not the test it once was — a builtin is
;; called `length' keeps the builtin colour, and one called `if' keeps the ;; drawn plain now, so both answers are plain and the row proves nothing — so
;; keyword colour. ;; the claim rests on `if', which the language draws and the program may not
;; repaint. Asked for every kind, so the program's own rule is live.
(test-flan--check (test-flan--check
"a program function named length is still drawn as the builtin" "a program function named if is still drawn as the special form"
(eq (test-flan-mode--dyn-face "(length xs)" "length") (let ((flan-font-lock-dynamically t))
'font-lock-builtin-face)) (eq (test-flan-mode--dyn-face "(if a 1 2)" "if") 'font-lock-keyword-face)))
(test-flan--check
"and one named if is still drawn as the special form"
(eq (test-flan-mode--dyn-face "(if a 1 2)" "if") 'font-lock-keyword-face))
;; A buffer inside a package writes `step' for what the program calls ;; A buffer inside a package writes `step' for what the program calls
;; `sim/step'. One package offers it, so it resolves — the rule `flan--lookup' ;; `sim/step'. One package offers it, so it resolves — the rule `flan--lookup'
;; already follows for eldoc and `M-.'. ;; already follows for eldoc and `M-.'.
(test-flan--check (test-flan--check
"an unqualified name that one package offers is drawn" "an unqualified name that one package offers is drawn"
(eq (test-flan-mode--dyn-face "(step)" "step") 'flan-function-face)) (let ((flan-font-lock-dynamically '(macro fn)))
(eq (test-flan-mode--dyn-face "(step)" "step") 'flan-function-face)))
(test-flan--check (test-flan--check
"and one that two packages offer is not, because that would be a guess" "and one that two packages offer is not, because that would be a guess"
(null (test-flan-mode--dyn-face "(draw)" "draw"))) (let ((flan-font-lock-dynamically '(macro fn)))
(null (test-flan-mode--dyn-face "(draw)" "draw"))))
;; With no session there is nothing to say, and the buffer has to be drawn ;; With no session there is nothing to say, and the buffer has to be drawn
;; exactly as `flan-mode' alone draws it. ;; exactly as `flan-mode' alone draws it.
@ -612,7 +640,10 @@
(push (get-text-property (1+ i) 'face) acc)) (push (get-text-property (1+ i) 'face) acc))
(nreverse acc))))) (nreverse acc)))))
(let ((text "(defn settle [row i32] ()\n (let [vel gravity]\n (set ticks vel)))\n")) ;; The text carries a macro call, because the default draws macros and nothing
;; else: a body of plain functions and globals is the same picture either way
;; and the last of the three claims below would be asserting nothing.
(let ((text "(defn settle [row i32] ()\n (let [vel gravity]\n (with-retry (set ticks vel))))\n"))
(test-flan--check (test-flan--check
"a buffer with no session is drawn exactly as the mode alone draws it" "a buffer with no session is drawn exactly as the mode alone draws it"
(equal (test-flan-mode--faces text 'none) (equal (test-flan-mode--faces text 'none)

View File

@ -504,8 +504,12 @@ already rely on it — so nothing here is a stand-in for the real thing."
;; And the buffer is redrawn for it. No refresh command is run here on ;; And the buffer is redrawn for it. No refresh command is run here on
;; purpose: the claim is that evaluating is enough. ;; purpose: the claim is that evaluating is enough.
(font-lock-ensure) (font-lock-ensure)
;; Asked for every kind, because the default draws macros alone and this
;; claim is about the redraw and not about which kinds are wanted.
(test-flan--check (test-flan--check
"a name installed now is drawn now, with nothing else asked for" "a name installed now is drawn now, with nothing else asked for"
(let ((flan-font-lock-dynamically t))
(flan--dynamic-sync)
(save-excursion (save-excursion
(goto-char (point-max)) (goto-char (point-max))
(insert "\n(freshly-added)\n") (insert "\n(freshly-added)\n")
@ -513,10 +517,11 @@ already rely on it — so nothing here is a stand-in for the real thing."
(prog1 (eq (get-text-property (- (point) (length "(freshly-added)\n") -1) (prog1 (eq (get-text-property (- (point) (length "(freshly-added)\n") -1)
'face) 'face)
'flan-function-face) 'flan-function-face)
(delete-region (- (point) (length "\n(freshly-added)\n")) (point))))) (delete-region (- (point) (length "\n(freshly-added)\n")) (point))))))
(flan--dynamic-sync)
(test-flan--check (test-flan--check
"and a macro is not drawn like a function" "and a macro is drawn, which the default asks for on its own"
(save-excursion (save-excursion
(goto-char (point-max)) (goto-char (point-max))
(insert "\n(twice 1)\n") (insert "\n(twice 1)\n")