Files
sydnix/users/crumb/programs/emacs/lib/syd-handle-repl.el
2025-02-13 15:32:30 -07:00

268 lines
11 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;;; syd-handle-repl.el -*- lexical-binding: t; -*-
(eval-when-compile (require 'cl-lib))
(require 'syd-prelude)
(require 'syd-project)
(syd-add-hook 'on-init-ui-hook
(defun syd--set-popup-rules-for-repls-h ()
(require 'doom-popup)
(set-popup-rule!
(lambda (bufname _)
(when (boundp 'syd-repl-mode)
(buffer-local-value 'syd-repl-mode (get-buffer bufname))))
:ttl (lambda (buf)
(unless (plist-get syd-repl-plist :persist)
(when-let (process (get-buffer-process buf))
(set-process-query-on-exit-flag process nil)
(kill-process process)
(kill-buffer buf))))
:size 0.25
:quit nil)))
;;; State & settings
(defvar +syd-major-mode-repl-alist '()
"TODO: An alist pairing major-modes (symbols) with plists describing REPLs.")
(defvar +syd-repl-buffers (make-hash-table :test 'equal)
"A hashmap mapping pairs (MAJOR-MODE . PROJECT-ROOT) to their corresponding
buffers. Indeed, this implies a single REPL per language per project. Is this
limitation worth overcoming? I'm not sure! I've yet to butt heads with it.")
(defvar-local syd-repl-plist nil
"A plist describing the repl associated with the current buffer.
This is little more than a cache. Its value can almost always be equivalently
derived from `+syd-major-mode-repl-alist'.")
(defun set-repl-handler! (modes command &rest plist)
"Defines a REPL for MODES.
MODES is either a single major mode symbol or a list of them. COMMAND is a
function that creates and returns the REPL buffer.
COMMAND can either be a function that takes no arguments, or an interactive
command that will be called interactively. COMMANDS must return either the repl
buffer or a function that takes no arguments and returns the repl buffer.
PLIST is a property list that map special attributes to this repl. These are
recognized:
:persist BOOL
If non-nil, this REPL won't be killed when its window is closed.
:send-region FUNC
A function that accepts a BEG and END, and sends the contents of the region
to the REPL. Defaults to `+eval/send-region-to-repl'.
:send-buffer FUNC
A function of no arguments that sends the contents of the buffer to the REPL.
Defaults to `+eval/region', which will run the :send-region specified function
or `+eval/send-region-to-repl'."
(declare (indent defun))
(dolist (mode (ensure-list modes))
(setf (alist-get mode +syd-major-mode-repl-alist)
(cons command plist))))
;;; Repls
;;;###autoload
(define-minor-mode syd-repl-mode
"A minor mode for repl buffers. One use is to universally customise the
display of all repl buffers."
:after-hook (format "syd-repl-mode after %s" (current-buffer)))
(defun syd--repl-from-major-mode ()
"TODO:"
(pcase-let ((`(_ ,fn . ,plist) (assq major-mode +syd-major-mode-repl-alist)))
(list fn plist)))
(defun syd--clean-repl-buffers ()
"Remove any key/value pairs from `+syd-repl-buffers' whose values involve a
not-alive buffer."
(maphash (lambda (repl-key buffer)
(unless (buffer-live-p buffer)
(remhash repl-key +syd-repl-buffers)))
+syd-repl-buffers))
(defun syd--get-repl-key ()
(cons major-mode (syd-project-root)))
(defun syd--goto-end-of-repl ()
"Try to move point to the last comint prompt or the end of the buffer."
(unless (or (derived-mode-p 'term-mode)
(eq (current-local-map) (bound-and-true-p term-raw-map)))
(goto-char (if (and (derived-mode-p 'comint-mode)
(cdr comint-last-prompt))
(cdr comint-last-prompt)
(point-max)))))
(cl-defun syd--call-repl-handler (repl-handler &key plist repl-key)
"Spawn a new repl buffer using REPL-HANDLER. REPL-HANDLER's return value will
be returned.
If REPL-HANDLER fails to return a buffer, this `syd--call-repl-handler' will
throw an error. `syd-repl-mode' will be enabled in the new buffer, and the
buffer will be cached in `+syd-repl-buffers'.
REPL-HANDLER will be called interactively if supported."
(let ((repl-buffer (save-window-excursion
(if (commandp repl-handler)
(call-interactively repl-handler)
(funcall repl-handler)))))
(unless repl-buffer
(error "REPL handler %S couldn't open the REPL buffer" fn))
(unless (bufferp repl-buffer)
(error "REPL handler %S failed to return a buffer" fn))
(with-current-buffer repl-buffer
;; It is important that `syd-repl-mode' is enabled before the buffer is
;; displayed by `display-fn'.
(syd-repl-mode 1)
(when plist
;; Cache the plist at `syd-repl-plist'.
(setq syd-repl-plist plist)))
(puthash repl-key repl-buffer +syd-repl-buffers)
repl-buffer))
;; #+begin_src dot :file /tmp/repl.svg :results file graphics
;; digraph {
;; bgcolor="transparent"
;;
;; node [
;; fillcolor=gray95
;; color=black
;; shape=record
;; ]
;;
;; "Start" [shape=diamond]
;; "Start" -> x1
;; x1 [label="Is the user currently in the repl buffer,\nOR has a repl handler NOT been provided?"]
;;
;; x1 -> x2 [label="Yes"]
;; x1 -> x3 [label="No"]
;; x2 [label="Find entry in +syd-repl-buffers;\nis it a live buffer?"]
;; x2 -> x4 [label="Yes"]
;; x4 [label="Call display-fn on the\n+syd-repl-buffers entry, and use the result"]
;; x2 -> x5 [label="No"]
;; x5 [label="Call the provided repl-handler. Ensure it returns\na valid buffer, and pass the resultto display-fn.\nSet the plist, enable repl mode, update\n+syd-repl-buffers. Use the buffer returned by display-fn"]
;; x3 [label="Use entry found in +syd-repl-buffers"]
;; }
;; #+end_src
(cl-defun syd--ensure-in-repl-buffer
(&key repl-handler plist (display-fn #'get-buffer-create))
"Display the repl buffer associated with the current major mode and project.
A repl buffer will be created (using REPL-HANDLER) if necessary.
If an active repl buffer is found in `+syd-repl-buffers', it will be displayed
by the given DISPLAY-FN.
PLIST is a plist of repl-specific options."
(syd--clean-repl-buffers)
(let* ((repl-key (syd--get-repl-key))
(maybe-repl-buffer (gethash repl-key +syd-repl-buffers)))
(cl-check-type maybe-repl-buffer (or buffer null))
(let ((repl-buffer
(if (or (eq maybe-repl-buffer (current-buffer))
(null repl-handler))
;; * If the current buffer is the repl buffer, we can be sure
;; that it is not nil and can be returned as-is.
;; * If we were not given a repl-handler, there's nothing else we
;; can do. Return what was found in `+syd-repl-buffers', and
;; hope it's the right thing.
maybe-repl-buffer
;; If the repl buffer found in `+syd-repl-buffers' is live and
;; well, we can return that. If not, we're going to have to spawn
;; a new repl buffer with `repl-handler' and `display-fn'.
(if (buffer-live-p maybe-repl-buffer)
(funcall display-fn maybe-repl-buffer)
(funcall display-fn
(syd--call-repl-handler repl-handler
:plist plist
:repl-key repl-key))))))
(when (bufferp repl-buffer)
(with-current-buffer repl-buffer
(syd--goto-end-of-repl))
repl-buffer))))
(defun syd--known-repls ()
"Return a list of all known mode-repl pairs, each as a two-element list.
More precisely, the return value is a list of mode-repl pairs, where each
mode-repl pair is a two-element list (MAJOR-MODE HANDLER) where MAJOR-MODE is a
symbol, and HANDLER is a (possibly interactive) procedure.
See also: `+syd-major-mode-repl-alist'."
(mapcar (lambda (xs) (list (car xs) (cadr xs)))
+syd-major-mode-repl-alist))
(defun syd--pretty-mode-name (mode)
"Convert MODE (a symbol or string) into a string appropriate for human
presentation."
(let ((mode* (if (symbolp mode) (symbol-name mode) mode)))
(if (not (string-match "^\\([a-z-]+\\)-mode$" mode*))
(error "Given string/symbol is not a major mode: %s" mode*)
(string-join (split-string
(capitalize (match-string-no-properties 1 mode*))
"-")
" "))))
(defun syd-prompt-for-repl ()
"Prompt the user for a repl to open. Returns the chosen repl-handler
function."
;; REVIEW: Doom scans all interned symbols for anything that looks like
;; "open-XXXX-repl." Is this worth doing?
(let* ((repls (mapcar (lambda (xs)
(pcase-let ((`(,mode ,fn) xs))
(list (syd--pretty-mode-name mode) fn)))
(syd--known-repls)))
(choice (or (completing-read "Open a REPL for: "
(mapcar #'car repls))
(user-error "Aborting"))))
(cadr (assoc choice repls))))
(defun syd-send-region-to-repl (beg end)
(interactive "r")
(let ((selection (buffer-substring-no-properties beg end))
(buffer (syd--ensure-in-repl-buffer)))))
(cl-defun +syd-open-repl (&key prompt-p display-fn)
"TODO: Open a repl via DISPLAY-FN. When PROMPT-P, the user will be
unconditionally prompted for a repl choice.
If Evil-mode is active, insert state will be enabled."
(pcase-let* ((`(,major-mode-fn ,plist) (syd--repl-from-major-mode))
(repl-handler (if (or prompt-p (not major-mode-fn))
(syd-prompt-for-repl)
major-mode-fn))
(region (when (use-region-p)
(buffer-substring-no-properties (region-beginning)
(region-end)))))
(unless (commandp repl-handler)
(error "Couldn't find a REPL for %s" major-mode))
(with-current-buffer (syd--ensure-in-repl-buffer :repl-handler repl-handler
:plist plist
:display-fn display-fn)
;; Start the user in insert mode at the end of the input line.
(when (bound-and-true-p evil-mode)
(call-interactively #'evil-append-line))
(when region
(insert region))
t)))
;;;###autoload
(defun +syd/open-repl-other-window (prompt-p)
"Like `+syd-open-repl', but opens in a different window. The repl
corresponding to the current major mode and project will be opened, unless a
prefix argument is given, in which case the user will be prompted for a repl."
(interactive "P")
(+syd-open-repl :prompt-p prompt-p
:display-fn #'pop-to-buffer))
(provide 'syd-handle-repl)