A binding ends its let's bindings only when its value ends in an open block, the message names the let's own line, and Emacs offers no binding column after such a value

This commit is contained in:
Joseph Ferano 2026-09-26 11:11:32 +07:00
parent 3d5caf25ae
commit 17ff7cb348
5 changed files with 52 additions and 12 deletions

View File

@ -1347,10 +1347,26 @@ whose brackets have closed is no block POS can join."
(unless (eql min 0) (push (cons 0 nil) out))
(nreverse out)))
(defun flan-fln--open-ended-p (l)
"Non-nil if the joined line L ends in a block still open at its end: a
trailing `fn(x) =>', `= match x', `= if c', `x =' or `f():', and not a
block lambda inside brackets, whose block closes with them."
(let ((last (flan-fln--logical-end l)))
(and (flan-fln--opener-p l last)
(= (car (save-excursion (syntax-ppss (flan-fln--code-end last))))
(car (save-excursion (syntax-ppss (flan-fln--first-char l))))))))
(defun flan-fln--levels (pos)
"Columns TAB offers POS's line outside brackets, deepest first."
"Columns TAB offers POS's line outside brackets, deepest first.
Not the column of a let's binding whose value ended in an open block: that
block ends the let's bindings (lib/indent_reader.ml's `binding_lines'), and
the reader refuses any line there."
(let* ((prev (flan-fln--prev-code pos))
(stack (mapcar #'car (flan-fln--stack pos))))
(stack (mapcar #'car
(seq-remove (lambda (e)
(and (cdr e) (flan-fln--binding-let (cdr e))
(flan-fln--open-ended-p (cdr e))))
(flan-fln--stack pos)))))
(cond
;; A lambda's block goes under the line its `=>' ends, which may be a
;; line of a call wrapped inside its brackets.

View File

@ -924,6 +924,14 @@ defconst(k, 3)
(test-flan-fln--tabs "fn f()\n let a = 1\n| b = 2" 1) 6)
(test-flan-fln--is "at the top level too"
(test-flan-fln--tabs "let a = 1\n|" 2) 4)
(dolist (c '(("fn f()\n let a = 0\n g = fn(x) =>\n x + 1\n|" (8 2 0) "a lambda's block")
("fn f()\n let a = 1\n b = match a\n 1 -> 2\n|" (8 2 0) "a match's arms")
("let a = 0\n g = fn(x) =>\n x + 1\n|" (6 0) "a global's lambda block")
("fn f()\n let a = 1\n g = map(xs, fn(x) =>\n x + 1)\n|" (6 2 0)
"but a lambda's brackets closed, where the column stays")))
(test-flan-fln--is (format "no binding column after a binding's open block: %s" (nth 2 c))
(test-flan-fln--in (car c) (flan-fln--block-levels (point)))
(cadr c)))
(test-flan-fln--is "but not after a tab between let and its name"
(list (test-flan-fln--tabs "fn f()\n let\ta = 1\n|" 1)
(test-flan-fln--tabs "fn f()\n let\ta = 1\n|" 2))

View File

@ -1704,8 +1704,10 @@ and binding_lines : 'a. ?global:bool -> st -> first:Form.t -> name_col:int -> on
let_misaligned t.loc ~let_line:first.loc.Loc.line ~name:(text_of first)
~name_col
in
(* [block] is the binding before this line when its value took the
lines under it, which ends the let's bindings. *)
(* [block] is the binding before this line when its value ended in a
block still open at its end — a trailing [fn(x) =>], [match], [if],
[x =] or [f():] — which ends the let's bindings. A block lambda whose
brackets close after its block does not. *)
let rec go ?block acc =
match (peek p).tok with
| DEDENT -> ignore (advance p); List.rev acc
@ -1715,14 +1717,16 @@ and binding_lines : 'a. ?global:bool -> st -> first:Form.t -> name_col:int -> on
| _ when binding_line ~global p ->
if (peek p).loc.Loc.col <> name_col then misaligned (peek p);
(match block with
| Some (name, line) -> let_after_block (peek p).loc ~let_line:line ~name
| Some name -> let_after_block (peek p).loc ~let_line:first.loc.Loc.line ~name
| None -> ());
let i0 = p.i and t0 = peek p in
let t0 = peek p in
let x = one () in
let took = ref false in
for k = i0 to p.i - 1 do if p.toks.(k).tok = INDENT then took := true done;
go ?block:(if !took then Some (text_of_tok t0, t0.loc.Loc.line) else None)
(x :: acc)
(* The last token the value took, past its line's end. *)
let rec last_real k =
if k > 0 && p.toks.(k).tok = NEWLINE then last_real (k - 1) else k
in
let open_block = p.toks.(last_real (p.i - 1)).tok = DEDENT in
go ?block:(if open_block then Some (text_of_tok t0) else None) (x :: acc)
| _ -> not_binding ()
and not_binding () =
failk "let-block" (where_ p)

View File

@ -206,7 +206,9 @@ Each item: the proposal, then the reason in one line.
there, is refused, and so is a tab between `let` and its first name. A
binding whose value is a block (`= match x`, a lambda header, `=` and the
lines under it) is its let's last; the printer starts a new `let` after
one. At the top level each such line is one more global, `(def col i32 …)`,
one. A block lambda whose brackets close after its block,
`g = map(xs, fn(x) =>` over `x + 1)`, is not: its block is shut when the
value ends, and more bindings may follow. At the top level each such line is one more global, `(def col i32 …)`,
and may be `name: T` alone as a global's own line may; a pattern there is
refused, since a global binds one name. A `let` is otherwise flat: its scope is the rest of its
block. To end it early, put it in a `do:` block.

View File

@ -779,7 +779,17 @@ let () =
refuses "a binding after a deeper block" "fn f()\n let g =\n h()\n y = 2\n g"
"indent/let-after-block" "after g";
refuses "a binding after a later binding's block" "fn f()\n let a = 0\n g = fn(x) =>\n x\n h = 2\n h"
"indent/let-after-block" "one more binding of the let on line 3, after g, whose value is the block above it. A binding whose value is a block ends its let's bindings, so this one needs a let of its own, at that let's column:\n\n let h = 2";
"indent/let-after-block" "one more binding of the let on line 2, after g, whose value is the block above it. A binding whose value is a block ends its let's bindings, so this one needs a let of its own, at that let's column:\n\n let h = 2";
(* A block lambda whose brackets close ends no group: its block is shut
when its value ends. *)
reads ~global:false "a binding after a later binding's bracketed lambda"
"let a = 1\n g = map(xs, fn(x) =>\n x + 1)\n h = 2\nh"
"(let [a 1 g (map xs (fn [x] (+ x 1))) h 2] h)";
reads ~global:false "a binding after a first binding's bracketed lambda"
"let g = map(xs, fn(x) =>\n x + 1)\n h = 2\nh"
"(let [g (map xs (fn [x] (+ x 1))) h 2] h)";
refuses "a binding after a later binding's match" "fn f()\n let a = 1\n b = match a\n 1 -> 2\n _ -> 3\n c = 4\n c"
"indent/let-after-block" "the let on line 2, after b";
refuses "a global after a global's block" "let a = 0\n g =\n h()\n b = 2"
"indent/let-after-block" "after g";
(* Only a line shaped as a binding gets the alignment advice. *)