flan/emacs/flan-repl.el
Joseph Ferano 668268b6cc The wait for a reply is a setting, and the directory installs
The 30s deadline in the reply reader was a literal and its message said only
that nothing had arrived. It is flan-dev-reply-timeout now, and the message
names the daemon buffer to look in -- a first compile on a cold cache is the
case that legitimately runs long, and the build log is what says so -- and the
setting to raise. Still never resent: a request the daemon took and died on
may already have run.

Package headers on all eight client files, so package-install-file on the
directory works and the client is not reachable only by load-path. The
daemon-buffer defcustom moves up beside the other buffer names, because the
reply reader now names it and the byte-compiler reads a file in order.
2026-09-17 21:48:03 +07:00

152 lines
6.4 KiB
EmacsLisp

;;; flan-repl.el --- A prompt for a running Flan program -*- 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 buffer to type expressions at, sent to the program `flan dev' is running
;; and answered with the value they had *there*. It adds no protocol and no
;; compiler support: every line goes through the same `eval-expr' request that
;; C-x C-e uses.
;;
;; Derived from `comint-mode', for the same reason `flan-mode' derives from
;; `lisp-mode': history, the input ring, and kill/yank behaviour already exist
;; and are not worth rewriting. There is no subprocess behind it — the
;; "process" is a stub comint needs in order to have a prompt at all.
;;
;; Two things about it that are decisions, not accidents:
;;
;; - **It is program-scoped.** A name typed here resolves against the running
;; program's top-level namespace, so in sand you write `sim/settle' and not
;; `settle'. A buffer visiting a package's own file gets the alias applied
;; for it, because the file says which package it belongs to; a prompt has no
;; file and nothing to derive it from.
;;
;; - **A value and the program's output are different things** and arrive by
;; different routes. The value of the expression appears at the prompt;
;; anything the program printed while evaluating it goes to *flan-output*,
;; riding along on the same reply. Showing them in one place would be
;; convenient and wrong.
;;; Code:
(require 'comint)
(require 'flan-mode)
(require 'flan-dev)
(defcustom flan-repl-buffer "*flan-repl*"
"Name of the Flan REPL buffer."
:type 'string
:group 'flan)
(defvar flan-repl-prompt "flan> "
"Prompt shown in `flan-repl-mode'.")
(defvar flan-repl-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-o") #'flan-show-output)
(define-key map (kbd "C-c C-d") #'flan-describe)
(define-key map (kbd "C-c C-q") #'flan-disconnect)
map)
"Keymap for `flan-repl-mode'.")
(define-derived-mode flan-repl-mode comint-mode "Flan-REPL"
"Type expressions; they are evaluated in the running program.
\\{flan-repl-mode-map}"
:syntax-table flan-mode-syntax-table
(setq-local comint-prompt-regexp (concat "^" (regexp-quote flan-repl-prompt)))
(setq-local comint-prompt-read-only t)
(setq-local comint-input-sender #'flan-repl--send)
;; Nothing is echoed back by a process, because there is no process.
(setq-local comint-process-echoes nil)
;; The prompt gets completion, eldoc and M-. for the same names a buffer
;; does, and against the same program: they read the client's cache, which
;; is program-scoped, which is exactly what a prompt is.
(flan-dev-setup)
(setq-local font-lock-defaults '(flan-font-lock-keywords)))
(defun flan-repl--complete-p (text)
"Is TEXT a whole form?
Parens balanced and not inside a string or comment. RET on a half-typed form
should open a line, not send something the reader will reject."
(let ((state (with-temp-buffer
(set-syntax-table flan-mode-syntax-table)
(insert text)
(parse-partial-sexp (point-min) (point-max)))))
(and (<= (nth 0 state) 0) ; depth
(not (nth 3 state)) ; in a string
(not (nth 4 state))))) ; in a comment
(defun flan-repl--output (text)
"Insert TEXT into the REPL buffer above the next prompt."
(let ((proc (get-buffer-process (current-buffer))))
(comint-output-filter proc (concat text "\n" flan-repl-prompt))))
(defun flan-repl--send (_proc text)
"Evaluate TEXT in the running program and show what it was."
(let ((code (string-trim text)))
(cond
((string-empty-p code) (flan-repl--output ""))
(t
(let ((reply (condition-case err
(flan-dev--request
(list :op "eval-expr" :code code :file "<repl>"))
(error (list :status "error"
:message (error-message-string err))))))
(flan-repl--output
(if (equal (plist-get reply :status) "ok")
(or (plist-get reply :value) "")
(concat "error: " (or (plist-get reply :message) "rejected")
(let ((loc (plist-get reply :loc)))
(if loc (concat " (" loc ")") ""))))))))))
(defun flan-repl-return ()
"Send the input if it is a whole form, otherwise open a line."
(interactive)
(let ((input (buffer-substring-no-properties
(process-mark (get-buffer-process (current-buffer)))
(point-max))))
(if (flan-repl--complete-p input)
(comint-send-input)
(insert "\n"))))
(define-key flan-repl-mode-map (kbd "RET") #'flan-repl-return)
;;;###autoload
(defun flan-repl ()
"Open a prompt on the program `flan dev' is running.
Connects first if it has to."
(interactive)
(unless (and flan-dev--connection (process-live-p flan-dev--connection))
(call-interactively #'flan-connect))
(let ((buf (get-buffer-create flan-repl-buffer)))
(with-current-buffer buf
(unless (derived-mode-p 'flan-repl-mode)
(flan-repl-mode)
;; comint wants a process to hang a prompt and a process mark off.
;; There is nothing to run, so this one does nothing and is never
;; written to; every request goes over the daemon's socket instead.
(let ((proc (start-process "flan-repl" buf "cat")))
(set-process-query-on-exit-flag proc nil)
(comint-output-filter proc flan-repl-prompt))))
(pop-to-buffer buf)))
(provide 'flan-repl)
;;; flan-repl.el ends here