flan/emacs/test-flan-mode.el

781 lines
33 KiB
EmacsLisp

;;; test-flan-mode.el --- Indentation, from written-out shapes -*- lexical-binding: t; -*-
;; Loaded by test-flan-cider.el, which runs under `dune test'. It is not a
;; file of its own in test/dune deliberately: `emacs/*.el' is already a
;; dependency of that stanza, so a new `.el' here needs no build change.
;;
;; Every case is a piece of Flan written the way the corpus writes it, with
;; every line's indentation thrown away and put back. That is the whole test:
;; if what comes out is what went in, the indenter agrees with the code that
;; already exists. A case that is *not* in the corpus is written from
;; `clojure-mode''s behaviour and says so.
;;
;; The cases come from the bug report and from reading the tree for the shapes
;; that share it: a `let' binding vector, `defn' parameters with and without a
;; return type, `handler-bind' and `restart-case' clause parameters, and struct
;; literals in both the `:x' and the `.x' spelling — the two must agree,
;; because a lane is converting the corpus from one to the other and the
;; indenter is not allowed to notice.
;;; Code:
(require 'flan-mode)
;; The harness, which is test-flan-cider.el's: it counts and it reports, and
;; this file is loaded from there.
(declare-function test-flan--check "test-flan-cider" (name ok))
(defun test-flan-mode--reindent (text)
"Strip TEXT's indentation and let `flan-mode' put it back."
;; Quiet: `indent-region' reports progress, and in batch that lands in the
;; middle of the line a failure is being printed on.
(let ((inhibit-message t))
(with-temp-buffer
(insert text)
(flan-mode)
(goto-char (point-min))
;; Leading whitespace off every line but the first: the first line's column
;; is the buffer's, not the indenter's, and a form is indented relative to
;; where it starts. `eobp' rather than `forward-line''s return value,
;; which is 0 for a move to the end of a buffer that has no final newline
;; and would take the whole last line with it.
(forward-line 1)
(while (not (eobp))
(skip-chars-forward " \t")
(delete-region (line-beginning-position) (point))
(forward-line 1))
(indent-region (point-min) (point-max))
(buffer-substring-no-properties (point-min) (point-max)))))
(defun test-flan-mode--check (name text)
"Check that TEXT is what `flan-mode' indents it to."
(let ((got (test-flan-mode--reindent text)))
(test-flan--check name (equal got text))
(unless (equal got text)
;; Line by line, and quoted: the difference is always a count of leading
;; spaces, which is exactly what two blocks of text printed as-is make
;; hardest to see.
(let ((want (split-string text "\n")) (had (split-string got "\n")))
(while (or want had)
(unless (equal (car want) (car had))
(message " want %S\n got %S" (car want) (car had)))
(setq want (cdr want) had (cdr had)))))))
(message "\n-- indentation")
;; The bug, as reported: sand.flan's `settle'. The second and later bindings
;; went one column too far, because the enclosing open is `[' and the symbol
;; after it is the first binding's *name*, so the old check fell through to
;; `lisp-indent-function', which treated the vector as a call and aligned under
;; the first argument — `(+ gravity …)' — instead of under `vel'.
(test-flan-mode--check
"a let's bindings align name under name"
"(defn settle [row i32 col i32] ()
(let [vel (+ gravity (at velocity row col))
y (min (- rows 1) (+ row (i32 vel)))]
(while (> y row)
(set y (- y 1)))))")
;; The same shape one level in, which is where the report was actually hit: a
;; binding whose value is a struct literal, so the vector's alignment has to
;; survive a `{' in the middle of it.
(test-flan-mode--check
"and still does with a struct literal in a value"
"(let [tip (rl/Vector2 {.x 15.0 .y 12.0})
vel (+ gravity 1.0)]
(draw tip vel))")
;; The two spellings must be indented identically. Nothing in the indenter
;; looks at the key — a brace aligns under its first element whatever that
;; element is — and this is the test that keeps it that way while the corpus
;; moves from `:x' to `.x'.
(test-flan-mode--check
"a struct literal aligns under its first field"
"(rl/Rectangle {.x 0.0 .y 0.0
.width (f32 screen-width)
.height (f32 screen-height)})")
;; The colon spelling is gone from the language — the parser refuses it — but a
;; map literal will be written this way, and the indenter must not have learned
;; the difference: it aligns under the first element of a brace and never looks
;; at what that element is spelled like.
(test-flan-mode--check
"and a brace of keyword keys aligns exactly the same way"
"(counts {:north 0 :south 1
:east 2
:west 3})")
;; `defn' carries a return type between the parameters and the body, and it is
;; optional. Both spellings indent the body by two, which is why the spec is
;; `:defn' and not a count: a count would have to know whether the type is
;; there.
(test-flan-mode--check
"a defn with a return type indents its body by two"
"(defn look [n i64 label str] i64
(print label)
n)")
(test-flan-mode--check
"and a defn without one indents it the same"
"(defn show-trim [s str] ()
(print s)
(println \"\"))")
;; A parameter list that wraps is the binding-vector rule again: it is a
;; vector, so it aligns under its first element and not under its second.
(test-flan-mode--check
"a wrapped parameter list aligns under the first parameter"
"(defn move-grain [row i32 col i32
to-row i32 to-col i32
vel f32] ()
(set moved true))")
;; `handler-bind' clauses: the vector is the special argument, and each clause
;; inside it is `(Name [params] body…)' — `defn' with the name left off. The
;; body indents by two from the clause's own paren, which is the rule the head
;; cannot be looked up for: `StorageExhausted' is a name the program invented.
(test-flan-mode--check
"a handler-bind clause indents its body under its own paren"
"(handler-bind
[(StorageExhausted [c]
(set failures (+ failures 1))
(invoke-restart 'retry))]
(load-all))")
(test-flan-mode--check
"and two clauses in one vector line up with each other"
"(handler-bind [(AssetMissing [c] (set seen (+ seen 10)))
(Corrupt [c] (set other (+ other 1)))]
(load-all))")
;; `restart-case' is 1 rather than 0: with 0 a protected form written on the
;; head's line would drag every clause out to align under it.
(test-flan-mode--check
"restart-case clauses indent by two under a protected form on the head's line"
"(defn fetch [n i32] i32
(restart-case (middle n)
(use-placeholder [] -1)
(retry [] 7)))")
(test-flan-mode--check
"and by two when the protected form is on its own line"
"(restart-case
(do (agent/poll)
(game-update))
(continue [] (do)))")
;; `declare-c' writes its parameters, return type and C symbol down the page in
;; one column.
(test-flan-mode--check
"declare-c puts everything after the name in one column"
"(declare-c is-mouse-button-pressed
[button MouseButton] bool
\"IsMouseButtonPressed\")")
;; An ordinary call, in the two cases every Lisp separates.
(test-flan-mode--check
"a call with an argument on the head's line aligns under that argument"
"(rl/draw-text \"hello\"
10 20 30)")
(test-flan-mode--check
"a call whose head is alone on its line indents its arguments by a body"
"(rl/draw-rectangle-lines-ex
(rl/Rectangle {.x 0.0 .y 0.0})
(f32 2.0) (rl/get-color 0x303030FF))")
;; A `do' is all body, so its second form aligns under its first.
(test-flan-mode--check
"a do aligns its forms with the first one"
"(do (agent/poll)
(game-update))")
;; Field access is an ordinary call whose head begins with a dot. `.' is a
;; symbol constituent, so nothing special is needed — but if it ever stopped
;; being one, `(.x v)' would start indenting as data and this is what would
;; say so.
(test-flan-mode--check
"field access indents as the call it is"
"(set total
(+ (.bytes c)
(.align c)))")
;; `#_' discards the form after it, so the reader sees pairs either way and the
;; alignment must not shift. See `lib/reader.ml'.
(test-flan-mode--check
"a discarded binding does not disturb the pairs around it"
"(let [#_a #_1 b 2
c 3]
(print c))")
;; A labelled loop, from `test/programs/loops.flan'. The label is a keyword
;; written where the binding vector would otherwise go, so it pushes everything
;; along by one — and until the indenter was told, the body of a labelled loop
;; was dragged out under its own binding vector.
(test-flan-mode--check
"a labelled dotimes indents its body by two, not under its vector"
"(dotimes :outer [a 3]
(dotimes [b 3]
(when (= b 2) (break :outer))
(print b) (println \"\")))")
;; `until' takes one on the same rule, and there the special argument is a test
;; rather than a vector — which is the case that shows the rule is about the
;; label and not about brackets.
(test-flan-mode--check
"and so does a labelled until, whose special argument is a test"
"(until :count (> n 10)
(set n (+ n 1))
(when (= n 3) (break :count)))")
;; The unlabelled form must not have moved, which is the other half of the
;; same claim.
(test-flan-mode--check
"an unlabelled dotimes is where it always was"
"(dotimes [b 3]
(print b)
(println \"\"))")
;; `dotimes' binding vectors now hold up to four elements — name, start, stop,
;; step — and the indenter must not have learned a count. The vector is one
;; sexp whatever is in it, so the body is two in from the head in every arity,
;; and a vector that wraps aligns under its own first element. From
;; `test/programs/dotimes-range.flan'.
(test-flan-mode--check
"a three-element dotimes vector indents its body by two"
"(dotimes [i 2 5]
(print i)
(println \"\"))")
(test-flan-mode--check
"and a four-element one, counting down, does the same"
"(dotimes [i 9 -1 -1]
(print i)
(println \"\"))")
(test-flan-mode--check
"and a binding vector that wraps aligns under its first element"
"(dotimes [i 0
(length items)
2]
(print i))")
;; The object and dispatch forms all begin `def', so they reach the `:defn'
;; fallback and indent their bodies by two. `defmethod' is the one with two
;; names before the parameter vector — the generic and the dispatch value —
;; which is exactly the case a count would have got wrong. From
;; `test/programs/dev-classes.flan'.
(test-flan-mode--check
"a defmethod indents its body by two, past generic and dispatch value"
"(defmethod area point [p]
(* (get p :x)
(get p :y)))")
(test-flan-mode--check
"a defclass puts its slot vector in the body column"
"(defclass point
[x y])")
(test-flan-mode--check
"and a defmulti indents its body by two"
"(defmulti describe [thing] dyn
(class-of thing))")
;; `fn' is `defn' with no name and no return type: the parameter vector, then
;; the body. From `test/programs/higher-order.flan', written down the page.
(test-flan-mode--check
"an fn indents its body under its parameters by two"
"(sort-by s (fn [a b]
(< a b)))")
;; A macro body indents by two like any `def…' form. It reaches that through
;; the `\\`def' fallback rather than through an entry, which is worth pinning:
;; the fallback is what covers every definer nobody listed.
(test-flan-mode--check
"a defmacro indents its body by two"
"(defmacro both [& args]
`(do ~@args))")
;; A macro's parameter list destructures now, so it holds names and a `&'
;; rest marker rather than one blob. It is still a vector, so a list that
;; wraps aligns name under name like every other vector here — and `&' is a
;; name character, so it does not split the marker off from what follows.
(test-flan-mode--check
"a wrapped destructuring parameter list aligns under its first parameter"
"(defmacro guard [test message
& body]
`(when ~test ~@body))")
;; A generic `defn' writes its constraint map between the return type and the
;; body — `test/programs/generic-map-reject.flan'. It is a brace, so it aligns
;; under its first element like every other brace, and the body after it is
;; still two in from `defn'.
(test-flan-mode--check
"a where clause sits in the body column and does not move the body"
"(defn is-seen [k $t] bool
{:where (is-hashable $t)}
(let [m (map-new t i32)]
(put m k 1)))")
;; A comma is whitespace, so a vector written with commas is the same vector
;; and lines up the same way. It reads as one now because the syntax table
;; says so; before, a comma was punctuation and sat in the middle of a name.
(test-flan-mode--check
"commas between bindings change nothing"
"(let [vel 1.0,
y 2.0]
(print y))")
;; A `with-' form is a body, qualified or not, whatever it takes on the head's
;; line — `clojure-indent-function''s fallback. From `examples/core-2d-camera.flan'.
(test-flan-mode--check
"a qualified with- form indents its body by two past an argument"
"(rl/with-mode-2d camera
(rl/draw-rectangle-rec player rl/red)
(rl/draw-grid 10 1.0))")
(test-flan-mode--check
"a qualified def form indents its body by two"
"(m/defthing name
(body))")
;; A qualified name finds the spec of its unqualified part.
(test-flan-mode--check
"a qualified when keeps when's spec"
"(rl/when (ready?)
(go))")
;; `default…' begins with `def' and is not a definer.
(test-flan-mode--check
"a default-prefixed call aligns its arguments"
"(default-color a
b)")
;; A cond result on its own line sits under its test, not deeper.
(test-flan-mode--check
"a cond result on its own line aligns with its test"
"(cond
(= k 1)
(one)
:else
(other))")
(test-flan-mode--check
"a match result on its own line aligns with its pattern"
"(match v
(Int n) n
(List items)
(length items))")
;; A trailing argument of an ordinary call aligns under the first argument,
;; even when it is long; only a `with-', `def' or specced head gives a body.
(test-flan-mode--check
"an ordinary call's arguments align under the first one"
"(push missing
`(when (ok) (go)))")
;;; Font lock
;; 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'."
(with-temp-buffer
(insert text)
(flan-mode)
(font-lock-ensure)
(goto-char (point-min))
(search-forward needle)
(get-text-property (- (point) (length needle)) 'face)))
(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/is-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")
("(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.
;; A dot in the middle of a name is not a label: `\_<' anchors the rule to the
;; start of a symbol, and `a.b' is one symbol starting at `a'.
(test-flan--check "a dot inside a name does not start a label"
(null (test-flan-mode--face-at "(f alpha.beta)" ".beta")))
;; The pass over the static table. Each row is a piece of the corpus and the
;; face the name in it should carry; several of these were drawn as nothing at
;; all until the table was brought back into line with `lib/parse.ml' and
;; `lib/check.ml'.
(dolist (case '(;; A definer that was missing: the head was not a keyword and
;; the name after it was not a function name.
("(defmacro both [& args] `(do ~@args))" "defmacro"
font-lock-keyword-face "defmacro's head")
("(defmacro both [& args] `(do ~@args))" "both"
font-lock-function-name-face "and the macro's name")
("(declare-c mouse-down? [b i32] bool \"X\")" "declare-c"
font-lock-keyword-face "declare-c's head")
;; The defining forms that arrived with the object system and
;; with the def/defonce/defconst trio.
("(def ticks i64 0)" "def" font-lock-keyword-face "def's head")
("(def ticks i64 0)" "ticks" font-lock-function-name-face
"and the name it introduces")
("(defonce seed i64 1)" "defonce" font-lock-keyword-face
"defonce")
;; A private defn: the head as a whole, not `defn' and a
;; stray `-', and the name after it.
("(defn- mix [a i32] i32 a)" "defn-" font-lock-keyword-face
"defn-'s head")
("(defn- mix [a i32] i32 a)" "mix"
font-lock-function-name-face "and the private function's name")
("(defclass point [x y])" "defclass" font-lock-keyword-face
"defclass")
("(defgeneric area [self] dyn)" "defgeneric"
font-lock-keyword-face "defgeneric")
("(defmethod area point [p] 1)" "defmethod"
font-lock-keyword-face "defmethod")
;; `handler-case' is implemented now and is a keyword like the
;; other three condition forms.
("(handler-case (go) [(E [c] 1)])" "handler-case"
font-lock-keyword-face "handler-case")
;; Special forms the old list had never heard of.
("(cond (= a 1) 2)" "cond" font-lock-keyword-face "cond")
;; until is a prelude macro, drawn as the control flow it is.
("(until (> i 3) (go))" "until" font-lock-keyword-face "until")
("(and a b)" "and" font-lock-keyword-face "and")
("(handler-bind [(E [c] 1)] (go))" "handler-bind"
font-lock-keyword-face "handler-bind")
("(restart-case (go) (retry [] 1))" "restart-case"
font-lock-keyword-face "restart-case")
("(signal ArithError {.op 1})" "signal"
font-lock-keyword-face "signal")
("(dotimes [b 3] (break :outer))" "break"
font-lock-keyword-face "break")
;; The two array constructors, which the parser reads itself
;; because their dimensions are in brackets — see
;; `test/programs/array-fill.flan'.
("(array-fill [rows cols] 255)" "array-fill"
font-lock-keyword-face "array-fill")
("(array-gen [n] f)" "array-gen"
font-lock-keyword-face "array-gen")
;; 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")
("(declare apply [(Fn [i64] i64)] i64)" "Fn"
font-lock-type-face "Fn")
("(declare call [(CFn [i64] i64)] i64)" "CFn"
font-lock-type-face "CFn")
("(defn is-seen [k $t] bool 1)" "$t" font-lock-type-face
"a type variable")
;; The package alias of a qualified name, `clojure-mode''s
;; rule for a namespace.
("(rl/draw-text \"hi\" 1 2 3)" "rl" font-lock-type-face
"a package alias")
("(defn f [x int] float 1.0)" "int" font-lock-type-face
"int, the builtin alias")
("(defn f [s [const u8]] 1)" "const" font-lock-type-face
"const, in a read-only slice type")
;; Constants that stand for themselves.
("(set done true)" "true" font-lock-constant-face "true")
("(= o None)" "None" font-lock-constant-face "None")
("(set x nil)" "nil" font-lock-constant-face "nil")
("(def buf (Vec u8) uninit)" "uninit" font-lock-constant-face
"uninit, which is only ever written here")))
(test-flan--check (format "%s is drawn" (nth 3 case))
(eq (test-flan-mode--face-at (nth 0 case) (nth 1 case))
(nth 2 case))))
;; `Unit' is a name the resolver answers to and the parser refuses — unit is
;; written `()'. Drawing it as a type would advertise a spelling that does not
;; compile, which is the same rule that keeps `await' out of the keyword list.
(test-flan--check
"Unit is not drawn as a type, because the parser refuses the word"
(null (test-flan-mode--face-at "(defn f [] Unit 1)" "Unit")))
;; `context/allocator' has a slash in it and is not a qualified name: there is
;; no package called `context'. The constants rule runs first for exactly this
;; reason, and this is what would notice if it stopped.
(test-flan--check
"context/allocator is one constant and not an alias and a name"
(eq (test-flan-mode--face-at "(free-all (context/allocator))" "context/")
'font-lock-constant-face))
;; The name after a package alias is left for the running program to speak for
;; — see the dynamic section below. With no program it is undrawn, and that is
;; the state a file opened on its own is in.
(test-flan--check
"the name after the alias is left alone"
(null (test-flan-mode--face-at "(rl/draw-text \"hi\" 1 2 3)" "draw-text")))
;;; Font lock from the running program
;; `flan.el' adds a second set of rules that draw what the program *defines* —
;; a macro as a macro, a function as a function — and everything below drives
;; them from a fixture `flan--defs' rather than from a daemon. The live half,
;; where an evaluation is accepted and the name lights up without anyone asking
;; for it, is in test-flan.el, which has a program to evaluate against.
(require 'flan)
(defconst test-flan-mode--defs
;; The shape `Dev.defs' answers with: (NAME KIND SIGNATURE LOC DOC).
'(("settle" "fn" "settle [i32 i32] ()" "sand.flan:3" "")
("ticks" "var" "ticks i64" "" "")
("gravity" "const" "gravity f32" "" "")
("with-retry" "macro" "with-retry [args] Form" "" "")
("Pixel" "struct" "Pixel" "" "")
("Shape" "data" "Shape [Empty (Dot [x f64 y f64])]" "" "")
("Shape.Empty" "case" "Shape.Empty" "" "")
("Shape.Dot" "case" "(Shape.Dot [x f64 y f64])" "" "")
("point" "class" "point [x y]" "sand.flan:20:1" "")
("Key" "enum" "Key" "" "")
("sim/step" "fn" "sim/step [] ()" "sand.flan:9" "")
("a/draw" "fn" "a/draw [] ()" "" "")
("b/draw" "fn" "b/draw [] ()" "" "")
;; The program is allowed to define a name the language already uses. It
;; does not get to repaint it.
("length" "fn" "length [i32] i32" "" "")
("if" "fn" "if [i32] i32" "" "")
;; And a kind this editor has never heard of, which the daemon is allowed
;; to add.
("somenewthing" "sigil" "somenewthing" "" ""))
"A `defs' reply to draw against.")
(defun test-flan-mode--dyn-face (text needle &optional defs)
"The face on NEEDLE in TEXT with DEFS as what the program defines."
(let ((flan--defs (if (eq defs 'none) nil (or defs test-flan-mode--defs))))
(flan--dynamic-rebuild)
(with-temp-buffer
(insert text)
(flan-mode)
(flan--dynamic-install)
(font-lock-ensure)
(goto-char (point-min))
(search-forward needle)
(get-text-property (- (point) (length needle)) 'face))))
(message "\n-- font lock from the program")
;; `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"
(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.
;; Giving macros a kind of their own would have quietly dropped them out of
;; `flan-disassemble', `flan-disassemble-ir' and `flan-lowering' unless every
;; one of those learned the second word.
(test-flan--check
"a macro is offered by the completion table for a compiled body"
(let ((flan--defs test-flan-mode--defs))
(and (member "with-retry" (flan--compiled-names))
(member "settle" (flan--compiled-names)))))
(test-flan--check
"and a global is not, because it has no body"
(let ((flan--defs test-flan-mode--defs))
(not (member "ticks" (flan--compiled-names)))))
;; A constructor is `Type.Case' and is one symbol. The daemon lists each
;; case under that name, so the whole symbol is drawn, and it is drawn when
;; the data type is: a case is part of its type, not a kind to ask for apart.
(test-flan--check
"a constructor is drawn whole, case half included"
(let ((flan-font-lock-dynamically '(data)))
(eq (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" ".Dot")
'flan-type-face)))
(test-flan--check
"and not when data types are not asked for"
(null (test-flan-mode--dyn-face "(match s (Shape.Dot) 1)" "Shape.Dot")))
(test-flan--check
"a case the program does not have draws only its type half"
(let ((flan-font-lock-dynamically t))
(and (eq (test-flan-mode--dyn-face "(match s (Shape.Nope) 1)" "Shape")
'flan-type-face)
(null (test-flan-mode--dyn-face "(match s (Shape.Nope) 1)" ".Nope")))))
(test-flan--check
"a class the program defines is drawn when classes are asked for"
(let ((flan-font-lock-dynamically '(class)))
(eq (test-flan-mode--dyn-face "(point 1 2)" "point") 'flan-type-face)))
(test-flan--check
"a name the program has never heard of is left alone"
(null (test-flan-mode--dyn-face "(never-defined 1)" "never-defined")))
(test-flan--check
"and so is a kind this editor does not know"
(null (test-flan-mode--dyn-face "(somenewthing)" "somenewthing")))
;; 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 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"
(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"
(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.
(test-flan--check
"with no session a program name is undrawn"
(null (test-flan-mode--dyn-face "(settle 1 2)" "settle" 'none)))
(test-flan--check
"and the language is drawn as it always was"
(eq (test-flan-mode--dyn-face "(let [a 1] a)" "let" 'none)
'font-lock-keyword-face))
;; Whole-buffer, because "nothing changes" is a claim about every character and
;; not about one name: a corpus file drawn with no session must come out
;; character for character and face for face as it did before any of this.
(defun test-flan-mode--faces (text &optional defs)
"Every (POS . FACE) in TEXT, with DEFS as what the program defines."
(let ((flan--defs (if (eq defs 'none) nil defs)))
(flan--dynamic-rebuild)
(with-temp-buffer
(insert text)
(flan-mode)
(flan--dynamic-install)
(font-lock-ensure)
(let (acc)
(dotimes (i (1- (point-max)))
(push (get-text-property (1+ i) 'face) acc))
(nreverse acc)))))
;; 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)
(let ((flan-font-lock-dynamically nil))
(test-flan-mode--faces text test-flan-mode--defs))))
;; ...and turning the setting off with a session up is the same picture,
;; which is what makes it an off switch rather than a reconnect.
(test-flan--check
"and turning the option off gives that same picture back"
(equal (test-flan-mode--faces text 'none)
(let ((flan-font-lock-dynamically nil))
(test-flan-mode--faces text test-flan-mode--defs))))
(test-flan--check
"while with one it is drawn differently"
(not (equal (test-flan-mode--faces text 'none)
(test-flan-mode--faces text test-flan-mode--defs)))))
;; Installing twice must not leave two copies of the rule behind: a refresh
;; happens after every accepted evaluation, and a rule added each time would
;; make redisplay slower for as long as the session lasted.
(test-flan--check
"installing repeatedly leaves one copy of the rule"
(let ((flan--defs test-flan-mode--defs))
(flan--dynamic-rebuild)
(with-temp-buffer
(flan-mode)
(font-lock-ensure)
(dotimes (_ 5) (flan--dynamic-install))
(= 1 (seq-count (lambda (k) (equal k (car flan--dynamic-keywords)))
font-lock-keywords)))))
;;; Electric pairs
;; `electric-pair-mode' reads the syntax table and nothing else, so the three
;; bracket pairs Flan writes work without a word being said about them — and
;; that is exactly why it wants a test: the table gained entries in this pass,
;; and a mistake in one of them would show up here first.
(message "\n-- electric pairs")
(defun test-flan-mode--pair (open)
"Type OPEN in a Flan buffer with `electric-pair-mode' on, and read the line."
(with-temp-buffer
(flan-mode)
(electric-pair-local-mode 1)
(let ((last-command-event open))
(call-interactively #'self-insert-command))
(buffer-string)))
(dolist (case '((?\( "()" "a paren")
(?\[ "[]" "a bracket")
(?{ "{}" "a brace")
(?\" "\"\"" "a string")))
(test-flan--check (format "%s closes itself" (nth 2 case))
(equal (test-flan-mode--pair (nth 0 case)) (nth 1 case))))
(provide 'test-flan-mode)
;;; test-flan-mode.el ends here