From 612411021c8bc212a6108a1d0ab2138b17b6674d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 21 Sep 2026 19:06:16 +0700 Subject: [PATCH] The colour goes on the language, and the program is left plain --- emacs/flan-mode.el | 27 ++++++---- emacs/flan.el | 45 +++++++++++----- emacs/test-flan-mode.el | 117 +++++++++++++++++++++++++--------------- emacs/test-flan.el | 23 ++++---- 4 files changed, 138 insertions(+), 74 deletions(-) diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 86562657..578c4c70 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -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) (2 font-lock-function-name-face nil t)) (,(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 ;; `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. @@ -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 ;; constraint map accepts, is covered by the same rule. ("\\_<:\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face) - ;; A field. The label in a struct literal — `{.x 1.0}' — and the accessor - ;; `(.x v)' are the same name and are drawn the same way. Without this - ;; rule every field label in the corpus is unfontified, which is what the - ;; colon-to-dot change left behind: the keyword rule above used to cover - ;; them and no longer does, because the colon belongs to keywords now. - ("\\_<\\.\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face) + ;; A field — the label in `{.x 1.0}' and the accessor in `(.x v)' — is left + ;; plain, for the reason the builtins are: it is part of reading the + ;; program, not part of reading the language. clojure-mode draws Java + ;; interop the same way. + ;; ;; 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 ;; 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) ;; A type variable, `$t', which is what a generic `defn' names its ;; parameter types with and what `{:where (ordered? $t)}' constrains. - ("\\_<\\$\\(?:\\sw\\|\\s_\\)*" . font-lock-type-face) - ("\\_<\\(?:0x[0-9a-fA-F]+\\|-?[0-9]+\\(?:\\.[0-9]+\\)?\\)\\_>" - . font-lock-constant-face)) + ("\\_<\\$\\(?:\\sw\\|\\s_\\)*" . font-lock-type-face)) + ;; A number is not drawn. clojure-mode has no rule for one either: a + ;; 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'. 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* diff --git a/emacs/flan.el b/emacs/flan.el index 9efff105..f080f3cc 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -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 ;; exactly as it did before any of this existed. -(defcustom flan-font-lock-dynamically t - "Whether to colour names by what the running program says they are. +(defcustom flan-font-lock-dynamically '(macro) + "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 -function as a function, a global as a global and a struct, union, data type, -enum or alias as a type. Names the program has never heard of are left -alone, which is what makes a typo visible. +A list of kinds drawn from `flan--dynamic-faces': `macro', `fn', `var', +`const', `struct', `data', `union', `enum', `alias', `extern', `builtin'. +t draws all of them and nil draws none. + +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 `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 -changes — completion, eldoc and \\[xref-find-definitions] read the same -answer and go on working." - :type 'boolean +Nothing else changes with this setting — completion, eldoc and +\\[xref-find-definitions] read the same answer and go on working." + :type '(choice (const :tag "Every kind" t) + (const :tag "None" nil) + (repeat :tag "These kinds" symbol)) :group 'flan :set (lambda (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'. 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 "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'. (tails (make-hash-table :test #'equal))) (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 (puthash (car d) face tbl) (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 () "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 () "Add or remove the dynamic rules in the current buffer, and redraw it. diff --git a/emacs/test-flan-mode.el b/emacs/test-flan-mode.el index bdcfd7f1..4727dacd 100644 --- a/emacs/test-flan-mode.el +++ b/emacs/test-flan-mode.el @@ -329,8 +329,9 @@ ;;; Font lock -;; The colon-to-dot change left every field label in the corpus unfontified: -;; the keyword rule used to cover them, and the colon belongs to keywords now. +;; What is drawn is the language; what is drawn plain is the program. A field +;; 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) "The face on the first character of NEEDLE in TEXT, under `flan-mode'." @@ -344,14 +345,22 @@ (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") ("(rl/Vector2 {.x 1.0 .y 2.0})" ".y" "and the second one") ("(set total (.bytes c))" ".bytes" "a field accessor") - ;; The keyword rule is still there: an enum member is one. - ("(rl/mouse-button-down? :mouse-left)" ":mouse-left" "an enum member"))) - (test-flan--check (format "%s is a constant" (nth 2 case)) - (eq (test-flan-mode--face-at (nth 0 case) (nth 1 case)) - 'font-lock-constant-face))) + ("(push v 42)" "42" "a number") + ("(set mask 0xFF)" "0xFF" "a hex number") + ("(push v x)" "push" "a builtin") + ("(println (length xs))" "length" "and another"))) + (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 ;; has one in the middle. @@ -407,15 +416,10 @@ font-lock-keyword-face "array-fill") ("(array-gen [n] f)" "array-gen" font-lock-keyword-face "array-gen") - ;; A builtin is a call, not a form the parser knows, and it is - ;; drawn as one. - ("(push v 1)" "push" font-lock-builtin-face "a builtin") - ("(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") + ;; A builtin is a call and not a form the parser knows, and it + ;; is left plain — the rows for that are above, with the field + ;; labels and the numbers. + ;; ;; Types. ("(defn f [x dyn] dyn x)" "dyn" font-lock-type-face "dyn") ("(def v (Vec u8))" "Vec" font-lock-type-face "Vec") @@ -508,23 +512,47 @@ (message "\n-- font lock from the program") -(dolist (case '(("(settle 1 2)" "settle" flan-function-face "a function") - ("(set ticks 0)" "ticks" flan-global-face "a global") - ("(print gravity)" "gravity" flan-global-face "a constant") - ("(with-retry (go))" "with-retry" flan-macro-face "a macro") - ("(def p (Vec Pixel))" "Pixel" flan-type-face "a struct") - ("(defn f [s Shape] () 1)" "Shape" flan-type-face "a data type") - ("(def k Key)" "Key" flan-type-face "an enum"))) - (test-flan--check (format "%s the program defines is drawn as one" (nth 3 case)) - (eq (test-flan-mode--dyn-face (nth 0 case) (nth 1 case)) - (nth 2 case)))) +;; `flan-font-lock-dynamically' is a list of kinds and defaults to macros +;; alone, so a macro is drawn and everything else the program defines is not. +;; A macro is the one kind a reader cannot work out from the call, because its +;; arguments are not evaluated; a function drawn as a function says only that +;; the name exists. cider-mode's default is the same shape. +(test-flan--check + "a macro the program defines is drawn as one" + (eq (test-flan-mode--dyn-face "(with-retry (go))" "with-retry") + 'flan-macro-face)) + +(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 ;; asserted as a difference and not only as two faces. (test-flan--check "a macro and a function are not drawn the same" - (not (eq (test-flan-mode--dyn-face "(with-retry (go))" "with-retry") - (test-flan-mode--dyn-face "(settle 1 2)" "settle")))) + (let ((flan-font-lock-dynamically '(macro fn))) + (not (eq (test-flan-mode--dyn-face "(with-retry (go))" "with-retry") + (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 ;; 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. (test-flan--check "a constructor's type half is drawn" - (eq (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" "Shape") - 'flan-type-face)) + (let ((flan-font-lock-dynamically t)) + (eq (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" "Shape") + 'flan-type-face))) (test-flan--check "and its case half is not" @@ -561,28 +590,27 @@ "and so is a kind this editor does not know" (null (test-flan-mode--dyn-face "(somenewthing)" "somenewthing"))) -;; The static rules win, and they win in both directions: a program function -;; called `length' keeps the builtin colour, and one called `if' keeps the -;; keyword colour. +;; The static rules win. `length' is not the test it once was — a builtin is +;; drawn plain now, so both answers are plain and the row proves nothing — so +;; 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 - "a program function named length is still drawn as the builtin" - (eq (test-flan-mode--dyn-face "(length xs)" "length") - 'font-lock-builtin-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 program function named if is still drawn as the special form" + (let ((flan-font-lock-dynamically t)) + (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 ;; `sim/step'. One package offers it, so it resolves — the rule `flan--lookup' ;; already follows for eldoc and `M-.'. (test-flan--check "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 "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 ;; exactly as `flan-mode' alone draws it. @@ -612,7 +640,10 @@ (push (get-text-property (1+ i) 'face) 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 "a buffer with no session is drawn exactly as the mode alone draws it" (equal (test-flan-mode--faces text 'none) diff --git a/emacs/test-flan.el b/emacs/test-flan.el index a346716b..d43ab6f2 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -504,19 +504,24 @@ 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 ;; purpose: the claim is that evaluating is enough. (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 "a name installed now is drawn now, with nothing else asked for" - (save-excursion - (goto-char (point-max)) - (insert "\n(freshly-added)\n") - (font-lock-ensure) - (prog1 (eq (get-text-property (- (point) (length "(freshly-added)\n") -1) - 'face) - 'flan-function-face) - (delete-region (- (point) (length "\n(freshly-added)\n")) (point))))) + (let ((flan-font-lock-dynamically t)) + (flan--dynamic-sync) + (save-excursion + (goto-char (point-max)) + (insert "\n(freshly-added)\n") + (font-lock-ensure) + (prog1 (eq (get-text-property (- (point) (length "(freshly-added)\n") -1) + 'face) + 'flan-function-face) + (delete-region (- (point) (length "\n(freshly-added)\n")) (point)))))) + (flan--dynamic-sync) (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 (goto-char (point-max)) (insert "\n(twice 1)\n")