;;; inf-haskell.el --- Interaction with an inferior Haskell process. ;; Copyright (C) 2004, 2005, 2006, 2007 Free Software Foundation, Inc. ;; Author: Stefan Monnier ;; Keywords: Haskell ;; 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 2, 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: ;; The code is made of 2 parts: a major mode for the buffer that holds the ;; inferior process's session and a minor mode for use in source buffers. ;;; Code: (require 'comint) (require 'shell) ;For directory tracking. (require 'compile) (require 'haskell-mode) (eval-when-compile (require 'cl)) ;; Here I depart from the inferior-haskell- prefix. ;; Not sure if it's a good idea. (defcustom haskell-program-name ;; Arbitrarily give preference to hugs over ghci. (or (cond ((not (fboundp 'executable-find)) nil) ((executable-find "hugs") "hugs \"+.\"") ((executable-find "ghci") "ghci")) "hugs \"+.\"") "The name of the command to start the inferior Haskell process. The command can include arguments." ;; Custom only supports the :options keyword for a few types, e.g. not ;; for string. ;; :options '("hugs \"+.\"" "ghci") :group 'haskell :type '(choice string (repeat string))) (defconst inferior-haskell-info-xref-re "\t-- Defined at \\(.+\\):\\([0-9]+\\):\\([0-9]+\\)$") (defconst inferior-haskell-error-regexp-alist ;; The format of error messages used by Hugs. `(("^ERROR \"\\(.+?\\)\"\\(:\\| line \\)\\([0-9]+\\) - " 1 3) ;; Format of error messages used by GHCi. ("^\\(.+?\\):\\([0-9]+\\):\\(\\([0-9]+\\):\\)?\\( \\|\n +\\)\\(Warning\\)?" 1 2 4 ,@(if (fboundp 'compilation-fake-loc) '((6)))) ;; Info xrefs. ,@(if (fboundp 'compilation-fake-loc) `((,inferior-haskell-info-xref-re 1 2 3 0)))) "Regexps for error messages generated by inferior Haskell processes. The format should be the same as for `compilation-error-regexp-alist'.") (define-derived-mode inferior-haskell-mode comint-mode "Inf-Haskell" "Major mode for interacting with an inferior Haskell process." (set (make-local-variable 'comint-prompt-regexp) "^\\*?[A-Z][\\._a-zA-Z0-9]*\\( \\*?[A-Z][\\._a-zA-Z0-9]*\\)*> ") (set (make-local-variable 'comint-input-autoexpand) nil) ;; Setup directory tracking. (set (make-local-variable 'shell-cd-regexp) ":cd") (condition-case nil (shell-dirtrack-mode 1) (error ;The minor mode function may not exist or not accept an arg. (set (make-local-variable 'shell-dirtrackp) t) (add-hook 'comint-input-filter-functions 'shell-directory-tracker nil 'local))) ;; Setup `compile' support so you can just use C-x ` and friends. (set (make-local-variable 'compilation-error-regexp-alist) inferior-haskell-error-regexp-alist) (if (and (not (boundp 'minor-mode-overriding-map-alist)) (fboundp 'compilation-shell-minor-mode)) ;; If we can't remove compilation-minor-mode bindings, at least try to ;; use compilation-shell-minor-mode, so there are fewer ;; annoying bindings. (compilation-shell-minor-mode 1) ;; Else just use compilation-minor-mode but without its bindings because ;; things like mouse-2 are simply too annoying. (compilation-minor-mode 1) (let ((map (make-sparse-keymap))) (dolist (keys '([menu-bar] [follow-link])) ;; Preserve some of the bindings. (define-key map keys (lookup-key compilation-minor-mode-map keys))) (add-to-list 'minor-mode-overriding-map-alist (cons 'compilation-minor-mode map))))) (defun inferior-haskell-string-to-strings (string &optional separator) "Split the STRING into a list of strings. The SEPARATOR regexp defaults to \"\\s-+\"." (let ((sep (or separator "\\s-+")) (i (string-match "[\"]" string))) (if (null i) (split-string string sep) ; no quoting: easy (append (unless (eq i 0) (split-string (substring string 0 i) sep)) (let ((rfs (read-from-string string i))) (cons (car rfs) (inferior-haskell-string-to-strings (substring string (cdr rfs)) sep))))))) (defun inferior-haskell-command (arg) (inferior-haskell-string-to-strings (if (null arg) haskell-program-name (read-string "Command to run haskell: " haskell-program-name)))) (defvar inferior-haskell-buffer nil "The buffer in which the inferior process is running.") (defun inferior-haskell-start-process (command) "Start an inferior haskell process. With universal prefix \\[universal-argument], prompts for a command, otherwise uses `haskell-program-name'. It runs the hook `inferior-haskell-hook' after starting the process and setting up the inferior-haskell buffer." (interactive (list (inferior-haskell-command current-prefix-arg))) (setq inferior-haskell-buffer (apply 'make-comint "haskell" (car command) nil (cdr command))) (with-current-buffer inferior-haskell-buffer (inferior-haskell-mode) (run-hooks 'inferior-haskell-hook))) (defun inferior-haskell-process (&optional arg) (or (if (buffer-live-p inferior-haskell-buffer) (get-buffer-process inferior-haskell-buffer)) (progn (let ((current-prefix-arg arg)) (call-interactively 'inferior-haskell-start-process)) ;; Try again. (inferior-haskell-process arg)))) ;;;###autoload (defalias 'run-haskell 'switch-to-haskell) ;;;###autoload (defun switch-to-haskell (&optional arg) "Show the inferior-haskell buffer. Start the process if needed." (interactive "P") (let ((proc (inferior-haskell-process arg))) (pop-to-buffer (process-buffer proc)))) (eval-when-compile (unless (fboundp 'with-selected-window) (defmacro with-selected-window (win &rest body) `(save-selected-window (select-window ,win) ,@body)))) (defcustom inferior-haskell-wait-and-jump nil "If non-nil, wait for file loading to terminate and jump to the error." :type 'boolean :group 'haskell) (defun inferior-haskell-wait-for-prompt (proc) "Wait until PROC sends us a prompt. The process PROC should be associated to a comint buffer." (with-current-buffer (process-buffer proc) (while (progn (goto-char comint-last-input-end) (and (not (re-search-forward comint-prompt-regexp nil t)) (accept-process-output proc)))))) ;;;###autoload (defun inferior-haskell-load-file (&optional reload) "Pass the current buffer's file to the inferior haskell process." (interactive) (let ((file buffer-file-name) (proc (inferior-haskell-process))) (save-buffer) (with-current-buffer (process-buffer proc) ;; Not sure if it's useful/needed and if it actually works. ;; (unless (equal (file-name-as-directory default-directory) ;; (file-name-directory file)) ;; (inferior-haskell-send-string ;; proc (concat ":cd " (file-name-directory file) "\n"))) (compilation-forget-errors) (let ((parsing-end (marker-position (process-mark proc)))) (inferior-haskell-send-command proc (if reload ":reload" (concat ":load \"" file "\""))) ;; Move the parsing-end marker after sending the command so ;; that it doesn't point just to the insertion point. ;; Otherwise insertion may move the marker (if done with ;; insert-before-markers) and we'd then miss some errors. (if (boundp 'compilation-parsing-end) (if (markerp compilation-parsing-end) (set-marker compilation-parsing-end parsing-end) (setq compilation-parsing-end parsing-end)))) (with-selected-window (display-buffer (current-buffer)) (goto-char (point-max))) (when inferior-haskell-wait-and-jump (inferior-haskell-wait-for-prompt proc) (ignore-errors ;Don't beep if there were no errors. (next-error)))))) (defun inferior-haskell-send-command (proc str) (setq str (concat str "\n")) (with-current-buffer (process-buffer proc) (inferior-haskell-wait-for-prompt proc) (goto-char (process-mark proc)) (insert-before-markers str) (move-marker comint-last-input-end (point)) (comint-send-string proc str))) (defun inferior-haskell-reload-file () "Tell the inferior haskell process to reread the current buffer's file." (interactive) (inferior-haskell-load-file 'reload)) (defun inferior-haskell-type (expr &optional insert-value) "Query the haskell process for the type of the given expression. If optional argument `insert-value' is non-nil, insert the type above point in the buffer. This can be done interactively with the \\[universal-argument] prefix. The returned info is cached for reuse by `haskell-doc-mode'." (interactive (let ((sym (haskell-ident-at-point))) (list (read-string (if (> (length sym) 0) (format "Show type of (default %s): " sym) "Show type of: ") nil nil sym) current-prefix-arg))) (if (string-match "\\`\\s_+\\'" expr) (setq expr (concat "(" expr ")"))) (let* ((proc (inferior-haskell-process)) (type (with-current-buffer (process-buffer proc) (let ((parsing-end ; Remember previous spot. (marker-position (process-mark proc)))) (inferior-haskell-send-command proc (concat ":type " expr)) ;; Find new point. (goto-char (point-max)) (inferior-haskell-wait-for-prompt proc) ;; Back up to the previous end-of-line. (end-of-line 0) ;; Extract the type output (buffer-substring-no-properties (save-excursion (goto-char parsing-end) (line-beginning-position 2)) (point)))))) (if (not (string-match (concat "\\`" (regexp-quote expr) "[ \t]+::[ \t]*") type)) (error "No type info: %s" type) ;; Cache for reuse by haskell-doc. (when (and (boundp 'haskell-doc-mode) haskell-doc-mode (boundp 'haskell-doc-user-defined-ids) ;; Haskell-doc only works for idents, not arbitrary expr. (string-match "\\`(?\\(\\s_+\\|\\(\\sw\\|\\s'\\)+\\)?[ \t]*::[ \t]*" type)) (let ((sym (match-string 1 type))) (setq haskell-doc-user-defined-ids (cons (cons sym (substring type (match-end 0))) (remove-if (lambda (item) (equal (car item) sym)) haskell-doc-user-defined-ids))))) (if (interactive-p) (message type)) (when insert-value (beginning-of-line) (insert type "\n")) type))) (defun inferior-haskell-info (sym) "Query the haskell process for the info of the given expression." (interactive (let ((sym (haskell-ident-at-point))) (list (read-string (if (> (length sym) 0) (format "Show info of (default %s): " sym) "Show info of: ") nil nil sym)))) (let ((proc (inferior-haskell-process))) (with-current-buffer (process-buffer proc) (let ((parsing-end ; Remember previous spot. (marker-position (process-mark proc)))) (inferior-haskell-send-command proc (concat ":info " sym)) ;; Find new point. (goto-char (point-max)) (inferior-haskell-wait-for-prompt proc) ;; Move to previous end-of-line (end-of-line 0) (let ((result (buffer-substring-no-properties (save-excursion (goto-char parsing-end) (line-beginning-position 2)) (point)))) ;; Move back to end of process buffer (goto-char (point-max)) (if (interactive-p) (message "%s" result)) result))))) (defun inferior-haskell-find-definition (sym) "Attempt to locate and jump to the definition of the given expression." (interactive (let ((sym (haskell-ident-at-point))) (list (read-string (if (> (length sym) 0) (format "Find definition of (default %s): " sym) "Find definition of: ") nil nil sym)))) (let ((info (inferior-haskell-info sym))) (if (not (string-match inferior-haskell-info-xref-re info)) (error "No source information available") (let ((file (match-string-no-properties 1 info)) (line (string-to-number (match-string-no-properties 2 info))) (col (string-to-number (match-string-no-properties 3 info)))) (when file ;; Push current location marker on the ring used by `find-tag' (require 'etags) (ring-insert find-tag-marker-ring (point-marker)) (pop-to-buffer (find-file-noselect file)) (when line (goto-line line) (when col (move-to-column col)))))))) (provide 'inf-haskell) ;; arch-tag: 61804287-63dd-4052-bc0e-90f691b34b40 ;;; inf-haskell.el ends here