diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index 881a8265..af056b46 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -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 "")) (cdr at) + (when indent (list :indent indent)) (when arg (list :pause t))))) "expression" end)) diff --git a/emacs/test-flan-fln-live.el b/emacs/test-flan-fln-live.el index 33e3cbae..078ded7c 100644 --- a/emacs/test-flan-fln-live.el +++ b/emacs/test-flan-fln-live.el @@ -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") diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index d1b0ff17..c92e05c2 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -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" diff --git a/lib/dev.ml b/lib/dev.ml index 0f4e7e86..f51ad7ac 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -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 diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index a5ea3a81..436bba59 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -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 diff --git a/lib/source.ml b/lib/source.ml index 102da4fd..d6429e19 100644 --- a/lib/source.ml +++ b/lib/source.ml @@ -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 = diff --git a/test/test_syntax.ml b/test/test_syntax.ml index d8dc6426..e9c7fe03 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -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:"" "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:"" "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:"" "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:"" "(f 1)" with | [ f ] -> span_is "a paren snippet" f (7, 3, 7, 8)