;;; syd-handle-repl.el -*- lexical-binding: t; -*- (eval-when-compile (require 'cl-lib)) (require 'syd-prelude) (require 'syd-project) (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)) (add-hook 'on-init-ui-hook #'syd--set-popup-rules-for-repls-h 'append) ;;; 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)