268 lines
11 KiB
EmacsLisp
268 lines
11 KiB
EmacsLisp
;;; 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)
|