The editor batch: results at line end, inspect at point, a buffer that keeps

This commit is contained in:
Joseph Ferano 2026-09-20 20:30:38 +07:00
commit 6ef0f46bea
5 changed files with 532 additions and 127 deletions

View File

@ -170,21 +170,31 @@ one point is in, and only then is it asked whether it is a definition.
### `C-x C-e` — evaluate an expression
The expression before point is compiled, run **inside the running program**, and
its value shown beside it, as `=> 2` after the form you evaluated. Not a copy of
its value shown as `=> 2` at the end of the line the form is on. Not a copy of
the program, not a simulation — the actual process, with its actual state.
So in a game you can type `(len enemies)` and get the real number.
The value goes away on your next keystroke, like every other bit of feedback
about a command that just ran. Set `flan-inline-result` to `nil` and it goes to
the echo area instead; that is also where it goes when there is nowhere in a
buffer to draw it, as at the REPL.
**Where it appears.** At the end of the *line*, which is where the watch buffer's
ghost text goes too — the two are the same thing seen twice and they are drawn
the same way. It used to sit at the end of the form instead, so that the two
features could be told apart by position; what that cost is a value wedged into
the middle of a line whenever the form was not the last thing on it, reflowing
everything after it while you read.
The two are different questions and the settings are not two spellings of one
thing. `flan-echo-result` is about the sentence an *install* reports — which
names landed and what it cost — and an install has no value to draw. Where they
would overlap, on an expression, the inline value wins and the echo is not also
written: the same number twice is how a reader learns to skip both.
**And in the echo area, always.** The overlay goes away on your next keystroke,
like every other bit of feedback about a command that just ran — so the same
value is written to the echo area as well, where it is still there afterwards.
The echo used to be suppressed whenever the overlay drew, on the argument that
the same number twice teaches a reader to skip both; between saying it twice and
losing it before it was read, saying it twice is the smaller cost.
The two settings are still different questions, and neither now suppresses the
other. `flan-inline-result` decides the overlay: set it to `nil` and the echo
area is the only place a value appears, which is also what happens when there is
nowhere in a buffer to draw one, as at the REPL. `flan-echo-result` decides the
echo area, and it also covers the sentence an *install* reports — which names
landed and what it cost — since an install has no value to draw.
The form before point can also be a declaration — a `defvar` typed at the top of
a file — in which case `C-x C-e` installs it rather than refusing it, and says
@ -366,8 +376,16 @@ everything it had is still there.
## Looking at values
**`C-c C-i`** inspects a value. Give it an expression; you get its fields, one
per line.
**`C-c C-i`** inspects a value: the expression point is on, with no prompt. You
get its fields, one per line. It takes the innermost thing point is on rather
than only the one behind point, so the middle of a name works as well as the end
of one — point anywhere in `enemies` takes `enemies`, and on the open paren of
`(len enemies)` takes the whole call.
**`C-u C-c C-i`** opens the minibuffer instead, pre-filled with whatever was at
point — for inspecting something that is not written in the buffer, or is
written somewhere else. That is also what you get when there is nothing at point
to take.
| Key | Does |
|---|---|
@ -847,6 +865,23 @@ if one is written, and the body alike.
`{.x 1.0}`. The colon is still a constant too; it means an enum member, `:green`,
and a key in a map.
**The keywords are the parser's.** Both lists — the forms that introduce a
top-level name, and the forms with meaning to the checker — are read off
`Parse.decl` and `Parse.form` in `lib/parse.ml` and kept whole, rather than
topped up whenever somebody notices a gap. So `defmacro`, `defclass`,
`defgeneric`, `defmulti`, `defmethod` and `declare-c` colour as definitions, and
`when`, `cond`, `and`, `or`, `break`, `continue`, `recur`, `fn`, `quote`,
`array`, `signal`, `error` and the four condition forms — `handler-bind`,
`handler-case`, `restart-case`, `invoke-restart` — colour as keywords. A macro
*you* define is still drawn as an ordinary call: macro-ness is erased by the
time the daemon can be asked about a name, so there is nothing to ask.
**`#_` greys out the form after it**, the way it reads: the discarded form is
given the comment syntax class, so it is drawn as a comment and skipped by
everything else that skips comments. Chaining works as the reader's does —
`#_#_ a b` discards both, with nothing counting — and an unfinished form is
left alone, so `#_(` does not grey the rest of the file while you are typing it.
Nothing here needs a running program. Indentation and colouring are the major
mode's, so they work in a file you have only opened.
@ -955,6 +990,23 @@ other three are unaffected.
next time that buffer's evaluation is accepted — so it disappears when you fix
the thing rather than when you dismiss it.
**And it is kept.** Every compiler message the editor is handed is also appended
to `*flan-diagnostics*`, one entry per message with the time it arrived above
it: `M-x flan-show-diagnostics`. The overlay is feedback about one command and
is gone at the next keystroke; this is the copy you can still quote an hour
later, without going through `*Messages*` for it. Nothing clears it — not
connecting, not a successful evaluation, not quitting the program — until you
say `M-x flan-clear-diagnostics`. Each entry's message line is left in the shape
the compiler wrote it, `file:line:col: message`, so `RET` and `M-g M-n` on one
go to the code and a line yanked out of the buffer reads the same as one pasted
from a terminal.
**A build that failed is in `*flan-dev*`**, and it jumps too: the buffer is in
`compilation-minor-mode`, so `M-g M-n`, `M-g M-p` and `RET` on a diagnostic take
you to the line it names. That is the buffer to read when `M-x flan` reports a
daemon that exited before it was ready — a `main` that does not compile means no
socket at all, and this is the only account of why.
**"No .flan-dev.sock found above this buffer"** — nothing is running, or you are
outside the project. Start one with `M-x flan`.
@ -987,7 +1039,8 @@ Use `C-c C-g` if you need frames.
| `C-c C-r` | a prompt on the running program |
| `C-c C-b` | a stopped program: condition, restarts, stack |
| `C-c C-M-b` | the same restarts, as a one-key prompt |
| `C-c C-i` | inspect a value |
| `C-c C-i` | inspect the expression at point |
| `C-u C-c C-i` | ...or one you type in the minibuffer |
| `C-c C-m` | what the macro call at point expands to |
| `C-u C-c C-m` | ...all the way, rather than one step |
| `C-c C-a` | disassemble; `C-u` first for LLVM IR |
@ -1001,6 +1054,8 @@ Use `C-c C-g` if you need frames.
Commands with no key: `M-x flan` (start a program), `M-x flan-quit`
(stop it), `M-x flan-watch` (the watch buffer), `M-x flan-watch-stop`,
`M-x flan-watch-ghost-mode` (the same values inline),
`M-x flan-show-diagnostics` / `M-x flan-clear-diagnostics` (every compiler
message this session has been handed, and emptying that),
`M-x flan-inspect-address` (what is at an address), `M-x flan-macroexpand-all`
(the `C-u` half of `C-c C-m`, by name), `M-x flan-allocations` /
`M-x flan-leaks` (where the memory went, and what is still held), and
@ -1017,11 +1072,12 @@ in the buffer).
| `flan-daemon-args` | `nil` | extra arguments for `flan dev``("--llvm")`, `("--debug")` |
| `flan-socket-name` | `".flan-dev.sock"` | what `C-c C-z` searches for |
| `flan-echo-result` | `t` | report an accepted evaluation in the echo area |
| `flan-inline-result` | `t` | show an expression's value beside the form, not in the echo area |
| `flan-inline-result` | `t` | also show an expression's value at the end of its line |
| `flan-names-shown` | `4` | how many names to list before summarising |
| `flan-output-buffer` | `"*flan-output*"` | where the program's output goes |
| `flan-poll-interval` | `1.0` | seconds between checks for whether it stopped |
| `flan-daemon-buffer` | `"*flan-dev*"` | the daemon's own log |
| `flan-diagnostics-buffer` | `"*flan-diagnostics*"` | every compiler message, kept |
| `flan-start-timeout` | `60` | seconds to wait for a program to come up |
| `flan-lower-buffer` | `"*flan-lowering*"` | where `C-c C-l` writes |
| `flan-lower-program` | `"flan"` | the compiler `C-c C-l` shells out to |

