;;; intero.el --- Complete development mode for Haskell ;; Copyright (c) 2016 Chris Done ;; Copyright (c) 2015 Athur Fayzrakhmanov ;; Copyright (c) 2013 Herbert Valerio Riedel ;; Copyright (c) 2007 Stefan Monnier ;; Package-Requires: ((flycheck "26") (company "0.9.0")) ;; 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., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; ;; Mode that enables: ;; ;; * Flycheck type checking ✓ ;; * Company mode completion ✓ ;; * Go to definition ✓ ;; * Type of selection ✓ ;; * Info ✓ ;; * REPL ;; * Find uses ;; * List all types in all expressions ;;; Code: (require 'flycheck) (require 'cl-lib) (require 'company) (require 'comint) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Configuration (defconst intero-package-version "intero-0.1.8" "Package version to auto-install.") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Modes (defvar intero-mode-map (make-sparse-keymap) "Intero minor mode's map.") (define-minor-mode intero-mode "Minor mode for Intero" :lighter " Intero" :keymap intero-mode-map (when (buffer-file-name) (if intero-mode (progn (flycheck-select-checker 'intero) (flycheck-mode) (add-to-list (make-local-variable 'company-backends) 'company-intero) (company-mode)) (message "Intero mode disabled.")))) (define-key intero-mode-map (kbd "C-c C-t") 'intero-type-at) (define-key intero-mode-map (kbd "C-c C-i") 'intero-info) (define-key intero-mode-map (kbd "M-.") 'intero-goto-definition) (define-key intero-mode-map (kbd "C-c C-l") 'intero-repl-load) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Buffer-local variables/state (defvar intero-callbacks (list) "List of callbacks waiting for output. FIFO.") (make-variable-buffer-local 'intero-callbacks) (defvar intero-arguments (list) "Arguments used to call the stack process.") (make-variable-buffer-local 'intero-arguments) (defvar intero-targets (list) "Targets used for the stack process.") (make-variable-buffer-local 'intero-targets) (defvar intero-project-root nil "The project root of the current buffer.") (make-variable-buffer-local 'intero-project-root) (defvar intero-package-name nil "The package name associated with the current buffer.") (make-variable-buffer-local 'intero-package-name) (defvar intero-deleting nil "The process of the buffer is being deleted.") (make-variable-buffer-local 'intero-deleting) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Interactive commands (defun intero-cd () "Change directory in the backend process." (interactive) (intero-async-call 'backend (concat ":cd " (read-directory-name "Change Intero directory: ")))) (defun intero-type-at (insert) "Get the type of the thing or selection at point." (interactive "P") (let ((ty (apply #'intero-get-type-at (intero-thing-at-point)))) (if insert (save-excursion (goto-char (line-beginning-position)) (insert (format "%s\n" (with-temp-buffer (when (fboundp 'haskell-mode) (haskell-mode)) (insert ty) (font-lock-fontify-buffer) (buffer-string))))) (message "%s" (with-temp-buffer (when (fboundp 'haskell-mode) (haskell-mode)) (insert ty) (font-lock-fontify-buffer) (buffer-string)))))) (defun intero-info (insert) "Get the info of the thing at point." (interactive "P") (let ((info (intero-get-info-of (intero-ident-at-point)))) (message "%s" (with-temp-buffer (when (fboundp 'haskell-mode) (haskell-mode)) (insert info) (font-lock-fontify-buffer) (buffer-string))))) (defun intero-goto-definition () "Jump to the definition of the thing at point." (interactive) (let ((result (apply #'intero-get-loc-at (intero-thing-at-point)))) (when (string-match "\\(.*?\\):(\\([0-9]+\\),\\([0-9]+\\))-(\\([0-9]+\\),\\([0-9]+\\))$" result) (let ((file (match-string 1 result)) (line (string-to-number (match-string 2 result))) (col (string-to-number (match-string 3 result)))) (find-file file) (pop-mark) (goto-char (point-min)) (forward-line (1- line)) (forward-char (1- col)))))) (defun intero-restart () "Simply restart the process with the same configuration as before." (interactive) (when (intero-buffer-p 'backend) (let ((targets (with-current-buffer (intero-buffer 'backend) intero-targets))) (intero-destroy 'backend) (intero-get-worker-create 'backend targets (current-buffer))))) (defun intero-targets () "Set the targets to use for stack ghci." (interactive) (let ((targets (split-string (read-from-minibuffer "Targets: ") " " t))) (intero-destroy) (intero-get-worker-create 'backend targets (current-buffer)))) (defun intero-destroy (&optional worker) "Stop the current worker process and kill its associated." (interactive) (if worker (intero-delete-worker worker) (intero-delete-worker 'backend))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; DevelMain integration (defun intero-devel-reload () "Reload the module `DevelMain' and then run `DevelMain.update'. This is for doing live update of the code of servers or GUI applications. Put your development version of the program in `DevelMain', and define `update' to auto-start the program on a new thread, and use the `foreign-store' package to access the running context across :load/:reloads in Intero." (interactive) (unwind-protect (with-current-buffer (or (get-buffer "DevelMain.hs") (if (y-or-n-p "You need to open a buffer named DevelMain.hs. Find now?") (ido-find-file) (error "No DevelMain.hs buffer."))) (message "Reloading ...") (intero-async-call 'backend ":l DevelMain" (current-buffer) (lambda (buffer reply) (if (string-match "^OK, modules loaded" reply) (intero-async-call 'backend "DevelMain.update" buffer (lambda (_buffer reply) (message "DevelMain updated. Output was:\n%s" reply))) (progn (message "DevelMain FAILED. Switch to DevelMain.hs and compile that.") (switch-to-buffer buffer) (flycheck-buffer) (flycheck-list-errors)))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Flycheck integration (defun intero-check (checker cont) "Run a check and pass the status onto CONT." (let ((file-buffer (current-buffer))) (write-region (point-min) (point-max) (buffer-file-name)) (clear-visited-file-modtime) (intero-async-call 'backend (concat ":l " (buffer-file-name)) (list :cont cont :file-buffer file-buffer :checker checker) (lambda (state string) (with-current-buffer (plist-get state :file-buffer) (funcall (plist-get state :cont) 'finished (intero-parse-errors-warnings (plist-get state :checker) (current-buffer) string)) (when (string-match "OK, modules loaded: \\(.*\\)\\.$" string) (let ((modules (match-string 1 string))) (intero-async-call 'backend (concat ":m + " (replace-regexp-in-string modules "," "")) nil (lambda (_st _)))))))))) (flycheck-define-generic-checker 'intero "A syntax and type checker for Haskell using an Intero worker process." :start 'intero-check :modes '(haskell-mode) :next-checkers '((warning . haskell-hlint))) (add-to-list 'flycheck-checkers 'intero) (defun intero-parse-errors-warnings (checker buffer string) "Parse from the given STRING a list of flycheck errors and warnings, adding CHECKER and BUFFER to each one." (with-temp-buffer (insert string) (goto-char (point-min)) (let ((messages (list))) (while (search-forward-regexp (concat "[\r\n]\\([A-Z]?:?[^ \r\n:][^:\n\r]+\\):\\([0-9()-:]+\\):" "[ \n\r]+\\([[:unibyte:][:nonascii:]]+?\\)\n[^ ]") nil t 1) (let* ((file (match-string 1)) (location-raw (match-string 2)) (msg (match-string 3)) (type (cond ((string-match "^Warning:" msg) (setq msg (replace-regexp-in-string "^Warning: *" "" msg)) 'warning) ((string-match "^Splicing " msg) 'splice) (t 'error))) (location (intero-parse-error (concat file ":" location-raw ": x"))) (line (plist-get location :line)) (column (plist-get location :col))) (setq messages (cons (flycheck-error-new-at line column type msg :checker checker :buffer (when (string= (buffer-file-name buffer) file) buffer) :filename file) messages)))) messages))) (defconst intero-error-regexp-alist `((,(concat "^ *\\(?1:[^\t\r\n]+?\\):" "\\(?:" "\\(?2:[0-9]+\\):\\(?4:[0-9]+\\)\\(?:-\\(?5:[0-9]+\\)\\)?" ;; "121:1" & "12:3-5" "\\|" "(\\(?2:[0-9]+\\),\\(?4:[0-9]+\\))-(\\(?3:[0-9]+\\),\\(?5:[0-9]+\\))" ;; "(289,5)-(291,36)" "\\)" ":\\(?6: Warning:\\)?") 1 (2 . 3) (4 . 5) (6 . nil)) ;; error/warning locus ;; multiple declarations ("^ \\(?:Declared at:\\| \\) \\(?1:[^ \t\r\n]+\\):\\(?2:[0-9]+\\):\\(?4:[0-9]+\\)$" 1 2 4 0) ;; info locus ;; this is the weakest pattern as it's subject to line wrapping et al. (" at \\(?1:[^ \t\r\n]+\\):\\(?2:[0-9]+\\):\\(?4:[0-9]+\\)\\(?:-\\(?5:[0-9]+\\)\\)?[)]?$" 1 2 (4 . 5) 0)) ;; info locus "Regexps used for matching GHC compile messages.") (defun intero-parse-error (string) "Parse the line number from the error." (let ((span nil)) (cl-loop for regex in intero-error-regexp-alist do (when (string-match (car regex) string) (setq span (list :file (match-string 1 string) :line (string-to-number (match-string 2 string)) :col (string-to-number (match-string 4 string)) :line2 (when (match-string 3 string) (string-to-number (match-string 3 string))) :col2 (when (match-string 5 string) (string-to-number (match-string 5 string))))))) span)) (defun intero-call-in-buffer (buffer func &rest args) "Utility function which calls FUNC in BUFFER with ARGS." (with-current-buffer buffer (apply func args))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Company integration (auto-completion) (defun company-intero (command &optional arg &rest ignored) (interactive (list 'interactive)) (cl-case command (interactive (company-begin-backend 'company-intero)) (prefix (let ((prefix-info (intero-completions-grab-prefix))) (when prefix-info (cl-destructuring-bind (_beg _end prefix _type) prefix-info prefix)))) (candidates (intero-get-completions arg)))) (defun intero-completions-grab-prefix (&optional minlen) "Grab prefix at point for possible completion." (when (intero-completions-can-grab-prefix) (let ((prefix (cond ((intero-completions-grab-identifier-prefix))))) (cond ((and minlen prefix) (when (>= (length (nth 2 prefix)) minlen) prefix)) (prefix prefix))))) (defun intero-completions-can-grab-prefix () "Check if the case is appropriate for grabbing completion prefix." (when (not (region-active-p)) (when (looking-at-p (rx (| space line-end punct))) (when (not (bobp)) (save-excursion (backward-char) (not (looking-at-p (rx (| space line-end))))))))) (defun intero-completions-grab-identifier-prefix () "Grab identifier prefix." (let ((pos-at-point (intero-ident-pos-at-point)) (p (point))) (when pos-at-point (let* ((start (car pos-at-point)) (end (cdr pos-at-point)) (type 'haskell-completions-identifier-prefix) (case-fold-search nil) value) (when (<= p end) (setq end p) (setq value (buffer-substring-no-properties start end)) (when (string-match-p (rx bos upper) value) (save-excursion (goto-char (line-beginning-position)) (when (re-search-forward (rx "import" (? (1+ space) "qualified") (1+ space) upper (1+ (| alnum "."))) p ;; bound t) ;; no-error (if (equal p (point)) (setq type 'haskell-completions-module-name-prefix) (when (re-search-forward (rx (| " as " "(")) start t) (setq type 'haskell-completions-identifier-prefix)))))) (when (nth 8 (syntax-ppss)) (setq type 'haskell-completions-general-prefix)) (when value (list start end value type))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; REPL (defconst intero-prompt-regexp "^\4 ") (defun intero-repl-load () "Load the current file in the REPL." (interactive) (let ((file (buffer-file-name)) (repl-buffer (intero-repl-buffer))) (with-current-buffer repl-buffer (comint-send-string (get-buffer-process (current-buffer)) (concat ":l " file "\n"))) (pop-to-buffer repl-buffer))) (defun intero-repl () "Start up the REPL for this stack project." (interactive) (switch-to-buffer (intero-repl-buffer))) (defun intero-repl-buffer () (let* ((root (intero-project-root)) (package-name (intero-package-name)) (name (format "*intero:%s:%s:repl*" (file-name-nondirectory root) package-name))) (if (get-buffer name) (get-buffer name) (with-current-buffer (get-buffer-create name) (cd root) (intero-repl-mode) (current-buffer))))) (define-derived-mode intero-repl-mode comint-mode "Intero-REPL" "Interactive prompt for Intero." (when (and (not (eq major-mode 'fundamental-mode)) (eq this-command 'intero-repl-mode)) (error "You probably meant to run: M-x intero-repl")) (set (make-local-variable 'comint-prompt-regexp) intero-prompt-regexp) (let ((arguments (intero-make-options-list intero-targets))) (insert (propertize (format "Starting:\n stack ghci %s\n" (mapconcat #'identity arguments " ")) 'face 'font-lock-comment-face)) (let ((script (with-current-buffer (find-file-noselect (make-temp-file "intero-script")) (insert ":set prompt \"\" :set -fobject-code :set prompt \"\\4 \" ") (basic-save-buffer) (buffer-file-name)))) (let ((process (apply #'start-process "intero" (current-buffer) "stack" "ghci" (append arguments (list "--verbosity" "silent") (list "--ghci-options" (concat "-ghci-script=" script)))))) (when (process-live-p process) (message "Started Intero process for REPL.")))))) (font-lock-add-keywords 'intero-repl-mode '(("\\(\4\\)" (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) ?λ)))))) (define-key intero-repl-mode-map [remap move-beginning-of-line] 'intero-repl-beginning-of-line) (define-key intero-repl-mode-map [remap delete-backward-char] 'intero-repl-delete-backward-char) (defun intero-repl-delete-backward-char () "Delete backwards, excluding the prompt." (interactive) (unless (looking-back intero-prompt-regexp) (call-interactively 'delete-backward-char))) (defun intero-repl-beginning-of-line () "Go to the beginning of the line, excluding the prompt." (interactive) (if (search-backward-regexp intero-prompt-regexp (line-beginning-position) t 1) (goto-char (+ 2 (line-beginning-position))) (call-interactively 'move-beginning-of-line))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Buffer operations (defun intero-thing-at-point () "Return (list START END) of something at the point." (if (region-active-p) (list (region-beginning) (region-end)) (let ((pos (intero-ident-pos-at-point))) (if pos (list (car pos) (cdr pos)) (list (point) (point)))))) (defun intero-ident-at-point () "Return the identifier under point, or nil if none found. May return a qualified name." (let ((reg (intero-ident-pos-at-point))) (when reg (buffer-substring-no-properties (car reg) (cdr reg))))) (defun intero-ident-pos-at-point () "Return the span of the identifier under point, or nil if none found. May return a qualified name." (save-excursion ;; Skip whitespace if we're on it. That way, if we're at "map ", we'll ;; see the word "map". (if (and (not (eobp)) (eq ? (char-syntax (char-after)))) (skip-chars-backward " \t")) (let ((case-fold-search nil)) (cl-multiple-value-bind (start end) (list (progn (skip-syntax-backward "w_") (point)) (progn (skip-syntax-forward "w_") (point))) ;; If we're looking at a module ID that qualifies further IDs, add ;; those IDs. (goto-char start) (while (and (looking-at "[[:upper:]]") (eq (char-after end) ?.) ;; It's a module ID that qualifies further IDs. (goto-char (1+ end)) (save-excursion (when (not (zerop (skip-syntax-forward (if (looking-at "\\s_") "_" "w'")))) (setq end (point)))))) ;; If we're looking at an ID that's itself qualified by previous ;; module IDs, add those too. (goto-char start) (if (eq (char-after) ?.) (forward-char 1)) ;Special case for "." (while (and (eq (char-before) ?.) (progn (forward-char -1) (not (zerop (skip-syntax-backward "w'")))) (skip-syntax-forward "'") (looking-at "[[:upper:]]")) (setq start (point))) ;; This is it. (unless (= start end) (cons start end)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Query/commands (defun intero-get-all-types () "Get all types in all expressions in all modules." (intero-blocking-call 'backend ":all-types")) (defun intero-get-type-at (beg end) "Get the type at the given region denoted by BEG and END." (replace-regexp-in-string "\n$" "" (intero-blocking-call 'backend (format ":type-at %S %d %d %d %d %S" (buffer-file-name) (save-excursion (goto-char beg) (line-number-at-pos)) (save-excursion (goto-char beg) (1+ (current-column))) (save-excursion (goto-char end) (line-number-at-pos)) (save-excursion (goto-char end) (1+ (current-column))) (buffer-substring-no-properties beg end))))) (defun intero-get-info-of (thing) "Get info for the thing." (replace-regexp-in-string "\n$" "" (intero-blocking-call 'backend (format ":i %s" thing)))) (defun intero-get-loc-at (beg end) "Get the location of the identifier denoted by BEG and END." (replace-regexp-in-string "\n$" "" (intero-blocking-call 'backend (format ":loc-at %S %d %d %d %d %S" (buffer-file-name) (save-excursion (goto-char beg) (line-number-at-pos)) (save-excursion (goto-char beg) (1+ (current-column))) (save-excursion (goto-char end) (line-number-at-pos)) (save-excursion (goto-char end) (1+ (current-column))) (buffer-substring-no-properties beg end))))) (defun intero-get-uses-at (beg end) "Get the uses of the identifier denoted by BEG and END." (replace-regexp-in-string "\n$" "" (intero-blocking-call 'backend (format ":uses %S %d %d %d %d %S" (buffer-file-name) (save-excursion (goto-char beg) (line-number-at-pos)) (save-excursion (goto-char beg) (1+ (current-column))) (save-excursion (goto-char end) (line-number-at-pos)) (save-excursion (goto-char end) (1+ (current-column))) (buffer-substring-no-properties beg end))))) (defun intero-get-completions (prefix) "Get completions for a PREFIX." (mapcar #'read (cdr (split-string (intero-blocking-call 'backend (format ":complete repl %S" prefix)) "\n" t)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Process communication (defun intero-delete-worker (worker) "Delete the given worker." (when (intero-buffer-p worker) (with-current-buffer (intero-get-buffer-create worker) (when (get-buffer-process (current-buffer)) (setq intero-deleting t) (kill-process (get-buffer-process (current-buffer))) (delete-process (get-buffer-process (current-buffer)))) (kill-buffer (current-buffer))))) (defun intero-blocking-call (worker cmd) "Make a synchronous call of CMD to the process." (let ((result (list nil))) (intero-async-call worker cmd result (lambda (result reply) (setf (car result) reply))) (with-current-buffer (intero-buffer worker) (while (not (null intero-callbacks)) (sleep-for 0.0001))) (car result))) (defun intero-async-call (worker cmd &optional state callback) "Make an asynchronous call of CMD (string) to the process, calling CALLBACK as (CALLBACK STATE REPLY)." (with-current-buffer (intero-buffer worker) (setq intero-callbacks (append intero-callbacks (list (list state (or callback #'ignore) cmd))))) (process-send-string (intero-process worker) (concat cmd "\n"))) (defun intero-buffer (worker) "Get the worker buffer for the current directory." (let ((buffer (intero-get-buffer-create worker))) (if (get-buffer-process buffer) buffer (intero-get-worker-create worker nil (current-buffer))))) (defun intero-process (worker) "Get the worker process for the current directory." (get-buffer-process (intero-buffer worker))) (defun intero-get-worker-create (worker &optional targets source-buffer) "Start an Intero worker." (let* ((buffer (intero-get-buffer-create worker))) (if (get-buffer-process buffer) buffer (if (intero-installed-p) (intero-start-process-in-buffer buffer targets source-buffer) (intero-auto-install buffer targets source-buffer))))) (defun intero-auto-install (buffer &optional targets source-buffer) "Automatically install Intero." (let ((source-buffer (or source-buffer (current-buffer)))) (switch-to-buffer buffer) (erase-buffer) (insert "Intero is not installed in the Stack environment. Installing automatically ... ") (redisplay) (cl-case (call-process "stack" nil (current-buffer) t "build" intero-package-version) (0 (message "Installed successfully! Starting Intero in a moment ...") (bury-buffer buffer) (switch-to-buffer source-buffer) (intero-start-process-in-buffer buffer targets source-buffer)) (1 (insert (propertize "Could not install Intero! We don't know why it failed. Please read the above output and try installing manually. If that doesn't work, report this as a problem. " 'face 'compilation-error)))))) (defun intero-start-process-in-buffer (buffer &optional targets source-buffer) "Start an Intero worker in BUFFER for TARGETS, automatically performing a initial actions in SOURCE-BUFFER, if specified." (let* ((options (intero-make-options-list (or targets (list (buffer-local-value 'intero-package-name buffer))))) (arguments options) (process (with-current-buffer buffer (when debug-on-error (message "Intero arguments: %S" arguments)) (message "Booting up intero ...") (apply #'start-process "stack" buffer "stack" "ghci" arguments)))) (process-send-string process ":set -fobject-code\n") (process-send-string process ":set prompt \"\\4\"\n") (with-current-buffer buffer (erase-buffer) (setq intero-targets targets) (setq intero-arguments arguments) (setq intero-callbacks (list (list source-buffer (lambda (source-buffer _msg) (when source-buffer (with-current-buffer source-buffer (when flycheck-mode (run-with-timer 0 nil 'intero-call-in-buffer (current-buffer) 'flycheck-buffer)))) (message "Booted up intero!")))))) (set-process-filter process (lambda (process string) (when (buffer-live-p (process-buffer process)) (with-current-buffer (process-buffer process) (goto-char (point-max)) (insert string) (intero-read-buffer))))) (set-process-sentinel process 'intero-sentinel) buffer)) (defun intero-make-options-list (targets) "Make the stack ghci options list." (append (list "--with-ghc" "intero" "--docker-run-args=--interactive=true --tty=false" "--no-load" "--no-build") (let ((dir (make-temp-file "intero" t))) (list "--ghci-options" (format "%S" (concat "-odir=" dir)) "--ghci-options" (format "%S" (concat "-hidir=" dir)))) targets)) (defun intero-sentinel (process change) "Handle a CHANGE to the PROCESS." (when (buffer-live-p (process-buffer process)) (when (and (not (process-live-p process))) (if (with-current-buffer (process-buffer process) intero-deleting) (message "Intero process deleted.") (intero-show-process-problem process change))))) (defun intero-installed-p () "Is intero installed in the stack environment?" (redisplay) (= 0 (call-process "stack" nil nil nil "exec" "--" "intero" "--version"))) (defun intero-show-process-problem (process change) "Show the user that a CHANGE occurred on PROCESS, causing it to end." (message "Problem with Intero!") (switch-to-buffer (process-buffer process)) (goto-char (point-max)) (insert "\n---\n\n") (insert (propertize (concat "This is the buffer where Emacs talks to intero. It's normally hidden, but a problem occcured. It may be obvious if there is some text above this message indicating a problem. The process ended. Here is the reason that Emacs gives us: " " " change "\n" "For troubleshooting purposes, here are the arguments used to launch intero: " (format " stack ghci %s" (mapconcat #'identity intero-arguments " ")) " After fixing this problem, you could switch back to your code and run M-x intero-restart to try again. You can kill this buffer when you're done reading it.\n") 'face 'compilation-error))) (defun intero-read-buffer () "In the process buffer, we read what's in it." (let ((repeat t)) (while repeat (setq repeat nil) (goto-char (point-min)) (when (search-forward "\4" (point-max) t 1) (let* ((next-callback (pop intero-callbacks)) (state (nth 0 next-callback)) (func (nth 1 next-callback))) (let ((string (buffer-substring (point-min) (1- (point))))) (if next-callback (progn (with-temp-buffer (funcall func state string)) (setq repeat t)) (when debug-on-error (warn "Received output but no callback in `intero-callbacks': %S" string))))) (delete-region (point-min) (point)))))) (defun intero-get-buffer-create (worker) "Get or create the stack buffer for this current directory and the given targets." (let* ((cabal-file (intero-cabal-find-file)) (package-name (intero-package-name cabal-file)) (buffer-name (intero-buffer-name worker)) (default-directory (file-name-directory cabal-file))) (with-current-buffer (get-buffer-create buffer-name) (setq intero-package-name package-name) (cd default-directory) (current-buffer)))) (defun intero-buffer-p (worker) "Does a buffer exist for a given worker?" (get-buffer (intero-buffer-name worker))) (defun intero-buffer-name (worker) "For a given WORKER, create a buffer name." (let* ((root (intero-project-root)) (package-name (intero-package-name))) (concat " intero:" (format "%s" worker) ":" package-name " " root))) (defun intero-project-root () "Get the directory where the stack.yaml is placed for this project, or the global one." (if intero-project-root intero-project-root (setq intero-project-root (with-temp-buffer (save-excursion (call-process "stack" nil (current-buffer) nil "path" "--project-root" "--verbosity" "silent")) (buffer-substring (line-beginning-position) (line-end-position)))))) (defun intero-package-name (&optional cabal-file) "Get the current package name from a nearby .cabal file. If there is none, return empty string." (or intero-package-name (setq intero-package-name (let ((cabal-file (or cabal-file (intero-cabal-find-file)))) (if cabal-file (replace-regexp-in-string ".cabal$" "" (file-name-nondirectory cabal-file)) ""))))) (defun intero-cabal-find-file (&optional dir) "Search for package description file upwards starting from DIR. If DIR is nil, `default-directory' is used as starting point for directory traversal. Upward traversal is aborted if file owner changes. Uses `intero-cabal-find-pkg-desc' internally." (let ((use-dir (or dir default-directory))) (while (and use-dir (not (file-directory-p use-dir))) (setq use-dir (file-name-directory (directory-file-name use-dir)))) (when use-dir (catch 'found (let ((user (nth 2 (file-attributes use-dir))) ;; Abbreviate, so as to stop when we cross ~/. (root (abbreviate-file-name use-dir))) ;; traverse current dir up to root as long as file owner doesn't change (while (and root (equal user (nth 2 (file-attributes root)))) (let ((cabal-file (intero-cabal-find-pkg-desc root))) (when cabal-file (throw 'found cabal-file))) (let ((proot (file-name-directory (directory-file-name root)))) (if (equal proot root) ;; fix-point reached? (throw 'found nil) (setq root proot)))) nil))))) (defun intero-cabal-find-pkg-desc (dir &optional allow-multiple) "Find a package description file in the directory DIR. Returns nil if none or multiple \".cabal\" files were found. If ALLOW-MULTIPLE is non nil, in case of multiple \".cabal\" files, a list is returned instead of failing with a nil result." ;; This is basically a port of Cabal's ;; Distribution.Simple.Utils.findPackageDesc function ;; http://hackage.haskell.org/packages/archive/Cabal/1.16.0.3/doc/html/Distribution-Simple-Utils.html ;; but without the exception throwing. (let* ((cabal-files (cl-remove-if 'file-directory-p (cl-remove-if-not 'file-exists-p (directory-files dir t ".\\.cabal\\'"))))) (cond ((= (length cabal-files) 1) (car cabal-files)) ;; exactly one candidate found (allow-multiple cabal-files) ;; pass-thru multiple candidates (t nil)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (provide 'intero) ;;; intero.el ends here