--- /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)