From 02fcb1725e7993f52659c22cecea079de303e0bd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 00:52:55 +0700 Subject: [PATCH 1/2] 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 --- emacs/MANUAL.md | 12 +-- emacs/flan-fln-mode.el | 173 +++++++++++++++++++++++++++++------- emacs/test-flan-fln-live.el | 61 ++++++++++++- emacs/test-flan-fln.el | 122 +++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 40 deletions(-) diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 48d1da8d..36e24899 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -1195,9 +1195,9 @@ Use `C-c C-g` if you need frames. | 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: an elif's condition, an else's block, 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-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-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 (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 `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-s` | same | step through the top-level `fn` at point | | `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) | | `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 | 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 | | `C-c <` / `C-c >` | `<` / `>` | shift the region's lines a level | | `M-` / `M-` | 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) | `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, enclosing statement, top-level form. @@ -1227,7 +1227,7 @@ enclosing statement, top-level form. - **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. +- **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. `flan-fln-indent-offset` (2) is one level. `flan-fln-smartparens` (`t`) turns diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index dfd90147..50db36f2 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -258,6 +258,67 @@ A word, and only when a space or the end of the line follows it." (setq bol n)) 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 (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))))))) (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 (goto-char (flan-fln--first-char l)) - (if (looking-at "elif[ \t]+") - (cons (match-end 0) (flan-fln--code-end (flan-fln--logical-end l))) - (flan-fln--body-bounds l)))) + (let ((end (flan-fln--joined-end l))) + (cond + ((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) "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." (save-excursion (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)) (end (flan-fln--code-end (flan-fln--logical-end l)))) (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. (when (string-match "\\`-[a-zA-Z$_*]" n) (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) (dolist (part (split-string n "\\." t)) (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) (skip-chars-backward " \t\n" first) (point)))))) -(defun flan-fln--bare-let-p (start) - "Non-nil if START's statement is a `let' with no block of its own." - (and (save-excursion (goto-char (flan-fln--first-char start)) - (looking-at "let[ \t]")) - (= (flan-fln--statement-last start) (flan-fln--logical-end start)))) +(defun flan-fln--let-p (start) + "Non-nil if START's statement is a `let'. +A let takes no block: lines under it are its value's (`= match x', a lambda +header), and its name lasts to the end of the block it is in." + (save-excursion (goto-char (flan-fln--first-char start)) + (looking-at "let[ \t]"))) (defun flan-fln--block-rest (start) "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) "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)) (arm (and l (flan-fln--arm l))) (s (flan-fln--statement-start-at pos))) (cond (arm (flan-fln--arm-to-send l arm)) ((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))))) ;;;###autoload (defun flan-fln-eval-statement (&optional arg) "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', -the `let' and the rest of its block, which is what it is in scope for; the +With an active region, the lines it touches instead. On a `let', 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 installed. ARG is as for \\[flan-fln-eval-last]." (interactive "P") @@ -1032,17 +1105,24 @@ Before it at the same level, else out to the line that owns this block." (goto-char start) (back-to-indentation) (and (looking-at (concat (regexp-opt flan-fln--opener-words t) - "\\([ \t]\\|$\\)")) - (let ((w (match-string-no-properties 1)) - (alone (string= (match-string-no-properties 2) "")) - (end (flan-fln--code-end last))) + "\\(?:[ \t]\\|$\\)")) + (let* ((w (match-string-no-properties 1)) + (w-end (match-end 1)) + (end (flan-fln--code-end last)) + (alone (>= w-end end))) (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-")) (not (re-search-forward "[ \t]=[ \t]" end t))) - ((member w '("if" "elif")) - (not (re-search-forward "[ \t]then[ \t]" end t))) + ((member w '("if" "elif")) (not (flan-fln--then start))) (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 (goto-char (flan-fln--code-end last)) (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) ;; `let x =' and `def colors =' with the value as a block, ;; which the author's list leaves out and the reader reads. - (looking-back "[ \t]=" bol) - ;; `let f = fn(x)' with its body under it. - (looking-back "\\_' 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]\\|\\_" + . font-lock-constant-face) ;; The words inside a line: `for i in range(n)', `if c then a else b', a ;; `where' constraint. ("[ \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) ;; A type: after the `: ' of an annotation and after `-> '. ("[^ \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. ("\\_<\\([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\\)\\_>" diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el index 078ded7c..4c6d3a06 100644 --- a/emacs/test-flan-fln-live.el +++ b/emacs/test-flan-fln-live.el @@ -52,6 +52,33 @@ fn pick(d: Dir) -> i64 _ -> 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(): if 1 < 2 and 3 < 4 @@ -59,6 +86,9 @@ comment(): elif 1 > 2 or 3 > 4 0 + if 2 < 1 then 5 + elif 2 == 1 then 6 + else 7 twice(4) if 2 > 1 twice(2) @@ -158,6 +188,27 @@ comment(): (test-flan--check (funcall name "and moves to the next") (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 ;; answers `:pause' only when a form the reader made starts exactly ;; there (`Ast.mark_pause'). Each kind of target once, and one position @@ -217,7 +268,15 @@ comment(): ("n < 2" "an if statement") ("t = 0" "a let") ("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)) (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" diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index cd114184..544fb8a0 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -584,6 +584,76 @@ comment: (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-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" @@ -606,6 +676,30 @@ comment: (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))) + ;;; Indentation (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--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) +(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 |" (flan-fln-dedent-or-delete 1) From ba3ad6b1bca388d8bf9689ff493cc69b8420105c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 01:05:00 +0700 Subject: [PATCH 2/2] In a .fln buffer ik takes the value on a one-line else or elif line, a wrapped fn header's return type is coloured, and C-u C-c C-c on a let the let above merges in stops at its value --- emacs/flan-fln-mode.el | 58 ++++++++++++++++++++++++++++++++----- emacs/test-flan-fln-live.el | 10 ++++++- emacs/test-flan-fln.el | 25 ++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 50db36f2..34b3cdad 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -285,6 +285,23 @@ depth, outside strings and comments, or nil." (flan-fln--joined-end l)))) (and m (car m)))) +(defun flan-fln--clause-value (l) + "Bounds of the value on the clause line L itself: `x' of `else x' or of +`elif c then x'; nil when the clause's value is its block." + (let ((end (flan-fln--joined-end l)) + (then (flan-fln--then l))) + (save-excursion + (goto-char (flan-fln--first-char l)) + (cond + ((and (looking-at "elif[ \t]") then) + (goto-char then) + (skip-chars-forward " \t") + (forward-char 4) + (skip-chars-forward " \t") + (and (< (point) end) (cons (point) end))) + ((and (looking-at "else[ \t]+") (< (match-end 0) end)) + (cons (match-end 0) end)))))) + (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." @@ -675,7 +692,21 @@ forms, where the clause line itself is not." (arm (plist-get arm :value)) ((and l (flan-fln--clause-line-p l)) (flan-fln--clause-target l)) (t (let ((s (flan-fln--statement-start-at pos))) - (and s (flan-fln--statement-bounds s))))))) + (and s (or (flan-fln--merged-let-value s) + (flan-fln--statement-bounds s)))))))) + +(defun flan-fln--merged-let-value (s) + "Bounds of the value of the `let' at S when the let before it takes it in. +The reader merges consecutive lets into one binding vector, so a second +`let b = 2' starts no form of its own; its value, or its value's block, is +the form that runs where the line stands." + (let ((p (and (flan-fln--let-p s) (flan-fln--sibling s -1)))) + (when (and p (flan-fln--let-p p)) + (let ((v (flan-fln--value-start s)) + (end (flan-fln--joined-end s))) + (if (and v (< v end)) + (cons v (cdr (flan-fln--statement-bounds s))) + (flan-fln--body-bounds s)))))) (defun flan-fln--clause-target (l) "What a clause line L stops at: an elif's condition, else its block. @@ -686,8 +717,7 @@ A one-line `else x' stops at x." (cond ((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)) + ((flan-fln--clause-value l)) (t (flan-fln--body-bounds l)))))) (defun flan-fln--condition (l) @@ -1505,8 +1535,20 @@ lambda or a `Fn(...)' type, and not after a match arm's." (setq found (save-excursion (save-match-data (goto-char (line-beginning-position)) - (re-search-forward "\\(?:^[ \t]*fn-?[ \t]\\|\\_ i64 restart retry() \"Try again\" 4 +fn t2(k: i64) -> i64 + let a = 1 + let b = 2 + let c: i64 = 3 + a + b + c + k + fn dir(d: Dir) -> i64 match d Dir.north -> 7 @@ -276,7 +282,9 @@ comment(): ("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"))) + ("Dir.north" "an enum member's arm, at its value") + ("let b = 2" "a let the let above takes in, at its value") + ("let c: i64" "a typed one, at its value"))) (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" diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index 544fb8a0..10721aaf 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -639,6 +639,18 @@ of its line with AT-END." (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)") @@ -699,6 +711,13 @@ of its line with AT-END." (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 "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 @@ -954,6 +973,12 @@ of its line with AT-END." "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)))))