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

View File

@ -56,6 +56,9 @@ comment():
if 1 < 2 and if 1 < 2 and
3 < 4 3 < 4
twice(1) twice(1)
elif 1 > 2 or
3 > 4
0
twice(4) twice(4)
if 2 > 1 if 2 > 1
twice(2) twice(2)
@ -172,6 +175,36 @@ comment():
(flan-fln-eval-last) (flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e on a wrapped condition evaluates all of it") (test-flan--check (funcall name "C-x C-e on a wrapped condition evaluates all of it")
(funcall shows "true")) (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) (funcall goto "if 2 > 1" t)
(flan-fln-eval-last) (flan-fln-eval-last)
(test-flan--check (funcall name "C-x C-e at the end of an if line evaluates the condition") (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)")) "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--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--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") (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--is "and the same from the end of the condition's first line"
(test-flan-fln--last-at "elif n == 0") (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 (defconst test-flan-fln--wrapped
"fn f(o: Option(i64)) -> i64 "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--is "an arm's value wrapped onto a second line goes whole, moved right"
(test-flan-fln--wrapped-at "None" #'flan-fln-eval-last) (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--is "from its second line too"
(test-flan-fln--wrapped-at " 2\n if" #'flan-fln-eval-last) (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--is "and C-c C-e on it sends the same"
(test-flan-fln--wrapped-at "None" #'flan-fln-eval-statement) (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--is "a wrapped if condition, from the end of its first line"
(test-flan-fln--wrapped-at "if 1 < 2" #'flan-fln-eval-last) (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--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) (test-flan-fln--wrapped-at "x = 1 +" #'flan-fln-eval-last)
"x = 1 +\n 2") "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--is "an arm whose pattern binds nothing sends its value"
(test-flan-fln--wrapped-at "Some(_)" #'flan-fln-eval-last) "5") (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" (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) | Some l, None -> Some (l, 1)
| _ -> None | _ -> None
in 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 = and handle_op t req =
match Wire.string_field req "op" with 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 (* NEWLINE, INDENT and DEDENT, at bracket depth zero only: inside ( [ { a
line break is whitespace. A line continues the one before it when either 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"). *) 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 arr = Array.of_list toks in
let n = Array.length arr in let n = Array.length arr in
(* A snippet from the editor starts wherever it was written, and its first (* 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 out = ref [] in
let add tok loc = out := { tok; loc; sp = true } :: !out in let add tok loc = out := { tok; loc; sp = true } :: !out in
let stack = ref [ base ] 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 depth = ref 0 in
let binop t = match t.tok with NAME s -> is_binop s | _ -> false in let binop t = match t.tok with NAME s -> is_binop s | _ -> false in
for i = 0 to n - 1 do 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 && arr.(i + 1).sp
in in
let continues = (binop p && p.sp) || (binop t && spaced_after) 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. (* A continuation line sits deeper than the statement it continues.
One at or left of that statement's column is not read as joining 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 it: that would pull a line into a block it was written outside
of, silently. *) of, silently. *)
if continues && t.loc.Loc.col <= List.hd !stack then if continues && t.loc.Loc.col <= top then
failk "continuation" t.loc failk "continuation" t.loc
"%s" "%s"
(if binop t then (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 above, but it is not indented past the start of that \
line (column %d). Indent it further to continue the line, \ line (column %d). Indent it further to continue the line, \
or give %s a value on its left" 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 else
Printf.sprintf Printf.sprintf
"the line above ends with the operator %s, so this line \ "the line above ends with the operator %s, so this line \
continues it, but it is not indented past the start of \ continues it, but it is not indented past the start of \
that line (column %d). Indent it further, or finish the \ that line (column %d). Indent it further, or finish the \
line above" line above"
(show p.tok) (List.hd !stack)); (show p.tok) top);
if not continues then begin if not continues then begin
first_line := false;
let at = point p.loc in let at = point p.loc in
add NEWLINE at; add NEWLINE at;
let col = t.loc.Loc.col in 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 (** All top-level forms in a [.fln] source string. [col] is the column the
text's top level starts at, 1 for a file. *) 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 snippet = col <> None in
let col = Option.value col ~default:1 in let col = Option.value col ~default:1 in
let saved = !source 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' (file, Array.of_list (String.split_on_char '\n'
(String.make (line - 1) '\n' ^ String.make (col - 1) ' ' ^ src))); (String.make (line - 1) '\n' ^ String.make (col - 1) ' ' ^ src)));
Fun.protect ~finally:(fun () -> source := saved) (fun () -> 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 s = { p = { toks; i = 0 }; lets = [] } in
let fs = stmts s in let fs = stmts s in
(match (peek s.p).tok with (match (peek s.p).tok with

View File

@ -33,15 +33,21 @@ type syntax = Paren | Indented
let code_syntax = ref Paren let code_syntax = ref Paren
let code_at : (int * int) option ref = ref None 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 let syntax_of_field = function
| Some ("indented" | "fln") -> Indented | Some ("indented" | "fln") -> Indented
| _ -> Paren | _ -> Paren
let with_code ~syntax ~at f = let with_code ?indent ~syntax ~at f =
let s = !code_syntax and a = !code_at in let s = !code_syntax and a = !code_at and i = !code_indent in
code_syntax := syntax; code_syntax := syntax;
code_at := at; 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 (* The paren reader started at a line and column: [Reader.read_all] always
starts at 1:1. *) starts at 1:1. *)
@ -65,7 +71,7 @@ let read_code ?(expr = false) ~file code =
match !code_syntax with match !code_syntax with
| Paren -> read_paren ~line ~col ~file code | Paren -> read_paren ~line ~col ~file code
| Indented -> | 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 -> | (first :: _ :: _ as forms) when expr ->
let last = List.nth forms (List.length forms - 1) in let last = List.nth forms (List.length forms - 1) in
let loc = let loc =

View File

@ -488,6 +488,22 @@ let () =
| [ _; _ ] -> () | [ _; _ ] -> ()
| _ -> fail "a snippet with leading spaces" | _ -> fail "a snippet with leading spaces"
| exception e -> fail "a snippet with leading spaces: %s" (diag_text e)); | 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 () -> Source.with_code ~syntax:Source.Paren ~at:(Some (7, 3)) (fun () ->
match Source.read_code ~file:"<buf>" "(f 1)" with match Source.read_code ~file:"<buf>" "(f 1)" with
| [ f ] -> span_is "a paren snippet" f (7, 3, 7, 8) | [ f ] -> span_is "a paren snippet" f (7, 3, 7, 8)