elisp/stack-mode/stack-mode.el
changeset 80 e848f1863d6b
child 81 4da7819d1a1c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/elisp/stack-mode/stack-mode.el	Wed Sep 16 16:53:07 2015 -0500
@@ -0,0 +1,927 @@
+;;; stack-mode.el --- A minor mode enabling various features based on
+;;; stack-ide.
+
+;; Copyright (c) 2015 Chris Done.
+
+;; 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Imports
+
+(require 'haskell-mode)
+(require 'haskell-cabal)
+(require 'cl-lib)
+(require 'fifo)
+(require 'flycheck)
+(require 'json)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Modes
+
+(define-minor-mode stack-mode
+  "A minor mode enabling various features based on stack-ide.
+
+Automatically starts and stops flycheck-mode when you
+enable/disable it. It makes this assumption in the interest of
+easier user experience. Disable with `stack-mode-manage-flycheck'."
+  :lighter " Stack"
+  :keymap (let ((map (make-sparse-keymap)))
+            (define-key map (kbd "M-.") 'stack-mode-goto)
+            (define-key map (kbd "C-c C-k") 'stack-mode-clear)
+            (define-key map (kbd "C-c C-t") 'stack-mode-type)
+            (define-key map (kbd "C-c C-i") 'stack-mode-info)
+            (define-key map (kbd "C-c C-l") 'stack-mode-load)
+            map)
+  (when (buffer-file-name)
+    (if stack-mode
+        (progn (when (bound-and-true-p interactive-haskell-mode)
+                 (when (y-or-n-p "interactive-haskell-mode is enabled. Disable it?")
+                   (interactive-haskell-mode -1)))
+               (when stack-mode-manage-flycheck
+                 (flycheck-mode 1)
+                 (flycheck-select-checker 'stack-ide)
+                 (flycheck-buffer)))
+      (when stack-mode-manage-flycheck
+        (flycheck-mode -1)))))
+
+(define-derived-mode inferior-stack-mode fundamental-mode "Inferior-Stack-IDE"
+  "Major mode for interacting with an inferior stack-ide process.")
+
+(define-key inferior-stack-mode-map (kbd "C-c C-c") 'stack-mode-stop)
+(define-key inferior-stack-mode-map (kbd "C-c C-k") 'stack-mode-clear)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Customization
+
+(defgroup stack-mode nil
+  "IDE backend support for Haskell."
+  :group 'haskell)
+
+(defcustom stack-mode-proc-path
+  "stack"
+  "Path to the stack executable."
+  :type 'string
+  :group 'stack-mode)
+
+(defcustom stack-mode-manage-flycheck
+  t
+  "Automatically start and stop flycheck when the minor mode is
+enabled/disabled."
+  :type 'boolean
+  :group 'stack-mode)
+
+(defcustom stack-mode-print-error-messages
+  nil
+  "Print error messages after loading the project?"
+  :type 'boolean
+  :group 'stack-mode)
+
+(defcustom stack-mode-show-popup
+  nil
+  "Show type and info messages in a popup?"
+  :type 'boolean
+  :group 'stack-mode)
+
+(defvar stack-mode-queue nil)
+(make-variable-buffer-local 'stack-mode-queue)
+
+(defvar stack-mode-back-queue nil)
+(make-variable-buffer-local 'stack-mode-back-queue)
+
+(defvar stack-mode-buffer nil)
+(make-variable-buffer-local 'stack-mode-buffer)
+
+(defvar stack-mode-name nil)
+(make-variable-buffer-local 'stack-mode-name)
+
+(defvar stack-mode-tried-to-start nil)
+(make-variable-buffer-local 'stack-mode-tried-to-start)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Interactive functions
+
+(defun stack-mode-status ()
+  "Print the status of the current stack process."
+  (interactive)
+  (if (stack-mode-buffer)
+      (if (stack-mode-process)
+          (if (process-live-p (stack-mode-process))
+              (message "The process is live.")
+            (message "There is a Stack process, but it's dead."))
+        (message "There is a stack buffer, but no Stack process."))
+    (message "There is no Stack buffer.")))
+
+(defun stack-mode-start ()
+  "Start an inferior process and buffer."
+  (interactive)
+  (if (stack-mode-live-p)
+      (switch-to-buffer (stack-mode-buffer))
+    (with-current-buffer (stack-mode-buffer)
+      (setq buffer-read-only t)
+      (inferior-stack-mode)
+      (stack-mode-set-initial-command)
+      (setq stack-mode-buffer "")
+      (let* ((project-name (stack-mode-name))
+             (name (stack-mode-process-name project-name))
+             (args (append (list name
+                                 nil
+                                 stack-mode-proc-path
+                                 "ide"
+                                 "start")
+                           (list project-name)))
+             (process (or (get-process name)
+                          (progn (stack-mode-log "Starting: %S" args)
+                                 (apply #'start-process
+                                        args)))))
+        (set-process-sentinel process 'stack-mode-sentinel)
+        (set-process-filter process 'stack-mode-filter)))))
+
+(defun stack-mode-set-initial-command ()
+  "Set the initial command callback. The `stack ide` command will
+reload targets on start-up, so that's the default command we'll
+start with."
+  (setq stack-mode-current-command
+        (list :json nil
+              :data nil
+              :cont 'stack-mode-loading-callback
+              :label nil))
+  (setq stack-mode-queue (fifo-make))
+  (stack-mode-log "Set initial command."))
+
+(defun stack-mode-stop ()
+  "Stop the process."
+  (interactive)
+  (with-current-buffer (stack-mode-buffer)
+    (when (stack-mode-process)
+      (setq stack-mode-current-command nil)
+      (setq stack-mode-buffer "")
+      (kill-process (stack-mode-process))
+      (delete-process (stack-mode-process)))))
+
+(defun stack-mode-reset ()
+  "Reset the process."
+  (interactive)
+  (with-current-buffer (stack-mode-buffer)
+    (when (stack-mode-process)
+      (setq stack-mode-current-command nil)
+      (setq stack-mode-buffer "")
+      (setq stack-mode-queue (fifo-make)))))
+
+(defun stack-mode-restart ()
+  "Restart the process with a fresh command queue."
+  (interactive)
+  (stack-mode-stop)
+  (stack-mode-start))
+
+(defun stack-mode-live-p ()
+  "Is the process alive?"
+  (and (stack-mode-process)
+       (process-live-p (stack-mode-process))))
+
+(defun stack-mode-clear ()
+  "Clear the interaction buffer."
+  (interactive)
+  (with-current-buffer (stack-mode-buffer)
+    (let ((inhibit-read-only t))
+      (erase-buffer))))
+
+(defun stack-mode-load ()
+  "Load the current buffer's file."
+  (interactive)
+  (save-buffer)
+  (with-current-buffer (stack-mode-buffer)
+    (stack-mode-reload)))
+
+(defun stack-mode-goto ()
+  "Go to definition of thing at point."
+  (interactive)
+  (let ((filename (buffer-file-name))
+        (module-name (haskell-guess-module-name))
+        (span (stack-mode-span)))
+    (let* ((span-info
+            (stack-mode-get-span-info
+             module-name
+             (with-current-buffer (stack-mode-buffer)
+               (file-relative-name filename default-directory))
+             span))
+           (infos
+            (stack-contents
+             span-info))
+           (_ (when (and (vectorp infos) (= 0 (length infos)))
+                (error "Couldn't find location for this. Is the module loaded in the backend?
+Run `M-x stack-mode-list-loaded-modules' to see what's loaded.")))
+           (parts (mapcar #'identity (elt infos 0)))
+           (info (stack-contents (elt parts 0)))
+           (span (elt parts 1))
+           (scope (stack-lookup 'tag (stack-lookup 'idScope info)))
+           (def-span (stack-lookup-contents
+                      'idDefSpan
+                      (stack-lookup 'idProp info))))
+      (cond
+       ((listp def-span)
+        (stack-mode-goto-span def-span))
+       (t
+        (let* ((imported-from
+                (stack-lookup
+                 'idImportedFrom
+                 (stack-lookup 'idScope info)))
+               (imported-module (stack-lookup 'moduleName imported-from))
+               (defined-in (stack-lookup
+                            'idDefinedIn
+                            (stack-lookup 'idProp info)))
+               (package (stack-lookup 'modulePackage defined-in))
+               (package-name (stack-lookup 'packageName package))
+               (package-ver (stack-lookup 'packageVersion package))
+               (module (stack-lookup 'moduleName defined-in)))
+          (message "Imported via %s, defined in %s (%s-%s)"
+                   (haskell-fontify-as-mode imported-module 'haskell-mode)
+                   (haskell-fontify-as-mode module 'haskell-mode)
+                   package-name
+                   package-ver)))))))
+
+(defun stack-mode-list-loaded-modules ()
+  "List the loaded modules in the backend."
+  (interactive)
+  (let ((modules
+         (stack-contents
+          (with-current-buffer (stack-mode-buffer)
+            (stack-mode-call
+             `((tag . "RequestGetLoadedModules")
+               (contents
+                . [])))))))
+    (pop-to-buffer (stack-mode-buffer))
+    (stack-mode-log "Loaded modules: %s"
+                    (mapconcat #'identity
+                               (sort (mapcar #'identity modules) #'string<)
+                               "\n"))))
+
+(defun stack-mode-info ()
+  "Display the info of the thing at point."
+  (interactive)
+  (let* ((filename (buffer-file-name))
+         (module-name (haskell-guess-module-name))
+         (points (stack-mode-points))
+         (orig (point))
+         (span (stack-mode-span-from-points (car points)
+                                            (cdr points)))
+         (info (stack-mode-get-span-info
+                module-name
+                (with-current-buffer (stack-mode-buffer)
+                  (file-relative-name filename default-directory))
+                span))
+         (info-contents (stack-contents (elt (elt (stack-contents info) 0) 0)))
+         (scope (stack-lookup 'idScope info-contents))
+         (prop (stack-lookup 'idProp info-contents))
+         (qual (stack-lookup 'idImportQual scope))
+         (from (stack-lookup 'idImportedFrom scope))
+         (span (stack-lookup 'idImportSpan scope))
+
+         (space (stack-lookup 'idSpace prop))
+         (idDefSpan (stack-lookup 'idDefSpan prop))
+         (idDefinedIn (stack-lookup 'idDefinedIn prop))
+         (modulePackage (stack-lookup 'modulePackage idDefinedIn))
+         (moduleName (stack-lookup 'moduleName idDefinedIn))
+         (packageVersion (stack-lookup 'packageVersion modulePackage))
+         (packageKey (stack-lookup 'packageKey modulePackage))
+         (packageName (stack-lookup 'packageKey modulePackage))
+         (idType (stack-lookup 'idType prop))
+         (idName (stack-lookup 'idName prop)))
+    (let ((info-string (concat
+                        "Identifier: " (haskell-fontify-as-mode idName 'haskell-mode) "\n"
+                        "Type: " (haskell-fontify-as-mode idType 'haskell-mode) "\n"
+                        "Module: " (haskell-fontify-as-mode moduleName 'haskell-mode) "\n"
+                        "Package: "  (if (string= "main" packageName)
+                                         "(this one)"
+                                       packageName))))
+      (cond (stack-mode-show-popup
+             (when (boundp popup-tip)
+               (popup-tip info-string)))
+            (t (message info-string))))))
+
+(defun stack-mode-type (&optional insert-value)
+  "Display type info of thing at point."
+  (interactive "P")
+  (let* ((filename (buffer-file-name))
+         (module-name (haskell-guess-module-name))
+         (points (stack-mode-points))
+         (orig (point))
+         (span (stack-mode-span-from-points (car points)
+                                            (cdr points))))
+    (let* ((types (stack-contents
+                   (stack-mode-get-exp-types
+                    module-name
+                    (with-current-buffer (stack-mode-buffer)
+                      (file-relative-name filename default-directory))
+                    span)))
+           (types (mapcar #'identity types))
+           (code (buffer-substring-no-properties
+                  (car points)
+                  (cdr points)))
+           (type (stack-contents (car types)))
+           (ty (stack-lookup 'text type)))
+      (if insert-value
+          (let ((ident-pos (haskell-ident-pos-at-point)))
+            (cond
+             ((region-active-p)
+              (delete-region (region-beginning)
+                             (region-end))
+              (insert "(" code " :: " ty ")")
+              (goto-char (1+ orig)))
+             ((= (line-beginning-position) (car ident-pos))
+              (goto-char (line-beginning-position))
+              (insert code " :: " (haskell-fontify-as-mode ty 'haskell-mode)
+                      "\n"))
+             (t
+              (save-excursion
+                (goto-char (car ident-pos))
+                (let ((col (current-column)))
+                  (save-excursion (insert "\n")
+                                  (indent-to col))
+                  (insert code " :: " (haskell-fontify-as-mode ty 'haskell-mode)))))))
+        (unless (null types)
+          (let ((type-string (format "%s"
+                                     (mapconcat (lambda (type)
+                                                  (haskell-fontify-as-mode
+                                                   (concat
+                                                    code
+                                                    " :: "
+                                                    (elt type 0))
+                                                   'haskell-mode))
+                                                (cl-subseq types 0 1)
+                                                "\n"))))
+            (cond (stack-mode-show-popup (popup-tip type-string))
+                  (t (message type-string)))))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Process filters and sentinel
+
+(defun stack-mode-filter (process response)
+  (with-current-buffer (stack-mode-buffer (stack-mode-name-from-process process))
+    (if stack-mode-current-command
+        (let* ((lines (split-string (concat stack-mode-buffer response) "\n")))
+          (setq stack-mode-buffer (car (last lines)))
+          (setq lines (butlast lines))
+          (let ((data (plist-get stack-mode-current-command :data))
+                (cont (plist-get stack-mode-current-command :cont)))
+            (while lines
+              (let ((line (pop lines)))
+                (stack-mode-log
+                 "%s <- %s"
+                 (if (plist-get stack-mode-current-command :label)
+                     (format "[%s]" (plist-get stack-mode-current-command :label))
+                   "")
+                 (haskell-fontify-as-mode line 'javascript-mode))
+                (when (let* ((error-msg nil)
+                             (json (condition-case e
+                                       (json-read-from-string line)
+                                     (error "Problem reading JSON from server, probably an error message:\n%s" line)))
+                             (ret (condition-case e
+                                      (funcall cont data json)
+                                    (error (setq error-msg e)
+                                           :error))))
+                        (cl-ecase ret
+                          (:done t)
+                          (:continue nil)
+                          (:error
+                           (setq stack-mode-buffer "")
+                           (setq stack-mode-current-command nil)
+                           (setq stack-mode-queue nil)
+                           (error "Command handler error: %S\n\nThe command queue has been cleared."
+                                  error-msg))
+                          (t
+                           (error "A command handler must return either :done or :continue,
+but it returned: %S
+command was: %S" ret stack-mode-current-command))))
+                  (cl-loop for line in lines
+                           do (stack-mode-log
+                               "Extraneous lines after command completed: %s"
+                               (haskell-fontify-as-mode line 'javascript-mode)))
+                  (setq stack-mode-current-command nil)
+                  (setq lines nil)
+                  (stack-mode-queue-trigger))))))
+      (stack-mode-log "Ignoring: %s"
+                      (haskell-fontify-as-mode response 'javascript-mode)))))
+
+(defun stack-mode-sentinel (process event)
+  (with-current-buffer (stack-mode-buffer (stack-mode-name-from-process process))
+    (stack-mode-log "Process event: %s" event)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Command queue
+
+(defvar stack-mode-current-command nil
+  "Current command handler.")
+(make-variable-buffer-local 'stack-mode-current-command)
+
+(defvar stack-mode-buffer ""
+  "A buffer for the process.")
+(make-variable-buffer-local 'stack-mode-buffer)
+
+(defvar stack-mode-queue nil
+  "Command queue.")
+(make-variable-buffer-local 'stack-mode-queue)
+
+(defun stack-mode-queue ()
+  "Get the FIFO queue of this process."
+  (or stack-mode-queue
+      (setq stack-mode-queue (fifo-make))))
+
+(defun stack-mode-back-queue ()
+  "Get the FIFO back queue of this process."
+  (or stack-mode-back-queue
+      (setq stack-mode-back-queue (fifo-make))))
+
+(defun stack-mode-enqueue-front (json data cont &optional label)
+  "Enqueue a JSON command to the command queue, calling (CONT
+DATA line) for each response line until CONT returns nil. This is
+the first priority queue, anything pushed to this queue will be
+run before anything in the back queue."
+  (cond
+   ((stack-mode-live-p)
+    (stack-mode-log "[%s] => %s" label (haskell-fontify-as-mode (json-encode json) 'javascript-mode))
+    (fifo-push (stack-mode-queue)
+               (list :json json :data data :cont cont :label label))
+    (stack-mode-queue-trigger))
+   (t (stack-mode-try-start))))
+
+(defun stack-mode-enqueue (json data cont &optional label)
+  "Same as `stack-mode-front', but puts it on the back
+queue. Items are only moved onto the front queue when the front
+queue is empty. This lets a command which consists of a few back
+and forth steps to continue its processing uninterrupted."
+  (cond
+   ((stack-mode-live-p)
+    (stack-mode-log "[%s] ~> %s" label (haskell-fontify-as-mode (json-encode json) 'javascript-mode))
+    (fifo-push (stack-mode-back-queue)
+               (list :json json :data data :cont cont :label label))
+    (stack-mode-queue-trigger))
+   (t (stack-mode-try-start))))
+
+(defun stack-mode-try-start ()
+  "Try to start, but only try once."
+  (cond
+   ((not stack-mode-tried-to-start)
+    (setq stack-mode-tried-to-start t)
+    (message "Starting a Stack IDE backend process for this project: %s, stack directory: %s"
+             (stack-mode-cabal-name)
+             (stack-mode-dir))
+    (stack-mode-start))
+   (t (message "Attempted to run a Stack IDE command, but the server isn't started. We already tried once this session. Run `M-x stack-mode-restart` to confirm that you want to start it."))))
+
+(defun stack-mode-call (json)
+  "Call a JSON command. Wait for any existing queued commands to
+complete, then sends the request, blocking on the
+response. Returns the response."
+  (let ((data (list nil)))
+    (stack-mode-enqueue
+     json data
+     (lambda (data reply)
+       (setcar data reply)
+       :done))
+    (stack-mode-queue-flush)
+    (car-safe data)))
+
+(defun stack-mode-queue-processed-p ()
+  "Return t if command queue has been completely processed."
+  (and (fifo-null-p stack-mode-queue)
+       (null stack-mode-current-command)))
+
+(defun stack-mode-queue-flush ()
+  "Block till PROCESS's command queue has been completely processed.
+This uses `accept-process-output' internally."
+  (let ((proc (stack-mode-process)))
+    (while (not (stack-mode-queue-processed-p))
+      (stack-mode-queue-trigger)
+      (accept-process-output proc 1))))
+
+(defun stack-mode-queue-trigger ()
+  "Trigger the next command in the queue if there is no current
+command."
+  (if stack-mode-current-command
+      (unless (fifo-null-p (stack-mode-queue))
+        (stack-mode-log "Stack command queue is currently active, waiting ..."))
+    (when (fifo-null-p (stack-mode-queue))
+      (stack-mode-log "Command queue is now empty.")
+      (unless (fifo-null-p (stack-mode-back-queue))
+        (stack-mode-log "Pushing next item from back queue to front queue ...")
+        (fifo-push (stack-mode-queue)
+                   (fifo-pop (stack-mode-back-queue)))))
+    (unless (fifo-null-p (stack-mode-queue))
+      (setq stack-mode-current-command
+            (fifo-pop (stack-mode-queue)))
+      (stack-mode-log
+       "[%S] -> %s"
+       (plist-get stack-mode-current-command :label)
+       (haskell-fontify-as-mode
+        (json-encode (plist-get stack-mode-current-command :json))
+        'javascript-mode))
+      (process-send-string
+       (stack-mode-process)
+       (concat (json-encode (plist-get stack-mode-current-command :json))
+               "\n")))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Project functions
+
+(defun stack-mode-packages ()
+  "Get packages for the Stack configuration."
+  (split-string (shell-command-to-string "stack ide packages") "\n" t))
+
+(defun stack-mode-process ()
+  "Get the current process."
+  (get-process (stack-mode-process-name (stack-mode-name))))
+
+(defun stack-mode-buffer (&optional name)
+  "The inferior buffer."
+  (let ((default-directory (stack-mode-dir)))
+    (get-buffer-create
+     (stack-mode-buffer-name
+      (or name
+          (stack-mode-name))))))
+
+(defun stack-mode-name-from-process (proc)
+  "Get the name of the project from the process."
+  (substring (process-name proc) (length "stack:")))
+
+(defun stack-mode-process-name (name)
+  "Name for the inferior process."
+  (format "stack:%s"
+          name))
+
+(defun stack-mode-buffer-name (name)
+  "Name for the inferior buffer."
+  (format "*stack:%s*"
+          name))
+
+(defun stack-mode-dir ()
+  "The directory for the project."
+  (file-name-directory (haskell-cabal-find-file)))
+
+(defun stack-mode-name ()
+  "The name for the current project based on the current
+directory."
+  (or stack-mode-name
+      (setq stack-mode-name
+            (stack-mode-cabal-name))))
+
+(defun stack-mode-cabal-name ()
+  "Get the name of the session to use, based on the cabal file."
+  (let ((cabal-file (haskell-cabal-find-file)))
+    (if (string-match "\\([^\\/]+\\)\\.cabal$" cabal-file)
+        (let ((name (match-string 1 cabal-file)))
+          (when (not (member name (stack-mode-packages)))
+            (message "This cabal project ā€œ%sā€ isn't in your stack.yaml configuration." name))
+          name)
+      (progn (message "Couldn't figure out cabal file, assuming no project.")
+             nil))))
+
+(defun stack-mode-log (&rest args)
+  "Log a string to the inferior buffer."
+  (with-current-buffer (stack-mode-buffer)
+    (goto-char (point-max))
+    (let ((inhibit-read-only t))
+      (insert (apply #'format args)
+              "\n"))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Commands
+
+(defun stack-mode-reload ()
+  "Compile the code and fetch compile errors."
+  (with-current-buffer (stack-mode-buffer)
+    (stack-mode-enqueue
+     `((tag . "RequestUpdateSession")
+       (contents . []))
+     nil
+     'stack-mode-loading-callback)))
+
+;; (defun stack-mode-load-buffer ()
+;;   "Compile the code and fetch compile errors."
+;;   (interactive)
+;;   (with-current-buffer (stack-mode-buffer)
+;;     (stack-mode-enqueue
+;;      `((tag . "RequestUpdateSession")
+;;        (contents . [((tag . "RequestUpdateTargets")
+;;                      (contents . ((tag . "TargetsInclude")
+;;                                   (contents . ["src/Stack/Package.hs"]))))]))
+;;      nil
+;;      'stack-mode-loading-callback)))
+
+(defun stack-mode-get-span-info (module file span)
+  "Get the span info of the given location."
+  (with-current-buffer (stack-mode-buffer)
+    (stack-mode-call
+     `((tag . "RequestGetSpanInfo")
+       (contents
+        . ((spanFilePath   . ,file)
+           (spanFromLine   . ,(plist-get span :sl))
+           (spanFromColumn . ,(plist-get span :sc))
+           (spanToLine     . ,(plist-get span :el))
+           (spanToColumn   . ,(plist-get span :ec))))))))
+
+(defun stack-mode-get-exp-types (module file span)
+  "Get the type info of the given location."
+  (with-current-buffer (stack-mode-buffer)
+    (stack-mode-call
+     `((tag . "RequestGetExpTypes")
+       (contents
+        . ((spanFilePath   . ,file)
+           (spanFromLine   . ,(plist-get span :sl))
+           (spanFromColumn . ,(plist-get span :sc))
+           (spanToLine     . ,(plist-get span :el))
+           (spanToColumn   . ,(plist-get span :ec))))))))
+
+(defun stack-mode-get-use-sites (module file span)
+  "Get all uses of an identifier."
+  )
+
+(defun stack-mode-get-completions (module string)
+  "Get all uses of an identifier."
+  )
+
+(defun stack-mode-loading-callback (_ reply)
+  "Callback for when loading modules."
+  (let ((tag (stack-tag reply)))
+    (cond
+     ((string= tag "ResponseUpdateSession")
+      (let* ((contents (stack-contents reply))
+             (tag (stack-tag contents)))
+        (cond
+         ((string= tag "UpdateStatusProgress")
+          (stack-mode-progress-callback _ reply)
+          :continue)
+         ((string= tag "UpdateStatusDone")
+          (stack-mode-enqueue-front
+           `((tag . "RequestGetSourceErrors")
+             (contents . []))
+           nil
+           'stack-mode-get-source-errors-callback)
+          :done)
+         (t :continue))))
+     (t
+      :continue))))
+
+(defun stack-mode-progress-callback (_ reply)
+  "Callback for status reports. Utilized in multiple places."
+  (let* ((contents (stack-contents reply))
+         (update (stack-contents contents))
+         (step (stack-lookup 'progressStep update))
+         (total (stack-lookup 'progressNumSteps update))
+         (msg (stack-lookup 'progressParsedMsg update)))
+    (message "[%s/%s] %s"
+             (propertize (number-to-string step) 'face 'compilation-line-number)
+             (propertize (number-to-string total) 'face 'compilation-line-number)
+             msg)))
+
+(defun stack-mode-get-source-errors-callback (_ reply)
+  "Handle the reply from getting source errors."
+  (let ((tag (stack-tag reply)))
+    (cond
+     ((string= tag "ResponseGetSourceErrors")
+      (let ((any-errors nil)
+            (warnings 0))
+        (cl-loop
+         for item in (mapcar #'identity (stack-contents reply))
+         do (let* ((kind (stack-lookup 'errorKind item))
+                   (span (stack-contents (stack-lookup 'errorSpan item)))
+                   (msg (stack-lookup 'errorMsg item))
+                   (fp (stack-lookup 'spanFilePath span))
+                   (sl (stack-lookup 'spanFromLine span))
+                   (sc (stack-lookup 'spanFromColumn span))
+                   (el (stack-lookup 'spanToLine span))
+                   (ec (stack-lookup 'spanToColumn span)))
+              (cond ((string= kind "KindError")
+                     (setq any-errors t))
+                    ((string= kind "KindWarning")
+                     (setq warnings (1+ warnings))))
+              (when
+                  stack-mode-print-error-messages
+                (message "%s"
+                         (propertize
+                          (format "%s:(%d,%d)-(%d,%d): \n%s"
+                                  fp sl sc el ec msg)
+                          'face
+                          (cond
+                           ((string= kind "KindWarning")
+                            'compilation-warning)
+                           ((string= kind "KindError")
+                            'compilation-error)))))))
+        (unless any-errors
+          (if (= 0 warnings)
+              (message "OK.")
+            (message (propertize "OK (%d warning%s)." 'face 'compilation-warning)
+                     warnings
+                     (if (= 1 warnings) "" "s")))))
+      :done)
+     (t :done))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Span functions
+
+(defun stack-mode-points ()
+  "Get the current points; either a selected region or an
+identifier's points."
+  (if (region-active-p)
+      (cons (region-beginning) (region-end))
+    (let ((ident (haskell-ident-pos-at-point)))
+      (cons (car ident)
+            (cdr ident)))))
+
+(defun stack-mode-span-from-points (beg end)
+  "Get the span representation for the span from BEG to END."
+  (save-excursion
+    (list :sl (progn (goto-char beg)
+                     (line-number-at-pos))
+          :sc (1+ (current-column))
+          :el (progn (goto-char end)
+                     (line-number-at-pos))
+          :ec (1+ (current-column)))))
+
+(defun stack-mode-span ()
+  "Get the span from the haskell points."
+  (let ((points (or (haskell-spanable-pos-at-point)
+                    (haskell-ident-pos-at-point)
+                    (stack-mode-loose-ident-at-point))))
+    (if points
+        (stack-mode-span-from-points (car points) (cdr points))
+      (error "No identifier at point."))))
+
+(defun stack-mode-goto-span (span)
+  "Get buffer points from a span."
+  (with-current-buffer (stack-mode-buffer)
+    (find-file (stack-lookup 'spanFilePath span))
+    (goto-char (point-min))
+    (let ((beg (point)))
+      (goto-char (point-min))
+      (forward-line (1- (stack-lookup 'spanFromLine span)))
+      (goto-char (line-beginning-position))
+      (forward-char (1- (stack-lookup 'spanFromColumn span))))))
+
+(defun stack-mode-loose-ident-at-point ()
+  "Get the loose ident at point."
+  nil)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; JSON helpers
+
+(defun stack-mode-list->hashtable (xs)
+  "Convert a list to a hashtable."
+  (let ((h (make-hash-table)))
+    (cl-loop for (key . val)
+             in xs
+             do (puthash key val h))
+    h))
+
+(defun stack-lookup (key object)
+  "Get from a JSON object."
+  (cdr (assoc key (mapcar #'identity object))))
+
+(defun stack-contents (object)
+  "Get from a JSON object."
+  (stack-lookup 'contents object))
+
+(defun stack-tag (object)
+  "Get the tag of an object."
+  (stack-lookup 'tag object))
+
+(defun stack-lookup-contents (key object)
+  "Get from a JSON object."
+  (stack-contents (stack-lookup key object)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Flycheck integration
+
+(defun stack-mode-flycheck-start (checker flycheck-callback &optional buffer attempt-count)
+  "Run a compile on demand, triggered by Flycheck."
+  (when buffer (set-buffer buffer))
+  (let ((max-attempts 2))
+    (if (not (stack-mode-live-p))
+        (if (> (or attempt-count 0) max-attempts)
+            (stack-mode-log "Stack backend isn't ready for Flycheck use. Giving up (waited %d seconds)."
+                            max-attempts)
+          (stack-mode-log "Stack backend isn't ready. Waiting (%d attempts) ..."
+                          (or attempt-count 0))
+          (progn (stack-mode-log "Flycheck tried to use the Stack backend, but the Stack backend isn't started yet. Starting it ...")
+                 (stack-mode-try-start)
+                 (run-with-idle-timer 1 nil 'stack-mode-flycheck-start checker flycheck-callback
+                                      (current-buffer)
+                                      (1+ (or attempt-count 0)))))
+      (progn (stack-mode-log "Running Flycheck with Stack backend ...")
+             (write-region (point-min) (point-max) (buffer-file-name))
+             (clear-visited-file-modtime)
+             (let ((source-buffer (current-buffer))
+                   (label (format "flycheck %s" (buffer-name (current-buffer)))))
+               (with-current-buffer (stack-mode-buffer)
+                 (stack-mode-enqueue
+                  `((tag . "RequestUpdateSession")
+                    (contents . []))
+                  (list :flycheck-callback flycheck-callback
+                        :stack-buffer (current-buffer)
+                        :source-buffer source-buffer
+                        :label label)
+                  'stack-mode-flycheck-callback
+                  label)))))))
+
+(defun stack-mode-flycheck-callback (state reply)
+  "Callback for the flycheck loading. Once done, it will report
+  errors/warnings to CALLBACK."
+  (let ((tag (stack-tag reply)))
+    (cond
+     ((string= tag "ResponseUpdateSession")
+      (let* ((contents (stack-contents reply))
+             (tag (stack-tag contents)))
+        (cond
+         ((string= tag "UpdateStatusProgress")
+          (stack-mode-progress-callback nil reply)
+          :continue)
+         ((string= tag "UpdateStatusDone")
+          (stack-mode-enqueue-front
+           `((tag . "RequestGetSourceErrors")
+             (contents . []))
+           state
+           'stack-mode-flycheck-errors-callback
+           (plist-get state :label))
+          :done)
+         (t :continue))))
+     (t
+      :continue))))
+
+(defun stack-mode-flycheck-errors-callback (state reply)
+  "Collect error messages and pass them to FLYCHECK-CALLBACK."
+  (let ((tag (stack-tag reply)))
+    (cond
+     ((string= tag "ResponseGetSourceErrors")
+      (let ((messages (list)))
+        (cl-loop
+         for item in (mapcar #'identity (stack-contents reply))
+         do (let* ((kind (stack-lookup 'errorKind item))
+                   (span (stack-contents (stack-lookup 'errorSpan item)))
+                   (msg (stack-lookup 'errorMsg item))
+                   (filename (stack-lookup 'spanFilePath span))
+                   (sl (stack-lookup 'spanFromLine span))
+                   (sc (stack-lookup 'spanFromColumn span))
+                   (el (stack-lookup 'spanToLine span))
+                   (ec (stack-lookup 'spanToColumn span)))
+              (let ((orig (current-buffer))
+                    (buffer
+                     (with-current-buffer (plist-get state :stack-buffer)
+                       (let ((value (get-file-buffer filename)))
+                             (if (listp value)
+                                 (car value)
+                               value)))))
+                (if (not (null buffer))
+                 (add-to-list
+                  'messages
+                  (flycheck-error-new-at
+                   sl sc
+                   (cond
+                    ((string= kind "KindWarning") 'warning)
+                    ((string= kind "KindError") 'error)
+                    (t (message "kind: %s" kind)'error))
+                   msg
+                   :checker 'stack-ide
+                   :buffer buffer)
+                  t))
+                (set-buffer orig))))
+        ;; Calling it asynchronously is necessary for flycheck to
+        ;; work properly. See
+        ;; <https://github.com/flycheck/flycheck/pull/524#issuecomment-64947118>
+        ;;
+        ;; Also, the `stack-mode-call-in-buffer' utility is also
+        ;; needed because the reply needs to be called in the same
+        ;; buffer.
+        (run-with-idle-timer 0
+                             nil
+                             'stack-mode-call-in-buffer
+                             (plist-get state :source-buffer)
+                             (plist-get state :flycheck-callback)
+                             'finished
+                             messages)
+        (message "Flycheck done."))
+      :done)
+     (t :done))))
+
+(defun stack-mode-call-in-buffer (buffer func &rest args)
+  "Utility function which calls FUNC in BUFFER with ARGS."
+  (with-current-buffer buffer
+    (apply func args)))
+
+(flycheck-define-generic-checker 'stack-ide
+  "A syntax and type checker for Haskell using Stack's IDE support."
+  :start 'stack-mode-flycheck-start
+  :modes '(haskell-mode))
+
+(provide 'stack-mode)