The .fln mode knows else under a one-line if, one-line else, flat lets whose value goes on under them, typed lambda headers, restart reports and Dir.north arms, in its bounds, TAB, pause targets and colours

This commit is contained in:
Joseph Ferano 2026-09-26 00:52:55 +07:00
parent 3d97c73c32
commit 02fcb1725e
4 changed files with 328 additions and 40 deletions

View File

@ -1195,9 +1195,9 @@ Use `C-c C-g` if you need frames.
| Holy | Evil | Does | | Holy | Evil | Does |
|---|---|---| |---|---|---|
| `C-c C-c`, `C-M-x` | same | the top-level form: a declaration installed, anything else evaluated | | `C-c C-c`, `C-M-x` | same | the top-level form: a declaration installed, anything else evaluated |
| `C-u C-c C-c` | same | ...and stop at the innermost bracket group, else the statement on point's line: an elif's condition, an else's block, a match arm's value (`C-u C-u`: on entry) | | `C-u C-c C-c` | same | ...and stop at the innermost bracket group, else the statement on point's line: an elif's condition, an else's block or its value on the line, a match arm's value (`C-u C-u`: on entry) |
| `C-x C-e` | same, cursor on the line's last character | at a line's end, the innermost statement ending there: a match arm's value, an if/elif/while condition, or the whole statement a header or clause line opens; elsewhere, the term before point | | `C-x C-e` | same, cursor on the line's last character | at a line's end, the innermost statement ending there: a match arm's value, an if/elif/while condition, or the whole statement a header or clause line opens (a one-line `if c then a` with `else` under it included); elsewhere, the term before point |
| `C-c C-e` | same | the statement at point with its body and clauses, or the region's whole lines; on a bare `let x = v`, the `let` and the rest of its block | | `C-c C-e` | same | the statement at point with its body and clauses, or the region's whole lines; on a `let`, the `let` and the rest of its block |
| `C-c C-n` | same | `C-c C-e`, then move to the next statement | | `C-c C-n` | same | `C-c C-e`, then move to the next statement |
| `C-c C-s` | same | step through the top-level `fn` at point | | `C-c C-s` | same | step through the top-level `fn` at point |
| `C-c C-k` | same | the whole buffer | | `C-c C-k` | same | the whole buffer |
@ -1205,7 +1205,7 @@ Use `C-c C-g` if you need frames.
| `M-a` / `M-e` | `(` / `)` | statement: start / end (`)`: start of the next) | | `M-a` / `M-e` | `(` / `)` | statement: start / end (`)`: start of the next) |
| `C-M-u` | same | up to the enclosing bracket, or the line that owns the block | | `C-M-u` | same | up to the enclosing bracket, or the line that owns the block |
| `C-M-f` / `C-M-b` | same | brackets and terms, as everywhere | | `C-M-f` / `C-M-b` | same | brackets and terms, as everywhere |
| `TAB` | same | a line at a valid column stays; an empty or misplaced line goes deepest; each repeat steps out a level | | `TAB` | same | a line at a valid column stays; an empty or misplaced line goes deepest; each repeat steps out a level. One level deeper only after a line that opens a block: never after a `let`, unless its value goes on under it (`= match x`, `= if c`, a lambda header) |
| `DEL` in indentation | same | drop one level | | `DEL` in indentation | same | drop one level |
| `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level | | `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level |
| `M-<up>` / `M-<down>` | same | move the statement past its neighbour | | `M-<up>` / `M-<down>` | same | move the statement past its neighbour |
@ -1219,7 +1219,7 @@ Use `C-c C-g` if you need frames.
| — | `id` `ad` | top-level form with the comment block directly above it (`ad`: and the empty lines after it, or before it for the last form) | | — | `id` `ad` | top-level form with the comment block directly above it (`ad`: and the empty lines after it, or before it for the last form) |
`else`, `elif`, `on` and `restart` snap to their header's column as you type `else`, `elif`, `on` and `restart` snap to their header's column as you type
them. `indent-region` and `C-y` move lines only as a block, never one line them, a one-line `if c then a` and a `let s = if c` included. `indent-region` and `C-y` move lines only as a block, never one line
against another. expand-region steps term, group, statement, clause, against another. expand-region steps term, group, statement, clause,
enclosing statement, top-level form. enclosing statement, top-level form.
@ -1227,7 +1227,7 @@ enclosing statement, top-level form.
- **group**: a bracket pair and what is inside it. - **group**: a bracket pair and what is inside it.
- **statement**: a line, the deeper lines under it, lines inside brackets it leaves open, lines an operator continues, and `else`/`elif`/`on`/`restart` at its column. Blank and comment lines inside never end it. - **statement**: a line, the deeper lines under it, lines inside brackets it leaves open, lines an operator continues, and `else`/`elif`/`on`/`restart` at its column. Blank and comment lines inside never end it.
- **body**: a statement's own block, up to its first clause. - **body**: a statement's own block, up to its first clause.
- **clause**: one `else`/`elif`/`on`/`restart` line and its block. - **clause**: one `else`/`elif`/`on`/`restart` line and its block, or the value on its line (`else x`).
- **top-level form**: a column-0 line that is code, not a clause and not a continuation, through the last code line before the next one. - **top-level form**: a column-0 line that is code, not a clause and not a continuation, through the last code line before the next one.
`flan-fln-indent-offset` (2) is one level. `flan-fln-smartparens` (`t`) turns `flan-fln-indent-offset` (2) is one level. `flan-fln-smartparens` (`t`) turns

