Editor code cut out mid-line carries :indent, the column its statement starts at, so the reader accepts its wrapped lines as the file does and reports every location where it is in the buffer

This commit is contained in:
Joseph Ferano 2026-09-25 20:35:33 +07:00
parent c3ba4c9e08
commit f1bdcc8840
7 changed files with 113 additions and 37 deletions

View File

@ -548,32 +548,31 @@ or the fallback call's name, `defmethod(', must read as one of HEADS,
(defun flan-fln--client ()
(require 'flan))
(defun flan-fln--cut-text (beg end)
"The text BEG..END, as the reader must see it when BEG is mid-line.
(defun flan-fln--cut-indent (beg)
"The byte column of the statement BEG was cut out of, when BEG is mid-line.
A condition or an arm's value starts after `elif ' or `-> ', and a line that
continues it is indented past the statement's start in the file but perhaps
not past the value's own column, which the reader, seeded at that column,
requires. So every line after the first moves right by the width of what
was cut off in front of the first; errors on the first line keep their
exact column."
(let* ((text (buffer-substring-no-properties beg end))
(delta (save-excursion
(goto-char beg)
(- (current-column) (current-indentation)))))
(if (<= delta 0)
text
(replace-regexp-in-string "\n" (concat "\n" (make-string delta ?\s))
text t t))))
continues it is indented past the statement's start, perhaps not past the
cut. The reader is told where the statement starts (`:indent'), so it
reads the text as the file has it, and every location it reports is still
the buffer's own."
(save-excursion
(goto-char beg)
(when (> (current-column) (current-indentation))
(back-to-indentation)
(1+ (- (position-bytes (point))
(position-bytes (line-beginning-position)))))))
(defun flan-fln--eval-expression (beg end arg)
"Evaluate BEG..END as an expression, as `flan--eval-expression' does, with
the text shaped by `flan-fln--cut-text'."
"Evaluate BEG..END as an expression, as `flan--eval-expression' does, and
with `:indent' when BEG is mid-line."
(flan--report
(flan--request
(let ((at (flan--text-at beg end)))
(append (list :op "eval-expr" :code (flan-fln--cut-text beg end)
(let ((at (flan--text-at beg end))
(indent (flan-fln--cut-indent beg)))
(append (list :op "eval-expr" :code (car at)
:file (or buffer-file-name "<buffer>"))
(cdr at)
(when indent (list :indent indent))
(when arg (list :pause t)))))
"expression"
end))

View File

@ -56,6 +56,9 @@ comment():
if 1 < 2 and
3 < 4
twice(1)
elif 1 > 2 or
3 > 4
0
twice(4)
if 2 > 1
twice(2)
@ -172,6 +175,36 @@ comment():
(flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e on a wrapped condition evaluates all of it")
(funcall shows "true"))
;; An error on the wrapped line of a condition or value cut out
;; mid-line is reported where it is in the buffer, column and all.
(let ((refused
(lambda (needle bad fix)
(funcall goto needle t)
(let ((line (1+ (line-number-at-pos))) col)
(save-excursion
(forward-line 1)
(search-forward fix (line-end-position))
(replace-match bad t t)
(setq col (1+ (- (point) (line-beginning-position)
(length (car (last (split-string bad " "))))))))
(prog1 (list (condition-case err (progn (flan-fln-eval-last) nil)
(user-error (error-message-string err)))
(format ":%d:%d)" line col))
(save-excursion
(goto-char (point-min))
(search-forward bad)
(replace-match fix t t)))))))
(pcase-dolist (`(,what ,needle ,bad ,fix)
'(("an if condition" "if 1 < 2 and" "3 < 4 4" "3 < 4")
("an elif condition" "elif 1 > 2 or" "3 > 4 4" "3 > 4")
("an arm's value" ":east -> 1 +" "2 2" "2")))
(let ((r (funcall refused needle bad fix)))
(test-flan--check
(funcall name (format "an error on the wrapped line of %s is reported at its column" what))
(and (car r) (string-suffix-p (cadr r) (car r))))
(unless (and (car r) (string-suffix-p (cadr r) (car r)))
(message " want ...%s\n got %S" (cadr r) (car r))))))
(flan-clear-errors)
(funcall goto "if 2 > 1" t)
(flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e at the end of an if line evaluates the condition")

View File

@ -460,12 +460,12 @@ comment:
"twice(1)\n twice(2)"))
(test-flan-fln--is "C-x C-e at the end of an if line sends its condition"
(test-flan-fln--last-at "if n < 0") '("eval-expr" "n < 0"))
(test-flan-fln--is "and of an elif, all of its condition, the wrapped line moved right by what was cut off"
(test-flan-fln--is "and of an elif, all of its condition"
(test-flan-fln--last-at "or n == 1")
'("eval-expr" "n == 0\n or n == 1"))
'("eval-expr" "n == 0\n or n == 1"))
(test-flan-fln--is "and the same from the end of the condition's first line"
(test-flan-fln--last-at "elif n == 0")
'("eval-expr" "n == 0\n or n == 1"))
'("eval-expr" "n == 0\n or n == 1"))
(defconst test-flan-fln--wrapped
"fn f(o: Option(i64)) -> i64
@ -489,19 +489,29 @@ comment:
(test-flan-fln--is "an arm's value wrapped onto a second line goes whole, moved right"
(test-flan-fln--wrapped-at "None" #'flan-fln-eval-last)
"1 +\n 2")
"1 +\n 2")
(test-flan-fln--is "from its second line too"
(test-flan-fln--wrapped-at " 2\n if" #'flan-fln-eval-last)
"1 +\n 2")
"1 +\n 2")
(test-flan-fln--is "and C-c C-e on it sends the same"
(test-flan-fln--wrapped-at "None" #'flan-fln-eval-statement)
"1 +\n 2")
"1 +\n 2")
(test-flan-fln--is "a wrapped if condition, from the end of its first line"
(test-flan-fln--wrapped-at "if 1 < 2" #'flan-fln-eval-last)
"1 < 2 and\n 3 < 4")
"1 < 2 and\n 3 < 4")
(test-flan-fln--is "a statement wrapped by an operator, from the end of its first line"
(test-flan-fln--wrapped-at "x = 1 +" #'flan-fln-eval-last)
"x = 1 +\n 2")
(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "None")
(end-of-line)
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan-fln--is "a value cut mid-line says where its statement starts"
(list (plist-get r :line) (plist-get r :col) (plist-get r :indent))
'(6 13 5))))
(test-flan-fln--in (test-flan-fln--at test-flan-fln--wrapped "x = 1")
(end-of-line)
(let ((r (test-flan-fln--sending (flan-fln-eval-last))))
(test-flan--check "a whole statement does not" (null (plist-member r :indent)))))
(test-flan-fln--is "an arm whose pattern binds nothing sends its value"
(test-flan-fln--wrapped-at "Some(_)" #'flan-fln-eval-last) "5")
(test-flan-fln--is "nor one whose value does not use what it binds"

View File

@ -4371,7 +4371,8 @@ let rec handle t req =
| Some l, None -> Some (l, 1)
| _ -> None
in
Source.with_code ~syntax ~at (fun () -> handle_op t req)
let indent = Wire.int_field req "indent" in
Source.with_code ?indent ~syntax ~at (fun () -> handle_op t req)
and handle_op t req =
match Wire.string_field req "op" with

View File

@ -220,7 +220,7 @@ let point (l : Loc.t) = { l with Loc.line = l.Loc.eline; col = l.Loc.ecol }
(* NEWLINE, INDENT and DEDENT, at bracket depth zero only: inside ( [ { a
line break is whitespace. A line continues the one before it when either
side of the break is a spaced binary operator (spec §2 "Continuation"). *)
let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array =
let layout ?(snippet = false) ?(base = 1) ?indent (toks : token list) : token array =
let arr = Array.of_list toks in
let n = Array.length arr in
(* A snippet from the editor starts wherever it was written, and its first
@ -229,6 +229,11 @@ let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array =
let out = ref [] in
let add tok loc = out := { tok; loc; sp = true } :: !out in
let stack = ref [ base ] in
(* [indent] is the column of the statement a snippet was cut out of, when
the snippet starts after that statement's first word (an elif's
condition, an arm's value). Its first joined line continues as it does
in the file: deeper than the statement, not than the cut. *)
let first_line = ref true in
let depth = ref 0 in
let binop t = match t.tok with NAME s -> is_binop s | _ -> false in
for i = 0 to n - 1 do
@ -248,11 +253,16 @@ let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array =
&& arr.(i + 1).sp
in
let continues = (binop p && p.sp) || (binop t && spaced_after) in
let top =
match indent with
| Some c when !first_line && List.length !stack = 1 -> min c (List.hd !stack)
| _ -> List.hd !stack
in
(* A continuation line sits deeper than the statement it continues.
One at or left of that statement's column is not read as joining
it: that would pull a line into a block it was written outside
of, silently. *)
if continues && t.loc.Loc.col <= List.hd !stack then
if continues && t.loc.Loc.col <= top then
failk "continuation" t.loc
"%s"
(if binop t then
@ -261,15 +271,16 @@ let layout ?(snippet = false) ?(base = 1) (toks : token list) : token array =
line above, but it is not indented past the start of that \
line (column %d). Indent it further to continue the line, \
or give %s a value on its left"
(show t.tok) (List.hd !stack) (show t.tok)
(show t.tok) top (show t.tok)
else
Printf.sprintf
"the line above ends with the operator %s, so this line \
continues it, but it is not indented past the start of \
that line (column %d). Indent it further, or finish the \
line above"
(show p.tok) (List.hd !stack));
(show p.tok) top);
if not continues then begin
first_line := false;
let at = point p.loc in
add NEWLINE at;
let col = t.loc.Loc.col in
@ -1556,7 +1567,7 @@ and lines (s : st) (one : unit -> Form.t list) : Form.t list =
(** All top-level forms in a [.fln] source string. [col] is the column the
text's top level starts at, 1 for a file. *)
let read_all ?(line = 1) ?col ~file src =
let read_all ?(line = 1) ?col ?indent ~file src =
let snippet = col <> None in
let col = Option.value col ~default:1 in
let saved = !source in
@ -1566,7 +1577,7 @@ let read_all ?(line = 1) ?col ~file src =
(file, Array.of_list (String.split_on_char '\n'
(String.make (line - 1) '\n' ^ String.make (col - 1) ' ' ^ src)));
Fun.protect ~finally:(fun () -> source := saved) (fun () ->
let toks = layout ~snippet ~base:col (lex ~line ~col ~file src) in
let toks = layout ~snippet ~base:col ?indent (lex ~line ~col ~file src) in
let s = { p = { toks; i = 0 }; lets = [] } in
let fs = stmts s in
(match (peek s.p).tok with

View File

@ -33,15 +33,21 @@ type syntax = Paren | Indented
let code_syntax = ref Paren
let code_at : (int * int) option ref = ref None
(* The column of the statement editor code was cut out of, when the code
starts after that statement's first word: see [Indent_reader.layout]. *)
let code_indent : int option ref = ref None
let syntax_of_field = function
| Some ("indented" | "fln") -> Indented
| _ -> Paren
let with_code ~syntax ~at f =
let s = !code_syntax and a = !code_at in
let with_code ?indent ~syntax ~at f =
let s = !code_syntax and a = !code_at and i = !code_indent in
code_syntax := syntax;
code_at := at;
Fun.protect ~finally:(fun () -> code_syntax := s; code_at := a) f
code_indent := indent;
Fun.protect
~finally:(fun () -> code_syntax := s; code_at := a; code_indent := i) f
(* The paren reader started at a line and column: [Reader.read_all] always
starts at 1:1. *)
@ -65,7 +71,7 @@ let read_code ?(expr = false) ~file code =
match !code_syntax with
| Paren -> read_paren ~line ~col ~file code
| Indented ->
(match Indent_reader.read_all ~line ~col ~file code with
(match Indent_reader.read_all ~line ~col ?indent:!code_indent ~file code with
| (first :: _ :: _ as forms) when expr ->
let last = List.nth forms (List.length forms - 1) in
let loc =

View File

@ -488,6 +488,22 @@ let () =
| [ _; _ ] -> ()
| _ -> fail "a snippet with leading spaces"
| exception e -> fail "a snippet with leading spaces: %s" (diag_text e));
(* A condition cut out from after [elif ] at column 3: its wrapped line at
column 8 is deeper than the elif, which is what the file says, though not
deeper than the cut. [:indent] says where the statement starts; every
location stays the buffer's own. *)
Source.with_code ~indent:3 ~syntax:Source.Indented ~at:(Some (10, 8)) (fun () ->
(match Source.read_code ~expr:true ~file:"<buf>" "x == 0 or\n x == 1" with
| [ f ] -> span_is "a wrapped condition, cut mid-line" f (10, 8, 11, 14)
| _ -> fail "a wrapped condition read as more than one form"
| exception e -> fail "a wrapped condition: %s" (diag_text e));
match Source.read_code ~expr:true ~file:"<buf>" "x == 0 or\n x == 1" with
| _ -> fail "a continuation left of its statement was read"
| exception Loc.Error _ -> ());
Source.with_code ~syntax:Source.Indented ~at:(Some (10, 8)) (fun () ->
match Source.read_code ~expr:true ~file:"<buf>" "x == 0 or\n x == 1" with
| _ -> fail "without :indent, a wrapped line is measured from the cut"
| exception Loc.Error _ -> ());
Source.with_code ~syntax:Source.Paren ~at:(Some (7, 3)) (fun () ->
match Source.read_code ~file:"<buf>" "(f 1)" with
| [ f ] -> span_is "a paren snippet" f (7, 3, 7, 8)