.fln files open in flan-fln-mode, which evaluates, moves over, indents and edits statements, clauses and top-level forms, and every Flan mode derives from flan-base-mode

This commit is contained in:
Joseph Ferano 2026-09-25 17:13:12 +07:00
parent 0f9160f3bc
commit 8c814b39c1
11 changed files with 2393 additions and 32 deletions

View File

@ -1178,6 +1178,53 @@ Use `C-c C-g` if you need frames.
--- ---
## Indented files (.fln)
`.fln` files open in `flan-fln-mode`. The session keys (`C-c C-b`, `C-c C-i`,
`C-c C-k`, the REPL, watch, dape) work as in a `.flan` file; these differ.
| Holy | Evil | Does |
|---|---|---|
| `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 (`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; 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-n` | same | `C-c C-e`, then move to the next statement |
| `C-c C-k` | same | the whole buffer |
| `C-M-a` / `C-M-e` / `C-M-h` | `[[` / `]]` | top-level form: start, end, mark |
| `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-f` / `C-M-b` | same | brackets and terms, as everywhere |
| `TAB` | same | deepest valid column first; each repeat steps out a level |
| `DEL` in indentation | same | drop one level |
| `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level |
| `M-<up>` / `M-<down>` | same | move the statement past its neighbour |
| `M-<right>` / `M-<left>` | same | pull the next statement into this block / push its last one out |
| `M-r` | same | replace the block's owner with the statement at point |
| `M-k` | `das` | kill the statement's lines |
| — | `ie` `ae` | term |
| — | `is` `as` | statement (`as`: whole lines) |
| — | `ii` `ai` | body / whole statement |
| — | `ik` `ak` | clause's block / clause |
| — | `id` `ad` | top-level form (`ad`: with the blank lines after it) |
`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
against another. expand-region steps term, group, statement, clause,
enclosing statement, top-level form.
- **term**: a run with no space outside brackets — `f(a, b)`, `grid[r, c]`, `p.x`.
- **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.
- **body**: a statement's own block, up to its first clause.
- **clause**: one `else`/`elif`/`on`/`restart` line and its block.
- **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
on plain `smartparens-mode`, which pairs brackets and strings but not `'`.
---
## Full key reference ## Full key reference
| Key | Does | | Key | Does |
@ -1257,6 +1304,7 @@ fix is to delete `-dev` from it.
| File | What it is | | File | What it is |
|---|---| |---|---|
| `flan-mode.el` | the major mode: syntax, indentation, imenu, the keymap | | `flan-mode.el` | the major mode: syntax, indentation, imenu, the keymap |
| `flan-fln-mode.el` | the mode for indented `.fln` files: objects, keys, indentation |
| `flan.el` | the client — the socket, evaluation, xref, eldoc, completion | | `flan.el` | the client — the socket, evaluation, xref, eldoc, completion |
| `flan-repl.el` | the `*flan-repl*` buffer | | `flan-repl.el` | the `*flan-repl*` buffer |
| `flan-watch.el` | watched values: the program pushes, this paints them in a buffer and inline | | `flan-watch.el` | watched values: the program pushes, this paints them in a buffer and inline |

View File

