;;; haskell-ghci.el --- A GHCi interaction mode ;; Copyright (C) 2004, 2005, 2006, 2007 Free Software Foundation, Inc. ;; Copyright (C) 2001 Chris Webb ;; Copyright (C) 1998, 1999 Guy Lapalme ;; Keywords: inferior mode, GHCi interaction mode, Haskell ;;; This file is not part of GNU Emacs. ;; This file 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, or (at your option) ;; any later version. ;; This file 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., 59 Temple Place - Suite 330, ;; Boston, MA 02111-1307, USA. ;;; Commentary: ;; Purpose: ;; ;; To send a Haskell buffer to another buffer running a GHCi ;; interpreter. ;; ;; This mode is derived from version 1.1 of Guy Lapalme's ;; haskell-hugs.el, which can be obtained from: ;; ;; http://www.iro.umontreal.ca/~lapalme/Hugs-interaction.html ;; ;; This in turn was adapted from Chris Van Humbeeck's hugs-mode.el, ;; which can be obtained from: ;; ;; http://www-i2.informatik.rwth-aachen.de/Forschung/FP/Haskell/hugs-mode.el ;; ;; ;; Installation: ;; ;; To use with Moss and Thorn's haskell-mode.el ;; ;; http://www.haskell.org/haskell-mode ;; ;; add this to .emacs: ;; ;; (add-hook 'haskell-mode-hook 'turn-on-haskell-ghci) ;; ;; ;; Customisation: ;; ;; The name of the GHCi interpreter is in haskell-ghci-program-name. ;; ;; Arguments can be sent to the GHCi interpreter when it is started by ;; setting haskell-ghci-program-args (empty by default) to a list of ;; string args to pass it. This value can be set interactively by ;; calling C-c C-s with an argument (i.e. C-u C-c C-s). ;; ;; `haskell-ghci-hook' is invoked in the *ghci* buffer once GHCi is ;; started. ;; ;; All functions/variables start with `turn-{on,off}-haskell-ghci' or ;; `haskell-ghci-'. ;;; Code: (defgroup haskell-ghci nil "Major mode for interacting with an inferior GHCi session." :group 'haskell :prefix "haskell-ghci-") (defun turn-on-haskell-ghci () "Turn on Haskell interaction mode with a GHCi interpreter running in an another Emacs buffer named *ghci*. Maps the following commands in the haskell keymap: \\\\[haskell-ghci-start-process] to create the GHCi buffer and start a GHCi process in it. \\[haskell-ghci-load-file] to save the current buffer and load it by sending the :load command to GHCi. \\[haskell-ghci-reload-file] to send the :reload command to GHCi without saving the buffer. \\[haskell-ghci-show-ghci-buffer] to show the GHCi buffer and go to it." (local-set-key "\C-c\C-s" 'haskell-ghci-start-process) (local-set-key "\C-c\C-l" 'haskell-ghci-load-file) (local-set-key "\C-c\C-r" 'haskell-ghci-reload-file) (local-set-key "\C-c\C-n" 'haskell-ghci-locate-next-error) (local-set-key "\C-c\C-b" 'haskell-ghci-show-ghci-buffer)) (defun turn-off-haskell-ghci () "Turn off Haskell interaction mode with a GHCi interpreter within a buffer." (local-unset-key "\C-c\C-s") (local-unset-key "\C-c\C-l") (local-unset-key "\C-c\C-r") (local-unset-key "\C-c\C-b")) (define-derived-mode haskell-ghci-mode comint-mode "Haskell GHCi" "Major mode for interacting with an inferior GHCi session. The commands available from within a Haskell script are: \\\\[haskell-ghci-start-process] to create the GHCi buffer and start a GHCi process in it. \\[haskell-ghci-load-file] to save the current buffer and load it by sending the :load command to GHCi. \\[haskell-ghci-reload-file] to send the :reload command to GHCi without saving the buffer. \\[haskell-ghci-show-ghci-buffer] to show the GHCi buffer and go to it. \\Commands: \\[comint-send-input] after end of GHCi output sends line as input to GHCi. \\[comint-send-input] before end of GHCI output copies rest of line and sends it to GHCI as input. \\[comint-kill-input] and \\[backward-kill-word] are kill commands, imitating normal Unix input editing. \\[comint-interrupt-subjob] interrupts the comint or its current subjob if any. \\[comint-stop-subjob] stops, likewise. \\[comint-quit-subjob] sends quit signal.") ;; GHCi interface: (require 'comint) (require 'shell) (defvar haskell-ghci-process nil "The active GHCi subprocess corresponding to current buffer.") (defvar haskell-ghci-process-buffer nil "*Buffer used for communication with GHCi subprocess for current buffer.") (defcustom haskell-ghci-program-name "ghci" "*The name of the GHCi interpreter program." :type 'string :group 'haskell-ghci) (defcustom haskell-ghci-program-args nil "*A list of string args to pass when starting the GHCi interpreter." :type '(repeat string) :group 'haskell-ghci) (defvar haskell-ghci-load-end nil "Position of the end of the last load command.") (defvar haskell-ghci-error-pos nil "Position of the end of the last load command.") (defvar haskell-ghci-send-end nil "Position of the end of the last send command.") (defun haskell-ghci-start-process (arg) "Start a GHCi process and invoke `haskell-ghci-hook' if not nil. Prompt for a list of args if called with an argument." (interactive "P") (if arg ;; XXX [CDW] Fix to use more natural 'string' version of the ;; XXX arguments rather than a sexp. (setq haskell-ghci-program-args (read-minibuffer (format "List of args for %s:" haskell-ghci-program-name) (prin1-to-string haskell-ghci-program-args)))) ;; Start the GHCi process in a new comint buffer. (message "Starting GHCi process `%s'." haskell-ghci-program-name) (setq haskell-ghci-process-buffer (apply 'make-comint "ghci" haskell-ghci-program-name nil haskell-ghci-program-args)) (setq haskell-ghci-process (get-buffer-process haskell-ghci-process-buffer)) ;; Select GHCi buffer temporarily. (set-buffer haskell-ghci-process-buffer) (haskell-ghci-mode) (make-local-variable 'shell-cd-regexp) (make-local-variable 'shell-dirtrackp) ;; Track directory changes using the `:cd' command. (setq shell-cd-regexp ":cd") (setq shell-dirtrackp t) (add-hook 'comint-input-filter-functions 'shell-directory-tracker nil 'local) ;; GHCi prompt should be of the form `ModuleName> '. (setq comint-prompt-regexp "^\\*?[[:upper:]][\\._[:alnum:]]*\\( \\*?[[:upper:]][\\._[:alnum:]]*\\)*> ") ;; History syntax of comint conflicts with Haskell, e.g. !!, so better ;; turn it off. (setq comint-input-autoexpand nil) (setq comint-process-echoes nil) (run-hooks 'haskell-ghci-hook) ;; Clear message area. (message "")) (defun haskell-ghci-wait-for-output () "Wait until output arrives and go to the last input." (while (progn (goto-char comint-last-input-end) (not (re-search-forward comint-prompt-regexp nil t))) (accept-process-output haskell-ghci-process))) (defun haskell-ghci-send (&rest string) "Send `haskell-ghci-process' the arguments (one or more strings). A newline is sent after the strings and they are inserted into the current buffer after the last output." (haskell-ghci-wait-for-output) ; wait for prompt (goto-char (point-max)) ; position for this input (apply 'insert string) (comint-send-input) (setq haskell-ghci-send-end (marker-position comint-last-input-end))) (defun haskell-ghci-go (load-command cd) "Save the current buffer and load its file into the GHCi process. The first argument LOAD-COMMAND specifies how the file should be loaded: as a new file (\":load \") or as a reload (\":reload \"). If the second argument CD is non-nil, change directory in the GHCi process to the current buffer's directory before loading the file. If the variable `haskell-ghci-command' is set then its value will be sent to the GHCi process after the load command. This can be used for a top-level expression to evaluate." (hack-local-variables) ; in case they've changed (save-buffer) (let ((file (if (string-equal load-command ":load ") (concat "\"" buffer-file-name "\"") "")) (dir (expand-file-name default-directory)) (cmd (and (boundp 'haskell-ghci-command) haskell-ghci-command (if (stringp haskell-ghci-command) haskell-ghci-command (symbol-name haskell-ghci-command))))) (if (and haskell-ghci-process-buffer (eq (process-status haskell-ghci-process) 'run)) ;; Ensure the GHCi buffer is selected. (set-buffer haskell-ghci-process-buffer) ;; Start Haskell-GHCi process. (haskell-ghci-start-process nil)) (if cd (haskell-ghci-send (concat ":cd " dir))) ;; Wait until output arrives and go to the last input. (haskell-ghci-wait-for-output) (haskell-ghci-send load-command file) ;; Error message search starts from last load command. (setq haskell-ghci-load-end (marker-position comint-last-input-end)) (setq haskell-ghci-error-pos haskell-ghci-load-end) (if cmd (haskell-ghci-send cmd)) ;; Wait until output arrives and go to the last input. (haskell-ghci-wait-for-output))) (defun haskell-ghci-load-file (cd) "Save a ghci buffer file and load its file. If CD (prefix argument if interactive) is non-nil, change directory in the GHCi process to the current buffer's directory before loading the file. If there is an error, set the cursor at the error line otherwise show the *ghci* buffer." (interactive "P") (haskell-ghci-gen-load-file ":load " cd)) (defun haskell-ghci-reload-file (cd) "Save a ghci buffer file and load its file. If CD (prefix argument if interactive) is non-nil, change the GHCi process to the current buffer's directory before loading the file. If there is an error, set the cursor at the error line otherwise show the *ghci* buffer." (interactive "P") (haskell-ghci-gen-load-file ":reload " cd)) (defun haskell-ghci-gen-load-file (cmd cd) "Save a ghci buffer file and load its file or reload depending on CMD. If CD is non-nil, change the process to the current buffer's directory before loading the file. If there is an error, set the cursor at the error line otherwise show the *ghci* buffer." ;; Execute (re)load command. (save-excursion (haskell-ghci-go cmd cd)) ;; Show *ghci* buffer. (pop-to-buffer haskell-ghci-process-buffer) (goto-char haskell-ghci-load-end) ;; Did we finish loading without error? (if (re-search-forward "^Ok, modules loaded" nil t) (progn (goto-char (point-max)) (recenter 2) (message "There were no errors.")) ;; Something went wrong. If possible, be helpful and pinpoint the ;; first error in the file whilst leaving the error visible in the ;; *ghci* buffer. (goto-char haskell-ghci-load-end) (haskell-ghci-locate-next-error))) (defun haskell-ghci-locate-next-error () "Go to the next error shown in the *ghci* buffer." (interactive) (if (buffer-live-p haskell-ghci-process-buffer) (progn (pop-to-buffer haskell-ghci-process-buffer) (goto-char haskell-ghci-error-pos) (if (re-search-forward "^[^\/]*\\([^:\n]+\\):\\([0-9]+\\)" nil t) (let ((efile (buffer-substring (match-beginning 1) (match-end 1))) (eline (string-to-int (buffer-substring (match-beginning 2) (match-end 2))))) (recenter 2) (setq haskell-ghci-error-pos (point)) (message "GHCi error on line %d of %s." eline (file-name-nondirectory efile)) (if (file-exists-p efile) (progn (find-file-other-window efile) (goto-line eline) (recenter)))) ;; We got an error without a file and line number, so put the ;; point at end of the *ghci* buffer ready to deal with it. (goto-char (point-max)) (recenter -2) (message "No more errors found."))) (message "No *ghci* buffer found."))) (defun haskell-ghci-show-ghci-buffer () "Go to the *ghci* buffer." (interactive) (if (or (not haskell-ghci-process-buffer) (not (buffer-live-p haskell-ghci-process-buffer))) (haskell-ghci-start-process nil)) (pop-to-buffer haskell-ghci-process-buffer)) (provide 'haskell-ghci) ;; arch-tag: f0bade4b-288d-4329-9791-98c1e24167ac ;;; haskell-ghci.el ends here