534 lines
25 KiB
EmacsLisp
534 lines
25 KiB
EmacsLisp
;;; flan-watch.el --- Watched values, in a pinned buffer or inline -*- lexical-binding: t; -*-
|
|
|
|
;; Author: Joseph Ferano <joseph@ferano.io>
|
|
;; Version: 0.1.0
|
|
;; Package-Requires: ((emacs "29.1"))
|
|
;; Keywords: languages, lisp, tools
|
|
|
|
;; The headers above are what make this directory installable. M-x
|
|
;; package-install-file on it reads them, and a file with no Version: is not a
|
|
;; package as far as package.el is concerned -- until now the client was
|
|
;; reachable only by adding it to load-path by hand, which is a thing to
|
|
;; explain to every person who wants to try it.
|
|
;;
|
|
;; 29.1 is the floor because it is the oldest Emacs any of this has been run
|
|
;; against, not because some function here is known to need it. dape, which
|
|
;; flan-dape drives, asks for 29.1 as well and is a soft dependency: it is
|
|
;; reached through declare-function, so the rest of the client loads and works
|
|
;; without it and it is deliberately not listed above. The compiler this talks
|
|
;; to is not an Emacs package and cannot be listed here either -- emacs/MANUAL.md
|
|
;; says what has to be on PATH.
|
|
|
|
;; A HUD for a running Flan program: a buffer that always shows the current
|
|
;; frame's values and nothing else. Ported from the author's Clojure
|
|
;; `clj-watch', with one thing kept and one thing inverted.
|
|
;;
|
|
;; There are two pictures of one table. The buffer is below; ghost text —
|
|
;; `flan-watch-ghost-mode', the same values shown inline at the calls that wrote
|
|
;; them — is further down, with its own rationale. Everything between here and
|
|
;; there is common to both.
|
|
;;
|
|
;; KEPT, and it is the good idea in the original: **the program decides what is
|
|
;; shown**. There is no watch-expression machinery here, no per-variable
|
|
;; registration, no UI for building a query. The program says what it wants
|
|
;; seen, from inside its own loop, and this paints it. Everything a watch list
|
|
;; would need — where it lives, whether it survives a restart, whether it gets
|
|
;; committed by accident — stops being a question when the list is the code.
|
|
;;
|
|
;; INVERTED: the original polls. Emacs calls `(watch/render)' on a timer and
|
|
;; paints the string that comes back, which in Clojure costs an eval and an eval
|
|
;; is cheap. Here it is not. An evaluation *compiles a module and dlopens it*
|
|
;; — tens of milliseconds and a new .so each time, in a directory nothing
|
|
;; sweeps — so polling at 5Hz would produce hundreds of shared objects a minute
|
|
;; to read a number that was already in a register.
|
|
;;
|
|
;; So the program pushes. It calls into the runtime's watch table from its own
|
|
;; loop; this reads the table, which is memory rather than an evaluation. Both
|
|
;; halves are cheap for opposite reasons, and two things fall out that a poll
|
|
;; could not have given:
|
|
;;
|
|
;; the values update at *frame rate* rather than at the repaint rate — the
|
|
;; interval only decides how often the picture is repainted, not how fresh
|
|
;; it is;
|
|
;;
|
|
;; and the last frame's values are still there while the program is
|
|
;; *stopped*. A break loop is precisely when no thunk can run at a frame
|
|
;; boundary, because there are no more frames, and precisely when you want to
|
|
;; see what the last one held.
|
|
;;
|
|
;; The daemon reads the table and sends it on the editor's connection, as it
|
|
;; sends the program's output, so Emacs never waits on a timer for it. The
|
|
;; paint is `replace-buffer-contents' rather than erase-and-insert, because it
|
|
;; diffs, so point and scroll survive a repaint instead of being yanked to the
|
|
;; top five times a second.
|
|
;;
|
|
;; What a program writes:
|
|
;;
|
|
;; (defn step [] i64
|
|
;; (set ticks (+ ticks 1))
|
|
;; (watch "ticks" ticks)
|
|
;; (watch "player" player)
|
|
;; ticks)
|
|
;;
|
|
;; `watch' renders any value the way `print' does — a struct, a slice, a dyn
|
|
;; value — into the table. The scalar entry points underneath it,
|
|
;; `flan_dev_watch_i64' and the rest, are reachable through `declare-c' too.
|
|
|
|
;;; Code:
|
|
|
|
(require 'flan)
|
|
(require 'subr-x)
|
|
|
|
(defgroup flan-watch nil
|
|
"A pinned watch buffer for a running Flan program."
|
|
:group 'flan
|
|
:prefix "flan-watch-")
|
|
|
|
(defcustom flan-watch-buffer "*flan-watch*"
|
|
"Buffer the watch table is painted into."
|
|
:type 'string)
|
|
|
|
(defcustom flan-watch-interval 0.2
|
|
"Seconds between repaints.
|
|
|
|
This is the *repaint* rate, not the watch rate. The program writes its values
|
|
every frame whatever this is; all this decides is how often the daemon sends
|
|
the table, which is why a slow value here costs freshness and nothing else.
|
|
Taken when the table is armed, so a change applies from the next `flan-watch'."
|
|
:type 'number)
|
|
|
|
(defvar flan-watch--rows nil
|
|
"The last table painted, as a list of (NAME . VALUE).")
|
|
(defvar flan-watch--consumers nil
|
|
"Which pictures of the table are currently wanted: `buffer', `ghost', or both.
|
|
|
|
There is one table, one arming message and one push, and more than one way to
|
|
look at what they produce. Holding the subscription here rather than in the
|
|
watch buffer is what lets ghost text outlive that buffer being closed — the
|
|
original design made the buffer *be* the subscription, which was right when it
|
|
was the only consumer and wrong the moment it was not.")
|
|
|
|
;;; Painting
|
|
|
|
(defun flan-watch--format (rows overflow)
|
|
"The buffer's text for ROWS. OVERFLOW means some name found no slot."
|
|
(if (null rows)
|
|
(concat "nothing is being watched\n\n"
|
|
"The program decides what is shown. Write into the watch table\n"
|
|
"from your own loop:\n\n"
|
|
" (watch \"ticks\" ticks)\n")
|
|
(let ((w (apply #'max (mapcar (lambda (r) (length (car r))) rows))))
|
|
(concat
|
|
(mapconcat (lambda (r)
|
|
;; Padded before it is propertised, not with a width in the
|
|
;; format string: `format' has no `%-*s', and a face on the
|
|
;; padding would underline trailing space.
|
|
(concat (propertize (car r) 'face
|
|
'font-lock-variable-name-face)
|
|
(make-string (+ 2 (- w (length (car r)))) ?\s)
|
|
(cdr r)))
|
|
rows "\n")
|
|
"\n"
|
|
;; Reported rather than swallowed. A name past the table's limit is a
|
|
;; value that simply never appears, and a buffer that said nothing about
|
|
;; it would be lying by omission — the reader would go looking for a bug
|
|
;; in the program. A flag and not a number: the only count the runtime
|
|
;; could keep is of write *attempts* that missed, and those happen every
|
|
;; frame, so one name too many would read as thousands.
|
|
(if overflow
|
|
(concat "\nthe table is full: some names found no slot,"
|
|
" and their values are not shown.\n"
|
|
"It holds 64.\n")
|
|
"")))))
|
|
|
|
(defun flan-watch--paint (text)
|
|
"Replace the watch buffer's contents with TEXT."
|
|
(when-let* ((buf (get-buffer flan-watch-buffer)))
|
|
(let ((tmp (get-buffer-create " *flan-watch-src*")))
|
|
(with-current-buffer tmp
|
|
(erase-buffer)
|
|
(insert text))
|
|
(with-current-buffer buf
|
|
(let ((inhibit-read-only t))
|
|
;; `replace-buffer-contents' diffs rather than erasing, so point and
|
|
;; the window's scroll position survive every tick. `erase-buffer'
|
|
;; and insert would yank the cursor to the top five times a second,
|
|
;; which makes the buffer unusable for the one thing you want to do
|
|
;; in it — look at a particular line while the program runs.
|
|
(replace-buffer-contents tmp))))))
|
|
|
|
;;; Ghost text — the same table, painted at the code instead of beside it
|
|
|
|
;; The buffer shows everything at once, which is what you want while the game
|
|
;; runs. Ghost text shows one value where it was written, which is what you
|
|
;; want while you read one function. They are two pictures of one table, so
|
|
;; this is an addition and not a replacement, and neither one polls separately:
|
|
;; both are painted from the same reply, in `flan-watch--absorb'.
|
|
;;
|
|
;; WHERE A VALUE ATTACHES. The table carries a name and a rendered string and
|
|
;; no source location. A location is not actually missing: `(watch "hp" hp)'
|
|
;; or `(watch-i64 "hp" hp)' is *in the buffer*,
|
|
;; and the name in the table is the string literal in it. So the anchor is
|
|
;; found by searching the text rather than by being told, which costs one
|
|
;; regexp scan per displayed buffer per repaint and needs nothing new from the
|
|
;; daemon.
|
|
;;
|
|
;; The head of that call cannot be hardcoded. `watch-i64' is a name the
|
|
;; program's author chose in their own `declare-c'; only the C symbol behind it
|
|
;; is fixed, and the editor never sees that. Hence
|
|
;; `flan-watch-ghost-call-regexp', whose default matches the shape the manual
|
|
;; suggests and which anyone who named theirs differently can change. Matching
|
|
;; the string literal and not the head is what makes that safe: a name in the
|
|
;; table came from one of these call sites by construction.
|
|
;;
|
|
;; WHEN IT UPDATES. On the same push, from the same frame. The two pictures
|
|
;; therefore cannot disagree — they are one table read, painted twice. As
|
|
;; with the buffer, the push interval decides how often the picture is
|
|
;; repainted and not how fresh it is: the program writes every frame
|
|
;; regardless.
|
|
;;
|
|
;; Overlays are deleted and re-placed from scratch on every repaint rather than
|
|
;; being tracked across edits. That is the whole answer to the invalidation
|
|
;; problem the earlier sketch of this called the work: a line that moved does
|
|
;; not leave an overlay behind, because no overlay outlives a tick. The cost is
|
|
;; bounded by only scanning buffers that are *shown in a window* — an overlay in
|
|
;; a buffer nobody is looking at is invisible anyway, and a project's other
|
|
;; hundred files are not scanned. It also means a file you scroll to gets its
|
|
;; ghost text within one tick with nothing to hook.
|
|
;;
|
|
;; TWO SITES, ONE NAME. Both get the overlay and both show the same value, and
|
|
;; the text says so. That is the honest picture rather than a failure to pick:
|
|
;; the table has one slot per name and the last writer in the frame wins, so
|
|
;; the value at both sites really is the same value, and it is whichever call
|
|
;; ran last. Silently showing it at one site would suggest the other is not
|
|
;; running; showing it at both without a word would read as a coincidence.
|
|
;;
|
|
;; A LOOP. Inline shows the last value written, exactly as the buffer does.
|
|
;; "The last of 4000 iterations" is usually not the interesting one, but every
|
|
;; better answer is a UI for building a query, which is the thing this design
|
|
;; exists not to have. Settled, not open.
|
|
;;
|
|
;; STOPPED. The values survive a break — that is a property of pushing rather
|
|
;; than polling, and half the reason for it. But they are the *last frame's*,
|
|
;; and inline they sit in the middle of live-looking code with no modeline next
|
|
;; to them, so they say "last frame" in words and change face. The watch buffer
|
|
;; does not need to: you opened it deliberately and `flan:stopped(...)' is
|
|
;; already in view.
|
|
|
|
(defcustom flan-watch-ghost-call-regexp "watch\\(?:-[[:alnum:]]+\\)*"
|
|
"Regexp matching the head of a call that writes the watch table.
|
|
|
|
Matched against the symbol after an open paren, with the watched name as a
|
|
string literal after it. The default covers `watch', `watch-i64', `watch-f64',
|
|
`watch-str' and the two-segment accumulator names `watch-num-i64' and
|
|
`watch-num-f64' — hence the `*\=' rather than a `?\=', which stopped at one
|
|
hyphenated segment and so found no site for a numeric watch at all. It is a setting because the Flan name is the program author's
|
|
own `declare-c' binding — only the C symbol behind it is fixed, and the editor
|
|
never sees that."
|
|
:type 'regexp)
|
|
|
|
(defface flan-watch-ghost-face '((t :inherit shadow))
|
|
"Face for an inline watched value while the program is running.")
|
|
|
|
(defface flan-watch-ghost-stale-face '((t :inherit warning))
|
|
"Face for an inline watched value while the program is stopped.
|
|
|
|
Distinct from the running face because the value is the last frame's, and
|
|
inline it has no modeline beside it to say so.")
|
|
|
|
(defvar flan-watch--ghost-overlays nil
|
|
"Every overlay this has placed, across every buffer.")
|
|
|
|
(defun flan-watch--ghost-clear ()
|
|
"Remove every overlay this has placed."
|
|
(mapc #'delete-overlay flan-watch--ghost-overlays)
|
|
(setq flan-watch--ghost-overlays nil))
|
|
|
|
(defun flan-watch--ghost-buffers ()
|
|
"Flan buffers currently shown in some window, on any frame."
|
|
(let (bufs)
|
|
(dolist (w (window-list-1 nil 'nomini t))
|
|
(let ((b (window-buffer w)))
|
|
(when (and (provided-mode-derived-p (buffer-local-value 'major-mode b)
|
|
'flan-base-mode)
|
|
(not (memq b bufs)))
|
|
(push b bufs))))
|
|
bufs))
|
|
|
|
(defun flan-watch--ghost-sites ()
|
|
"Watch call sites in the current buffer, as a list of (NAME . END-OF-LINE)."
|
|
;; Either syntax: `(watch "name" v)' in a .flan file, `watch("name", v)' in
|
|
;; a .fln one, where the call is the name glued to its parenthesis.
|
|
(let ((re (concat "\\(?:(\\s-*\\(?:" flan-watch-ghost-call-regexp "\\)\\s-+"
|
|
"\\|\\_<\\(?:" flan-watch-ghost-call-regexp "\\)(\\s-*\\)"
|
|
"\"\\([^\"\n]*\\)\""))
|
|
(sites nil))
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(while (re-search-forward re nil t)
|
|
;; Everything wanted from this match is read out *before* the check.
|
|
;; `syntax-ppss' moves point and clobbers the match data, so reading
|
|
;; either afterwards puts the scan back where it started and the loop
|
|
;; never ends. Found the hard way.
|
|
(let ((beg (match-beginning 0))
|
|
(end (match-end 0))
|
|
(name (match-string-no-properties 1))
|
|
(eol (line-end-position)))
|
|
;; A call inside a string or a comment is text that looks like code.
|
|
(unless (nth 8 (syntax-ppss beg))
|
|
(push (cons name eol) sites))
|
|
(goto-char end))))
|
|
(nreverse sites)))
|
|
|
|
(defun flan-watch--ghost-row (name rows)
|
|
"The row in ROWS for the site name NAME, or nil.
|
|
|
|
Falls back to a prefix match against a name the runtime clipped: it holds 31
|
|
bytes of a name, so a longer one in the source will never match it exactly.
|
|
Guarded on the row's length so a short name cannot be claimed by accident."
|
|
(or (assoc name rows)
|
|
(let (hit)
|
|
(dolist (r rows hit)
|
|
(when (and (not hit)
|
|
(>= (string-bytes (car r)) 31)
|
|
(string-prefix-p (car r) name))
|
|
(setq hit r))))))
|
|
|
|
(defun flan-watch--ghost-text (value sites stale)
|
|
"The after-string for a value: VALUE, seen at SITES places, STALE if stopped."
|
|
(propertize
|
|
(concat " => " value
|
|
(when (> sites 1) (format " one slot, %d sites" sites))
|
|
(when stale " last frame"))
|
|
'face (if stale 'flan-watch-ghost-stale-face 'flan-watch-ghost-face)
|
|
;; Without this, point at end of line lands on the ghost text rather than
|
|
;; on the buffer's own last column.
|
|
'cursor t))
|
|
|
|
(defun flan-watch--ghost-paint (rows overflow)
|
|
"Place inline overlays for ROWS. OVERFLOW means some name found no slot."
|
|
(flan-watch--ghost-clear)
|
|
(let ((stale (and flan--stopped t)))
|
|
(dolist (buf (flan-watch--ghost-buffers))
|
|
(with-current-buffer buf
|
|
(let* ((sites (flan-watch--ghost-sites))
|
|
(counts (make-hash-table :test #'equal)))
|
|
(dolist (s sites)
|
|
(puthash (car s) (1+ (gethash (car s) counts 0)) counts))
|
|
(dolist (s sites)
|
|
(let* ((row (flan-watch--ghost-row (car s) rows))
|
|
(text
|
|
(cond
|
|
(row (flan-watch--ghost-text
|
|
(cdr row) (gethash (car s) counts 1) stale))
|
|
;; A site with no row and a full table is a value that
|
|
;; exists and was dropped, which is the one case worth
|
|
;; saying out loud — the buffer's overflow line says the
|
|
;; same thing without being able to say *which*. A site
|
|
;; with no row and room to spare has simply not run yet,
|
|
;; and annotating every one of those at startup would be
|
|
;; noise.
|
|
(overflow (flan-watch--ghost-text
|
|
"no slot: the table is full" 1 stale)))))
|
|
(when text
|
|
(let ((ov (make-overlay (cdr s) (cdr s))))
|
|
(overlay-put ov 'after-string text)
|
|
(overlay-put ov 'flan-watch-ghost t)
|
|
(push ov flan-watch--ghost-overlays))))))))))
|
|
|
|
;;; The push
|
|
|
|
(defun flan-watch--absorb (reply)
|
|
"Paint REPLY, a watch table from the daemon."
|
|
(pcase (plist-get reply :status)
|
|
("ok"
|
|
(setq flan-watch--rows
|
|
(mapcar (lambda (r) (cons (nth 0 r) (nth 1 r)))
|
|
(plist-get reply :watch)))
|
|
(flan-watch--paint
|
|
(flan-watch--format flan-watch--rows (plist-get reply :overflow)))
|
|
(when (memq 'ghost flan-watch--consumers)
|
|
(flan-watch--ghost-paint flan-watch--rows (plist-get reply :overflow))))
|
|
(_
|
|
;; The overlays go rather than being left at their last values. Inline
|
|
;; there is nowhere to print the error, so a stale number beside live code
|
|
;; would be the whole picture and it would be wrong; the watch buffer, if
|
|
;; it is open, says what happened.
|
|
(flan-watch--ghost-clear)
|
|
(flan-watch--paint
|
|
(format "error:\n%s\n" (or (plist-get reply :message) "refused"))))))
|
|
|
|
;; The daemon sends the table on the connection every `flan-watch-interval'
|
|
;; while it is armed, changed or not, the same way it sends the program's
|
|
;; output. Nothing here sends a request on a timer, so nothing here can leave
|
|
;; a reply in flight for another request to read.
|
|
;;
|
|
;; Each push closes the numeric slots' accumulation window, so their count,
|
|
;; range and mean are "since the last push" rather than since the program
|
|
;; started — except while the program is stopped, when the window is left open
|
|
;; and the numbers from the moment it stopped stay on the screen. The daemon
|
|
;; decides that, since it knows whether the program is stopped when it reads
|
|
;; the table.
|
|
|
|
(defun flan-watch--on-push (kind frame)
|
|
"Paint FRAME when KIND says it is the watch table."
|
|
(when (and (equal kind "watch") flan-watch--consumers)
|
|
(flan-watch--absorb frame)))
|
|
|
|
(defun flan-watch--on-connect ()
|
|
"Arm the table again on a new connection, if anything is watching.
|
|
A restarted daemon has a new program, and the new program's table is off."
|
|
(when flan-watch--consumers
|
|
(let ((r (ignore-errors (flan-watch--arm t))))
|
|
(unless (equal (plist-get r :status) "ok")
|
|
(flan-watch--paint
|
|
(format "error:\n%s\n"
|
|
(or (plist-get r :message) "the table could not be armed")))))))
|
|
|
|
(defun flan-watch--on-disconnect ()
|
|
"Say that the table is no longer live."
|
|
(when flan-watch--consumers
|
|
(flan-watch--ghost-clear)
|
|
(flan-watch--paint "error:\nnot connected to a running program\n")))
|
|
|
|
(defun flan-watch--arm (on)
|
|
"Ask the daemon to arm the table and push it, or to disarm it when ON is nil."
|
|
(flan--request (if on
|
|
`(:op "watch-enable" :on t :interval ,flan-watch-interval)
|
|
'(:op "watch-enable" :on nil))))
|
|
|
|
;;; Commands
|
|
|
|
(defvar flan-watch-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
;; As `flan-doc-mode-map'.
|
|
(define-key map "q" #'quit-window)
|
|
map)
|
|
"Keys in `flan-watch-mode'.")
|
|
|
|
(flan-evil-own-keys 'flan-watch-mode-map)
|
|
|
|
(define-derived-mode flan-watch-mode special-mode "flan-watch"
|
|
"Major mode for the pinned watch buffer."
|
|
(setq-local truncate-lines t)
|
|
;; Killing the buffer cancels the buffer's half of the subscription, and only
|
|
;; that. When it was the only consumer the table is disarmed, so a program
|
|
;; nobody is watching is back to paying a load and a branch per watch call;
|
|
;; when ghost text is also on, the table stays armed.
|
|
(add-hook 'kill-buffer-hook #'flan-watch--buffer-killed nil t))
|
|
|
|
(defun flan-watch--buffer-killed ()
|
|
"Drop the buffer consumer, the watch buffer being killed."
|
|
(when (memq 'buffer flan-watch--consumers)
|
|
(flan-watch--drop 'buffer)))
|
|
|
|
(defun flan-watch--subscribe (consumer)
|
|
"Arm the table for CONSUMER.
|
|
|
|
Arming is a message, not something the daemon infers. The program is the
|
|
writer, so it has to be told somebody is looking — and while nobody is, nothing
|
|
writes the table at all, which is what makes a watch call in a program nobody is
|
|
debugging a load and a not-taken branch. It is sent once for the first
|
|
consumer: two of them looking at one table is still one table."
|
|
(flan--live-connection)
|
|
(unless flan-watch--consumers
|
|
(let ((r (flan-watch--arm t)))
|
|
(unless (equal (plist-get r :status) "ok")
|
|
(user-error "flan: %s" (or (plist-get r :message) "watch refused")))))
|
|
(unless (memq consumer flan-watch--consumers)
|
|
(push consumer flan-watch--consumers))
|
|
(add-hook 'flan-push-functions #'flan-watch--on-push)
|
|
(add-hook 'flan-connected-hook #'flan-watch--on-connect)
|
|
(add-hook 'flan-disconnected-hook #'flan-watch--on-disconnect))
|
|
|
|
(defun flan-watch--drop (consumer)
|
|
"Stop painting CONSUMER, and tear everything down if it was the last one."
|
|
(setq flan-watch--consumers (delq consumer flan-watch--consumers))
|
|
(when (eq consumer 'ghost) (flan-watch--ghost-clear))
|
|
(unless flan-watch--consumers (flan-watch-stop)))
|
|
|
|
;;;###autoload
|
|
(defun flan-watch ()
|
|
"Open the watch buffer and start painting the running program's values."
|
|
(interactive)
|
|
(flan-watch--subscribe 'buffer)
|
|
(with-current-buffer (get-buffer-create flan-watch-buffer)
|
|
(unless (eq major-mode 'flan-watch-mode) (flan-watch-mode)))
|
|
;; Painted before the first push, rather than left blank until one arrives.
|
|
;; An empty buffer is the same picture as a broken one, and the likeliest
|
|
;; reason for it here is the honest one — the program is not calling into the
|
|
;; table — which is worth saying in words rather than by showing nothing.
|
|
(flan-watch--paint (flan-watch--format nil 0))
|
|
(display-buffer flan-watch-buffer))
|
|
|
|
;;;###autoload
|
|
(define-minor-mode flan-watch-ghost-mode
|
|
"Show watched values inline, beside the calls that wrote them.
|
|
|
|
An addition to `flan-watch' rather than a replacement: the buffer is what you
|
|
want when you want everything at once, and this is what you want when you are
|
|
reading one function. Both are painted from the same table read, so they
|
|
cannot disagree, and turning either off leaves the other running."
|
|
:global t
|
|
:lighter " flan-ghost"
|
|
(if flan-watch-ghost-mode
|
|
;; A refused arming leaves the mode off rather than on-and-doing-nothing.
|
|
(condition-case err
|
|
(flan-watch--subscribe 'ghost)
|
|
(error (setq flan-watch-ghost-mode nil) (signal (car err) (cdr err))))
|
|
(flan-watch--drop 'ghost)))
|
|
|
|
(defun flan-watch-stop ()
|
|
"Stop painting, and tell the program to stop writing the table.
|
|
|
|
Tears down both consumers. `flan-watch--drop' is the way to stop one of them."
|
|
(interactive)
|
|
(setq flan-watch--consumers nil)
|
|
;; Directly, not through the mode function: that would call back into
|
|
;; `flan-watch--drop' and back into here.
|
|
(setq flan-watch-ghost-mode nil)
|
|
(flan-watch--ghost-clear)
|
|
(remove-hook 'flan-push-functions #'flan-watch--on-push)
|
|
(remove-hook 'flan-connected-hook #'flan-watch--on-connect)
|
|
(remove-hook 'flan-disconnected-hook #'flan-watch--on-disconnect)
|
|
;; Only on a connection that is already live. `flan--request' reconnects,
|
|
;; which is right for something a person did and wrong here: there is
|
|
;; nothing to disarm when the connection has gone, because the table went
|
|
;; with the program.
|
|
(when (process-live-p flan--connection)
|
|
(ignore-errors (flan-watch--arm nil))))
|
|
|
|
;;; What ghost text still cannot show
|
|
|
|
;; Built, above, on a buffer search rather than on a location from the runtime
|
|
;; — see the rationale there. An earlier note in this file said ghost text was
|
|
;; gated on a `(watch ...)' form in the checker, because nothing in the table
|
|
;; carries a file:line:col. That is still true of the *table* and it turned out
|
|
;; not to be the blocker: the call site is in the buffer, and the name in the
|
|
;; table is the string literal in it, so the anchor is searched for instead of
|
|
;; being told. Nothing new is asked of the daemon.
|
|
;;
|
|
;; What the search cannot do, and what it costs:
|
|
;;
|
|
;; - A watch call written by a macro has no literal to find, so it gets no
|
|
;; ghost text. The watch buffer still shows it. This is the honest limit
|
|
;; of anchoring on text: the editor is reading source, not debug info, and
|
|
;; a form that only exists after expansion is not in the source.
|
|
;;
|
|
;; - A name assembled at run time — anything that is not a literal at the call
|
|
;; site — is the same case for the same reason.
|
|
;;
|
|
;; - The module in the running program can be older than the buffer. A site
|
|
;; you have typed but not yet installed with `C-c C-c' has no row, and one
|
|
;; you deleted but not yet installed still writes. Deliberately unreported:
|
|
;; the stale-caller machinery in `flan.el' already tracks what needs
|
|
;; re-evaluating, and a second opinion about it here would be a worse one.
|
|
;;
|
|
;; Still genuinely blocked on the checker, and unchanged by any of this: a
|
|
;; **struct or a slice**. Rendering one is a compile-time walk over its type,
|
|
;; the runtime has scalar entry points only, and no amount of searching the
|
|
;; buffer produces a value the table does not hold. See docs/BUILT.md.
|
|
|
|
(provide 'flan-watch)
|
|
;;; flan-watch.el ends here
|