Indentation is ported from clojure-mode, which has the shapes Flan uses
The reported bug — the second and later bindings of a let one column too far —
was never one missing rule. `flan-indent-function' checked the head of the
enclosing form, and inside a binding vector the enclosing open is `[' and the
symbol after it is the first binding's name, so it fell through to Emacs's
`lisp-indent-function', which treats the vector as a call and aligns under the
first argument instead of the first binding.
Emacs Lisp is the wrong reference. It has no vectors-as-bindings, no maps and
no bracket variety, so every rule Flan needs has to be added by hand and the
binding vector is simply the first one hit. The indenter is rewritten from
clojure-mode's source instead: `clojure-mode' is neither an ancestor nor a
dependency — flan-mode still needs nothing beyond stock Emacs — it is the file
whose rules were read and written out again.
A bracket aligns under its first element, and that one rule fixes the binding
vector, `defn' parameter lists, `restart-case' and `handler-bind' clause
parameters and both spellings of a struct literal at once. `{:x 1}' and
`{.x 1}' indent identically because nothing here looks at the key, which is
what the colon-to-dot lane needs of it.
Where Flan diverges it is handled on purpose. `defn' is `:defn' rather than a
count because the return type between the parameters and the body is optional.
A clause — `(Name [params] body)' — is recognised by its shape, since its head
is a condition class or a restart name and can never be in a table; clojure-mode
reaches the same clauses by backtracking out to the enclosing form, which buys
generality this language has no other use for. Special arguments indent by one
body rather than Clojure's two, and a call whose head is alone on its line
indents its arguments by a body rather than aligning them under the head,
because that is how the whole corpus is written.
Checked by reindenting every .flan file in the tree: the only lines that move
are sand.flan's reported bug, raylib.flan's hand-wrapped parameter vectors —
which is the fix — and lone-`;' comment continuations, which stock
`lisp-indent-line' has always moved.
test-flan-mode.el is loaded from test-flan-cider.el rather than given a stanza
of its own, because emacs/*.el is already a dependency of that test.
Two failures in test-flan-cider.el that predate this: fixture frames lacked
`:fetched', so folding one open went looking for a daemon, and `layout' was
identified by being the last request when `flan-cnr-show' now makes three.
This commit is contained in:
parent
380924e553
commit
0d549a6b6e
@ -1,13 +1,51 @@
|
||||
;;; flan-mode.el --- Major mode for Flan -*- lexical-binding: t; -*-
|
||||
|
||||
;; Derived from lisp-mode, which is most of the work: Flan is s-expressions, so
|
||||
;; sexp motion, paren matching, `beginning-of-defun' and indentation all
|
||||
;; already do the right thing. What is left is what the language actually adds
|
||||
;; — its own literals and its own set of forms that indent like a body.
|
||||
;; Derived from `prog-mode', borrowing `lisp-mode''s machinery for the parts
|
||||
;; that are simply s-expressions: sexp motion, paren matching and
|
||||
;; `beginning-of-defun' already do the right thing.
|
||||
;;
|
||||
;; Indentation is the part that does not, and it is ported from
|
||||
;; `clojure-mode''s source rather than from `lisp-mode''s. That is a
|
||||
;; deliberate change of reference and it is worth being exact about what it
|
||||
;; means: `clojure-mode' is neither an ancestor nor a dependency here — this
|
||||
;; mode ships in the Flan repository and requires nothing outside stock Emacs —
|
||||
;; it is the file whose rules were read and written out again below.
|
||||
;;
|
||||
;; The reason is that Emacs Lisp has none of the shapes Flan is written in. It
|
||||
;; has no vectors-as-bindings, no maps, no bracket variety, so
|
||||
;; `lisp-indent-function' treats `[a 1 b 2]' as a function call and aligns
|
||||
;; continuation lines under `1' — the first *argument* — instead of under `a',
|
||||
;; the first *binding*. That was the reported bug in `sand.flan''s `settle',
|
||||
;; and it was never one missing rule: every rule has to be added by hand when
|
||||
;; the base language does not have the shape. Clojure's rules already cover
|
||||
;; brackets-mean-binding, pairs-align, maps and `#_'.
|
||||
;;
|
||||
;; Where Flan diverges from Clojure it diverges on purpose, and each divergence
|
||||
;; is written down at the place that handles it:
|
||||
;;
|
||||
;; - `defn' carries a **return type between the parameter vector and the
|
||||
;; body**, and it is optional — `(defn show [x f32] …)' has none. Both are
|
||||
;; handled by not caring: the spec is `:defn', so everything after the head
|
||||
;; indents two, which is right for the name, the parameters, a return type
|
||||
;; that is there and a body whether or not one preceded it.
|
||||
;; - field access is `(.x v)', an ordinary call whose head happens to begin
|
||||
;; with a dot. `.' is a symbol constituent in the syntax table below, so
|
||||
;; nothing special is needed for it to read as a head.
|
||||
;; - field labels are moving from `:x' to `.x', so `{.x 1.0}' is a struct
|
||||
;; literal and not a map with symbol keys. The indenter is correct for
|
||||
;; both spellings *because* it never looks at the key: a brace aligns under
|
||||
;; its first element whatever that element is spelled like.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'lisp-mode)
|
||||
;; `thing-at-point', which the indenter reads the enclosing form's head with.
|
||||
(require 'thingatpt)
|
||||
|
||||
;; Bound by `calculate-lisp-indent' around the call to `lisp-indent-function',
|
||||
;; and declared in `lisp-mode' without a `defvar', so say so here rather than
|
||||
;; let the byte-compiler call it a free variable.
|
||||
(defvar calculate-lisp-indent-last-sexp)
|
||||
;; For `imenu-generic-expression', which is set below and which would
|
||||
;; otherwise be made buffer-local before its own defvar had run.
|
||||
(require 'imenu)
|
||||
@ -33,6 +71,8 @@
|
||||
(autoload 'flan-dev "flan-dev" nil t)
|
||||
(autoload 'flan-dev-quit "flan-dev" nil t)
|
||||
(autoload 'flan-dev-restart-program "flan-dev" nil t)
|
||||
;; Bound below, like the rest, and it was the one missing an autoload.
|
||||
(autoload 'flan-disassemble "flan-dev" nil t)
|
||||
|
||||
(defgroup flan nil
|
||||
"Editing and evaluating Flan."
|
||||
@ -59,6 +99,11 @@
|
||||
;; A keyword resolves against an enum at the call site, so it reads as a
|
||||
;; constant rather than as a string.
|
||||
("\\_<:\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
|
||||
;; A field, in both of the spellings that exist while the corpus moves from
|
||||
;; `{:x 1}' to `{.x 1}': the label in a struct literal and the accessor
|
||||
;; `(.x v)' are the same name and are drawn the same way, which is also
|
||||
;; what the keyword rule above did for the spelling being replaced.
|
||||
("\\_<\\.\\(?:\\sw\\|\\s_\\)+" . font-lock-constant-face)
|
||||
;; The machine types, which are ordinary symbols but never anything else.
|
||||
("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|Unit\\|Never\\|Ptr\\|Option\\)\\_>"
|
||||
. font-lock-type-face)
|
||||
@ -160,25 +205,231 @@ line is off screen."
|
||||
(setq-local comment-add 1)
|
||||
(setq-local font-lock-defaults '(flan-font-lock-keywords))
|
||||
(setq-local indent-line-function #'lisp-indent-line)
|
||||
;; Spaces. The whole corpus is written with them, and alignment that is
|
||||
;; correct here is alignment under a specific *column* — a tab makes that
|
||||
;; depend on a setting the file cannot carry.
|
||||
(setq-local indent-tabs-mode nil)
|
||||
(setq-local lisp-indent-function #'flan-indent-function)
|
||||
(setq-local outline-regexp ";;;;+[ \t]*")
|
||||
(setq-local imenu-generic-expression flan-imenu-generic-expression)
|
||||
;; Buffer-locally, because this answers for Flan and nothing else.
|
||||
(add-hook 'which-func-functions #'flan-current-defun-name nil t))
|
||||
|
||||
;;; Indentation
|
||||
|
||||
;; Ported from `clojure-mode', as the header says. Three pieces, and the
|
||||
;; middle one is where the reported bug lived.
|
||||
|
||||
(defconst flan-indent-specs
|
||||
;; A number N: the first N arguments are *special* and the rest are a body.
|
||||
;; `:defn': everything after the head is a body. These are the same two
|
||||
;; values `clojure-mode' uses, and they mean the same thing here.
|
||||
'(;; Binding forms. The vector is the one special argument; the body
|
||||
;; follows it. This is the entry that makes `let' correct, but note that
|
||||
;; it is *not* what fixed the reported bug — alignment inside the vector is
|
||||
;; `flan--data-form-p' below, and it would be right even with no entry
|
||||
;; here.
|
||||
("let" . 1)
|
||||
("loop" . 1)
|
||||
("dotimes" . 1)
|
||||
;; The clause vector, then the protected body. Same shape as `let'.
|
||||
("handler-bind" . 1)
|
||||
("handler-case" . 1)
|
||||
;; Test first, body after.
|
||||
("if" . 1)
|
||||
("when" . 1)
|
||||
("unless" . 1)
|
||||
("while" . 1)
|
||||
("until" . 1)
|
||||
("match" . 1)
|
||||
("with-allocator" . 1)
|
||||
;; The protected form, then the clauses. It has to be 1 rather than 0:
|
||||
;; with 0 the clauses are ordinary arguments, and a protected form written
|
||||
;; on the head's line — `(restart-case (middle n)' — would drag every
|
||||
;; clause out to align under it.
|
||||
("restart-case" . 1)
|
||||
;; All body.
|
||||
("do" . 0)
|
||||
("cond" . 0)
|
||||
("defer" . 0)
|
||||
("try" . 0)
|
||||
;; `defn' is `:defn' rather than a count because the return type between
|
||||
;; the parameters and the body is optional; a count would have to know
|
||||
;; whether one is there, and `:defn' does not care.
|
||||
("defn" . :defn)
|
||||
;; `(declare-c NAME [params] RET "CSymbol")'. The name is the one special
|
||||
;; argument; everything after it is written down the page in one column.
|
||||
("declare" . 1)
|
||||
("declare-c" . 1))
|
||||
"How each form indents, by name.
|
||||
Anything not named here that begins with `def' is treated as `:defn' by
|
||||
`flan-indent-function'; anything else indents as a function call.")
|
||||
|
||||
(defun flan--indent-spec (name)
|
||||
"The indent spec for the form called NAME, or nil."
|
||||
(and name (cdr (assoc name flan-indent-specs))))
|
||||
|
||||
(defun flan--non-logical-sexp-p ()
|
||||
"Non-nil if what follows point is read but produces no form.
|
||||
Today that is only `#_', the discard reader macro — see `lib/reader.ml'. A
|
||||
discarded form is skipped rather than counted, so `(let [#_a b 1] …)' still
|
||||
aligns as the pairs it will be once the reader is done with it."
|
||||
(looking-at-p "#_"))
|
||||
|
||||
(defun flan--forward-sexp (&optional n)
|
||||
"Move forward over N sexps, skipping discarded ones."
|
||||
(setq n (or n 1))
|
||||
(let ((forward-sexp-function nil))
|
||||
(while (> n 0)
|
||||
(while (flan--non-logical-sexp-p) (forward-sexp 1))
|
||||
(forward-sexp 1)
|
||||
(setq n (1- n)))))
|
||||
|
||||
(defun flan--backward-sexp (&optional n)
|
||||
"Move backward over N sexps, skipping discarded ones."
|
||||
(setq n (or n 1))
|
||||
(let ((forward-sexp-function nil))
|
||||
(while (> n 0)
|
||||
(backward-sexp 1)
|
||||
(while (and (not (bobp))
|
||||
(ignore-errors
|
||||
(save-excursion (backward-sexp 1)
|
||||
(flan--non-logical-sexp-p))))
|
||||
(backward-sexp 1))
|
||||
(setq n (1- n)))))
|
||||
|
||||
(defun flan--data-form-p ()
|
||||
"Non-nil if the form at point is data rather than a call.
|
||||
Point is on the opening delimiter.
|
||||
|
||||
**This is the fix.** A `[' or a `{' is not a function call, so nothing in it
|
||||
is an argument and there is no first argument to align under. Aligning under
|
||||
the first *element* instead is what makes a binding vector line its names up
|
||||
name-under-name, and by the same rule it lines up `defn' parameter lists,
|
||||
`restart-case' clause parameters, `handler-bind' clause vectors, and both
|
||||
spellings of a struct literal — `{:x 1}' and `{.x 1}' — with no case for any
|
||||
of them, because the rule is about the bracket and never about what is
|
||||
written inside it.
|
||||
|
||||
A head that is not a symbol is the third case: `((f x) y)' has nothing to look
|
||||
a spec up under."
|
||||
(or (memq (char-after) '(?\[ ?\{))
|
||||
(not (looking-at ".\\(?:\\sw\\|\\s_\\)"))))
|
||||
|
||||
(defun flan--normal-indent (last-sexp)
|
||||
"Align with the argument above, as an ordinary call does.
|
||||
Point is just after the open paren of the enclosing form; LAST-SEXP is where
|
||||
the sexp before the one being indented starts."
|
||||
(goto-char last-sexp)
|
||||
(forward-sexp 1)
|
||||
(flan--backward-sexp 1)
|
||||
(let ((last-sexp-start nil))
|
||||
(if (ignore-errors
|
||||
;; Back up until we reach a sexp that starts its own line: that is
|
||||
;; the one every Lisp aligns under.
|
||||
(while (string-match "[^[:blank:]]"
|
||||
(buffer-substring (line-beginning-position)
|
||||
(point)))
|
||||
(setq last-sexp-start (prog1 (point) (forward-sexp -1))))
|
||||
t)
|
||||
(current-column)
|
||||
;; Nothing above but the head itself, so there are two cases and Flan
|
||||
;; answers them differently from Clojure's default.
|
||||
(if (and last-sexp-start (< last-sexp-start (line-end-position)))
|
||||
;; An argument shares the head's line. Align under it — this is the
|
||||
;; alignment every Lisp agrees on.
|
||||
(progn (goto-char last-sexp-start) (current-column))
|
||||
;; The head is alone on its line. Clojure's default would align the
|
||||
;; arguments under the *head*; the Flan corpus indents them by a body
|
||||
;; instead, which is `clojure-indent-style''s `align-arguments' and is
|
||||
;; what every hand-written call in the tree does:
|
||||
;;
|
||||
;; (rl/draw-rectangle-lines-ex
|
||||
;; (rl/Rectangle {.x 0.0 .y 0.0})
|
||||
;; (f32 2.0) (rl/get-color 0x303030FF))
|
||||
(+ (current-column) lisp-body-indent -1)))))
|
||||
|
||||
(defun flan--clause-form-p ()
|
||||
"Non-nil if the form at point is a clause: `(name [params] body…)'.
|
||||
Point is just after the open paren, on the head.
|
||||
|
||||
This is `defn' with the name left off, and it is how `handler-bind',
|
||||
`handler-case' and `restart-case' all write their clauses. Their heads are
|
||||
condition classes and restart names — things a program invents — so no table
|
||||
here could ever list them; the shape is what can be recognised."
|
||||
(save-excursion
|
||||
(ignore-errors
|
||||
(flan--forward-sexp 1) ; over the head
|
||||
(skip-chars-forward " \t\n\r,")
|
||||
(eq (char-after) ?\[))))
|
||||
|
||||
(defun flan--count-indent (method indent-point last-sexp head-column)
|
||||
"Indent inside a form whose first METHOD arguments are special.
|
||||
INDENT-POINT, LAST-SEXP and HEAD-COLUMN are as in `flan-indent-function';
|
||||
point is just after the open paren."
|
||||
(let ((pos -1))
|
||||
(condition-case nil
|
||||
(while (and (<= (point) indent-point) (not (eobp)))
|
||||
(flan--forward-sexp 1)
|
||||
(setq pos (1+ pos)))
|
||||
;; Past the last sexp in the form: count as if one more were here, which
|
||||
;; is what indenting an empty line at the end of a form means.
|
||||
(scan-error (setq pos (1+ pos))))
|
||||
(cond
|
||||
;; The first argument that is body rather than special.
|
||||
((= pos (1+ method)) (+ lisp-body-indent head-column))
|
||||
;; Further body arguments line up with the one above.
|
||||
((> pos (1+ method)) (flan--normal-indent last-sexp))
|
||||
;; Still in the special arguments. Clojure indents these to twice the
|
||||
;; body indent so they cannot be mistaken for body; Flan uses one, because
|
||||
;; the corpus is written that way throughout —
|
||||
;;
|
||||
;; (handler-bind
|
||||
;; [(StorageExhausted [c] …)]
|
||||
;; (load-all))
|
||||
;;
|
||||
;; — and there is nothing to confuse: the special arguments come first, so
|
||||
;; a reader never has to tell them apart by column.
|
||||
(t (+ lisp-body-indent head-column)))))
|
||||
|
||||
(defun flan-indent-function (indent-point state)
|
||||
"Indent like Lisp, with Flan's body forms as special forms.
|
||||
INDENT-POINT and STATE are as for `lisp-indent-function'."
|
||||
(let ((open (elt state 1)))
|
||||
(or (and open
|
||||
(save-excursion
|
||||
(goto-char (1+ open))
|
||||
(let ((head (and (looking-at "\\(\\sw\\|\\s_\\)+")
|
||||
(match-string 0))))
|
||||
(when (member head '("defn" "let" "if" "while" "until"
|
||||
"dotimes" "match" "do" "loop" "defer"))
|
||||
(+ (current-column) 1)))))
|
||||
(lisp-indent-function indent-point state))))
|
||||
"Indent a line inside a Flan form.
|
||||
INDENT-POINT and STATE are as for `lisp-indent-function'; the spec for the
|
||||
enclosing form comes from `flan-indent-specs'. Returns nil to leave the
|
||||
decision to `calculate-lisp-indent'."
|
||||
(goto-char (elt state 1))
|
||||
(if (flan--data-form-p)
|
||||
;; A vector, a map, or a head that is not a symbol: align under the
|
||||
;; first element.
|
||||
(1+ (current-column))
|
||||
(forward-char 1)
|
||||
(let* ((name (thing-at-point 'symbol))
|
||||
(method (flan--indent-spec name))
|
||||
(last-sexp calculate-lisp-indent-last-sexp)
|
||||
(head-column (1- (current-column))))
|
||||
(cond
|
||||
((integerp method)
|
||||
(flan--count-indent method indent-point last-sexp head-column))
|
||||
((eq method :defn) (+ lisp-body-indent head-column))
|
||||
;; No spec. Anything else spelled `def…' is a definition and indents
|
||||
;; like one, which covers `defstruct', `defunion', `defenum', `defvar',
|
||||
;; `defconst' and `defalias' without naming them.
|
||||
((and name (string-match-p "\\`def" name))
|
||||
(+ lisp-body-indent head-column))
|
||||
;; A clause: `(name [params] body…)'. `handler-bind', `handler-case'
|
||||
;; and `restart-case' all write their clauses this way, and the head is
|
||||
;; a condition class or a restart name — something the program invented,
|
||||
;; so it can never be in a table here. What can be recognised is the
|
||||
;; *shape*, which is `defn' with the name left off: a parameter vector
|
||||
;; where the first argument goes, and a body after it.
|
||||
;;
|
||||
;; `clojure-mode' reaches the same clauses by backtracking out to the
|
||||
;; enclosing form and reading a nested spec off it. That machinery buys
|
||||
;; generality this language has no other use for — these three forms are
|
||||
;; the whole of it — and the shape is unambiguous on its own.
|
||||
((flan--clause-form-p) (flan--count-indent 1 indent-point last-sexp
|
||||
head-column))
|
||||
(t (flan--normal-indent last-sexp))))))
|
||||
|
||||
;;;###autoload
|
||||
(add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode))
|
||||
|
||||
@ -379,16 +379,18 @@
|
||||
(test-flan--check "and the number past the last is abort"
|
||||
(equal sent '(:op "abort"))))))
|
||||
|
||||
;; The stack and its locals. Nothing produces this data today; the fixture is
|
||||
;; the shape a `backtrace' verb would have to answer with, and building the
|
||||
;; buffer against it is how it is ready when one exists.
|
||||
;; The stack and its locals. The fixture is the shape `backtrace' and `locals'
|
||||
;; answer with, and both frames carry `:fetched t' because their locals are
|
||||
;; already here: without it `flan-cnr-toggle-frame' goes and asks the daemon
|
||||
;; for them, and there is no daemon behind these tests on purpose.
|
||||
(let* ((state (list :condition "Missing"
|
||||
:restarts '("retry")
|
||||
:stack (list (list :fn "sim/settle" :loc "sand.flan:42:3"
|
||||
:fetched t
|
||||
:locals '(("i" "i32" "7")
|
||||
("b" "Blob" "(Blob {:id 7})")))
|
||||
(list :fn "sim/step" :loc "sand.flan:60:1"
|
||||
:locals nil))))
|
||||
:fetched t :locals nil))))
|
||||
(buf (test-flan--cnr state))
|
||||
(text (with-current-buffer buf (buffer-string))))
|
||||
(test-flan--check "frames are numbered, innermost first"
|
||||
@ -430,7 +432,8 @@
|
||||
(flan-inspect-buffer " *test-inspect*"))
|
||||
(with-current-buffer (test-flan--cnr
|
||||
(list :condition "Missing" :restarts '("retry")
|
||||
:stack (list (list :fn "f" :locals '(("b" "Blob" "…"))))))
|
||||
:stack (list (list :fn "f" :fetched t
|
||||
:locals '(("b" "Blob" "…"))))))
|
||||
(goto-char (point-min))
|
||||
(search-forward " 0: > f")
|
||||
(flan-cnr-toggle-frame)
|
||||
@ -494,6 +497,9 @@
|
||||
(buffer-string))))
|
||||
(test-flan--check "the condition's name is what `layout' is asked for"
|
||||
(equal (plist-get (car (last asked)) :op) "break"))
|
||||
;; By op rather than by position: `flan-cnr-show' asks three things now —
|
||||
;; `break', `layout', `backtrace' — and which of them is last is not what
|
||||
;; this is testing.
|
||||
(test-flan--check "and it is sent back verbatim, qualified as it came"
|
||||
(equal (plist-get
|
||||
(seq-find (lambda (f)
|
||||
@ -539,6 +545,15 @@
|
||||
(or (test-flan--caught #'flan-cnr-show) ""))))
|
||||
|
||||
|
||||
;; `flan-mode' itself — indentation, which is a function from text to text and
|
||||
;; so belongs with the other fixture-driven checks rather than with anything
|
||||
;; that needs a daemon. Loaded rather than run separately because
|
||||
;; `emacs/*.el' is already a dependency of test/dune's stanza, so a file here
|
||||
;; needs no build change to be run.
|
||||
(load (expand-file-name "test-flan-mode.el"
|
||||
(file-name-directory load-file-name))
|
||||
nil t)
|
||||
|
||||
(message "\n%d checks, %d failures" test-flan--ran test-flan--failures)
|
||||
(kill-emacs (if (> test-flan--failures 0) 1 0))
|
||||
|
||||
|
||||
208
emacs/test-flan-mode.el
Normal file
208
emacs/test-flan-mode.el
Normal file
@ -0,0 +1,208 @@
|
||||
;;; 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, keyword spelling"
|
||||
"(rl/Rectangle {:x 0.0 :y 0.0
|
||||
:width (f32 screen-width)
|
||||
:height (f32 screen-height)})")
|
||||
|
||||
(test-flan-mode--check
|
||||
"and identically in the dot spelling it is moving to"
|
||||
"(rl/Rectangle {.x 0.0 .y 0.0
|
||||
.width (f32 screen-width)
|
||||
.height (f32 screen-height)})")
|
||||
|
||||
;; `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 string] i64
|
||||
(print label)
|
||||
n)")
|
||||
|
||||
(test-flan-mode--check
|
||||
"and a defn without one indents it the same"
|
||||
"(defn show-trim [s string]
|
||||
(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 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))")
|
||||
|
||||
(provide 'test-flan-mode)
|
||||
;;; test-flan-mode.el ends here
|
||||
Loading…
x
Reference in New Issue
Block a user