View File

@ -677,19 +677,56 @@ and whether that may be followed is the answer"))
(display-buffer buf)
buf))
(defun flan-inspect--expression-at-point ()
"The text of the expression point is on, or nil.
The innermost thing point is on, and not merely the one behind it: point in
the middle of a name is where a key pressed without looking lands, and
`backward-sexp\=' alone answers with the half of the name already typed past
which reads as the command being broken rather than as point being one
character early. So the end of the thing under point is found first, and the
form is taken back from there; on an opening delimiter the form that opens
there is taken instead, which is the rule `flan-macroexpand\=' follows for the
same reason.
Innermost, so point on `enemies\=' inside `(len enemies)\=' takes `enemies\=' and
not the call which is the right answer for a command that shows you one
value, and the enclosing call is a paren away in either direction.
Nil when there is nothing there to take, which is a buffer position with no
form at it at all."
(ignore-errors
(save-excursion
(skip-chars-forward " \t")
(if (looking-at-p "[([{]")
(buffer-substring-no-properties
(point) (progn (forward-sexp) (point)))
;; Inside a form: out to its end, so that the text taken is the whole
;; of it. Nothing to step over when point is already after one — at
;; whitespace, at a closing delimiter, or at the end of the buffer.
(unless (or (eobp)
(memq (char-syntax (char-after)) '(?\s ?> ?\))))
(forward-sexp))
(buffer-substring-no-properties
(save-excursion (backward-sexp) (point)) (point))))))
;;;###autoload
(defun flan-inspect (expr)
"Inspect the value of EXPR in the running program.
Interactively, the expression before point, or one you type.
Interactively, the expression at point no prompt, because the expression is
already written in the buffer and retyping it is the whole cost of the
command. With a prefix argument, or with nothing at point to take, the
minibuffer opens instead, pre-filled with whatever was there.
This is the expression root: EXPR is evaluated where the evaluator stands, so
it works on a running program but cannot say which frame it means. `i\=' in the
break buffer uses `flan-inspect-slot\=' for a local, for exactly that reason."
(interactive
(list (read-string "Inspect: "
(ignore-errors
(buffer-substring-no-properties
(save-excursion (backward-sexp) (point)) (point))))))
(let ((at (flan-inspect--expression-at-point)))
(list (if (and at (not current-prefix-arg) (not (string-blank-p at)))
at
(read-string "Inspect: " at)))))
;; A new root, and therefore an empty stack. That is the whole of why `l\='
;; cannot walk out of one root into another: there is never an entry from a
;; different root left underneath it.

View File

@ -111,17 +111,37 @@
:group 'languages
:prefix "flan-")
;; Both lists are read off `Parse.decl' and `Parse.form' (lib/parse.ml), which
;; are where the two questions are actually decided: a head `decl' has an arm
;; for introduces a name, and a head `form' has an arm for is never a call.
;; Kept whole rather than topped up one name at a time — the list that is
;; patched only when somebody notices a gap is the list that is always a
;; release behind the parser.
(defconst flan--definers
'("defn" "defvar" "defconst" "defstruct" "defdata" "defunion" "defenum"
"defalias"
"declare" "import" "package")
'("defn" "defmacro" "defvar" "defconst" "defstruct" "defdata" "defunion"
"defenum" "defalias"
;; The object and dispatch heads (lib/parse.ml:1277-1334).
"defclass" "defgeneric" "defmulti" "defmethod"
"declare" "declare-c" "import" "package")
"Forms that introduce a top-level name.")
(defconst flan--special
'("let" "if" "do" "while" "until" "dotimes" "loop" "match" "set" "return"
"defer" "some" "none" "try" "zeroed" "uninit" "slice" "at" "len" "addr"
'("quote" "let" "if" "when" "cond" "and" "or" "do" "while" "until" "dotimes"
"loop" "recur" "break" "continue" "match" "set" "return" "fn" "array"
"defer" "some" "none" "try" "signal" "error"
"handler-bind" "handler-case" "restart-case" "invoke-restart"
"zeroed" "uninit" "slice" "at" "len" "addr"
"bytes" "cast" "true" "false" "nil" "print" "println")
"Forms with meaning to the checker.")
"Forms with meaning to the checker.
The heads `Parse.form' dispatches on, plus the literals and the handful of
builtins that are never anything else. Two groups of real heads are
deliberately left out: `quasiquote', `unquote' and `unquote-splicing', which
nobody writes as words the reader makes them out of \\=`, ~ and ~@, and the
sigils are not symbols for a keyword rule to reach and `find-restart',
`compute-restarts', `errdefer' and `await', which the parser recognises only
in order to refuse them. Drawing those four as keywords would advertise four
forms that cannot be used.")
(defvar flan-font-lock-keywords
`((,(concat "(" (regexp-opt flan--definers t) "\\_>"
@ -194,6 +214,95 @@ line is off screen."
table)
"Syntax table for `flan-mode'.")
;;; The discard reader macro
;; `#_' reads the form after it and throws it away (lib/reader.ml:16-24,
;; 202-262), which is how a form is commented out without counting its closing
;; parens. The table above knows nothing about it — it comes from
;; `lisp-mode-syntax-table', and Common Lisp has no such construct — so a
;; discarded form used to render as ordinary code, which is the one thing it
;; must not look like.
;;
;; The fix is `clojure-mode's, which has the same reader macro and the same
;; problem: a `syntax-propertize-function' that gives the span the comment
;; syntax class. Font lock then draws it as a comment with no rule of its own,
;; and every other thing that reads syntax classes — `forward-comment',
;; `comment-only-p', a package that skips comments — agrees for free.
;;
;; Generic comment fences (the `!' class) rather than a line comment, because a
;; discarded form is a region and may be one line or twenty. The `#' is the
;; opening fence and the last character of the form is the closing one, so
;; everything in between is inside a comment and is no longer read as
;; structure. That is the reader's own answer, and it leaves the *enclosing*
;; list's parens alone: only the discarded form's own last delimiter changes
;; class, so `(a #_(b c) d)' still balances.
(defun flan--discard-end (pos)
"Where the discard whose form starts at POS ends, or nil.
The reader's rule rather than a count: a discard reads *a form*, and reading
one skips leading discards of its own, so `#_#_ a b' throws away both a and b
with nothing ever counting to two (lib/reader.ml:202-262). Written as the
loop that recursion amounts to every `#_' seen owes one more form since
the walk over the buffer is the same either way.
Nil when a form is unfinished, which is every buffer halfway through being
typed: nothing is propertized then, rather than `#_(' greying out the rest of
the file while the parenthesis is still open."
(let ((owed 1)
;; The scan must not consult the properties it is being run to
;; compute: with `parse-sexp-lookup-properties' on, `forward-sexp'
;; would ask for the syntax of positions this pass has not reached.
;; The table alone is enough — the nesting is counted here, and
;; strings and escapes are the table's own business.
(parse-sexp-lookup-properties nil))
(ignore-errors
(save-excursion
(goto-char pos)
(while (> owed 0)
(forward-comment (buffer-size))
(if (looking-at-p "#_")
(progn (forward-char 2) (setq owed (1+ owed)))
(forward-sexp)
(setq owed (1- owed))))
;; `forward-sexp' at the end of the buffer stays put rather than
;; signalling, so a trailing `#_' with nothing after it would otherwise
;; come back as a span covering only itself — a two-character comment
;; drawn over the reader's one real discard error. It is unfinished
;; like any other, and answers nil like any other.
(and (> (point) pos) (point))))))
(defun flan--syntax-propertize (start end)
"Mark every `#_' discard between START and END as a comment.
For `syntax-propertize-function'."
(goto-char start)
(while (search-forward "#_" end t)
;; Both ends of the match are read off point before anything else runs, and
;; the `syntax-ppss' below is wrapped: it moves point and clobbers the
;; match data, so asking it first and reading the match afterwards is how
;; this would come to scan from the `#' instead of from the form — and how
;; a `#_' it declined would be found again, forever, at the same place.
(let* ((beg (- (point) 2))
(from (point))
;; `#_' written inside a string, or inside a `;' comment, is text
;; and not a reader macro. One inside a span an earlier discard
;; already covers is not reached at all: that discard's own scan
;; consumed it, and point jumps past the whole span below.
(quoted (save-excursion (nth 8 (syntax-ppss beg))))
(fin (and (not quoted) (flan--discard-end from))))
(when fin
(put-text-property beg (1+ beg)
'syntax-table (string-to-syntax "!"))
(put-text-property (1- fin) fin
'syntax-table (string-to-syntax "!"))
;; So that a change *inside* a discarded form re-propertizes from the
;; `#_' rather than from the line it was typed on; the other half is
;; `syntax-propertize-multiline' in the mode body.
(put-text-property beg fin 'syntax-multiline t))
;; Past the span when there was one, and past the `#_' when there was
;; not. Either way forward, which is the whole of why this terminates.
(goto-char (or fin from)))))
(defvar flan-mode-map
(let ((map (make-sparse-keymap)))
;; Autoloaded from flan.el, so the client loads on first use.
@ -270,6 +379,12 @@ line is off screen."
(setq-local lisp-indent-function #'flan-indent-function)
(setq-local outline-regexp ";;;;+[ \t]*")
(setq-local imenu-generic-expression flan-imenu-generic-expression)
;; `#_' — see the section above the syntax table.
(setq-local syntax-propertize-function #'flan--syntax-propertize)
;; A discarded form spans as many lines as it likes, so the region handed to
;; the propertizer has to be widened back to the `#_' that owns it.
(add-hook 'syntax-propertize-extend-region-functions
#'syntax-propertize-multiline nil t)
;; Buffer-locally, because this answers for Flan and nothing else.
(add-hook 'which-func-functions #'flan-current-defun-name nil t))

View File

@ -67,6 +67,11 @@
(require 'cl-lib)
(require 'xref)
(require 'eldoc)
;; For `compilation-minor-mode', which is what makes a `file:line:col:' line in
;; the daemon's buffer and in the diagnostics log somewhere `next-error' can
;; go. Flan's messages already have that shape, so nothing here teaches
;; `compile' a new one.
(require 'compile)
;; The macroexpansion buffer is a `flan-mode' buffer — what is in it is Flan
;; source and reading it is the point — so the mode has to exist by the time
;; this file defines one derived from it. Not a cycle: flan-mode.el autoloads
@ -89,16 +94,22 @@ that quietly did nothing, which is why it is on."
:type 'boolean)
(defcustom flan-inline-result t
"Whether an expression's value is shown beside the form it came from.
"Whether an expression's value is shown at the end of the line it came from.
The two knobs are not two ways of saying one thing. This one is about a
*value*: nothing else an evaluation produces has a place in the buffer to sit
beside, so an install still reports through `flan-echo-result' whatever this
is set to. Where they do overlap an expression whose value could go either
place the overlay wins and the echo is not also written, because saying the
same number twice is how a reader learns to stop reading both. The echo is
what happens when no overlay could be drawn: the REPL, a value that came back
with no region behind it, or this turned off."
is set to.
Where they overlap an expression, whose value can go both places both are
written. The overlay used to suppress the echo, on the argument that saying
one number twice teaches a reader to skip both; in practice the echo area is
the half that is still there after the next keystroke takes the overlay down,
and a value that is gone before it was read is worse than a value said twice.
So the two are now independent: this one decides the overlay, and
`flan-echo-result' decides the echo, and turning this off leaves the echo as
the only place a value appears which is also what happens where no overlay
could be drawn at all, as at the REPL."
:type 'boolean)
(defcustom flan-names-shown 4
@ -118,6 +129,18 @@ build that is merely slow can be watched, which is what the reply timeout
below points at."
:type 'string)
(defcustom flan-diagnostics-buffer "*flan-diagnostics*"
"Buffer every compiler diagnostic the editor is handed is appended to.
The overlay a refusal draws is about the command that just ran and is gone at
the next keystroke, which is right for feedback and wrong for a message you
want to keep: to quote one somewhere else you had to find it again in
*Messages*, among everything else Emacs had said since. This is the other
half one entry per message, in the order they arrived, never cleared by
anything but you. `M-x flan-clear-diagnostics' when it has grown past being
useful."
:type 'string)
(defcustom flan-reply-timeout 30
"Seconds to wait for one reply from the daemon before giving up.
@ -755,7 +778,25 @@ It builds the program first, which for a cold project is most of this."
(let ((inhibit-read-only t))
(erase-buffer)
(insert (mapconcat #'identity args " ") "\n\n"))
(setq default-directory (file-name-directory (expand-file-name file))))
;; The buffer's own, not just the `let' above, and now load-bearing
;; twice: a relative path in a diagnostic is relative to where the daemon
;; ran, and that is what the jump below resolves it against.
(setq default-directory (file-name-directory (expand-file-name file)))
;; This is where a `main' that does not compile says so, and Flan writes
;; a diagnostic as `file:line:col: message' — the GCC shape
;; `compilation-error-regexp-alist' was built around, and which its
;; `gnu' entry matches with nothing added here. (The two lines a
;; diagnostic carries under that one, the source line and its carets,
;; match nothing and are left as text, which is what they are.) So the
;; buffer only has to be told it is compilation output, and `M-g M-n',
;; `next-error' and RET on the line all reach the code.
;;
;; The minor mode and not `compilation-mode': the buffer is also the
;; daemon's own log and the program's stderr, arriving for as long as the
;; program runs, and `compilation-mode' would claim it as the output of
;; one finished command — killing the process on a `recompile', among
;; other things it has no business doing to a live session.
(compilation-minor-mode 1))
(make-process
:name "flan-daemon" :buffer buf
:command args
@ -1326,6 +1367,70 @@ else, and an overlay that survived a fix would be pointing at code that is
no longer wrong."
(flan-clear-errors))
;;; The diagnostics log
;; The overlay above is the whole of what a refusal used to leave behind, and
;; it is deliberately short-lived: it is feedback about one command and the
;; next keystroke takes it away. That is right for reading and wrong for
;; keeping. A message worth quoting somewhere else — into a bug report, into a
;; chat window, into a file — had to be fished back out of *Messages*, where it
;; sits among everything else Emacs has said and with no mark saying where one
;; message ends and the next begins.
;;
;; So both: the overlay, unchanged, and a buffer that accumulates. One entry
;; per message, the time it arrived on its own line above it, nothing ever
;; removed but by hand. The message line itself is left in exactly the shape
;; the compiler wrote it — `file:line:col: text' — which is what makes
;; `compilation-minor-mode' below able to jump from it, and also what makes a
;; line yanked out of here read the same as one pasted from a terminal.
(defun flan--diagnostics-buffer ()
"The diagnostics buffer, made and set up if this is the first message."
(or (get-buffer flan-diagnostics-buffer)
(with-current-buffer (get-buffer-create flan-diagnostics-buffer)
;; The same mode the daemon's buffer gets, for the same reason: every
;; entry names a file, a line and a column, so `next-error' and RET
;; should go there. A minor mode rather than `compilation-mode',
;; because this buffer is not the output of a command that ran once.
(compilation-minor-mode 1)
(current-buffer))))
(defun flan--record-diagnostic (loc msg)
"Append MSG, which the daemon reported at LOC, to the diagnostics log.
LOC may be nil: a refusal the daemon could not place is still a message
somebody may want to keep, and it is written without one."
(when (and (stringp msg) (> (length msg) 0))
(with-current-buffer (flan--diagnostics-buffer)
(let ((at-end (= (point) (point-max)))
(inhibit-read-only t))
(save-excursion
(goto-char (point-max))
(unless (bolp) (insert "\n"))
(insert (format-time-string "── %H:%M:%S ──────────────────────\n"))
(insert (if (stringp loc) (format "%s: %s\n" loc msg)
(format "%s\n" msg))
"\n"))
;; `flan--append-output's rule, and the same one: follow the tail only
;; for somebody who was already at it, since a reader scrolled back is
;; reading something.
(when at-end (goto-char (point-max)))))))
;;;###autoload
(defun flan-show-diagnostics ()
"Show every compiler message this session has been handed."
(interactive)
(display-buffer (flan--diagnostics-buffer)))
;;;###autoload
(defun flan-clear-diagnostics ()
"Empty the diagnostics log.
Nothing else empties it not connecting, not an accepted evaluation, not
quitting the program which is the point of it."
(interactive)
(with-current-buffer (flan--diagnostics-buffer)
(let ((inhibit-read-only t))
(erase-buffer))))
(defun flan--show-error (loc msg)
"Mark MSG at LOC, if LOC names a file some buffer is visiting.
Returns non-nil when it put an overlay somewhere."
@ -1381,12 +1486,21 @@ Returns non-nil when it put an overlay somewhere."
;; The shape is `flan-watch--ghost-text's, deliberately: " => " and a shadow
;; face. A watched value at a spy site and an expression's value at the form
;; you evaluated are the same thing seen two ways, and two spellings of it
;; would read as two features. What keeps them from fighting is that they sit
;; at different places — the watch's at end of line, this at end of form — and
;; where a one-line top-level form makes those the same position, two
;; after-strings at one position stack in priority order rather than one
;; hiding the other. They are also on different properties, so neither
;; clearing pass reaches into the other's overlays.
;; would read as two features. The position is the watch's too — the end of
;; the *line* the form ends on, not the end of the form. It used to be the end
;; of the form, on the argument that a value at a different place is how you
;; tell the two features apart; what that cost is a value wedged into the
;; middle of a line, reflowing the code around it, every time the form
;; evaluated was not the last thing on its line. Telling them apart is not a
;; job worth that, and it was never really the position doing it — the watch
;; buffer's values are standing annotations on a running program and this one
;; is gone at the next keystroke, which is a difference you can see.
;;
;; So they do now land at one position where a watched call and an evaluated
;; form share a line. Two after-strings at one position stack in priority
;; order rather than one hiding the other, which is what that case did before
;; on a one-line top-level form. They are also on different properties, so
;; neither clearing pass reaches into the other's overlays.
(defface flan-result-face '((t :inherit shadow))
"Face for an expression's value, shown beside the form it came from."
@ -1410,18 +1524,24 @@ Returns non-nil when it put an overlay somewhere."
(flan-clear-result))
(defun flan--show-result (value at)
"Draw VALUE after position AT. Returns non-nil when it drew one.
"Draw VALUE at the end of the line AT is on. Returns non-nil when it drew.
AT is a position in the form that was evaluated and the value goes at the end
of its line, for the reason the section above gives.
Empty overlays rather than a region: the value is not a property of any text,
so nothing should be highlighted and nothing should move when the buffer is
edited under it. Returning whether it drew is what lets the caller fall back
to the echo area instead of losing the value entirely."
edited under it. Whether it drew is no longer what decides the echo area
both are written now and is still answered, because the REPL and a value
with no buffer behind it are cases where nothing was drawn and the caller may
want to know."
(when (and flan-inline-result at (buffer-live-p (current-buffer)))
;; The old one first, so a second evaluation in one command — which is
;; what `flan-eval-buffer' and a macro of these amount to — leaves one
;; value and not a column of them.
(flan-clear-result)
(let ((ov (make-overlay at at nil t nil)))
(let* ((eol (save-excursion (goto-char at) (line-end-position)))
(ov (make-overlay eol eol nil t nil)))
(overlay-put ov 'flan-result t)
(overlay-put ov 'after-string
(propertize (concat " => " value)
@ -1821,10 +1941,11 @@ of the tenth name tells you neither how many there were nor which."
(defun flan--report (reply what &optional at)
"Report REPLY, describing WHAT was sent.
AT, when given, is where in the current buffer an expression's value may be
drawn the end of the form that was sent. A reply with no value in it never
reaches an overlay whatever AT says, because an install is a sentence and not
a value and has nothing to sit beside."
AT, when given, is the end of the form that was sent: the position that says
which *line* an expression's value belongs to, since that is where
`flan--show-result' draws it. A reply with no value in it never reaches an
overlay whatever AT says, because an install is a sentence and not a value and
has nothing to sit beside."
(if (equal (plist-get reply :status) "ok")
(let ((fns (plist-get reply :fns))
(names (plist-get reply :names))
@ -1839,13 +1960,20 @@ a value and has nothing to sit beside."
;; ...and a name that was just installed should complete, and have a
;; signature, from this moment rather than from the next connect.
(when (or fns names) (ignore-errors (flan-refresh-defs)))
;; The overlay first, because whether it drew is what decides the echo
;; area's business: a value shown beside the form does not want saying
;; again one line below it. Independent of `flan-echo-result' on
;; purpose — turning the echo off is a statement about the echo, and
;; it should not silently take the inline value with it.
(let ((shown (and value (ignore-errors (flan--show-result value at)))))
(when (and flan-echo-result (not shown))
;; Both, where both are possible. The overlay is the one that answers
;; "which form was that?" and the echo area is the one that is still
;; readable after the next keystroke has taken the overlay down —
;; between a value said twice and a value gone before it was read, the
;; second is the worse failure, and it is the one that kept happening.
;; The two settings are independent in both directions: turning the
;; echo off is a statement about the echo and does not take the inline
;; value with it, and turning the inline value off leaves the echo.
;;
;; The overlay first, still — not because the echo waits on the answer
;; any more, but because the order is what survives: a `message'
;; written first is wiped by whatever the drawing does to the buffer.
(when value (ignore-errors (flan--show-result value at)))
(when flan-echo-result
(cond
;; An expression's value, rendered inside the running program —
;; nothing was marshalled back, because nothing could be.
@ -1874,12 +2002,16 @@ a value and has nothing to sit beside."
(let ((vars (and fns (seq-difference names fns))))
(if vars (format " (also %s)"
(flan--names-phrase vars ""))
""))))))))
"")))))))
;; The daemon reports where, so mark it there. This must not itself
;; signal: the error the caller is owed is the daemon's, and losing it to a
;; bad location would report the wrong thing entirely.
(let ((loc (plist-get reply :loc))
(msg (plist-get reply :message)))
;; Logged before it is drawn, and whether or not it can be drawn: the
;; overlay needs a location and a buffer visiting the file, and the
;; message is worth keeping in either case.
(ignore-errors (flan--record-diagnostic loc (or msg "rejected")))
;; A refusal is not a value. `flan--show-error' clears the buffer it
;; marks, but a rejection the client cannot place — no `:loc', or a file
;; nobody is visiting — marks nothing, and leaving the last value up
@ -2004,34 +2136,44 @@ daemon reads that as stopping on entry instead."
;; question of it: the top-level form point is *in*, which for a bare
;; `(+ 1 1)' written at column 1 is that expression and nothing else.
;;
;; Hardcoded, which is the thing to be careful about: the authority is the
;; declaration arm of `Parse.expr' in lib/parse.ml (the "is a top-level
;; declaration, not an expression" refusal, around line 458), plus the
;; `declare'/`declare-c' arm a few forms below it. This list is that set and
;; has to be changed with it. `defunion' is being renamed `defdata', with
;; `defunion' becoming a C-style untagged union; both spellings are top-level
;; declarations either way, so this list wants `defdata' adding when that
;; lands rather than a swap.
;; Hardcoded, which is the thing to be careful about: the authority is
;; `Parse.decl' in lib/parse.ml — every head it has an arm for is a
;; declaration and belongs here — and this list is that set minus `package',
;; for the reason below. Re-derived whole when it is touched, rather than
;; topped up a name at a time: patching in the one name somebody noticed is
;; exactly how it came to be missing `defdata' and all four of the object and
;; dispatch heads for weeks after they landed. (`Parse.decl's top-level
;; `(do ...)' arm is not a head — it splices the declarations inside it, and
;; the forms this list is matched against are what comes out of that.)
;;
;; `package' is deliberately not here even though flan-mode.el's
;; `flan--definers' has it. This is the set of heads that *fail* when sent to
;; the expression evaluator, which is the bug `C-x C-e' was fixed for, and a
;; `package' form does not fail — `Parse.expr' has no arm refusing it. It is
;; still a declaration, so `C-c C-c' has to keep installing it; see
;; `flan--defun-heads' below, which is this list plus that one head.
;; It used to be derived from somewhere else: the declaration arm of
;; `Parse.expr', the "is a top-level declaration, not an expression" refusal,
;; on the reading that this is the set of heads that *fail* when sent to the
;; expression evaluator. That reading does not survive `declare' and
;; `declare-c', which have been in this list from the start and which
;; `Parse.expr' has no arm for at all: sent as expressions they parse as calls
;; of an unknown name, and the message that comes back is worse than the one
;; the declaration path gives. Which is an argument for installing them, not
;; for leaving them out. So the rule is the plain one — is it a declaration —
;; and `package' is a single named exception rather than the edge of a subtler
;; rule that was never quite true.
(defconst flan--declaration-heads
'("defmacro" "defn" "defvar" "defconst" "defstruct" "defunion" "defenum"
"defalias" "import" "declare" "declare-c")
'("defmacro" "defn" "defvar" "defconst"
"defstruct" "defdata" "defunion" "defenum" "defalias"
;; The object and dispatch heads (lib/parse.ml:1277-1334), which are
;; declarations in exactly the way `defn' is: each introduces a top-level
;; name and none of them is an expression.
"defclass" "defgeneric" "defmulti" "defmethod"
"import" "declare" "declare-c")
"Heads whose form is a declaration, and never an expression.")
;; The two keys ask *nearly* the same question, and `package' is where the two
;; readings come apart. `C-x C-e' asks "would this fail if I sent it to the
;; expression evaluator?", and a `package' form would not: `Parse.expr' has no
;; arm refusing it, so it falls through to a call of an unknown name and the
;; message would be a worse one than the declaration path gives. `C-c C-c'
;; asks the plain question — "is this a declaration?" — and `package' is one:
;; `Parse.decl' has an arm for it, `C-c C-c' has always installed it, and
;; routing it to the expression evaluator would take that away.
;; `package' is the one declaration `C-x C-e' does not claim, and it is kept
;; out to keep a behaviour rather than to follow a rule: the two keys have
;; always differed here, `C-c C-c' installing a `package' form and `C-x C-e'
;; leaving it to the expression evaluator. Nothing wants changing — a
;; `package' line is written once at the top of a file and is not a thing
;; anybody re-evaluates by hand — so the difference stays, named here where it
;; can be found rather than inferred from a list that is otherwise complete.
(defconst flan--defun-heads (cons "package" flan--declaration-heads)
"Heads `C-c C-c' recompiles and installs rather than evaluating.")
@ -2181,8 +2323,9 @@ declaration for it to live in."
;; start of the *next* line in every file that ends a line after a form
;; — which is every file. The declaration path never noticed, because
;; a newline more or less in the text sent changes nothing. This one
;; draws a value at END, and at the untrimmed one it lands in column 0
;; of the line below, over the gap before the next form.
;; draws a value on END's line, and the untrimmed END is on the line
;; below the form — so the value would sit at the end of the blank line
;; after it, or at the end of whatever form comes next.
(let ((end (save-excursion (goto-char (cdr b))
(skip-chars-backward " \t\n")
(point))))
@ -2458,8 +2601,10 @@ sends padded text."
(msg (plist-get r :message)))
;; The two refusals that reach here are a macro that never settles and
;; a ring, and both name a macro at a location — so mark it, exactly as
;; a refused evaluation is marked. This must not itself signal: the
;; error the caller is owed is the daemon's.
;; a refused evaluation is marked, and log it for the same reason.
;; This must not itself signal: the error the caller is owed is the
;; daemon's.
(ignore-errors (flan--record-diagnostic loc (or msg "refused")))
(ignore-errors (flan--show-error loc (or msg "refused")))
(user-error "flan: %s%s" (or msg "refused")
(if loc (format " (%s)" loc) ""))))

View File

@ -496,20 +496,23 @@ already rely on it — so nothing here is a stand-in for the real thing."
(test-flan--check "nor does a bare expression at top level"
(null (flan--declaration-before-point)))
;; ...and it still evaluates as one, which is the half of this key that
;; was already working and must not have moved. The value is now drawn
;; beside the form and no longer echoed, so both halves are asserted: an
;; overlay that says it, and an echo area that does not say it twice.
;; was already working and must not have moved. The value goes to both
;; places, so both halves are asserted: an overlay that says it, and an
;; echo area that says it too. The echo used to be suppressed whenever the
;; overlay drew; what that cost is a value gone at the next keystroke with
;; nothing left anywhere, which is worse than a number said twice.
(let ((said (test-flan--said (flan-eval-last-sexp))))
(test-flan--check "which C-x C-e evaluates and shows beside the form"
(let ((r (test-flan--result)))
(and r (string-match-p "5" r)
(string-match-p "=>" r))))
(test-flan--check "and does not also say it in the echo area"
(not (and said (string-match-p "=>" said))))
;; The overlay is anchored to the form that produced it, not to point
;; and not to the start of the line: two expressions one under the
;; other both returning 2 is the case the echo area cannot answer.
(test-flan--check "the value is drawn at the end of the form"
(test-flan--check "and says it in the echo area as well"
(and said (string-match-p "=> 5" said)))
;; The overlay is anchored to the line the form ends on — `flan-watch's
;; position, and now this one's too, so that a value never lands in the
;; middle of a line and reflows the code after it. Here the form is the
;; last thing in the buffer, so that line's end is `point-max'.
(test-flan--check "the value is drawn at the end of the form's line"
(let ((ovs (flan--result-overlays)))
(and (= 1 (length ovs))
(= (overlay-start (car ovs)) (point-max)))))
@ -531,14 +534,35 @@ already rely on it — so nothing here is a stand-in for the real thing."
(current-buffer))))))))
(delete-region beg (point-max)))
;; The position that changed, and the only arrangement that can show it: with
;; something written after the form on its line, the end of the form and the
;; end of the line are different places. The value belongs at the second,
;; where `flan-watch' has always put one — a value wedged into the middle of
;; a line reflows every character after it.
(goto-char (point-max))
(let ((beg (point)))
(insert "\n(+ 2 3) ; and a comment after it")
(search-backward ") ;")
(forward-char 1)
(test-flan--said (flan-eval-last-sexp))
(test-flan--check "the value goes to the end of the line, not of the form"
(let ((ovs (flan--result-overlays)))
(and (= 1 (length ovs))
(= (overlay-start (car ovs)) (point-max)))))
;; What a keystroke would do; there are no commands under --batch.
(flan-clear-result)
(goto-char (point-max))
(delete-region beg (point-max)))
;; Switched off, the echo area is what is left: the two settings are not two
;; spellings of one thing, and a value has to come out somewhere.
;; spellings of one thing, and turning the overlay off must not take the
;; value with it.
(goto-char (point-max))
(let ((beg (point)))
(insert "\n(+ 7 8)")
(let* ((flan-inline-result nil)
(said (test-flan--said (flan-eval-last-sexp))))
(test-flan--check "with the overlay off the value goes back to the echo area"
(test-flan--check "with the overlay off the value is still in the echo area"
(and said (string-match-p "15" said)
(string-match-p "=>" said)
(null (flan--result-overlays)))))
@ -599,12 +623,13 @@ already rely on it — so nothing here is a stand-in for the real thing."
(test-flan--check "and draws no value beside it"
(null (flan--result-overlays))))
;; `package' is the one head the two keys disagree about, and it is a real
;; disagreement rather than an oversight: `Parse.expr' has no arm refusing
;; it, so C-x C-e's "would this fail as an expression" says no, while
;; `Parse.decl' has one and C-c C-c has always installed it. Routing both
;; off one list would have quietly taken that away. Nothing is sent here —
;; the claim is only about which path each key picks.
;; `package' is the one head the two keys disagree about, and the
;; disagreement is a behaviour being kept rather than a rule being followed:
;; `C-c C-c' has always installed a `package' form and `C-x C-e' has always
;; left it to the expression evaluator, and nothing about a line written once
;; at the top of a file asks for that to change. Routing both keys off one
;; list would quietly take it away. Nothing is sent here — the claim is only
;; about which path each key picks.
(with-temp-buffer
(flan-mode)
(insert "(package demo)\n")
@ -618,6 +643,33 @@ already rely on it — so nothing here is a stand-in for the real thing."
(test-flan--check "and C-x C-e does not, for the reason it never has"
(null (flan--declaration-before-point))))
;; And every other head does agree, which is the half that had rotted: the
;; list was topped up by name and so lost `defdata' and all four object and
;; dispatch heads for as long as they had existed. A head missing from it
;; is not a quiet fallback — the form goes to the expression evaluator and
;; the daemon refuses it with "is a top-level declaration, not an
;; expression", which is the complaint this is here to keep from coming
;; back. Read off `Parse.decl', so the check is the derivation.
(with-temp-buffer
(flan-mode)
(dolist (head '("defmacro" "defn" "defvar" "defconst" "defstruct"
"defdata" "defunion" "defenum" "defalias"
"defclass" "defgeneric" "defmulti" "defmethod"
"import" "declare" "declare-c"))
(erase-buffer)
;; A body neither key needs to be valid: both questions are asked of the
;; head alone, and a form that parses is a different test.
(insert "(" head " thing [x])\n")
(goto-char (point-min))
(search-forward "thing")
(test-flan--check (format "C-c C-c installs a %s form" head)
(equal (flan--declaration-head-at
(car (flan--defun-bounds)) flan--defun-heads)
head))
(goto-char (point-max))
(test-flan--check (format "and C-x C-e installs one too, for %s" head)
(equal (car (flan--declaration-before-point)) head))))
;; A prefix on the expression path is a flag and not a position — there is
;; no inside for one to point at — so both `C-u' and `C-u C-u' mean the one
;; thing, which is `C-u C-x C-e's behaviour reached through the other key.