@ -89,14 +89,14 @@ does and does not buy."
:type '(repeat string)) :type '(repeat string))
(defun flan-dape--source () (defun flan-dape--source ()
"The .flan file this session is about. "The .flan or .fln file this session is about.
The buffer's own file, or the nearest one up from it — so M-x flan-debug The buffer's own file, or the nearest one up from it — so M-x flan-debug
from a *compilation* buffer or a dired still has an answer." from a *compilation* buffer or a dired still has an answer."
(or (and buffer-file-name (or (and buffer-file-name
(string-suffix-p ".flan" buffer-file-name) (string-match-p "\\.fla?n\\'" buffer-file-name)
buffer-file-name) buffer-file-name)
(car (directory-files default-directory t "\\.flan\\'")) (car (directory-files default-directory t "\\.fla?n\\'"))
(user-error "No .flan file here to debug"))) (user-error "No .flan or .fln file here to debug")))
(defun flan-dape--binary (source) (defun flan-dape--binary (source)
"Where the debug build of SOURCE goes. "Where the debug build of SOURCE goes.
@ -126,7 +126,7 @@ one made by `flan build'."
;; can just read `buffer-file-name'. `dape' itself expects an already ;; can just read `buffer-file-name'. `dape' itself expects an already
;; evaluated config, so `flan-debug' below must not hand it the raw entry. ;; evaluated config, so `flan-debug' below must not hand it the raw entry.
(defconst flan-dape-config (defconst flan-dape-config
'(modes (flan-mode) '(modes (flan-base-mode)
ensure dape-ensure-command ensure dape-ensure-command
command-cwd dape-command-cwd command-cwd dape-command-cwd
compile (flan-dape--compile-command (flan-dape--source)) compile (flan-dape--compile-command (flan-dape--source))
@ -178,7 +178,7 @@ common case is one command rather than a config prompt."
;; thing entirely. ;; thing entirely.
;;;###autoload ;;;###autoload
(with-eval-after-load 'flan-mode (with-eval-after-load 'flan-mode
(define-key (symbol-value 'flan-mode-map) (kbd "C-c C-g") #'flan-debug)) (define-key (symbol-value 'flan-base-mode-map) (kbd "C-c C-g") #'flan-debug))
;;; --dev and --debug are different builds ;;; --dev and --debug are different builds
;; ;;

1449
emacs/flan-fln-mode.el Normal file

File diff suppressed because it is too large Load Diff

View File

@ -438,17 +438,14 @@ For `syntax-propertize-function'."
;; not. Either way forward, which is the whole of why this terminates. ;; not. Either way forward, which is the whole of why this terminates.
(goto-char (or fin from))))) (goto-char (or fin from)))))
(defvar flan-mode-map ;; The keys both syntaxes share: everything that talks to the running program
;; about a name, a value or the session rather than about a piece of the text.
;; The keys that pick text out of the buffer -- which form C-c C-c means --
;; are each child mode's own, because what a form is differs between them.
(defvar flan-base-mode-map
(let ((map (make-sparse-keymap))) (let ((map (make-sparse-keymap)))
;; Autoloaded from flan.el, so the client loads on first use. ;; Autoloaded from flan.el, so the client loads on first use.
(define-key map (kbd "C-c C-c") #'flan-eval-defun)
;; The same command on the binding SLIME and CIDER put it on. Emacs binds
;; C-M-x to eval-defun only in `emacs-lisp-mode-map', so a mode derived
;; from `lisp-mode' inherits nothing and the key is undefined — which
;; reads as the client being broken rather than as the key being free.
(define-key map (kbd "C-M-x") #'flan-eval-defun)
(define-key map (kbd "C-c C-k") #'flan-eval-buffer) (define-key map (kbd "C-c C-k") #'flan-eval-buffer)
(define-key map (kbd "C-x C-e") #'flan-eval-last-sexp)
(define-key map (kbd "C-c C-z") #'flan-connect) (define-key map (kbd "C-c C-z") #'flan-connect)
(define-key map (kbd "C-c C-q") #'flan-disconnect) (define-key map (kbd "C-c C-q") #'flan-disconnect)
(define-key map (kbd "C-c C-d") #'flan-describe) (define-key map (kbd "C-c C-d") #'flan-describe)
@ -499,23 +496,45 @@ For `syntax-propertize-function'."
;; because it is the one that works from any state. ;; because it is the one that works from any state.
(define-key map (kbd "C-c C-M-x") #'flan-rerun) (define-key map (kbd "C-c C-M-x") #'flan-rerun)
map) map)
"Keymap for every Flan source buffer, `flan-mode' and `flan-fln-mode'.")
(defvar flan-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map flan-base-mode-map)
(define-key map (kbd "C-c C-c") #'flan-eval-defun)
;; The same command on the binding SLIME and CIDER put it on. Emacs binds
;; C-M-x to eval-defun only in `emacs-lisp-mode-map', so a mode derived
;; from `lisp-mode' inherits nothing and the key is undefined — which
;; reads as the client being broken rather than as the key being free.
(define-key map (kbd "C-M-x") #'flan-eval-defun)
(define-key map (kbd "C-x C-e") #'flan-eval-last-sexp)
map)
"Keymap for `flan-mode'.") "Keymap for `flan-mode'.")
;; The parent of both source modes. Everything the dev loop asks of a buffer
;; -- is this Flan, set up eldoc and completion, draw the program's names,
;; paint watched values -- asks it of this mode, so a .fln buffer gets it the
;; same way a .flan buffer does. What each syntax reads as a form is its
;; child's business.
(define-derived-mode flan-base-mode prog-mode "Flan"
"Parent mode of the Flan source modes, `flan-mode' and `flan-fln-mode'."
(setq-local comment-start ";")
(setq-local comment-start-skip ";+ *")
(setq-local comment-add 1)
;; Spaces. The whole corpus is written with them, and alignment that is
;; correct here is alignment under a specific *column* — a tab makes that
;; depend on a setting the file cannot carry. In a .fln file a tab in the
;; indentation is an error besides.
(setq-local indent-tabs-mode nil))
;;;###autoload ;;;###autoload
(define-derived-mode flan-mode prog-mode "Flan" (define-derived-mode flan-mode flan-base-mode "Flan"
"Major mode for editing Flan. "Major mode for editing Flan.
\\{flan-mode-map}" \\{flan-mode-map}"
:syntax-table flan-mode-syntax-table :syntax-table flan-mode-syntax-table
(setq-local comment-start ";")
(setq-local comment-start-skip ";+ *")
(setq-local comment-add 1)
(setq-local font-lock-defaults '(flan-font-lock-keywords)) (setq-local font-lock-defaults '(flan-font-lock-keywords))
(setq-local indent-line-function #'lisp-indent-line) (setq-local indent-line-function #'lisp-indent-line)
;; Spaces. The whole corpus is written with them, and alignment that is
;; correct here is alignment under a specific *column* — a tab makes that
;; depend on a setting the file cannot carry.
(setq-local indent-tabs-mode nil)
(setq-local lisp-indent-function #'flan-indent-function) (setq-local lisp-indent-function #'flan-indent-function)
(setq-local outline-regexp ";;;;+[ \t]*") (setq-local outline-regexp ";;;;+[ \t]*")
(setq-local imenu-generic-expression flan-imenu-generic-expression) (setq-local imenu-generic-expression flan-imenu-generic-expression)
@ -797,6 +816,12 @@ decision to `calculate-lisp-indent'."
;;;###autoload ;;;###autoload
(add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode)) (add-to-list 'auto-mode-alist '("\\.flan\\'" . flan-mode))
;; The indented syntax's mode lives in its own file; a buffer of it is the first
;; thing that loads it.
;;;###autoload
(autoload 'flan-fln-mode "flan-fln-mode" nil t)
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.fln\\'" . flan-fln-mode))
;;; The other Flan buffers under Evil ;;; The other Flan buffers under Evil

View File

@ -249,15 +249,19 @@ inline it has no modeline beside it to say so.")
(let (bufs) (let (bufs)
(dolist (w (window-list-1 nil 'nomini t)) (dolist (w (window-list-1 nil 'nomini t))
(let ((b (window-buffer w))) (let ((b (window-buffer w)))
(when (and (eq (buffer-local-value 'major-mode b) 'flan-mode) (when (and (provided-mode-derived-p (buffer-local-value 'major-mode b)
'flan-base-mode)
(not (memq b bufs))) (not (memq b bufs)))
(push b bufs)))) (push b bufs))))
bufs)) bufs))
(defun flan-watch--ghost-sites () (defun flan-watch--ghost-sites ()
"Watch call sites in the current buffer, as a list of (NAME . END-OF-LINE)." "Watch call sites in the current buffer, as a list of (NAME . END-OF-LINE)."
(let ((re (concat "(\\s-*" flan-watch-ghost-call-regexp ;; Either syntax: `(watch "name" v)' in a .flan file, `watch("name", v)' in
"\\s-+\"\\([^\"\n]*\\)\"")) ;; a .fln one, where the call is the name glued to its parenthesis.
(let ((re (concat "\\(?:(\\s-*\\(?:" flan-watch-ghost-call-regexp "\\)\\s-+"
"\\|\\_<\\(?:" flan-watch-ghost-call-regexp "\\)(\\s-*\\)"
"\"\\([^\"\n]*\\)\""))
(sites nil)) (sites nil))
(save-excursion (save-excursion
(goto-char (point-min)) (goto-char (point-min))

View File

@ -953,7 +953,7 @@ refuses: callers cannot silently discard a running program's state."
;; buffer is not what you meant — and it still only changes what is asked, ;; buffer is not what you meant — and it still only changes what is asked,
;; never which file the unasked case picks. ;; never which file the unasked case picks.
(let ((file (and buffer-file-name (let ((file (and buffer-file-name
(string-suffix-p ".flan" buffer-file-name) (string-match-p "\\.fla?n\\'" buffer-file-name)
(expand-file-name buffer-file-name)))) (expand-file-name buffer-file-name))))
(list (if (and file (not current-prefix-arg)) (list (if (and file (not current-prefix-arg))
file file
@ -1143,7 +1143,7 @@ the state with something to answer in it."
(defun flan-mode-line () (defun flan-mode-line ()
"The Flan connection indicator, for `mode-line-misc-info'." "The Flan connection indicator, for `mode-line-misc-info'."
(when (derived-mode-p 'flan-mode 'flan-repl-mode) (when (derived-mode-p 'flan-base-mode 'flan-repl-mode)
(pcase (flan-state) (pcase (flan-state)
;; First, and it names the condition: a stopped program looks exactly ;; First, and it names the condition: a stopped program looks exactly
;; like a running one from anywhere else in Emacs, and the whole reason ;; like a running one from anywhere else in Emacs, and the whole reason
@ -2108,7 +2108,7 @@ Leaves its face in `flan--dynamic-face' for the rule that calls this."
(defun flan--dynamic-install () (defun flan--dynamic-install ()
"Add or remove the dynamic rules in the current buffer, and redraw it. "Add or remove the dynamic rules in the current buffer, and redraw it.
Called for its effect on one buffer; `flan--dynamic-sync' does every buffer." Called for its effect on one buffer; `flan--dynamic-sync' does every buffer."
(when (derived-mode-p 'flan-mode) (when (derived-mode-p 'flan-base-mode)
;; Removed first in both branches, because adding is not idempotent: a ;; Removed first in both branches, because adding is not idempotent: a
;; second install would put the rule in twice and every refresh after that ;; second install would put the rule in twice and every refresh after that
;; would add another. ;; would add another.
@ -2127,7 +2127,7 @@ Called for its effect on one buffer; `flan--dynamic-sync' does every buffer."
;; A file opened while a session is already up: the two moments the table is ;; A file opened while a session is already up: the two moments the table is
;; rebuilt are both in the past by then, so the buffer has to ask on its way in. ;; rebuilt are both in the past by then, so the buffer has to ask on its way in.
(add-hook 'flan-mode-hook #'flan--dynamic-install) (add-hook 'flan-base-mode-hook #'flan--dynamic-install)
(defun flan--forget-defs () (defun flan--forget-defs ()
"Drop what is known about the program's names." "Drop what is known about the program's names."
@ -2414,14 +2414,14 @@ someone editing Flan with no program running and this file never loaded."
(setq-local mode-line-misc-info (setq-local mode-line-misc-info
(append mode-line-misc-info '((:eval (flan-mode-line))))))) (append mode-line-misc-info '((:eval (flan-mode-line)))))))
(add-hook 'flan-mode-hook #'flan-setup) (add-hook 'flan-base-mode-hook #'flan-setup)
;; Buffers that were already in flan-mode when this file loaded: the client is ;; Buffers that were already in flan-mode when this file loaded: the client is
;; autoloaded on first use, so by the time it arrives the file being edited has ;; autoloaded on first use, so by the time it arrives the file being edited has
;; long since had its mode hooks run. ;; long since had its mode hooks run.
(dolist (b (buffer-list)) (dolist (b (buffer-list))
(with-current-buffer b (with-current-buffer b
(when (derived-mode-p 'flan-mode) (flan-setup)))) (when (derived-mode-p 'flan-base-mode) (flan-setup))))
;;; Evaluating ;;; Evaluating
@ -2621,7 +2621,8 @@ columns already were, because a top-level form starts at column 1."
.fln file, the paren reader's for anything else. Sent explicitly because the .fln file, the paren reader's for anything else. Sent explicitly because the
daemon cannot tell from `:file' — an expansion shown in parens is sent back daemon cannot tell from `:file' — an expansion shown in parens is sent back
under the name of the .fln file it came from." under the name of the .fln file it came from."
(if (and buffer-file-name (string-suffix-p ".fln" buffer-file-name)) (if (or (derived-mode-p 'flan-fln-mode)
(and buffer-file-name (string-suffix-p ".fln" buffer-file-name)))
"indented" "indented"
"paren")) "paren"))

View File

@ -1926,6 +1926,11 @@ stopped program, which is the case where it should fire."
(file-name-directory load-file-name)) (file-name-directory load-file-name))
nil t) nil t)
;; The .fln mode: its objects, keys, indentation and text objects, from text.
(load (expand-file-name "test-flan-fln.el"
(file-name-directory load-file-name))
nil t)
;; Ghost text, which is the same kind of thing: rows in, overlays out, and the ;; Ghost text, which is the same kind of thing: rows in, overlays out, and the
;; buffer it reads is a fixture like any other reply here. Loaded for the same ;; buffer it reads is a fixture like any other reply here. Loaded for the same
;; reason. ;; reason.

171
emacs/test-flan-fln-live.el Normal file
View File

@ -0,0 +1,171 @@
;;; test-flan-fln-live.el --- The .fln keys against a real daemon -*- lexical-binding: t; -*-
;; Loaded by test-flan.el, near its end, with `flan-command' already the
;; compiler under test. test-flan-fln.el checks which text each key picks;
;; this checks the reader accepts that text as a whole form, on both backends:
;; a daemon of its own on a .fln file with no main, once on x86 and once on
;; LLVM, each key sent at least once, and a pause mark the daemon must find.
;;; Code:
(require 'flan-fln-mode)
(declare-function test-flan--check "test-flan" (name ok))
(declare-function test-flan--result "test-flan" ())
(defvar test-flan-fln-live-dir)
(defvar test-flan-fln-live-socket)
(defconst test-flan-fln-live--program
"fn fib(n: i64) -> i64
if n < 2
n
else
fib(n - 1) + fib(n - 2)
fn twice(n: i64) -> i64 = n * 2
fn total(n: i32) -> i32
let t = 0
for i in range(n)
t = t + i
t
comment():
twice(4)
if 2 > 1
twice(2)
else
0
let x = 3
twice(x) + 1
")
(defun test-flan-fln-live--run (backend args)
(let* ((sock (concat test-flan-fln-live-socket "-fln-" backend))
(file (expand-file-name (format "fln-live-%s.fln" backend)
test-flan-fln-live-dir))
(flan-daemon-args args)
(name (lambda (s) (format "%s: %s" backend s)))
(value (lambda (code)
(plist-get (flan--request
(list :op "eval-expr" :code code :file "<test>"))
:value)))
(goto (lambda (needle &optional after)
(goto-char (point-min))
(search-forward needle)
(unless after (goto-char (match-beginning 0)))))
(shows (lambda (v)
(let ((r (test-flan--result)))
(prog1 (and r (string-match-p (concat "=> " (regexp-quote v) "\\'")
(string-trim r)))
(flan-clear-result))))))
(with-temp-file file (insert test-flan-fln-live--program))
(ignore-errors (delete-file sock))
(flan file sock)
(test-flan--check (funcall name "a daemon starts on a .fln file")
(process-live-p flan--connection))
(unwind-protect
(with-current-buffer (find-file-noselect file)
(test-flan--check (funcall name "which opens in flan-fln-mode")
(eq major-mode 'flan-fln-mode))
;; C-c C-c, from inside a fn changed in the buffer.
(funcall goto "n * 2")
(delete-char 5)
(insert "n * 3")
(flan-fln-eval-defun)
(test-flan--check (funcall name "C-c C-c installs the fn at point")
(equal (funcall value "(twice 7)") "21"))
;; C-x C-e at the end of a column-0 declaration.
(funcall goto "n * 3")
(delete-char 5)
(insert "n * 4")
(flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e at the end of a column-0 fn installs it")
(equal (funcall value "(twice 7)") "28"))
;; C-x C-e at the end of an inner statement: text from column 3.
(funcall goto "twice(4)" t)
(flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e sends the statement ending at point")
(funcall shows "16"))
;; ...and inside a line, the term before point.
(funcall goto "twice(4)")
(let ((at (point)))
(insert "fib(10) + ")
(goto-char (+ at (length "fib(10)")))
(flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e inside a line sends the term before point")
(funcall shows "55"))
(delete-region at (+ at (length "fib(10) + "))))
;; C-c C-e on a clause: the whole if, from column 3, clauses and all.
(funcall goto "else\n 0")
(flan-fln-eval-statement)
(test-flan--check (funcall name "C-c C-e on a clause sends its if, and it reads")
(funcall shows "8"))
;; A bare let: it and the rest of its block, which is its scope.
(funcall goto "let x = 3")
(flan-fln-eval-statement)
(test-flan--check (funcall name "C-c C-e on a bare let sends its scope with it")
(funcall shows "13"))
;; A region of several statements reads as one (do ...).
(funcall goto "twice(4)")
(transient-mark-mode 1)
(set-mark (point))
(funcall goto "else\n 0" t)
(flan-fln-eval-statement)
(test-flan--check (funcall name "C-c C-e on a region of statements evaluates them in order")
(funcall shows "8"))
;; C-c C-n sends and moves on.
(funcall goto "twice(4)")
(flan-fln-eval-statement-and-next)
(test-flan--check (funcall name "C-c C-n sends the statement")
(funcall shows "16"))
(test-flan--check (funcall name "and moves to the next")
(looking-at "if 2 > 1"))
;; 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
;; there (`Ast.mark_pause'). Each kind of target once, and one position
;; a column off to show the answer can be no.
(dolist (c '(("n - 1)" "a call, from its name")
("n < 2" "an if statement")
("t = 0" "a let")
("i in range" "a for")
("+ i" "an assignment")))
(funcall goto (car c))
(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"
(cadr c)))
(plist-get reply :pause)))
(flan-fln-eval-defun))
(test-flan--check (funcall name "and a plain C-c C-c takes the mark down")
(null (flan--pause-overlays)))
(funcall goto "fib(n - 1)")
(let* ((b (flan-fln--toplevel-bounds (point)))
(off (condition-case nil
(plist-get (flan--eval (flan--text (car b) (cdr b)) "defn"
nil nil (cons (1+ (point)) (+ 3 (point))))
:pause)
(user-error nil))))
(test-flan--check (funcall name "a position one column off the call is not taken")
(null off)))
(flan-fln-eval-defun)
(set-buffer-modified-p nil)
(kill-buffer))
;; Stopped whatever happened above: a daemon this started is its own to end.
(flan-quit))
(ignore-errors (delete-file sock))
(ignore-errors (delete-file file))))
(message "\nthe .fln keys, against a daemon on each backend")
(test-flan-fln-live--run "x86" nil)
(test-flan-fln-live--run "llvm" '("--llvm"))
;;; test-flan-fln-live.el ends here

647
emacs/test-flan-fln.el Normal file
View File

@ -0,0 +1,647 @@
;;; 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 left? = col > 0
if left? or right?
let side =
if not left?
1
elif not 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 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 left?
1
elif not 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 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 left?
1
elif not 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"))
(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 "def 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 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 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)")
(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))))
;;; 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 "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 "def 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 "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 its opening line's column"
(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--tabs " if a and\n|b" 1) 4)
(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"))
;;; 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 "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)
'("right?" "elif not right?" "if not left?" "let side ="
"if left? or right?" "let 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 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")
("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 ((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

View File

@ -2455,6 +2455,13 @@ already rely on it — so nothing here is a stand-in for the real thing."
(ignore-errors (delete-file socket6)) (ignore-errors (delete-file socket6))
(ignore-errors (delete-file scratch))) (ignore-errors (delete-file scratch)))
;; ── The .fln keys, against daemons of their own ────────────────────────
(setq test-flan-fln-live-dir (file-name-directory file)
test-flan-fln-live-socket socket)
(load (expand-file-name "test-flan-fln-live.el"
(file-name-directory load-file-name))
nil t)
(if (zerop test-flan--failures) (if (zerop test-flan--failures)
(message "flan.el: all tests passed") (message "flan.el: all tests passed")
(message "\n%d failure(s)" test-flan--failures) (message "\n%d failure(s)" test-flan--failures)

View File

@ -356,6 +356,10 @@ Each step lands on its own, with `dune test --root .` green.
`flan--pause-bounds` at `flan.el:2637-2660`) must equal the start `flan--pause-bounds` at `flan.el:2637-2660`) must equal the start
location the reader gave that form. `Ast.mark_pause` matches exactly location the reader gave that form. `Ast.mark_pause` matches exactly
(`ast.ml:491-492`). (`ast.ml:491-492`).
**Built** (`emacs/flan-fln-mode.el`; keys and objects in `emacs/MANUAL.md`,
"Indented files"). A line ending in `=` or `fn(…)` also opens a block for
TAB, and a body is its statement's own block, up to its first clause.
6. **Return-type inference** in `Check`, with the recursion refusal and the 6. **Return-type inference** in `Check`, with the recursion refusal and the
stale-caller cause. This is independent of steps 1-5 once the marker exists. stale-caller cause. This is independent of steps 1-5 once the marker exists.