Something went wrong. Try again.
A modern, tree-sitter-powered Emacs major mode for Haskell.
emacs haskell
Something went wrong. Try again.
curry-mode curry-repl.el
13 kB · 356 lines
at ci_fixes
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357;;; curry-repl.el --- GHCi REPL integration for curry-mode -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Tim McGilchrist;;;; Author: Tim McGilchrist <timmcgil@gmail.com>;; Maintainer: Tim McGilchrist <timmcgil@gmail.com>;; URL: https://tangled.org/tsmc.purely-functional.com/curry-mode;; Keywords: languages haskell
;; This file is not part of GNU Emacs.
;;; Commentary:
;; This library provides integration with GHCi (the GHC interactive;; environment) for the curry-mode package. It offers a comint-based;; REPL with tree-sitter syntax highlighting for input, and a minor;; mode for sending code from source buffers.
;;; License:
;; This program is free software; you can redistribute it and/or;; modify it under the terms of the GNU General Public License;; as published by the Free Software Foundation; either version 3;; of the License, or (at your option) any later version.;;;; This program is distributed in the hope that it will be useful,;; but WITHOUT ANY WARRANTY; without even the implied warranty of;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the;; GNU General Public License for more details.;;;; You should have received a copy of the GNU General Public License;; along with GNU Emacs; see the file COPYING. If not, write to the;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,;; Boston, MA 02110-1301, USA.
;;; Code:
(require 'comint)(require 'pulse)(require 'curry-mode)
(defgroup curry-repl nil "GHCi REPL integration for curry-mode." :prefix "curry-repl-" :group 'curry)
(defcustom curry-repl-program-name "ghci" "Program name for invoking GHCi.Can also be set to \"cabal repl\" or \"stack ghci\" forproject-aware REPL sessions." :type 'string :group 'curry-repl :package-version '(curry-mode . "0.1.0"))
(defcustom curry-repl-program-args nil "Command line arguments for `curry-repl-program-name'." :type '(repeat string) :group 'curry-repl :package-version '(curry-mode . "0.1.0"))
(defcustom curry-repl-buffer-name "*GHCi*" "Name of the GHCi REPL buffer." :type 'string :group 'curry-repl :package-version '(curry-mode . "0.1.0"))
(defcustom curry-repl-history-file (expand-file-name "curry-repl-history" user-emacs-directory) "File to persist GHCi REPL input history across sessions.Set to nil to disable history persistence." :type '(choice (file :tag "History file") (const :tag "Disable" nil)) :group 'curry-repl :package-version '(curry-mode . "0.1.0"))
(defcustom curry-repl-history-size 1000 "Maximum number of input history entries to persist." :type 'integer :group 'curry-repl :package-version '(curry-mode . "0.1.0"))
(defcustom curry-repl-fontify-input t "When non-nil, fontify REPL input using tree-sitter via `curry-mode'.This uses `comint-fontify-input-mode' (Emacs 29.1+) to provide fullsyntax highlighting for Haskell code you type in the REPL, while REPLoutput keeps its own highlighting.
Set to nil to use only the basic REPL font-lock keywords for input." :type 'boolean :group 'curry-repl :package-version '(curry-mode . "0.1.0"))
(defconst curry-repl--prompt-regexp "^\\(?:[A-Z][A-Za-z0-9_.]*\\(?:| [A-Z][A-Za-z0-9_.]*\\)*\\)?> \\|^\\*?[A-Z][A-Za-z0-9_.]*> \\|^ghci> \\|^> " "Regexp matching GHCi prompts.Matches \"Prelude> \", \"Main> \", \"ghci> \", \"> \",\"Prelude Data.List> \", and \"*Main> \" style prompts.")
(defvar-local curry-repl--source-buffer nil "Source buffer from which the REPL was last invoked.Used by `curry-repl-switch-to-source' to return to the source buffer.")
(defvar curry-repl-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map comint-mode-map) (define-key map (kbd "C-c C-z") #'curry-repl-switch-to-source) map) "Keymap for `curry-repl-mode'.")
(defvar curry-repl-font-lock-keywords '(;; GHC errors ("^\\([^ \t\n:]+\\):\\([0-9]+\\):\\([0-9]+\\): error:" (1 font-lock-warning-face)) ;; GHC warnings ("^\\([^ \t\n:]+\\):\\([0-9]+\\):\\([0-9]+\\): warning:" (1 font-lock-warning-face)) ;; Type display: "foo :: Int -> Int" ("^\\([a-z_][a-zA-Z0-9_']*\\) :: " (1 font-lock-function-name-face)) ;; Kind display ("^\\([A-Z][a-zA-Z0-9_']*\\) :: " (1 font-lock-type-face)) ;; GHCi commands ("^:[a-z]+" . font-lock-keyword-face)) "Font-lock keywords for the GHCi REPL buffer.Highlights errors, warnings, type information, and GHCi commands.")
(define-derived-mode curry-repl-mode comint-mode "GHCi" "Major mode for interacting with GHCi.
\\{curry-repl-mode-map}" (setq comint-prompt-regexp curry-repl--prompt-regexp) (setq comint-prompt-read-only t) (setq comint-process-echoes nil) ;; Strip ANSI escape sequences (ansi-color-for-comint-mode-on) (setq-local comment-start "-- ") (setq-local comment-end "") (setq-local font-lock-defaults '(curry-repl-font-lock-keywords t))
;; Error navigation (setq-local compilation-error-regexp-alist '(("^\\([^ \t\n:]+\\):\\([0-9]+\\):\\([0-9]+\\):" 1 2 3))) (compilation-shell-minor-mode)
;; Input history persistence (when curry-repl-history-file (setq comint-input-ring-file-name curry-repl-history-file) (setq comint-input-ring-size curry-repl-history-size) (setq comint-input-ignoredups t) (comint-read-input-ring t) (add-hook 'kill-buffer-hook #'comint-write-input-ring nil t))
;; Prettify symbols (setq-local prettify-symbols-alist (curry--prettify-symbols-alist))
;; Tree-sitter fontification for REPL input (when curry-repl-fontify-input (setq-local comint-indirect-setup-function #'curry-mode) (comint-fontify-input-mode)))
;;;###autoload(defun curry-repl-start () "Start a GHCi process in a new buffer.If a process is already running, switch to its buffer." (interactive) (if (comint-check-proc curry-repl-buffer-name) (pop-to-buffer curry-repl-buffer-name) (let* ((cmdlist (append (split-string curry-repl-program-name) curry-repl-program-args)) (buffer (apply #'make-comint-in-buffer "GHCi" curry-repl-buffer-name (car cmdlist) nil (cdr cmdlist)))) (with-current-buffer buffer (curry-repl-mode)) (pop-to-buffer buffer))))
(defun curry-repl-switch-to-source () "Switch from the REPL back to the source buffer that last invoked it." (interactive) (if (and curry-repl--source-buffer (buffer-live-p curry-repl--source-buffer)) (pop-to-buffer curry-repl--source-buffer) (message "No source buffer to return to")))
;;;###autoload(defun curry-repl-switch-to-repl () "Switch to the GHCi REPL, saving the current buffer as the source.If a REPL is already running, switch to it; otherwise start a new one.Use \\[curry-repl-switch-to-source] in the REPL to return." (interactive) (let ((source (current-buffer))) (if (comint-check-proc curry-repl-buffer-name) (pop-to-buffer curry-repl-buffer-name) (curry-repl-start)) (setq curry-repl--source-buffer source)))
(defun curry-repl--process () "Return the REPL process, or nil if not running." (get-buffer-process curry-repl-buffer-name))
(defun curry-repl--ensure-running () "Start a GHCi REPL if one is not already running." (unless (comint-check-proc curry-repl-buffer-name) (save-window-excursion (curry-repl-start))))
(defun curry-repl--send-string (string) "Send STRING to the GHCi REPL process.For multiline input, wraps in GHCi's :{...}:} block." (curry-repl--ensure-running) (let ((proc (curry-repl--process))) (if (string-match-p "\n" string) ;; Multiline: use GHCi's multiline input syntax (progn (comint-send-string proc ":{") (comint-send-string proc "\n") (comint-send-string proc string) (comint-send-string proc "\n") (comint-send-string proc ":}\n")) ;; Single line (comint-send-string proc (concat string "\n")))))
;;;###autoload(defun curry-repl-send-region (start end) "Send the region between START and END to the GHCi REPL." (interactive "r") (let ((text (string-trim (buffer-substring-no-properties start end)))) (curry-repl--send-string text) (pulse-momentary-highlight-region start end)))
;;;###autoload(defun curry-repl-send-buffer () "Send the entire buffer to the GHCi REPL." (interactive) (curry-repl-send-region (point-min) (point-max)))
;;;###autoload(defun curry-repl-send-definition () "Send the current definition to the GHCi REPL." (interactive) (if-let* ((node (treesit-defun-at-point)) (start (treesit-node-start node)) (end (treesit-node-end node))) (curry-repl-send-region start end) (user-error "No definition at point")))
;;;###autoload(defun curry-repl-send-line () "Send the current line to the GHCi REPL." (interactive) (let ((line (string-trim (thing-at-point 'line t)))) (curry-repl--send-string line) (pulse-momentary-highlight-one-line (point))))
;;;###autoload(defun curry-repl-load-file (&optional file) "Load FILE into GHCi via the `:load' command.Defaults to the current buffer's file." (interactive) (let ((file (or file (buffer-file-name)))) (unless file (user-error "Buffer is not visiting a file")) (when (buffer-modified-p) (save-buffer)) (curry-repl--ensure-running) (comint-send-string (curry-repl--process) (format ":load %s\n" (shell-quote-argument file)))))
;;;###autoload(defun curry-repl-reload () "Reload the current module in GHCi via `:reload'." (interactive) (when (buffer-modified-p) (save-buffer)) (curry-repl--ensure-running) (comint-send-string (curry-repl--process) ":reload\n"))
;;;###autoload(defun curry-repl-type-at-point () "Show the type of the expression at point via GHCi's `:type' command." (interactive) (let ((expr (or (when (use-region-p) (buffer-substring-no-properties (region-beginning) (region-end))) (thing-at-point 'symbol t)))) (unless expr (user-error "No expression at point")) (curry-repl--ensure-running) (comint-send-string (curry-repl--process) (format ":type %s\n" expr))))
;;;###autoload(defun curry-repl-info-at-point () "Show info about the identifier at point via GHCi's `:info' command." (interactive) (let ((sym (thing-at-point 'symbol t))) (unless sym (user-error "No identifier at point")) (curry-repl--ensure-running) (comint-send-string (curry-repl--process) (format ":info %s\n" sym))))
(defun curry-repl-clear-buffer () "Clear the GHCi REPL buffer." (interactive) (with-current-buffer curry-repl-buffer-name (let ((inhibit-read-only t)) (erase-buffer) (comint-send-input))))
(defun curry-repl-interrupt () "Interrupt the GHCi REPL process." (interactive) (when (comint-check-proc curry-repl-buffer-name) (interrupt-process (curry-repl--process))))
;;; Minor mode for source buffers
(defvar curry-repl-minor-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-z") #'curry-repl-switch-to-repl) (define-key map (kbd "C-c C-c") #'curry-repl-send-definition) (define-key map (kbd "C-c C-r") #'curry-repl-send-region) (define-key map (kbd "C-c C-b") #'curry-repl-send-buffer) (define-key map (kbd "C-c C-l") #'curry-repl-load-file) (define-key map (kbd "C-c C-k") #'curry-repl-reload) (define-key map (kbd "C-c C-t") #'curry-repl-type-at-point) (define-key map (kbd "C-c C-i") #'curry-repl-info-at-point)
(easy-menu-define curry-repl-minor-mode-menu map "GHCi REPL Menu" '("GHCi" ["Start/Switch to REPL" curry-repl-switch-to-repl] "--" ["Send Definition" curry-repl-send-definition] ["Send Region" curry-repl-send-region :active mark-active] ["Send Buffer" curry-repl-send-buffer] ["Send Line" curry-repl-send-line] "--" ["Load File" curry-repl-load-file] ["Reload" curry-repl-reload] "--" ["Type at Point" curry-repl-type-at-point] ["Info at Point" curry-repl-info-at-point] "--" ["Interrupt REPL" curry-repl-interrupt] ["Clear REPL Buffer" curry-repl-clear-buffer])) map) "Keymap for GHCi REPL interaction from source buffers.")
;;;###autoload(define-minor-mode curry-repl-minor-mode "Minor mode for interacting with GHCi from Haskell source buffers.
\\{curry-repl-minor-mode-map}" :lighter " GHCi" :keymap curry-repl-minor-mode-map)
(provide 'curry-repl)
;;; curry-repl.el ends here