From ce900acfe4d32284a42b63da8bdb7bb63e065e33 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 18:14:25 +0700 Subject: [PATCH 1/3] 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. --- emacs/MANUAL.md | 82 +++++++++++--- emacs/flan-inspect.el | 42 ++++++- emacs/flan-mode.el | 127 ++++++++++++++++++++- emacs/flan.el | 257 ++++++++++++++++++++++++++++++++---------- emacs/test-flan.el | 46 ++++++-- 5 files changed, 457 insertions(+), 97 deletions(-) diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index 1644d47..e59ff96 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -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 | diff --git a/emacs/flan-inspect.el b/emacs/flan-inspect.el index 2587989..d876dbe 100644 --- a/emacs/flan-inspect.el +++ b/emacs/flan-inspect.el @@ -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. diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 00905c7..0c2ca04 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -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)) diff --git a/emacs/flan.el b/emacs/flan.el index 31bcdf4..441d261 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -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) "")))) diff --git a/emacs/test-flan.el b/emacs/test-flan.el index 7a009a9..c41bbd7 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -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))))) From f4fcbe774571dfabcc61e73f0c0b2688ca5d76a3 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 18:17:48 +0700 Subject: [PATCH 2/3] Say what C-c C-i actually takes: the innermost thing, not the enclosing form --- emacs/MANUAL.md | 6 ++++-- emacs/flan-inspect.el | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/emacs/MANUAL.md b/emacs/MANUAL.md index e59ff96..09b6c1d 100644 --- a/emacs/MANUAL.md +++ b/emacs/MANUAL.md @@ -377,8 +377,10 @@ everything it had is still there. ## Looking at values **`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. +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 diff --git a/emacs/flan-inspect.el b/emacs/flan-inspect.el index d876dbe..2e76e0a 100644 --- a/emacs/flan-inspect.el +++ b/emacs/flan-inspect.el @@ -680,13 +680,18 @@ and whether that may be followed is the answer")) (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. +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." From 865c7e04459f1e9573faf4b2b5d281bdb7f7ef9d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 18:23:33 +0700 Subject: [PATCH 3/3] C-c C-c on a defclass went to the expression evaluator and was refused flan--declaration-heads was topped up a name at a time, so it had lost defdata and all four of the object and dispatch heads. A head missing from it is not a quiet fallback: the form goes to the expression evaluator, and the daemon answers "defclass is a top-level declaration, not an expression" at the line you pressed the key on. Confirmed both ways against a real daemon on test/programs/dev-class.flan -- the old list gets that refusal, the new one installs, and defgeneric and defmethod install through the same path (area@point, area). Re-derived whole from Parse.decl, which is the honest authority. The comment said the authority was the declaration arm of Parse.expr, and that was never quite true: declare and declare-c have been in this list from the start and Parse.expr has no arm for either. So the rule is the plain one -- is it a declaration -- and package is a single named exception, kept because the two keys have always differed there and nothing about a line written once at the top of a file asks for that to change. The rationale in test-flan.el's package check said the same stale thing and says the surviving reason now. The new check reads the list off Parse.decl and asserts both keys on every head, so the derivation is the test rather than a claim beside it. --- emacs/flan.el | 58 +++++++++++++++++++++++++++------------------- emacs/test-flan.el | 40 +++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/emacs/flan.el b/emacs/flan.el index 441d261..6e16b11 100644 --- a/emacs/flan.el +++ b/emacs/flan.el @@ -2136,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.") diff --git a/emacs/test-flan.el b/emacs/test-flan.el index c41bbd7..6c218a1 100644 --- a/emacs/test-flan.el +++ b/emacs/test-flan.el @@ -623,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") @@ -642,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.