;;; 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.") (define-minor-mode syd-repl-mode "A minor mode for repl buffers. One use is to universally customise the display of all repl buffers.") (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--call-repl-handler (repl-handler) "Call REPL-HANDLER, and error out if it does not return a buffer. 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))))) (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)))) (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* ((root (syd-project-root)) (repl-key (cons major-mode root)) (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 (if (buffer-live-p maybe-repl-buffer) maybe-repl-buffer (syd--call-repl-handler repl-handler))) (displayed-repl-buffer (funcall display-fn repl-buffer))) ;; Repl buffers are to be saved in `+syd-repl-buffers'; we've just ;; opened one, so do so! (puthash 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 (syd-repl-mode 1) (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)))) (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 _) (when (boundp 'syd-repl-mode) (buffer-local-value 'syd-repl-mode (get-buffer bufname)))) :ttl (lambda (buf) (unless (plist-get +eval-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)