Files
sydnix/users/crumb/programs/emacs/lib/syd-handle-repl.el
Madeleine Sydney d27a3b60ce fix: Enable syd-repl-mode /before/ displaying the buffer
This ensures that display-buffer-alist predicates can test for repl buffers.
2025-01-30 03:27:24 -07:00

189 lines
7.6 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)
(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)
;;;###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--call-repl-handler (repl-handler &optional plist maybe-repl-buffer)
"Call REPL-HANDLER, and error out if it does not return a buffer.
REPL-HANDLER will be called interactively if supported."
(let ((repl-buffer
(if (buffer-live-p maybe-repl-buffer)
maybe-repl-buffer
(let ((repl-buffer*
(save-window-excursion
(if (commandp repl-handler)
(call-interactively repl-handler)
(funcall repl-handler)))))
(cond ((null repl-buffer*)
(error "REPL handler %S couldn't open the REPL buffer" repl-handler))
((not (bufferp repl-buffer*))
(error "REPL handler %S failed to return a buffer" repl-handler))
(t repl-buffer*))))))
;; Repl buffers are to be saved in `+syd-repl-buffers'; we've just
;; opened one, so do so.
(puthash (syd--get-repl-key) repl-buffer +syd-repl-buffers)
;; If it isn't a buffer, we return nil.
(when (bufferp repl-buffer)
(with-current-buffer repl-buffer
(when plist
(setq syd-repl-plist plist))
(syd-repl-mode 1))
repl-buffer)))
(defun syd--goto-end-of-repl ()
"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--ensure-in-repl-buffer
(&key repl-handler plist (display-fn #'get-buffer-create))
"TODO: 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))
(unless (or (eq maybe-repl-buffer (current-buffer))
(null repl-handler))
(let* ((repl-buffer (syd--call-repl-handler repl-handler
plist
maybe-repl-buffer))
(displayed-repl-buffer (funcall display-fn repl-buffer)))
(when (bufferp displayed-repl-buffer)
(with-current-buffer displayed-repl-buffer
(syd--goto-end-of-repl))
displayed-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))))
(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))
(set-popup-rule!
(lambda (bufname _)
(pp bufname)
(with-current-buffer bufname
(pp (boundp 'syd-repl-mode))
(pp syd-repl-mode))
(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)
(provide 'syd-handle-repl)