View File

@ -258,6 +258,67 @@ A word, and only when a space or the end of the line follows it."
(setq bol n)) (setq bol n))
bol)) bol))
;;; Words at a line's own depth
(defun flan-fln--find-top (re beg end)
"(BEG . END) of the first match of RE between BEG and END at BEG's bracket
depth, outside strings and comments, or nil."
(save-excursion
(goto-char beg)
(let ((depth (car (syntax-ppss beg))) hit)
(while (and (not hit) (re-search-forward re end t))
(let ((m (cons (match-beginning 0) (match-end 0))))
(let ((s (save-excursion (syntax-ppss (car m)))))
(if (and (= (car s) depth) (not (nth 8 s)))
(setq hit m)
(goto-char (cdr m))))))
hit)))
(defun flan-fln--joined-end (l)
"Where the code of the joined line starting at L ends."
(flan-fln--code-end (flan-fln--logical-end l)))
(defun flan-fln--then (l)
"Where the condition of the one-line if or elif at L ends, before its
`then', or nil when the line has no `then' of its own."
(let ((m (flan-fln--find-top "[ \t]+then[ \t]" (flan-fln--first-char l)
(flan-fln--joined-end l))))
(and m (car m))))
(defun flan-fln--value-start (l)
"Where the value of the joined line L starts, after its first `=' or
`+=' at the line's own depth, or nil when it binds or assigns nothing."
(let ((m (flan-fln--find-top "[ \t][-+*/]?=[ \t]+" (flan-fln--first-char l)
(flan-fln--joined-end l))))
(and m (cdr m))))
(defun flan-fln--lambda-header-p (pos end)
"Non-nil if a lambda with its body under it starts at POS and runs to END:
`fn(a, b)', or `fn(a: C, b) -> R', with no `= body' after it."
(save-excursion
(goto-char pos)
(and (looking-at "fn(")
(let ((close (ignore-errors (scan-lists (+ pos 2) 1 0))))
(and close (<= close end)
(progn (goto-char close) (skip-chars-forward " \t")
(or (>= (point) end)
(and (looking-at "->[ \t]")
(not (flan-fln--find-top "[ \t]=[ \t]" (point) end))))))))))
(defun flan-fln--value-opens-p (l)
"Non-nil if the value the joined line L binds or assigns goes on under it:
`= match x', `= if c' with no `then', `= handler-case', `= restart-case', or
a lambda header. These are the values lib/indent_reader.ml's `value_line'
reads a block for, besides a bare `=' and a call ending in `:'."
(let ((v (flan-fln--value-start l))
(end (flan-fln--joined-end l)))
(and v (< v end)
(save-excursion
(goto-char v)
(or (looking-at "\\(?:match\\|handler-case\\|handler-bind\\|restart-case\\)\\(?:[ \t]\\|$\\)")
(and (looking-at "if[ \t]") (not (flan-fln--then l)))
(flan-fln--lambda-header-p v end))))))
;;; Statements ;;; Statements
(defun flan-fln--statement-last (start &optional no-clauses) (defun flan-fln--statement-last (start &optional no-clauses)
@ -617,19 +678,27 @@ forms, where the clause line itself is not."
(and s (flan-fln--statement-bounds s))))))) (and s (flan-fln--statement-bounds s)))))))
(defun flan-fln--clause-target (l) (defun flan-fln--clause-target (l)
"What a clause line L stops at: an elif's condition, else its block." "What a clause line L stops at: an elif's condition, else its block.
A one-line `else x' stops at x."
(save-excursion (save-excursion
(goto-char (flan-fln--first-char l)) (goto-char (flan-fln--first-char l))
(if (looking-at "elif[ \t]+") (let ((end (flan-fln--joined-end l)))
(cons (match-end 0) (flan-fln--code-end (flan-fln--logical-end l))) (cond
(flan-fln--body-bounds l)))) ((looking-at "elif[ \t]+")
(cons (match-end 0) (or (flan-fln--then l) end)))
((and (looking-at "else[ \t]+") (< (match-end 0) end))
(cons (match-end 0) end))
(t (flan-fln--body-bounds l))))))
(defun flan-fln--condition (l) (defun flan-fln--condition (l)
"Bounds of the condition on the if, elif, while or until line L, or nil. "Bounds of the condition on the if, elif, while or until line L, or nil.
A loop's label, `while :outer c', is not part of it." A loop's label, `while :outer c', is not part of it."
(save-excursion (save-excursion
(goto-char (flan-fln--first-char l)) (goto-char (flan-fln--first-char l))
(when (looking-at "\\(?:if\\|elif\\|while\\|until\\)[ \t]+\\(?::[^ \t]+[ \t]+\\)?") ;; A one-line if's `then' ends no condition worth sending alone: the
;; line's last word is the value, and the if goes on to its clauses.
(when (and (not (flan-fln--then l))
(looking-at "\\(?:if\\|elif\\|while\\|until\\)[ \t]+\\(?::[^ \t]+[ \t]+\\)?"))
(let ((beg (match-end 0)) (let ((beg (match-end 0))
(end (flan-fln--code-end (flan-fln--logical-end l)))) (end (flan-fln--code-end (flan-fln--logical-end l))))
(and (< beg end) (cons beg end)))))) (and (< beg end) (cons beg end))))))
@ -684,7 +753,10 @@ a `(' -- a pattern's constructor, which binds nothing."
;; of the name (`is_neg_char' in lib/indent_reader.ml): `-n' is n. ;; of the name (`is_neg_char' in lib/indent_reader.ml): `-n' is n.
(when (string-match "\\`-[a-zA-Z$_*]" n) (when (string-match "\\`-[a-zA-Z$_*]" n)
(setq n (substring n 1))) (setq n (substring n 1)))
(unless (and skip-heads (eq (char-after) ?\()) ;; Nor, in a pattern, a dotted name: `Dir.north' is a value to
;; compare with, and only a bare name or `.x' binds.
(unless (and skip-heads (or (eq (char-after) ?\()
(string-match-p "\\`[^.]+\\." n)))
(push n names) (push n names)
(dolist (part (split-string n "\\." t)) (dolist (part (split-string n "\\." t))
(push part names))))) (push part names)))))
@ -818,11 +890,12 @@ before point. With ARG, stop there instead, as \\[flan-eval-last-sexp] does."
(save-excursion (goto-char last) (end-of-line) (save-excursion (goto-char last) (end-of-line)
(skip-chars-backward " \t\n" first) (point)))))) (skip-chars-backward " \t\n" first) (point))))))
(defun flan-fln--bare-let-p (start) (defun flan-fln--let-p (start)
"Non-nil if START's statement is a `let' with no block of its own." "Non-nil if START's statement is a `let'.
(and (save-excursion (goto-char (flan-fln--first-char start)) A let takes no block: lines under it are its value's (`= match x', a lambda
(looking-at "let[ \t]")) header), and its name lasts to the end of the block it is in."
(= (flan-fln--statement-last start) (flan-fln--logical-end start)))) (save-excursion (goto-char (flan-fln--first-char start))
(looking-at "let[ \t]")))
(defun flan-fln--block-rest (start) (defun flan-fln--block-rest (start)
"START's statement and every statement after it in the same block." "START's statement and every statement after it in the same block."
@ -837,20 +910,20 @@ before point. With ARG, stop there instead, as \\[flan-eval-last-sexp] does."
(defun flan-fln--statement-to-send (pos) (defun flan-fln--statement-to-send (pos)
"The statement at POS as sent by `C-c C-e': with its body and clauses, and "The statement at POS as sent by `C-c C-e': with its body and clauses, and
for a bare `let x = v', with the rest of its block, which is its scope." for a `let', with the rest of its block, which is its scope."
(let* ((l (flan-fln--line-statement pos)) (let* ((l (flan-fln--line-statement pos))
(arm (and l (flan-fln--arm l))) (arm (and l (flan-fln--arm l)))
(s (flan-fln--statement-start-at pos))) (s (flan-fln--statement-start-at pos)))
(cond (arm (flan-fln--arm-to-send l arm)) (cond (arm (flan-fln--arm-to-send l arm))
((null s) nil) ((null s) nil)
((flan-fln--bare-let-p s) (flan-fln--block-rest s)) ((flan-fln--let-p s) (flan-fln--block-rest s))
(t (flan-fln--statement-bounds s))))) (t (flan-fln--statement-bounds s)))))
;;;###autoload ;;;###autoload
(defun flan-fln-eval-statement (&optional arg) (defun flan-fln-eval-statement (&optional arg)
"Evaluate the statement at point, with its body and clauses. "Evaluate the statement at point, with its body and clauses.
With an active region, the lines it touches instead. On a bare `let x = v', With an active region, the lines it touches instead. On a `let', the
the `let' and the rest of its block, which is what it is in scope for; the `let' and the rest of its block, which is what it is in scope for; the
span sent is flashed so the extent is visible. A declaration at column 0 is span sent is flashed so the extent is visible. A declaration at column 0 is
installed. ARG is as for \\[flan-fln-eval-last]." installed. ARG is as for \\[flan-fln-eval-last]."
(interactive "P") (interactive "P")
@ -1032,17 +1105,24 @@ Before it at the same level, else out to the line that owns this block."
(goto-char start) (goto-char start)
(back-to-indentation) (back-to-indentation)
(and (looking-at (concat (regexp-opt flan-fln--opener-words t) (and (looking-at (concat (regexp-opt flan-fln--opener-words t)
"\\([ \t]\\|$\\)")) "\\(?:[ \t]\\|$\\)"))
(let ((w (match-string-no-properties 1)) (let* ((w (match-string-no-properties 1))
(alone (string= (match-string-no-properties 2) "")) (w-end (match-end 1))
(end (flan-fln--code-end last))) (end (flan-fln--code-end last))
(alone (>= w-end end)))
(cond (cond
((member w '("defer" "quote")) alone) ;; `else x' after an if's block is the whole else.
((member w '("defer" "quote" "else")) alone)
((member w '("fn" "fn-")) ((member w '("fn" "fn-"))
(not (re-search-forward "[ \t]=[ \t]" end t))) (not (re-search-forward "[ \t]=[ \t]" end t)))
((member w '("if" "elif")) ((member w '("if" "elif")) (not (flan-fln--then start)))
(not (re-search-forward "[ \t]then[ \t]" end t)))
(t t))))) (t t)))))
;; `let r = match n', `x = if c', `fn f(x) = match x', a lambda
;; header: the value goes on under the line.
(flan-fln--value-opens-p start)
;; A lambda header as a statement of its own.
(flan-fln--lambda-header-p (flan-fln--first-char start)
(flan-fln--code-end last))
(save-excursion (save-excursion
(goto-char (flan-fln--code-end last)) (goto-char (flan-fln--code-end last))
(let ((bol (line-beginning-position))) (let ((bol (line-beginning-position)))
@ -1050,9 +1130,7 @@ Before it at the same level, else out to the line that owns this block."
(looking-back "[ \t]->" bol) (looking-back "[ \t]->" bol)
;; `let x =' and `def colors =' with the value as a block, ;; `let x =' and `def colors =' with the value as a block,
;; which the author's list leaves out and the reader reads. ;; which the author's list leaves out and the reader reads.
(looking-back "[ \t]=" bol) (looking-back "[ \t]=" bol))))))
;; `let f = fn(x)' with its body under it.
(looking-back "\\_<fn([^()]*)" bol))))))
(defun flan-fln--stack (pos) (defun flan-fln--stack (pos)
"The open block columns above POS's line, deepest first, as (COL . LINE)." "The open block columns above POS's line, deepest first, as (COL . LINE)."
@ -1077,15 +1155,21 @@ Before it at the same level, else out to the line that owns this block."
(defun flan-fln--clause-columns (word pos) (defun flan-fln--clause-columns (word pos)
"Columns of the lines above POS a clause WORD may sit under, deepest first." "Columns of the lines above POS a clause WORD may sit under, deepest first."
(let ((heads (cdr (assoc word flan-fln--clause-headers)))) (let ((re (concat (regexp-opt (cdr (assoc word flan-fln--clause-headers)))
"\\(?:[ \t]\\|$\\)")))
(delq nil (delq nil
(mapcar (lambda (e) (mapcar (lambda (e)
(and (cdr e) (let ((l (cdr e)))
(and l
(save-excursion (save-excursion
(goto-char (flan-fln--first-char (cdr e))) (goto-char (flan-fln--first-char l))
(and (looking-at (concat (regexp-opt heads) (or (looking-at re)
"\\(?:[ \t]\\|$\\)")) ;; `let s = if c' and `r = match x' take
(car e))))) ;; their clauses at the line's column.
(and (flan-fln--value-opens-p l)
(progn (goto-char (flan-fln--value-start l))
(looking-at re)))))
(car e))))
(flan-fln--stack pos))))) (flan-fln--stack pos)))))
(defun flan-fln--bracket-column (open) (defun flan-fln--bracket-column (open)
@ -1410,6 +1494,21 @@ it, so a block pasted at another depth stays one block."
(defconst flan-fln--name-re "\\([^][ \t\n(){},;\":]+\\)" (defconst flan-fln--name-re "\\([^][ \t\n(){},;\":]+\\)"
"A declared name: a run up to a bracket, a space, or the colon of `x: T'.") "A declared name: a run up to a bracket, a space, or the colon of `x: T'.")
(defun flan-fln--return-type-matcher (limit)
"Find the next return type up to LIMIT: after the `->' of a fn header, a
lambda or a `Fn(...)' type, and not after a match arm's."
(let (found)
(while (and (not found)
(re-search-forward "[ \t]->[ \t]+\\([$a-zA-Z][^][ \t\n(){},;\"=]*\\)"
limit t))
(let ((arrow (match-beginning 0)))
(setq found (save-excursion
(save-match-data
(goto-char (line-beginning-position))
(re-search-forward "\\(?:^[ \t]*fn-?[ \t]\\|\\_<C?[fF]n(\\)"
arrow t))))))
found))
(defvar flan-fln-font-lock-keywords (defvar flan-fln-font-lock-keywords
`(;; The header words, at the start of a line and followed by a space or the `(;; The header words, at the start of a line and followed by a space or the
;; end of it: `if(c, a)' is the fallback call and is not a header. ;; end of it: `if(c, a)' is the fallback call and is not a header.
@ -1421,6 +1520,14 @@ it, so a block pasted at another depth stays one block."
1 font-lock-type-face) 1 font-lock-type-face)
(,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re) (,(concat "^\\(?:def\\|once\\|const\\)[ \t]+" flan-fln--name-re)
1 font-lock-variable-name-face) 1 font-lock-variable-name-face)
;; A restart clause's name, `restart retry() "Try again"'.
(,(concat "^[ \t]*restart[ \t]+" flan-fln--name-re)
1 font-lock-function-name-face)
;; A lambda's `fn', glued to its parameters.
("\\(?:^\\|[ \t=(,]\\)\\(fn\\)(" 1 font-lock-keyword-face)
;; An enum member written `Dir.north', a constant as `:north' is.
("\\_<[A-Z][^][ \t\n(){},;\":.]*\\.[^][ \t\n(){},;\":.]+\\_>"
. font-lock-constant-face)
;; The words inside a line: `for i in range(n)', `if c then a else b', a ;; The words inside a line: `for i in range(n)', `if c then a else b', a
;; `where' constraint. ;; `where' constraint.
("[ \t]\\(then\\|else\\|in\\|where\\)[ \t]" 1 font-lock-keyword-face) ("[ \t]\\(then\\|else\\|in\\|where\\)[ \t]" 1 font-lock-keyword-face)
@ -1432,7 +1539,7 @@ it, so a block pasted at another depth stays one block."
("\\(?:^\\|[][ \t(){},]\\)\\(:[^][ \t\n(){},;\":]+\\)" 1 font-lock-constant-face) ("\\(?:^\\|[][ \t(){},]\\)\\(:[^][ \t\n(){},;\":]+\\)" 1 font-lock-constant-face)
;; A type: after the `: ' of an annotation and after `-> '. ;; A type: after the `: ' of an annotation and after `-> '.
("[^ \t\n:]:[ \t]+\\([$a-zA-Z][^][ \t\n(){},;\"=]*\\)" 1 font-lock-type-face) ("[^ \t\n:]:[ \t]+\\([$a-zA-Z][^][ \t\n(){},;\"=]*\\)" 1 font-lock-type-face)
("[ \t]->[ \t]+\\([$a-zA-Z][^][ \t\n(){},;\"=]*\\)" 1 font-lock-type-face) (flan-fln--return-type-matcher 1 font-lock-type-face)
;; The package half of a qualified name, as `flan-mode' draws it. ;; The package half of a qualified name, as `flan-mode' draws it.
("\\_<\\([a-zA-Z][a-zA-Z0-9!?*+=<>._-]*/\\)" 1 font-lock-type-face) ("\\_<\\([a-zA-Z][a-zA-Z0-9!?*+=<>._-]*/\\)" 1 font-lock-type-face)
("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|dyn\\|Never\\|Allocator\\|Ptr\\|Option\\|Vec\\|Map\\|C?Fn\\)\\_>" ("\\_<\\(?:[iu]\\(?:8\\|16\\|32\\|64\\)\\|f\\(?:32\\|64\\)\\|bool\\|string\\|dyn\\|Never\\|Allocator\\|Ptr\\|Option\\|Vec\\|Map\\|C?Fn\\)\\_>"

View File

@ -52,6 +52,33 @@ fn pick(d: Dir) -> i64
_ -> _ ->
twice(3) twice(3)
fn sign2(k: i64)
if k < 0 then -1
elif k == 0 then 0
else 1
fn size(k: i64) -> i64
if k < 10
1
else 2
fn lam(k: i64) -> i64
let add = fn(a: i64, b: i64) -> i64 = a + b
let dbl = fn(a: i64) -> i64
a * 2
add(dbl(k), 1)
fn rs() -> i64
restart-case
3
restart retry() \"Try again\"
4
fn dir(d: Dir) -> i64
match d
Dir.north -> 7
_ -> 8
comment(): comment():
if 1 < 2 and if 1 < 2 and
3 < 4 3 < 4
@ -59,6 +86,9 @@ comment():
elif 1 > 2 or elif 1 > 2 or
3 > 4 3 > 4
0 0
if 2 < 1 then 5
elif 2 == 1 then 6
else 7
twice(4) twice(4)
if 2 > 1 if 2 > 1
twice(2) twice(2)
@ -158,6 +188,27 @@ comment():
(test-flan--check (funcall name "and moves to the next") (test-flan--check (funcall name "and moves to the next")
(looking-at "if 2 > 1")) (looking-at "if 2 > 1"))
;; A one-line if with its clauses on the lines under it.
(funcall goto "if 2 < 1 then 5" t)
(flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e at the end of a one-line if that goes on evaluates the whole if")
(funcall shows "7"))
(funcall goto "elif 2 == 1")
(flan-fln-eval-statement)
(test-flan--check (funcall name "C-c C-e on a clause under a one-line if sends its if")
(funcall shows "7"))
;; The newer forms install from the buffer and run.
(pcase-dolist (`(,needle ,call ,want)
'(("fn sign2" "(sign2 0)" "0")
("fn size" "(size 20)" "2")
("fn lam" "(lam 3)" "7")
("fn rs" "(rs)" "3")
("fn dir" "(dir :north)" "7")))
(funcall goto needle)
(flan-fln-eval-defun)
(test-flan--check (funcall name (format "C-c C-c installs %s" needle))
(equal (funcall value call) want)))
;; The pause mark. What is sent is a line and column, and the daemon ;; The pause mark. What is sent is a line and column, and the daemon
;; answers `:pause' only when a form the reader made starts exactly ;; answers `:pause' only when a form the reader made starts exactly
;; there (`Ast.mark_pause'). Each kind of target once, and one position ;; there (`Ast.mark_pause'). Each kind of target once, and one position
@ -217,7 +268,15 @@ comment():
("n < 2" "an if statement") ("n < 2" "an if statement")
("t = 0" "a let") ("t = 0" "a let")
("i in range" "a for") ("i in range" "a for")
("+ i" "an assignment"))) ("+ i" "an assignment")
("if k < 0 then" "a one-line if")
("elif k" "a one-line elif, at its condition")
("else 1" "a one-line else, at its value")
("else 2" "a one-line else after a block, at its value")
("a: i64, b" "a typed lambda, from its fn")
("a * 2" "a typed lambda's block")
("restart retry" "a restart with a report, at its block")
("Dir.north" "an enum member's arm, at its value")))
(funcall goto (car c)) (funcall goto (car c))
(let ((reply (flan-fln-eval-defun '(4)))) (let ((reply (flan-fln-eval-defun '(4))))
(test-flan--check (funcall name (format "C-u C-c C-c marks %s where the reader starts it" (test-flan--check (funcall name (format "C-u C-c C-c marks %s where the reader starts it"

View File

@ -584,6 +584,76 @@ comment:
(test-flan-fln--is "on an on line, at its block" (test-flan-fln--is "on an on line, at its block"
(test-flan-fln--pause-at "on Error") '(18 5)) (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-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 ;;; Names and colours
(test-flan-fln--in "fn f(x: i64) -> i64\n comment:\n g(:key-r, x)\n 0x1F + 12\n" (test-flan-fln--in "fn f(x: i64) -> i64\n comment:\n g(:key-r, x)\n 0x1F + 12\n"
@ -606,6 +676,30 @@ comment:
(test-flan--check "and the name before the colon is not a keyword" (test-flan--check "and the name before the colon is not a keyword"
(null (funcall face "x:"))))) (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)))
;;; Indentation ;;; Indentation
(defun test-flan-fln--tabs (text n) (defun test-flan-fln--tabs (text n)
@ -660,6 +754,34 @@ comment:
(test-flan-fln--tabs " let v = [\n 1 2\n|]" 1) 2) (test-flan-fln--tabs " let v = [\n 1 2\n|]" 1) 2)
(test-flan-fln--is "after a line ending in an operator, deeper than its statement" (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--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)
(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 "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 |" (test-flan-fln--in "fn f() -> ()\n while a\n b()\n |"
(flan-fln-dedent-or-delete 1) (flan-fln-dedent-or-delete 1)