flan/emacs/test-flan-fln.el

1547 lines
78 KiB
EmacsLisp

;;; test-flan-fln.el --- The .fln mode, from written-out text -*- lexical-binding: t; -*-
;; Loaded by test-flan-cider.el, which runs under `dune test', for the reason
;; test-flan-mode.el is: `emacs/*.el' is already that stanza's dependency.
;; Nothing here needs a daemon; what the daemon makes of what these commands
;; send is test-flan-fln-live.el's, run from test-flan.el.
;;
;; Every snippet is text; each check says where point is with a `|' written
;; into it, which is removed before the check runs.
;;; Code:
(require 'flan-fln-mode)
(require 'flan)
(declare-function test-flan--check "test-flan-cider" (name ok))
(defmacro test-flan-fln--in (text &rest body)
"Run BODY in a .fln buffer holding TEXT, point where TEXT has its `|'."
(declare (indent 1))
`(with-temp-buffer
(insert ,text)
(flan-fln-mode)
(goto-char (point-min))
(when (search-forward "|" nil t)
(delete-char -1))
,@body))
(defun test-flan-fln--text (b)
(and b (cdr b) (buffer-substring-no-properties (car b) (cdr b))))
(defun test-flan-fln--is (name got want)
(test-flan--check name (equal got want))
(unless (equal got want)
(message " want %S\n got %S" want got)))
(defun test-flan-fln--thing (thing)
(test-flan-fln--text (bounds-of-thing-at-point thing)))
(message "\nthe .fln mode")
;;; One parent
(test-flan--check "flan-mode is a flan-base-mode"
(provided-mode-derived-p 'flan-mode 'flan-base-mode))
(test-flan--check "flan-fln-mode is a flan-base-mode"
(provided-mode-derived-p 'flan-fln-mode 'flan-base-mode))
(test-flan--check ".fln opens in flan-fln-mode"
(eq (cdr (assoc "\\.fln\\'" auto-mode-alist)) 'flan-fln-mode))
(test-flan-fln--in "fn f() -> i32 = 1\n"
(test-flan--check "a .fln buffer sends the indented syntax"
(equal (flan--syntax) "indented"))
(test-flan--check "and gets the client's completion, as a .flan one does"
(memq #'flan-completion-at-point completion-at-point-functions))
(test-flan--check "and the modeline indicator"
(member '(:eval (flan-mode-line)) mode-line-misc-info))
(test-flan--check "the shared keys reach it through the parent's map"
(eq (key-binding (kbd "C-c C-b")) 'flan-cnr-show))
(test-flan--check "and its own keys pick .fln forms"
(and (eq (key-binding (kbd "C-c C-c")) 'flan-fln-eval-defun)
(eq (key-binding (kbd "C-x C-e")) 'flan-fln-eval-last))))
(require 'flan-watch)
(let ((b (generate-new-buffer "ghost.fln")))
(with-current-buffer b (flan-fln-mode))
(switch-to-buffer b)
(test-flan--check "watch paints ghost text in a shown .fln buffer"
(memq b (flan-watch--ghost-buffers)))
(with-current-buffer b
(insert "fn f() -> ()\n watch-i64(\"x\", 1)\n")
(test-flan--check "and finds a watch call written as a .fln call"
(equal (mapcar #'car (flan-watch--ghost-sites)) '("x"))))
(kill-buffer b))
(require 'flan-dape)
(test-flan--check "dape offers its config in any Flan buffer"
(equal (plist-get flan-dape-config 'modes) '(flan-base-mode)))
(let ((buffer-file-name "/tmp/x.fln"))
(test-flan--check "and debugs the .fln file it was started from"
(equal (flan-dape--source) "/tmp/x.fln")))
;;; The objects
(defconst test-flan-fln--settle
"fn settle(row: i32, col: i32) -> ()
let vel = f32(gravity) + velocity[row, col]
while y > row
if 0 == grid[y, col]
grid[y, col] = grid[row, col]
; a comment inside the body
return
let is-left = col > 0
if is-left or is-right
let side =
if not is-left
1
elif not is-right
-1
else
if f32(rand()) < 0.5 then 1 else -1
grid[y, col + side] = grid[row, col]
y = y - 1
velocity[row, col] = 0.0
; trailing comment, not part of the function
fn step() -> ()
paint-at(i32(m.y) / cell-size,
i32(m.x) / cell-size)
if r >= 0 and r < rows - 1
and c >= 0
grid[r, c] = 1
step()
")
(defun test-flan-fln--at (text needle)
"TEXT with a `|' before the first NEEDLE."
(let ((i (string-search needle text)))
(concat (substring text 0 i) "|" (substring text i))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "if 0 == grid")
(test-flan-fln--is "a statement takes its body, through blank and comment lines"
(test-flan-fln--thing 'flan-fln-statement)
"if 0 == grid[y, col]
grid[y, col] = grid[row, col]
; a comment inside the body
return")
(test-flan-fln--is "its body is the lines under its first"
(test-flan-fln--thing 'flan-fln-body)
"grid[y, col] = grid[row, col]
; a comment inside the body
return"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "elif not is-right")
(test-flan-fln--is "on a clause, the statement is its header's, clauses and all"
(test-flan-fln--thing 'flan-fln-statement)
"if not is-left
1
elif not is-right
-1
else
if f32(rand()) < 0.5 then 1 else -1")
(test-flan-fln--is "and the clause is its own line and block"
(test-flan-fln--thing 'flan-fln-clause)
"elif not is-right
-1"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "1\n elif")
(test-flan-fln--is "a clause is not found from the header's own block"
(test-flan-fln--thing 'flan-fln-clause) nil))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "if f32(rand())")
(test-flan-fln--is "from inside else's block, the clause is the else"
(test-flan-fln--thing 'flan-fln-clause)
"else
if f32(rand()) < 0.5 then 1 else -1"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "let side =")
(test-flan-fln--is "let x = with the value as a block"
(test-flan-fln--thing 'flan-fln-statement)
"let side =
if not is-left
1
elif not is-right
-1
else
if f32(rand()) < 0.5 then 1 else -1"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "i32(m.x)")
(test-flan-fln--is "a line inside a bracket is part of its statement"
(test-flan-fln--thing 'flan-fln-statement)
"paint-at(i32(m.y) / cell-size,
i32(m.x) / cell-size)")
(test-flan-fln--is "a term is glued, brackets and all"
(test-flan-fln--thing 'flan-fln-term) "i32(m.x)")
(test-flan-fln--is "a group is a bracket pair"
(test-flan-fln--thing 'flan-fln-group)
"(i32(m.y) / cell-size,
i32(m.x) / cell-size)"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "and c >= 0")
(test-flan-fln--is "an operator continuation line is part of its statement"
(test-flan-fln--thing 'flan-fln-statement)
"if r >= 0 and r < rows - 1
and c >= 0
grid[r, c] = 1")
(test-flan-fln--is "and not the start of the body"
(test-flan-fln--thing 'flan-fln-body) "grid[r, c] = 1"))
;; The bit operators continue a line as the other spaced operators do.
;; Not through `test-flan-fln--in', whose `|' marks point and would eat one
;; half of `||'.
(dolist (op '("&&" "||" "^^"))
(with-temp-buffer
(insert "x = a " op "\n b\ny = a\n " op " b\n")
(flan-fln-mode)
(goto-char (point-min))
(forward-line 1)
(test-flan--check (concat "a line after a trailing " op " continues it")
(flan-fln--continuation-p (point)))
(forward-line 2)
(test-flan--check (concat "a line starting with " op " continues")
(flan-fln--continuation-p (point)))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "velocity[row, col] = 0.0")
(test-flan-fln--is "a top-level form ends before trailing comment lines"
(test-flan-fln--thing 'flan-fln-toplevel)
(substring test-flan-fln--settle 0
(+ (string-search "= 0.0" test-flan-fln--settle) 5))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "trailing comment")
(test-flan--check "a comment between forms belongs to the form above"
(string-prefix-p "fn settle"
(test-flan-fln--thing 'flan-fln-toplevel))))
(test-flan-fln--in "; a header comment\n|\nfn f() -> i32 = 1\n"
(test-flan-fln--is "before any form, the next one"
(test-flan-fln--thing 'flan-fln-toplevel) "fn f() -> i32 = 1"))
(test-flan-fln--in "let xs = [1 2\n3 4]\n + 1\nfn|x() -> i32 = 1\n"
(test-flan-fln--is "column 0 inside a bracket or after a leading operator is no form start"
(save-excursion (beginning-of-defun)
(buffer-substring-no-properties (point) (line-end-position)))
"fnx() -> i32 = 1"))
(test-flan-fln--in "x = 1\nhandler-case\n f()\non E(c)\n nil\n|restart y\n"
(test-flan--check "on and restart at column 0 are clauses, not forms"
(progn (beginning-of-defun)
(looking-at "handler-case"))))
(test-flan-fln--in "let on = 3\nfoo(x):|\n bar()\n"
(test-flan-fln--is "a term ends before the trailing colon of a call's block"
(test-flan-fln--text (flan-fln--term-before (point))) "foo(x)"))
(test-flan-fln--in "f(\\(, \\) , x.y)|\n"
(test-flan-fln--is "a character literal is not a bracket"
(test-flan-fln--text (flan-fln--term-before (point)))
"f(\\(, \\) , x.y)"))
;;; Top-level motion
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle " y = y - 1")
(beginning-of-defun)
(test-flan--check "C-M-a goes to the form's first line" (looking-at "fn settle"))
(end-of-defun)
(test-flan--check "C-M-e goes past its last code line, not its trailing comment"
(save-excursion (forward-line -1)
(looking-at " velocity\\[row, col\\] = 0.0")))
(end-of-defun)
(test-flan--check "and the next C-M-e ends the next form"
(= (point) (point-max)))
(goto-char (point-max))
(beginning-of-defun)
(test-flan--check "C-M-a from the end reaches the last form" (looking-at "fn step"))
(mark-defun)
(test-flan--check "C-M-h marks the form"
(let ((m (buffer-substring (region-beginning) (region-end))))
(and (string-prefix-p "fn step" (string-trim-left m "\n"))
(string-suffix-p " step()\n" m)))))
;;; Statement motion
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle " let is-left")
(flan-fln-backward-statement)
(test-flan--check "M-a at a statement's start goes to the one before at its level"
(looking-at "if 0 == grid"))
(flan-fln-backward-statement)
(test-flan--check "and out to the owner when there is none" (looking-at "while y"))
(flan-fln-forward-statement)
(test-flan--check "M-e goes to the end of the statement, body and all"
(looking-back "y = y - 1" (line-beginning-position)))
(flan-fln-forward-statement)
(test-flan--check "and again, to the end of the next"
(looking-back "= 0.0" (line-beginning-position))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle " -1")
(flan-fln-up)
(test-flan--check "C-M-u goes to the line that owns the block"
(looking-at "elif not is-right"))
(flan-fln-up)
(test-flan--check "and from there to its owner's" (looking-at "let side")))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "m.x)")
(flan-fln-up)
(test-flan--check "C-M-u inside a bracket goes to the bracket" (looking-at "(m.x)")))
;;; What each key sends
;; The request, captured where it leaves: the daemon's answer is the live
;; test's business, and here what matters is which text went out and where
;; it said the text starts.
(defvar test-flan-fln--sent nil)
(defmacro test-flan-fln--sending (&rest body)
`(progn
(setq test-flan-fln--sent nil)
(cl-letf (((symbol-function 'flan--request)
(lambda (form) (push form test-flan-fln--sent)
(list :status "ok" :value "0")))
((symbol-function 'pulse-momentary-highlight-region) #'ignore))
,@body)
(car test-flan-fln--sent)))
(defun test-flan-fln--sent-code (req)
(plist-get req :code))
(defconst test-flan-fln--prog
"fn fib(n: i64) -> i64
if n < 2
n
else
fib(n - 1) + fib(n - 2)
comment():
twice(4)
let x = 3
if x > 2
twice(x)
else
0
fn twice(n: i64) -> i64 = n * 2
")
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "fib(n - 1)")
(test-flan--check "C-c C-s is the .fln stepper"
(eq (key-binding (kbd "C-c C-s")) 'flan-fln-step-defun))
(let ((r (test-flan-fln--sending (flan-fln-step-defun))))
(test-flan--check "which installs the fn at point to step through"
(and (equal (plist-get r :op) "eval")
(eq (plist-get r :step) t)
(string-prefix-p "fn fib" (test-flan-fln--sent-code r))
(string-suffix-p "fib(n - 2)" (test-flan-fln--sent-code r))))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)")
(test-flan--check "and refuses what is not a declaration"
(condition-case nil
(progn (test-flan-fln--sending (flan-fln-step-defun)) nil)
(user-error t))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "fib(n - 1)")
(let ((r (test-flan-fln--sending (flan-fln-eval-defun))))
(test-flan--check "C-c C-c inside a fn installs the whole fn"
(and (equal (plist-get r :op) "eval")
(string-suffix-p "fib(n - 1) + fib(n - 2)"
(test-flan-fln--sent-code r))
(string-prefix-p "fn fib" (test-flan-fln--sent-code r))
(equal (plist-get r :syntax) "indented")))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)")
(let ((r (test-flan-fln--sending (flan-fln-eval-defun))))
(test-flan--check "C-c C-c on a column-0 call evaluates it"
(equal (plist-get r :op) "eval-expr"))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "\n let x")
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan--check "C-x C-e at a line's end sends the statement ending there"
(and (equal (plist-get r :op) "eval-expr")
(equal (test-flan-fln--sent-code r) "twice(4)")
(equal (plist-get r :line) 8)
(equal (plist-get r :col) 3)))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "\n else\n 0")
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan--check "the innermost one: the last line of a block, not the if"
(equal (test-flan-fln--sent-code r) "twice(x)"))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "(4)")
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan--check "C-x C-e inside a line sends the term before point"
(and (equal (test-flan-fln--sent-code r) "twice")
(equal (plist-get r :col) 3)))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "\n\ncomment")
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan--check "at the end of a fn's last line, the innermost statement, not the fn"
(equal (test-flan-fln--sent-code r) "fib(n - 1) + fib(n - 2)"))))
(test-flan-fln--in test-flan-fln--prog
(goto-char (point-max))
(skip-chars-backward "\n")
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan--check "a column-0 one-line fn at its end is installed"
(and (equal (plist-get r :op) "eval")
(string-suffix-p "fn twice(n: i64) -> i64 = n * 2"
(test-flan-fln--sent-code r))))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "else\n 0")
(let ((r (test-flan-fln--sending (flan-fln-eval-statement))))
(test-flan--check "C-c C-e on a clause sends its whole statement"
(and (equal (test-flan-fln--sent-code r)
"if x > 2\n twice(x)\n else\n 0")
(equal (plist-get r :line) 10)
(equal (plist-get r :col) 3)))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "let x = 3")
(let ((r (test-flan-fln--sending (flan-fln-eval-statement))))
(test-flan--check "C-c C-e on a bare let sends it with the rest of its block"
(equal (test-flan-fln--sent-code r)
"let x = 3\n if x > 2\n twice(x)\n else\n 0"))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)")
(let ((r (test-flan-fln--sending (flan-fln-eval-statement-and-next))))
(test-flan--check "C-c C-n sends the statement"
(equal (test-flan-fln--sent-code r) "twice(4)"))
(test-flan--check "and moves to the next" (looking-at "let x = 3"))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "twice(4)")
(transient-mark-mode 1)
(set-mark (point))
(search-forward "twice(x)")
(forward-char -3)
(let ((r (test-flan-fln--sending (flan-fln-eval-statement))))
(test-flan--check "C-c C-e sends the region's whole lines"
(equal (test-flan-fln--sent-code r)
"twice(4)\n let x = 3\n if x > 2\n twice(x)"))))
;; The pause target. The position sent is where the reader starts the form,
;; which for a call is its name and not its parenthesis; the live test checks
;; the daemon finds a form there.
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "n - 1)")
(let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4)))))
(test-flan-fln--is "C-u C-c C-c in a call marks the call, from its name"
(plist-get r :pause) '(5 5))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "< 2")
(let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4)))))
(test-flan-fln--is "outside a bracket, the statement on point's line"
(plist-get r :pause) '(2 3))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--prog "n < 2")
(let ((r (test-flan-fln--sending (flan-fln-eval-defun '(16)))))
(test-flan-fln--is "C-u C-u, the fn: stop on entry"
(plist-get r :pause) '(1 1))))
(test-flan-fln--in "fn f(x: i64) -> i64\n g((x| + 1), [x 2])\n"
(let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4)))))
(test-flan-fln--is "a free parenthesis marks the value inside it"
(plist-get r :pause) '(2 6))))
;;; A let's bindings on the lines under it
(defconst test-flan-fln--group
"fn f(p) -> i64
let a = 1
b = a + 1
{x .x} = p
g(a, b)
a + x
")
(test-flan-fln--in (test-flan-fln--at test-flan-fln--group "b = a")
(test-flan-fln--is "a binding line is part of its let's statement"
(test-flan-fln--thing 'flan-fln-statement)
"let a = 1\n b = a + 1\n {x .x} = p"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--group "{x .x}")
(let ((r (test-flan-fln--sending (flan-fln-eval-statement))))
(test-flan-fln--is "C-c C-e on a binding line sends the group and its scope"
(test-flan-fln--sent-code r)
"let a = 1\n b = a + 1\n {x .x} = p\n g(a, b)\n a + x")))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--group "a + 1")
(let ((r (test-flan-fln--sending (flan-fln-eval-defun '(4)))))
(test-flan-fln--is "C-u C-c C-c on a binding line marks its value"
(plist-get r :pause) '(3 11))))
(test-flan-fln--in (test-flan-fln--at "fn f()\n let a = 1\n (not) = 2\n g()\n" "(not)")
(test-flan-fln--is "a name in parentheses is a binding line"
(test-flan-fln--thing 'flan-fln-statement) "let a = 1\n (not) = 2"))
(test-flan-fln--in (test-flan-fln--at "let a: i32\n b: i64\n" "b:")
(test-flan-fln--is "a typed global with no value is one under a top-level let"
(test-flan-fln--thing 'flan-fln-statement) "let a: i32\n b: i64"))
(test-flan-fln--in (test-flan-fln--at "fn f()\n let f = fn(x) =>\n y = x\n y\n f\n" "y = x")
(test-flan-fln--is "a line of a lambda value's block is no binding"
(test-flan-fln--thing 'flan-fln-statement) "y = x"))
;;; Match arms, header lines, clauses, comments
(defconst test-flan-fln--arms
"fn pick(n: i64) -> i64
let r = match n
0 -> 10
1 ->
twice(1)
twice(2)
k -> k + 1
if n < 0
-1
elif n == 0
or n == 1
0
else
r
handler-case
f()
on Error(e)
nil
; a comment, twice(9)
r
comment:
twice(4)
")
(defun test-flan-fln--last-at (needle &optional fn)
"What FN, C-x C-e by default, sends with point at the end of NEEDLE's line."
(test-flan-fln--in (test-flan-fln--at test-flan-fln--arms needle)
(end-of-line)
(let ((r (test-flan-fln--sending (funcall (or fn #'flan-fln-eval-last)))))
(and r (list (plist-get r :op) (test-flan-fln--sent-code r))))))
(test-flan-fln--is "C-x C-e at the end of a match arm sends its value"
(test-flan-fln--last-at "0 -> 10") '("eval-expr" "10"))
(test-flan-fln--is "at the end of an arm with a block, the block"
(test-flan-fln--last-at "1 ->")
'("eval-expr" "twice(1)\n twice(2)"))
(test-flan-fln--is "an arm whose pattern binds a name sends the whole match"
(cadr (test-flan-fln--last-at "k -> k"))
(substring test-flan-fln--arms (string-search "let r" test-flan-fln--arms)
(+ (string-search "k + 1" test-flan-fln--arms) 5)))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--arms "twice(2)")
(test-flan-fln--is "C-c C-e on an arm's block sends the block, not the arm"
(test-flan-fln--sent-code
(test-flan-fln--sending
(search-backward "1 ->") (flan-fln-eval-statement)))
"twice(1)\n twice(2)"))
(test-flan-fln--is "C-x C-e at the end of an if line sends its condition"
(test-flan-fln--last-at "if n < 0") '("eval-expr" "n < 0"))
(test-flan-fln--is "and of an elif, all of its condition"
(test-flan-fln--last-at "or n == 1")
'("eval-expr" "n == 0\n or n == 1"))
(test-flan-fln--is "and the same from the end of the condition's first line"
(test-flan-fln--last-at "elif n == 0")
'("eval-expr" "n == 0\n or n == 1"))
(defconst test-flan-fln--wrapped
"fn f(o: Option(i64)) -> i64
match o
Some(_) -> 5
Some(n) -> n + 1
Some(m) -> 7
None -> 1 +
2
if 1 < 2 and
3 < 4
x = 1 +
2
")
(defun test-flan-fln--wrapped-at (needle fn)
(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped needle)
(end-of-line)
(let ((r (test-flan-fln--sending (funcall fn))))
(and r (test-flan-fln--sent-code r)))))
(test-flan-fln--is "an arm's value wrapped onto a second line goes whole"
(test-flan-fln--wrapped-at "None" #'flan-fln-eval-last)
"1 +\n 2")
(test-flan-fln--is "from its second line too"
(test-flan-fln--wrapped-at " 2\n if" #'flan-fln-eval-last)
"1 +\n 2")
(test-flan-fln--is "and C-c C-e on it sends the same"
(test-flan-fln--wrapped-at "None" #'flan-fln-eval-statement)
"1 +\n 2")
(test-flan-fln--is "a wrapped if condition, from the end of its first line"
(test-flan-fln--wrapped-at "if 1 < 2" #'flan-fln-eval-last)
"1 < 2 and\n 3 < 4")
(test-flan-fln--is "a statement wrapped by an operator, from the end of its first line"
(test-flan-fln--wrapped-at "x = 1 +" #'flan-fln-eval-last)
"x = 1 +\n 2")
(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "None")
(end-of-line)
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan-fln--is "a value cut mid-line says where its statement starts"
(list (plist-get r :line) (plist-get r :col) (plist-get r :indent))
'(6 13 5))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "x = 1")
(end-of-line)
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan--check "a whole statement does not" (null (plist-member r :indent)))))
(test-flan-fln--is "an arm whose pattern binds nothing sends its value"
(test-flan-fln--wrapped-at "Some(_)" #'flan-fln-eval-last) "5")
(test-flan-fln--is "nor one whose value does not use what it binds"
(test-flan-fln--wrapped-at "Some(m)" #'flan-fln-eval-last) "7")
(dolist (c '(("Some(_v) -> _v + 1" "a name starting with _")
("Some(éé) -> éé" "a name that is not ASCII")
("Some(N) -> N" "a capitalised name")
("Some(p) -> p.x" "a name used as a field's base")
("Some(n) -> -n" "a name its value negates")
("Some(p) -> -p.x" "a name whose field its value negates")))
(test-flan-fln--in (concat "fn f(o: Option(i64)) -> i64\n match o\n " (car c) "\n")
(goto-char (point-max))
(skip-chars-backward "\n")
(test-flan--check (format "an arm binding %s its value uses sends the match" (cadr c))
(string-prefix-p "match o"
(test-flan-fln--sent-code
(test-flan-fln--sending (flan-fln-eval-last)))))))
(test-flan--check "one whose value uses its binding sends the match"
(string-prefix-p "match o"
(test-flan-fln--wrapped-at "Some(n)" #'flan-fln-eval-last)))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "5")
(test-flan-fln--is "a match arm is a clause"
(test-flan-fln--thing 'flan-fln-clause) "Some(_) -> 5"))
(test-flan-fln--is "at the end of an else line, the whole if"
(cadr (test-flan-fln--last-at "else\n r"))
"if n < 0\n -1\n elif n == 0\n or n == 1\n 0\n else\n r")
(test-flan-fln--is "at the end of an on line, the whole handler-case"
(cadr (test-flan-fln--last-at "on Error"))
"handler-case\n f()\n on Error(e)\n nil")
(test-flan-fln--is "at the end of a fn header, the fn, installed"
(car (test-flan-fln--last-at "fn pick")) "eval")
(test-flan-fln--is "at the end of comment:, the whole block"
(test-flan-fln--last-at "comment:")
'("eval-expr" "comment:\n twice(4)"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--arms "; a comment")
(end-of-line)
(let ((r (test-flan-fln--sending
(condition-case nil (flan-fln-eval-last) (user-error nil)))))
(test-flan--check "C-x C-e on a comment line sends nothing from the comment"
(null r))))
(defun test-flan-fln--pause-at (needle)
(test-flan-fln--in (test-flan-fln--at test-flan-fln--arms needle)
(plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause)))
(test-flan-fln--is "C-u C-c C-c on an arm's pattern stops at its value"
(test-flan-fln--pause-at "0 -> 10") '(3 10))
(test-flan-fln--is "on an arm with a block, at the block"
(test-flan-fln--pause-at "1 ->") '(5 7))
(test-flan-fln--is "on an elif line, at its condition"
(test-flan-fln--pause-at "elif") '(10 8))
(test-flan-fln--is "and from its condition's continuation line too"
(test-flan-fln--pause-at "or n == 1") '(10 8))
(test-flan-fln--is "on an else line, at its block"
(test-flan-fln--pause-at "else\n r") '(14 5))
(test-flan-fln--is "on an on line, at its block"
(test-flan-fln--pause-at "on Error") '(18 5))
;;; One-line ifs with clauses under them, typed lambdas, restarts
(defconst test-flan-fln--oneline
"fn sign(n: i64)
if n < 0 then -1
elif n == 0 then 0
else 1
fn size(n: i64) -> i64
if n < 10
1
else 2
fn k(n: i64) -> i64
let add = fn(a: i64, b) -> i64 => a + b
let dbl = fn(a: Fn(i64) -> i64, b: i64) -> i64 =>
a(b) * 2
let r = match n
0 -> 1
_ -> 2
add(r, 1)
")
(defun test-flan-fln--oneline-at (needle fn &optional at-end)
"What FN sends with point at NEEDLE in the one-line program, or at the end
of its line with AT-END."
(test-flan-fln--in (test-flan-fln--at test-flan-fln--oneline needle)
(when at-end (end-of-line))
(let ((r (test-flan-fln--sending (funcall fn))))
(and r (list (test-flan-fln--sent-code r) (plist-get r :pause))))))
(test-flan-fln--is "an else under a one-line if belongs to its statement"
(car (test-flan-fln--oneline-at "else 1" #'flan-fln-eval-statement))
"if n < 0 then -1\n elif n == 0 then 0\n else 1")
(test-flan-fln--in (test-flan-fln--at test-flan-fln--oneline "elif n == 0")
(test-flan-fln--is "and the clause is its line"
(test-flan-fln--thing 'flan-fln-clause) "elif n == 0 then 0"))
(test-flan-fln--is "C-x C-e at the end of a one-line if that goes on sends the whole if"
(car (test-flan-fln--oneline-at "if n < 0" #'flan-fln-eval-last t))
"if n < 0 then -1\n elif n == 0 then 0\n else 1")
(test-flan-fln--is "and at the end of a one-line elif"
(car (test-flan-fln--oneline-at "elif n" #'flan-fln-eval-last t))
"if n < 0 then -1\n elif n == 0 then 0\n else 1")
(test-flan-fln--is "an else x after a block if belongs to it too"
(car (test-flan-fln--oneline-at "else 2" #'flan-fln-eval-statement))
"if n < 10\n 1\n else 2")
(test-flan-fln--is "C-u C-c C-c on a one-line elif stops at its condition"
(cadr (test-flan-fln--oneline-at "elif n" (lambda () (flan-fln-eval-defun '(4)))))
'(3 8))
(test-flan-fln--is "and on a one-line else, at its value"
(cadr (test-flan-fln--oneline-at "else 1" (lambda () (flan-fln-eval-defun '(4)))))
'(4 8))
(test-flan-fln--is "an else x after a block if too"
(cadr (test-flan-fln--oneline-at "else 2" (lambda () (flan-fln-eval-defun '(4)))))
'(9 8))
(test-flan-fln--is "C-u C-c C-c on a let the let above takes in stops at its value"
(test-flan-fln--in "fn t2(n: i64) -> i64\n let a = 1\n |let b = 2\n a + b + n\n"
(plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause))
'(3 11))
(test-flan-fln--is "and at its block when the value is one"
(test-flan-fln--in "fn t2(n: i64) -> i64\n let a = 1\n |let b =\n n + 1\n a + b\n"
(plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause))
'(4 5))
(test-flan-fln--is "the first let of a run stops at the let"
(test-flan-fln--in "fn t2(n: i64) -> i64\n n + 1\n |let a = 1\n a\n"
(plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause))
'(3 3))
(test-flan-fln--is "C-c C-e on a let whose value is a block sends its scope with it"
(car (test-flan-fln--oneline-at "let r" #'flan-fln-eval-statement))
"let r = match n\n 0 -> 1\n _ -> 2\n add(r, 1)")
(test-flan-fln--is "and on a typed lambda's let, its body and the let's scope"
(car (test-flan-fln--oneline-at "let dbl" #'flan-fln-eval-statement))
(substring test-flan-fln--oneline
(string-search "let dbl" test-flan-fln--oneline)
(1- (length test-flan-fln--oneline))))
(test-flan-fln--is "an arm whose pattern is Dir.north binds nothing"
(test-flan-fln--in "fn f(d: Dir) -> Dir\n match d\n Dir.north -> Dir.south\n"
(goto-char (point-max))
(skip-chars-backward "\n")
(test-flan-fln--sent-code (test-flan-fln--sending (flan-fln-eval-last))))
"Dir.south")
;;; Names and colours
(test-flan-fln--in "fn f(x: i64) -> i64\n comment:\n g(:key-r, x)\n 0x1F + 12\n"
(search-forward "x:")
(backward-char 1)
(test-flan-fln--is "a colon glued to a name is not part of it"
(thing-at-point 'symbol t) "x")
(search-forward "comment")
(test-flan-fln--is "nor to a name that takes a block"
(thing-at-point 'symbol t) "comment")
(test-flan--check "font-lock draws the buffer without an error"
(condition-case nil (progn (font-lock-ensure) t) (error nil)))
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "a number is drawn as one" (funcall face "12") 'font-lock-number-face)
(test-flan-fln--is "a keyword as a constant" (funcall face ":key-r") 'font-lock-constant-face)
(test-flan-fln--is "a header word as a keyword" (funcall face "fn") 'font-lock-keyword-face)
(test-flan-fln--is "a type after its colon" (funcall face "i64") 'font-lock-type-face)
(test-flan--check "and the name before the colon is not a keyword"
(null (funcall face "x:")))))
(test-flan-fln--in "fn f(d: Dir) -> i64
let g = fn(a: Fn(i64) -> i64, b) -> Vec(i64) =>
a(b)
restart-case
3
restart retry(v: i32) \"Try again\"
v
match d
Dir.north -> twice(1)
Circle(r) -> r
"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "a lambda's fn is a keyword" (funcall face "fn(a") 'font-lock-keyword-face)
(test-flan-fln--is "its parameter's function type is a type" (funcall face "Fn(i64)") 'font-lock-type-face)
(test-flan-fln--is "and its return type" (funcall face "Vec") 'font-lock-type-face)
(test-flan-fln--is "a restart's name is drawn as a name" (funcall face "retry") 'font-lock-function-name-face)
(test-flan-fln--is "its report as a string" (funcall face "\"Try") 'font-lock-string-face)
(test-flan-fln--is "an enum member as a constant" (funcall face "Dir.north") 'font-lock-constant-face)
(test-flan-fln--is "an arm's value is not a type" (funcall face "twice(1)") nil)
(test-flan-fln--is "nor after a pattern with parentheses" (funcall face "r\n") nil)))
(test-flan-fln--in "struct DiskFull :parent IoError
free: i64
type Row = Vec(i64)
macro repeat(i, n, & body)
quote
for ~i in range(~n)
~@body
fn gcd(a: i32, b: i32) -> i32
let x = a
let y = b
while y != 0
let r = x % y
x = y
y = r
x
"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "a struct's parent is a type" (funcall face "IoError") 'font-lock-type-face)
(test-flan-fln--is "and :parent a keyword" (funcall face ":parent") 'font-lock-constant-face)
(test-flan-fln--is "macro is a keyword" (funcall face "macro") 'font-lock-keyword-face)
(test-flan-fln--is "and its name a function's" (funcall face "repeat") 'font-lock-function-name-face)
(test-flan-fln--is "type is a keyword" (funcall face "type") 'font-lock-keyword-face)
(test-flan-fln--is "an alias's name is a type" (funcall face "Row") 'font-lock-type-face)
(test-flan-fln--is "and so is what it names" (funcall face "Vec(i64)") 'font-lock-type-face))
(goto-char (point-min))
(search-forward "Row")
(test-flan-fln--is "an alias installs as a defalias"
(flan-fln--declaration-head-at (line-beginning-position)) "defalias")
(goto-char (point-min))
(search-forward "~@body")
(test-flan-fln--is "a macro is one top-level form"
(test-flan-fln--thing 'flan-fln-toplevel)
"macro repeat(i, n, & body)
quote
for ~i in range(~n)
~@body")
(test-flan-fln--is "installed as a defmacro"
(flan-fln--declaration-head-at (car (flan-fln--toplevel-bounds (point))))
"defmacro")
(search-forward "while y")
(test-flan-fln--is "a while's statement is its header and block"
(test-flan-fln--thing 'flan-fln-statement)
"while y != 0
let r = x % y
x = y
y = r")
(goto-char (point-min))
(test-flan-fln--is "the struct's head is defstruct"
(flan-fln--declaration-head-at (point)) "defstruct")
(let ((imenu-generic-expression flan-fln-imenu-generic-expression))
(test-flan--check "imenu lists the macro"
(assoc "repeat" (cdr (assoc "Macros" (imenu--generic-function
imenu-generic-expression)))))))
(test-flan-fln--in "defmacro(m, [x]):
quote
~x
defmethod(area, point, [p]):
0
defclass(Shape, [w dyn]):
defstruct(Io, :parent, Error, [])
defconst(k, 3)
"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(dolist (c '(("defmacro" m font-lock-function-name-face)
("defmethod" area font-lock-function-name-face)
("defclass" Shape font-lock-type-face)
("defstruct" Io font-lock-type-face)
("defconst" k font-lock-variable-name-face)))
(test-flan-fln--is (format "a fallback %s's head is a keyword" (car c))
(funcall face (concat (car c) "(")) 'font-lock-keyword-face)
(test-flan-fln--is (format "and the name it defines, %s" (cadr c))
(save-excursion
(goto-char (point-min))
(search-forward (concat (car c) "("))
(get-text-property (point) 'face))
(nth 2 c))))
(let ((index (imenu--generic-function flan-fln-imenu-generic-expression)))
(test-flan--check "imenu lists a fallback defmacro"
(assoc "m" (cdr (assoc "Macros" index))))
(test-flan--check "a fallback defmethod"
(assoc "area" (cdr (assoc "Functions" index))))
(test-flan--check "a fallback defclass and defstruct"
(and (assoc "Shape" (cdr (assoc "Types" index)))
(assoc "Io" (cdr (assoc "Types" index)))))
(test-flan--check "and a fallback defconst"
(assoc "k" (cdr (assoc "Variables" index))))))
(test-flan-fln--in "fn far(a: i64,\n b: i64) -> Point\n match a\n Some(x) -> Other\n"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "a wrapped fn header's return type is a type" (funcall face "Point") 'font-lock-type-face)
(test-flan-fln--is "an arm's value after a call pattern is not" (funcall face "Other") nil)))
;;; Indentation
(defun test-flan-fln--tabs (text n)
"The column TEXT's `|' line reaches after N TABs."
(test-flan-fln--in text
(let ((last-command nil) (this-command 'indent-for-tab-command))
(dotimes (_ n)
(indent-for-tab-command)
(setq last-command 'indent-for-tab-command)))
(current-indentation)))
(defconst test-flan-fln--nest
"fn f() -> ()
while a
if b
c()
|")
(test-flan-fln--is "the first TAB after a block goes to its column"
(test-flan-fln--tabs test-flan-fln--nest 1) 6)
(test-flan-fln--is "each TAB after that steps out one"
(list (test-flan-fln--tabs test-flan-fln--nest 2)
(test-flan-fln--tabs test-flan-fln--nest 3)
(test-flan-fln--tabs test-flan-fln--nest 4)
(test-flan-fln--tabs test-flan-fln--nest 5))
'(4 2 0 6))
(test-flan-fln--is "a line of code already at a valid column stays there"
(test-flan-fln--tabs "fn f() -> ()\n if a\n b\n| let b = 2" 1) 2)
(test-flan-fln--is "and a second TAB steps it out"
(test-flan-fln--tabs "fn f() -> ()\n if a\n b\n| let b = 2" 2) 0)
(test-flan-fln--is "a line of code at no valid column goes to the deepest"
(test-flan-fln--tabs "fn f() -> ()\n if a\n b\n| let b = 2" 1) 4)
(test-flan-fln--is "after a header, one level deeper first"
(test-flan-fln--tabs "fn f() -> ()\n while a\n|" 1) 4)
(test-flan-fln--is "after a trailing colon too"
(test-flan-fln--tabs "rl/with-drawing():\n|" 1) 2)
(test-flan-fln--is "and after let x ="
(test-flan-fln--tabs "let colors =\n|" 1) 2)
(test-flan-fln--is "but not after a one-line fn"
(test-flan-fln--tabs "fn f() -> i32 = 1\n|" 1) 0)
(test-flan-fln--is "after if let, one level deeper"
(test-flan-fln--tabs "fn f() -> ()\n if let Some(g) = o\n|" 1) 4)
(test-flan-fln--is "and after a when with a block"
(test-flan-fln--tabs "fn f() -> ()\n when a > 1\n|" 1) 4)
(test-flan-fln--is "but not after a one-line when"
(test-flan-fln--tabs "fn f() -> ()\n when a then b()\n|" 1) 2)
(test-flan-fln--in "fn f() -> ()\n if let Some(g) = o\n g\n let w = when a then 1\n when b\n c()\n"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "if let's let is a keyword" (funcall face "let Some") 'font-lock-keyword-face)
(test-flan-fln--is "a value's when is a keyword" (funcall face "when a") 'font-lock-keyword-face)
(test-flan-fln--is "and so is a statement's" (funcall face "when b") 'font-lock-keyword-face)))
;; Swift optionals: the marks are not part of a name, `??' continues a line,
;; and `elif let' reads as `if let' does.
(test-flan-fln--in "fn f(o: i32?) -> i32\n if o? as g\n g!\n elif let Some(h) = p?.q\n h ?? 0\n"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "elif let's let is a keyword" (funcall face "let Some(h)") 'font-lock-keyword-face)
(test-flan-fln--is "a test's as is a keyword" (funcall face "as g") 'font-lock-keyword-face)
(test-flan-fln--is "a type's ? is the type's" (funcall face "?)") 'font-lock-type-face)
(test-flan-fln--is "an unwrap's ! is marked" (funcall face "!\n") 'font-lock-keyword-face)
(test-flan-fln--is "a chain's ? is marked" (funcall face "?.q") 'font-lock-keyword-face)
(test-flan-fln--is "?? is marked" (funcall face "??") 'font-lock-keyword-face))
(goto-char (point-min))
(search-forward "g!")
(backward-char 1)
(test-flan-fln--is "the name at x! is x" (thing-at-point 'symbol t) "g"))
;; A condition's `as' with no `?' before it, twice on a line; an `as' outside
;; a condition is left alone.
(test-flan-fln--in "fn f()\n left = when get(grid, r) as g and b(g) as h then g\n x = as y\n"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "a condition's as is a keyword" (funcall face "as g") 'font-lock-keyword-face)
(test-flan-fln--is "and a second one" (funcall face "as h") 'font-lock-keyword-face)
(test-flan-fln--is "an as outside a condition is not" (funcall face "as y") nil)))
(test-flan-fln--is "after if x? as g, one level deeper"
(test-flan-fln--tabs "fn f() -> ()\n if o? as g\n|" 1) 4)
(with-temp-buffer
(insert "x = a ??\n b\ny = a\n ?? b\n")
(flan-fln-mode)
(goto-char (point-min))
(forward-line 1)
(test-flan--check "a line after a trailing ?? continues it"
(flan-fln--continuation-p (point)))
(forward-line 2)
(test-flan--check "a line starting with ?? continues"
(flan-fln--continuation-p (point))))
;; The pipe: a line starting with `|>' goes under the value the statement
;; binds, then under the pipe above it. The first `|' of each text is point.
(test-flan-fln--is "a first |> goes under the let's value"
(test-flan-fln--tabs "let total = get(g)\n||> f()" 1) 12)
(test-flan-fln--is "and a second TAB one level in from the let"
(test-flan-fln--tabs "let total = get(g)\n||> f()" 2) 2)
(with-temp-buffer
(insert "let total = get(g)\n |> f()\n|> g()")
(flan-fln-mode)
(flan-fln-indent-line)
(test-flan-fln--is "a |> under a |> lines up with it" (current-indentation) 12))
(test-flan-fln--is "a |> under a bare statement is one level in"
(test-flan-fln--tabs "fn f() -> ()\n get(g)\n||> f()" 1) 4)
(test-flan-fln--in "|let y = x |> f()\nz = a\n |> g\nw = b |>\n h\n"
(font-lock-ensure)
(test-flan-fln--is "a |> inside a line is marked"
(save-excursion (search-forward "x |>") (get-text-property (- (point) 2) 'face))
'font-lock-keyword-face)
(test-flan-fln--is "a |> starting a line is marked"
(save-excursion (search-forward " |>") (get-text-property (- (point) 2) 'face))
'font-lock-keyword-face)
(forward-line 2)
(test-flan--check "a line starting with |> continues" (flan-fln--continuation-p (point)))
(forward-line 2)
(test-flan--check "a line after a trailing |> continues it" (flan-fln--continuation-p (point))))
(test-flan-fln--is "else goes to its if's column, whatever the depth"
(test-flan-fln--tabs "if a\n if b\n c\n |else" 1) 2)
(test-flan-fln--is "and a second TAB to the outer if's"
(test-flan-fln--tabs "if a\n if b\n c\n |else" 2) 0)
(test-flan-fln--is "on goes to its handler-case's"
(test-flan-fln--tabs " handler-case\n f()\n |on E(c)" 1) 2)
(test-flan-fln--is "inside a call, under its first argument"
(test-flan-fln--tabs " paint-at(i32(m.y) / cell-size,\n|i32(m.x))" 1) 11)
(test-flan-fln--is "inside a bracket with nothing after it, one level in"
(test-flan-fln--tabs " let v = [\n|1 2]" 1) 4)
(test-flan-fln--is "a closing bracket, at the bracket's own column"
(test-flan-fln--tabs " let v = [\n 1 2\n|]" 1) 4)
(test-flan-fln--is "a closer after RET in an open call, under the first argument"
(test-flan-fln--tabs " if and(state.paused\n|)" 1) 9)
(test-flan-fln--in "fn f()\n if and(state.paused|)\n"
(newline-and-indent)
(test-flan-fln--is "RET before the closer puts it under the first argument"
(list (current-indentation) (char-after)) '(9 ?\))))
(test-flan-fln--is "after a line ending in an operator, deeper than its statement"
(test-flan-fln--tabs " if a and\n|b" 1) 4)
(test-flan-fln--is "never deeper after a let"
(test-flan-fln--tabs "fn f()\n let r = 3\n|" 1) 2)
(test-flan-fln--is "TAB after a let stays at its column first"
(test-flan-fln--tabs "fn f()\n let a = 1\n|" 1) 2)
(test-flan-fln--is "and offers its next binding's column, under the first name"
(test-flan-fln--tabs "fn f()\n let a = 1\n|" 2) 6)
(test-flan-fln--is "after a binding line, that column comes first"
(test-flan-fln--tabs "fn f()\n let a = 1\n b = 2\n|" 1) 6)
(test-flan-fln--is "and a binding written there stays"
(test-flan-fln--tabs "fn f()\n let a = 1\n| b = 2" 1) 6)
(test-flan-fln--is "at the top level too"
(test-flan-fln--tabs "let a = 1\n|" 2) 4)
(dolist (c '(("fn f()\n let a = 0\n g = fn(x) =>\n x + 1\n|" (8 2 0) "a lambda's block")
("fn f()\n let a = 1\n b = match a\n 1 -> 2\n|" (8 2 0) "a match's arms")
("let a = 0\n g = fn(x) =>\n x + 1\n|" (6 0) "a global's lambda block")
("fn f()\n let a = 1\n g = map(xs, fn(x) =>\n x + 1)\n|" (6 2 0)
"but a lambda's brackets closed, where the column stays")
("fn f(c)\n let a = 1\n b = if c\n 1\n else\n 2\n|" (8 2 0)
"an if's else block")
("fn f(c)\n let a = 1\n b = if c\n 1\n|" (8 6 2 0)
"but an if's block, where its else may go")
("fn f(c, d)\n let a = 1\n b = if c\n 1\n elif d\n 2\n|" (8 6 2 0)
"and an elif's block, where another clause may go")
("fn f()\n let g = map(xs, fn(x) =>\n x + 1)\n|" (2 6 0)
"a first binding's bracketed lambda offers the column")))
(test-flan-fln--is (format "no binding column after a binding's open block: %s" (nth 2 c))
(test-flan-fln--in (car c) (flan-fln--block-levels (point)))
(cadr c)))
(test-flan-fln--is "but not after a tab between let and its name"
(list (test-flan-fln--tabs "fn f()\n let\ta = 1\n|" 1)
(test-flan-fln--tabs "fn f()\n let\ta = 1\n|" 2))
'(2 0))
(dolist (c '(("let r = match n" "a let's match")
("let r = if c" "a let's if")
("x = if c" "an assignment's if")
("let r = handler-case" "a let's handler-case")
("let f = fn(a, b) =>" "a lambda header")
("let f = fn(a: i64, b) -> i64 =>" "a typed lambda header")
("let f = fn(g: Fn(i64) -> i64) -> Option(i64) =>" "one with a function type in it")
("fn(a: i64) -> i64 =>" "a typed lambda as a statement")))
(test-flan-fln--is (format "unless its value goes on under it: %s" (cadr c))
(test-flan-fln--tabs (concat "fn f()\n " (car c) "\n|") 1) 4))
(test-flan-fln--is "but not a typed lambda with its body on the line"
(test-flan-fln--tabs "fn f()\n let f = fn(a: i64) -> i64 => a\n|" 1) 2)
(test-flan-fln--is "loop is no header in .fln, and opens nothing"
(test-flan-fln--tabs "fn f()\n let r = loop i = 0\n|" 1) 2)
(test-flan-fln--is "a header word being assigned opens nothing"
(test-flan-fln--tabs "fn f()\n for = 1\n|" 1) 2)
(test-flan-fln--in "fn f()\n handler-case\n g()\n on E(c)\n h(c)\n on = 2\n data += 1\n"
(font-lock-ensure)
(goto-char (point-min))
(search-forward "on = 2")
(test-flan--check "nor is a clause word being assigned a clause"
(not (flan-fln--clause-line-p (line-beginning-position))))
(test-flan-fln--is "or drawn as a keyword"
(get-text-property (match-beginning 0) 'face) nil)
(search-forward "data")
(test-flan-fln--is "and a header word assigned is not either"
(get-text-property (match-beginning 0) 'face) nil))
(test-flan-fln--is "a macro opens a block"
(test-flan-fln--tabs "macro repeat(i, n, & body)\n|" 1) 2)
(test-flan-fln--is "and a struct with a parent"
(test-flan-fln--tabs "struct DiskFull :parent IoError\n|" 1) 2)
(test-flan-fln--in "let speed: i64 = 3\n\nfn f()\n let x = 1\n x\n"
(font-lock-ensure)
(test-flan-fln--is "a top-level let's name is a variable's"
(save-excursion (goto-char (point-min)) (search-forward "speed")
(get-text-property (match-beginning 0) 'face))
'font-lock-variable-name-face)
(test-flan-fln--is "and installs as a def" (flan-fln--declaration-head-at (point-min)) "def")
(test-flan--check "imenu lists it"
(assoc "speed" (cdr (assoc "Variables" (imenu--generic-function
flan-fln-imenu-generic-expression)))))
(test-flan--check "it is no local let" (not (flan-fln--let-p (point-min))))
(goto-char (point-min))
(search-forward "let x")
(test-flan--check "one in a fn is" (flan-fln--let-p (line-beginning-position)))
(test-flan-fln--is "and its name is not a global's"
(get-text-property (match-end 0) 'face) nil))
(test-flan-fln--is "a class with a slot per line opens a block"
(test-flan-fln--tabs "class point\n|" 1) 2)
(test-flan-fln--is "not one on one line"
(test-flan-fln--tabs "class point(x, y)\n|" 1) 0)
(test-flan-fln--is "a method opens its block"
(test-flan-fln--tabs "method area(p: point)\n|" 1) 2)
(test-flan-fln--is "but not one with its value on the line"
(test-flan-fln--tabs "method kind(v) when :int = 1\n|" 1) 0)
(test-flan-fln--is "a multi with a block opens it"
(test-flan-fln--tabs "multi kind(v) -> dyn\n|" 1) 2)
(test-flan-fln--is "a generic never does"
(test-flan-fln--tabs "generic area(p) -> dyn\n|" 1) 0)
(test-flan-fln--in "class point(x, y)\n\ngeneric area(p) -> dyn\n\nmethod area(p: point)\n 1\n\nmulti kind(v) -> dyn = type-of(v)\n\nmethod kind(v) when :int = 2\n"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "class is a keyword" (funcall face "class") 'font-lock-keyword-face)
(test-flan-fln--is "and its name a type" (funcall face "point(") 'font-lock-type-face)
(test-flan-fln--is "a generic's name is a function's" (funcall face "area(p)") 'font-lock-function-name-face)
(test-flan-fln--is "a method's class is a type" (funcall face "point)") 'font-lock-type-face)
(test-flan-fln--is "a method's when is a keyword" (funcall face "when") 'font-lock-keyword-face))
(let ((index (imenu--generic-function flan-fln-imenu-generic-expression)))
(test-flan--check "imenu lists the class, the generic and the multi"
(and (assoc "point" (cdr (assoc "Types" index)))
(assoc "area" (cdr (assoc "Functions" index)))
(assoc "kind" (cdr (assoc "Functions" index))))))
(goto-char (point-min))
(search-forward "method area")
(test-flan-fln--is "a method installs as a defmethod"
(flan-fln--declaration-head-at (line-beginning-position)) "defmethod"))
(test-flan-fln--is "but not a struct on one line"
(test-flan-fln--tabs "struct Pt(x: i32, y: i32)\n|" 1) 0)
(test-flan-fln--is "nor one with a parent"
(test-flan-fln--tabs "struct D(free: i64) :parent IoError\n|" 1) 0)
(test-flan-fln--in "struct D(free: i64) :parent IoError\n\nunion U(a: i32)\n"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "a one-line struct's name is a type" (funcall face "D(") 'font-lock-type-face)
(test-flan-fln--is "its field's type too" (funcall face "i64") 'font-lock-type-face)
(test-flan-fln--is "and its parent" (funcall face "IoError") 'font-lock-type-face)
(test-flan-fln--is "a one-line union's name" (funcall face "U(") 'font-lock-type-face))
(goto-char (point-min))
(test-flan-fln--is "a one-line struct is a top-level form of one line"
(test-flan-fln--thing 'flan-fln-toplevel)
"struct D(free: i64) :parent IoError"))
(test-flan-fln--is "a one-line fn whose value is a match opens it"
(test-flan-fln--tabs "fn f(x) = match x\n|" 1) 2)
(test-flan-fln--is "no deeper after a one-line else"
(test-flan-fln--tabs "fn f()\n if a\n b\n else c\n|" 1) 2)
(test-flan-fln--is "nor after one that ends in a comment"
(test-flan-fln--tabs "fn f()\n if a then b\n else c ; c\n|" 1) 2)
(test-flan-fln--is "but deeper after an else alone with a comment"
(test-flan-fln--tabs "fn f()\n if a\n b\n else ; c\n|" 1) 4)
(test-flan-fln--is "after a one-line if, a new line stays at its column"
(test-flan-fln--tabs "fn f()\n if a then b\n|" 1) 2)
(test-flan-fln--is "else goes to a one-line if's column"
(test-flan-fln--tabs "if a\n if b then c\n |else d" 1) 2)
(test-flan-fln--is "and to a let's if"
(test-flan-fln--tabs "fn f()\n let s = if c\n 1\n |else" 1) 2)
(test-flan-fln--in "fn f() -> ()\n while a\n b()\n |"
(flan-fln-dedent-or-delete 1)
(test-flan-fln--is "backspace in the indentation drops one level"
(current-indentation) 2)
(flan-fln-dedent-or-delete 1)
(test-flan-fln--is "and another" (current-indentation) 0))
(test-flan-fln--in "fn f() -> ()\n ab|"
(flan-fln-dedent-or-delete 1)
(test-flan-fln--is "backspace after text deletes a character"
(buffer-substring (line-beginning-position) (point)) " a"))
(test-flan-fln--in "if a\n if b\n c\n els|"
(let ((last-command-event ?e))
(insert "e")
(run-hooks 'post-self-insert-hook))
(let ((last-command-event ?\s))
(insert " ")
(run-hooks 'post-self-insert-hook))
(test-flan-fln--is "else snaps to its if as it is typed"
(current-indentation) 2))
(test-flan-fln--in "fn f() -> ()\n if a\n b\n| c\n d\n"
(indent-region (point) (point-max))
(test-flan-fln--is "indent-region moves a block rigidly"
(buffer-substring (point) (point-max))
" c\n d\n"))
(test-flan-fln--in "fn f() -> ()\n if a\n b\n |c\n"
(indent-region (point-min) (point-max))
(test-flan-fln--is "and leaves lines at valid columns alone"
(buffer-string) "fn f() -> ()\n if a\n b\n c\n"))
(test-flan-fln--in "fn f() -> ()\n if a\n b\n |\n"
(kill-new "if x\n y\n else\n z")
(flan-fln-yank)
(test-flan-fln--is "a statement cut from its first character yanks as one block"
(buffer-string)
"fn f() -> ()\n if a\n b\n if x\n y\n else\n z\n"))
(test-flan-fln--in "fn f() -> ()\n if a\n |\n"
(kill-new " while x\n y\n")
(flan-fln-yank)
(test-flan-fln--is "whole lines yank at point's column"
(buffer-string)
"fn f() -> ()\n if a\n while x\n y\n\n"))
;;; A lambda's block inside brackets
(defconst test-flan-fln--lam
"fn f(xs) -> i64
let n = 1
sort-by(xs, fn(a, b) =>
let d = a - b
d < n)
map(xs, fn(x) =>
g(x)
x
)
h(n)
")
(defun test-flan-fln--lam-at (needle fn &optional at-end)
"What FN sends with point at NEEDLE in the lambda program, or at the end of
its line with AT-END."
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam needle)
(when at-end (end-of-line))
(let ((r (test-flan-fln--sending (funcall fn))))
(and r (list (test-flan-fln--sent-code r) (plist-get r :pause))))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam "sort-by")
(test-flan-fln--is "a call with a block lambda is one statement, block and closer too"
(test-flan-fln--thing 'flan-fln-statement)
"sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)")
(test-flan-fln--is "its body is the lambda's block, less the call's closer"
(test-flan-fln--thing 'flan-fln-body)
"let d = a - b\n d < n"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam "d < n")
(test-flan-fln--is "a line of the block is a statement of its own, less the closer"
(test-flan-fln--thing 'flan-fln-statement) "d < n"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam " )")
(test-flan-fln--is "a closer on a line of its own belongs to the call"
(test-flan-fln--thing 'flan-fln-statement)
"map(xs, fn(x) =>\n g(x)\n x\n )"))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam " x\n")
(test-flan-fln--is "and not to the statement above it"
(test-flan-fln--thing 'flan-fln-statement) "x"))
(test-flan-fln--is "C-c C-e on the header sends the call, block and all"
(car (test-flan-fln--lam-at "sort-by" #'flan-fln-eval-statement))
"sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)")
(test-flan-fln--is "and C-x C-e at the header's end"
(car (test-flan-fln--lam-at "sort-by" #'flan-fln-eval-last t))
"sort-by(xs, fn(a, b) =>\n let d = a - b\n d < n)")
(test-flan-fln--is "C-x C-e at the end of the block's last line sends its statement"
(car (test-flan-fln--lam-at "d < n" #'flan-fln-eval-last t))
"d < n")
(test-flan-fln--is "C-c C-e on a line of the block sends that statement"
(car (test-flan-fln--lam-at "g(x)" #'flan-fln-eval-statement))
"g(x)")
(dolist (c '(("let d" (4 5) "a statement in the block, where the reader starts it")
("g(x)" (7 5) "one in a block with its closer on a line of its own")
("a, b)" (3 15) "the lambda, from its fn")
("xs, fn(a" (3 3) "the call, from its name")))
(test-flan-fln--is (format "C-u C-c C-c marks %s" (nth 2 c))
(cadr (test-flan-fln--lam-at (car c) (lambda () (flan-fln-eval-defun '(4)))))
(cadr c)))
(test-flan-fln--is "TAB after a => inside brackets goes one level in"
(test-flan-fln--tabs "fn f()\n sort-by(xs, fn(a, b) =>\n|" 1) 4)
(test-flan-fln--is "and a line of the block stays in it"
(test-flan-fln--tabs "fn f()\n sort-by(xs, fn(a, b) =>\n g(a)\n| a < b)" 1) 4)
(test-flan-fln--is "under a header on a wrapped argument line, in from that line"
(test-flan-fln--tabs "f(a,\n fn(b) =>\n|" 1) 4)
(test-flan-fln--is "a closer on its own line goes to the call's column"
(test-flan-fln--tabs "fn f()\n m(xs, fn(x) =>\n x\n|)" 1) 2)
(test-flan-fln--is "after the block's brackets close, its column is no longer offered"
(list (test-flan-fln--tabs "fn f()\n app(k, fn(x) =>\n x)\n|g()" 1)
(test-flan-fln--tabs "fn f()\n app(k, fn(x) =>\n x)\n|g()" 2)
(test-flan-fln--tabs "fn f()\n app(k, fn(x) =>\n x)\n|g()" 3))
'(0 2 0))
(test-flan-fln--is "inside a bracket in the block, under its first argument"
(test-flan-fln--tabs "fn f()\n m(xs, fn(x) =>\n g(x,\n|y))" 1) 6)
(test-flan-fln--in "fn f()\n m(xs, fn(x) => x)\n"
(font-lock-ensure)
(test-flan-fln--is "=> is a keyword"
(save-excursion (goto-char (point-min)) (search-forward "=>")
(get-text-property (match-beginning 0) 'face))
'font-lock-keyword-face))
(defconst test-flan-fln--lam-arm
"fn f(n: i64) -> i64
match n
1 -> app(1, fn(a) =>
a * 2)
_ -> 0
")
(test-flan-fln--is "C-x C-e at the end of an arm whose value ends in => sends the value, block and all"
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam-arm "1 ->")
(end-of-line)
(test-flan-fln--sent-code (test-flan-fln--sending (flan-fln-eval-last))))
"app(1, fn(a) =>\n a * 2)")
(test-flan-fln--is "and C-u C-c C-c on its pattern stops at the value"
(test-flan-fln--in (test-flan-fln--at test-flan-fln--lam-arm "1 ->")
(plist-get (test-flan-fln--sending (flan-fln-eval-defun '(4))) :pause))
'(3 10))
(test-flan-fln--is "an else whose value ends in => takes the block too"
(test-flan-fln--in "fn f(c)\n if c\n g()\n |else app(1, fn(a) =>\n a)\n"
(test-flan-fln--text (flan-fln--clause-value (line-beginning-position))))
"app(1, fn(a) =>\n a)")
;;; Block editing
(test-flan-fln--in "fn f() -> ()\n if a\n |b()\n c()\n d()\n"
(flan-fln-slurp)
(test-flan-fln--is "slurp pulls the next statement into the block"
(buffer-string) "fn f() -> ()\n if a\n b()\n c()\n d()\n")
(flan-fln-barf)
(test-flan-fln--is "barf pushes the last one out again"
(buffer-string) "fn f() -> ()\n if a\n b()\n c()\n d()\n"))
(test-flan-fln--in "fn f() -> ()\n |a()\n if x\n y\n b()\n"
(flan-fln-move-statement-down)
(test-flan-fln--is "a statement moves down past its sibling's whole block"
(buffer-string) "fn f() -> ()\n if x\n y\n a()\n b()\n")
(test-flan--check "and point moves with it" (looking-at "a()"))
(flan-fln-move-statement-up)
(test-flan-fln--is "and back up"
(buffer-string) "fn f() -> ()\n a()\n if x\n y\n b()\n"))
(test-flan-fln--in "fn f() -> ()\n when(c):\n if x\n |y\n"
(flan-fln-raise-statement)
(test-flan-fln--is "raise replaces the owner with the statement"
(buffer-string) "fn f() -> ()\n when(c):\n y\n"))
(test-flan-fln--in "fn f() -> ()\n |if x\n y\n else\n z\n b()\n"
(flan-fln-kill-statement)
(test-flan-fln--is "kill takes the whole statement's lines"
(buffer-string) "fn f() -> ()\n b()\n")
(test-flan-fln--is "into the kill ring" (current-kill 0)
" if x\n y\n else\n z\n"))
;;; expand-region, where it is installed
(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/expand-region-[0-9]*")
(file-expand-wildcards "~/.emacs.d/elpa/expand-region-[0-9]*")))
(load-path (append dirs load-path)))
(if (not (require 'expand-region nil t))
(message " skip expand-region (not installed)")
(test-flan-fln--in (test-flan-fln--at test-flan-fln--settle "is-right\n -1")
(transient-mark-mode 1)
(let ((steps nil))
(dotimes (_ 6)
(er/expand-region 1)
(push (buffer-substring-no-properties (region-beginning) (region-end)) steps))
(setq steps (nreverse steps))
(test-flan-fln--is "term, then statement's clause, then statement, then out"
(mapcar (lambda (s) (car (split-string s "\n"))) steps)
'("is-right" "elif not is-right" "if not is-left" "let side ="
"if is-left or is-right" "let is-left = col > 0"))))))
;;; smartparens, where it is installed
(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/smartparens-[0-9]*")
(file-expand-wildcards "~/.emacs.d/elpa/smartparens-[0-9]*")
(file-expand-wildcards "~/.config/emacs/elpa/dash-[0-9]*")
(file-expand-wildcards "~/.emacs.d/elpa/dash-[0-9]*")))
(load-path (append dirs load-path)))
(if (not (require 'smartparens nil t))
(message " skip smartparens (not installed)")
;; A setup that puts sexp commands on the top-level keys, as the
;; author's does.
(define-key smartparens-mode-map (kbd "C-M-a") 'sp-backward-down-sexp)
(define-key smartparens-mode-map (kbd "C-M-u") 'sp-backward-up-sexp)
(let ((b (generate-new-buffer "sp.fln")))
(switch-to-buffer b)
(flan-fln-mode)
(test-flan--check "smartparens is on in a .fln buffer" smartparens-mode)
(test-flan--check "and C-M-a and C-M-u stay the mode's"
(and (eq (key-binding (kbd "C-M-a")) 'beginning-of-defun)
(eq (key-binding (kbd "C-M-u")) 'flan-fln-up)))
(execute-kbd-macro "f(")
(test-flan-fln--is "it pairs a bracket" (buffer-string) "f()")
(erase-buffer)
(execute-kbd-macro "'a")
(test-flan-fln--is "and not a quote" (buffer-string) "'a")
(set-buffer-modified-p nil)
(kill-buffer b))
(define-key smartparens-mode-map (kbd "C-M-a") nil)
(define-key smartparens-mode-map (kbd "C-M-u") nil)))
;;; Under Evil
(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/evil-[0-9]*")
(file-expand-wildcards "~/.emacs.d/elpa/evil-[0-9]*")
(file-expand-wildcards "~/.config/emacs/elpa/goto-chg-*")
(file-expand-wildcards "~/.emacs.d/elpa/goto-chg-*")))
(load-path (append dirs load-path)))
(if (not (require 'evil nil t))
(message " skip the .fln text objects (Evil is not installed)")
(evil-mode 1)
(unwind-protect
(let ((yanked
(lambda (text keys)
(let ((b (generate-new-buffer "objects.fln")))
(switch-to-buffer b)
(insert text)
(flan-fln-mode)
(evil-initialize-state)
(evil-normal-state)
(goto-char (point-min))
(search-forward "|")
(delete-char -1)
(execute-kbd-macro keys)
(prog1 (substring-no-properties (current-kill 0))
(set-buffer-modified-p nil)
(kill-buffer b))))))
(dolist (c `(("yiw" ,(test-flan-fln--at test-flan-fln--settle "settle(")
"settle")
("yie" ,(test-flan-fln--at test-flan-fln--settle "i32(m.x)")
"i32(m.x)")
("yis" ,(test-flan-fln--at test-flan-fln--settle "and c >= 0")
"if r >= 0 and r < rows - 1\n and c >= 0\n grid[r, c] = 1")
("yas" ,(test-flan-fln--at test-flan-fln--settle "and c >= 0")
" if r >= 0 and r < rows - 1\n and c >= 0\n grid[r, c] = 1\n")
("yii" ,(test-flan-fln--at test-flan-fln--settle "if not is-left")
" 1\n")
("yik" ,(test-flan-fln--at test-flan-fln--settle "if f32(rand())")
" if f32(rand()) < 0.5 then 1 else -1\n")
("yak" ,(test-flan-fln--at test-flan-fln--settle "if f32(rand())")
" else\n if f32(rand()) < 0.5 then 1 else -1\n")
("yik" ,(test-flan-fln--at test-flan-fln--wrapped "Some(_)")
"5")
("yak" ,(test-flan-fln--at test-flan-fln--wrapped "Some(_)")
" Some(_) -> 5\n")
("yik" ,(test-flan-fln--at test-flan-fln--oneline "else 1")
"1")
("yik" ,(test-flan-fln--at test-flan-fln--oneline "elif n")
"0")
("yik" ,(test-flan-fln--at test-flan-fln--oneline "else 2")
"2")
("yid" ,(test-flan-fln--at test-flan-fln--settle "paint-at")
,(substring test-flan-fln--settle
(string-search "fn step" test-flan-fln--settle)))))
(test-flan-fln--is (format "under Evil, %s" (car c))
(funcall yanked (nth 1 c) (car c)) (nth 2 c)))
(let ((deleted
(lambda (keys needle)
(let ((b (generate-new-buffer "delete.fln")))
(switch-to-buffer b)
(insert "fn f(x: i64) -> i64\n if x > 0\n a\n else\n 3\n x\n\nfn g() -> i32 = 1\n")
(flan-fln-mode)
(evil-initialize-state)
(evil-normal-state)
(goto-char (point-min))
(search-forward needle)
(goto-char (match-beginning 0))
(execute-kbd-macro keys)
(prog1 (buffer-string)
(set-buffer-modified-p nil)
(kill-buffer b))))))
(dolist (c '(("dak" "else"
"fn f(x: i64) -> i64\n if x > 0\n a\n x\n\nfn g() -> i32 = 1\n")
("das" "else"
"fn f(x: i64) -> i64\n x\n\nfn g() -> i32 = 1\n")
("dii" "if x"
"fn f(x: i64) -> i64\n if x > 0\n else\n 3\n x\n\nfn g() -> i32 = 1\n")
("dad" "else" "fn g() -> i32 = 1\n")
("dad" "fn g" "fn f(x: i64) -> i64\n if x > 0\n a\n else\n 3\n x\n")))
(test-flan-fln--is (format "under Evil, %s leaves no blank line" (car c))
(funcall deleted (car c) (nth 1 c)) (nth 2 c))))
;; Comments: a block directly on a form is the form's; one a
;; blank line away, or below it, is not.
(let ((deleted
(lambda (keys needle)
(let ((b (generate-new-buffer "comments.fln")))
(switch-to-buffer b)
(insert "; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n\n"
"; on g\nfn g() -> ()\n ; on b\n b()\n")
(flan-fln-mode)
(evil-initialize-state)
(evil-normal-state)
(goto-char (point-min))
(search-forward needle)
(goto-char (match-beginning 0))
(execute-kbd-macro keys)
(prog1 (buffer-string)
(set-buffer-modified-p nil)
(kill-buffer b))))))
(dolist (c '(("dad" "a()"
"; loose\n\n; after f\n\n; on g\nfn g() -> ()\n ; on b\n b()\n")
("dad" "b()"
"; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n")
("did" "on g"
"; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n\n")
("das" "b()"
"; loose\n\n; on f\nfn f() -> ()\n a()\n; after f\n\n; on g\nfn g() -> ()\n")))
(test-flan-fln--is (format "under Evil, %s on %s keeps comments with their forms"
(car c) (nth 1 c))
(funcall deleted (car c) (nth 1 c)) (nth 2 c)))
;; A lone comment belongs to no form: id and ad find nothing.
(dolist (text '("fn a() -> i64\n 1\n\n; lone\n\nfn b() -> i64\n 2\n"
"fn a() -> i64\n 1\n\n; lone\n"))
(dolist (keys '("did" "dad"))
(let ((b (generate-new-buffer "lone.fln")))
(switch-to-buffer b)
(insert text)
(flan-fln-mode)
(evil-initialize-state)
(evil-normal-state)
(goto-char (point-min))
(search-forward "; lone")
(goto-char (match-beginning 0))
(ignore-errors (execute-kbd-macro keys))
(test-flan-fln--is (format "under Evil, %s on a lone comment%s changes nothing"
keys (if (string-suffix-p "lone\n" text) " at the end" ""))
(buffer-string) text)
(set-buffer-modified-p nil)
(kill-buffer b))))
;; A comment deeper than a form, at the end of its block, is
;; that block's, not the next form's.
(dolist (c '(("dad" "fn a" "fn a() -> i64\n 1\n ; end of a\nfn b() -> i64\n 2\n"
"fn b() -> i64\n 2\n")
("dad" "fn b" "fn a() -> i64\n 1\n ; end of a\nfn b() -> i64\n 2\n"
"fn a() -> i64\n 1\n ; end of a\n")
("das" "let y"
"fn a(x: i64) -> i64\n if x > 0\n 1\n ; end of the if\n let y = 2\n y\n"
"fn a(x: i64) -> i64\n if x > 0\n 1\n ; end of the if\n y\n")))
(let ((b (generate-new-buffer "owned.fln")))
(switch-to-buffer b)
(insert (nth 2 c))
(flan-fln-mode)
(evil-initialize-state)
(evil-normal-state)
(goto-char (point-min))
(search-forward (nth 1 c))
(goto-char (match-beginning 0))
(execute-kbd-macro (car c))
(test-flan-fln--is (format "under Evil, %s on %s: a deeper comment stays with the block above"
(car c) (nth 1 c))
(buffer-string) (nth 3 c))
(set-buffer-modified-p nil)
(kill-buffer b))))
(let ((b (generate-new-buffer "keys.fln")))
(switch-to-buffer b)
(insert "fn f() -> i32 = 1\n")
(flan-fln-mode)
(evil-initialize-state)
(test-flan--check "under Evil, C-x C-e is still the mode's"
(eq (key-binding (kbd "C-x C-e")) 'flan-fln-eval-last))
(goto-char (point-min))
(end-of-line)
(backward-char)
(test-flan--check "under Evil, C-x C-e counts the cursor's character"
(= (flan-fln--point-for-last) (line-end-position)))
(kill-buffer b)))
(evil-mode -1))))
;;; test-flan-fln.el ends here