Six editor complaints from the dogfooding notes

Each was decided in DISCUSS.org; this is the elisp side of all six.

The eval value goes to the end of the line now, where flan-watch has
always put one, and it is echoed as well as drawn. Both of those
overturn a documented decision, so both docstrings and the MANUAL say
what the behaviour is rather than still arguing the old case: a value
wedged mid-line reflows the code after it, and a value that is gone at
the next keystroke with nothing left anywhere is worse than a number
said twice.

C-c C-i inspects what is at point with no prompt, and C-u opens the
minibuffer. It takes the form point is inside, not the one behind
point, because the middle of a name is where a key pressed without
looking lands.

Compiler messages are kept: *flan-diagnostics* accumulates one entry
per message, timestamped, never cleared by anything but
flan-clear-diagnostics. The overlay is unchanged. Each message line
keeps the compiler's own file:line:col: shape, which is both what makes
it jumpable and what makes it paste like a terminal line.

*flan-dev* gets compilation-minor-mode, so a main that does not compile
is a buffer you can M-g M-n through. Verified against a real diagnostic
rather than assumed: the default gnu entry matches the message line and
nothing else in the transcript, so there is no custom regexp here.

#_ is drawn as a comment, via a syntax-propertize-function that fences
the discarded span -- clojure-mode's approach, and the nesting rule is
the reader's, so #_#_ a b greys both with nothing counting.

And the two keyword lists are re-read off lib/parse.ml whole, rather
than topped up by name: defmacro, the four object/dispatch definers and
declare-c on one side; when, cond, and, or, break, continue, recur, fn,
quote, array, signal, error and the four condition forms on the other.
This commit is contained in:
Joseph Ferano 2026-09-20 18:14:25 +07:00
parent dc39631db7
commit ce900acfe4
5 changed files with 457 additions and 97 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,14 @@ 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 form point is *inside*, not merely
the one behind point, so the middle of a name works as well as the end of one.
**`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 +863,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 +988,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 +1037,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 +1052,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 +1070,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,51 @@ 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 form point is *in*, 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.
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,47 +1960,58 @@ 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))
(cond
;; An expression's value, rendered inside the running program —
;; nothing was marshalled back, because nothing could be.
(value (message "=> %s" value))
;; The daemon accepted it and had nothing to send. Say so rather
;; than claiming an install that did not happen.
(note (message "flan: %s — %s"
(flan--names-phrase names what) note))
(t
;; `:fns' are the bodies that were installed and `:names' is
;; everything the evaluation declared; a buffer of five functions
;; and two vars should not report as "five".
;;
;; A declaration with no body at all — a `defvar', a `defstruct' —
;; has none of the first, and leading with WHAT then read as "form
;; installed in 4 ms (also ticks)": the one name that actually
;; changed, parenthesised as an afterthought, beside a label that
;; says nothing. Where there are no functions the names *are* what
;; changed, so they are what the sentence is about, and the aside
;; is then empty by construction rather than a repeat of it. Now
;; that `C-x C-e' reaches this path too, that sentence is also how
;; you tell an installed declaration from an expression's `=>'.
(message "flan: %s installed in %.0f ms%s"
(flan--names-phrase (or fns names) what)
(or (plist-get reply :ms) 0)
(let ((vars (and fns (seq-difference names fns))))
(if vars (format " (also %s)"
(flan--names-phrase vars ""))
""))))))))
;; 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.
(value (message "=> %s" value))
;; The daemon accepted it and had nothing to send. Say so rather
;; than claiming an install that did not happen.
(note (message "flan: %s — %s"
(flan--names-phrase names what) note))
(t
;; `:fns' are the bodies that were installed and `:names' is
;; everything the evaluation declared; a buffer of five functions
;; and two vars should not report as "five".
;;
;; A declaration with no body at all — a `defvar', a `defstruct' —
;; has none of the first, and leading with WHAT then read as "form
;; installed in 4 ms (also ticks)": the one name that actually
;; changed, parenthesised as an afterthought, beside a label that
;; says nothing. Where there are no functions the names *are* what
;; changed, so they are what the sentence is about, and the aside
;; is then empty by construction rather than a repeat of it. Now
;; that `C-x C-e' reaches this path too, that sentence is also how
;; you tell an installed declaration from an expression's `=>'.
(message "flan: %s installed in %.0f ms%s"
(flan--names-phrase (or fns names) what)
(or (plist-get reply :ms) 0)
(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
@ -2181,8 +2313,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 +2591,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)))))