|
|
- ;;; haskell-process.el --- Communicating with the inferior Haskell process -*- lexical-binding: t -*-
-
- ;; Copyright (C) 2011 Chris Done
-
- ;; Author: Chris Done <chrisdone@gmail.com>
-
- ;; This file is not part of GNU Emacs.
-
- ;; This file is free software; you can redistribute it and/or modify
- ;; it under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation; either version 3, or (at your option)
- ;; any later version.
-
- ;; This file is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
-
- ;; You should have received a copy of the GNU General Public License
- ;; along with GNU Emacs; see the file COPYING. If not, write to
- ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
- ;; Boston, MA 02110-1301, USA.
-
- ;;; Code:
-
- (require 'cl-lib)
- (require 'json)
- (require 'url-util)
- (require 'haskell-compat)
- (require 'haskell-session)
- (require 'haskell-customize)
- (require 'haskell-string)
-
- (defconst haskell-process-prompt-regex "\4"
- "Used for delimiting command replies. 4 is End of Transmission.")
-
- (defvar haskell-reload-p nil
- "Used internally for `haskell-process-loadish'.")
-
- (defconst haskell-process-greetings
- (list "Hello, Haskell!"
- "The lambdas must flow."
- "Hours of hacking await!"
- "The next big Haskell project is about to start!"
- "Your wish is my IO ().")
- "Greetings for when the Haskell process starts up.")
-
- (defconst haskell-process-logo
- (expand-file-name "logo.svg" haskell-mode-pkg-base-dir)
- "Haskell logo for notifications.")
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Accessing commands -- using cl 'defstruct'
-
- (cl-defstruct haskell-command
- "Data structure representing a command to be executed when with
- a custom state and three callback."
- ;; hold the custom command state
- ;; state :: a
- state
- ;; called when to execute a command
- ;; go :: a -> ()
- go
- ;; called whenever output was collected from the haskell process
- ;; live :: a -> Response -> Bool
- live
- ;; called when the output from the haskell process indicates that the command
- ;; is complete
- ;; complete :: a -> Response -> ()
- complete)
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Building the process
-
- (defun haskell-process-compute-process-log-and-command (session hptype)
- "Compute the log and process to start command for the SESSION from the HPTYPE.
- Do not actually start any process.
- HPTYPE is the result of calling `'haskell-process-type`' function."
- (let ((session-name (haskell-session-name session)))
- (cl-ecase hptype
- ('ghci
- (append (list (format "Starting inferior GHCi process %s ..."
- haskell-process-path-ghci)
- session-name
- nil)
- (apply haskell-process-wrapper-function
- (list
- (append (haskell-process-path-to-list haskell-process-path-ghci)
- haskell-process-args-ghci)))))
- ('cabal-new-repl
- (append (list (format "Starting inferior `cabal new-repl' process using %s ..."
- haskell-process-path-cabal)
- session-name
- nil)
- (apply haskell-process-wrapper-function
- (list
- (append
- (haskell-process-path-to-list haskell-process-path-cabal)
- (list "new-repl")
- haskell-process-args-cabal-new-repl
- (let ((target (haskell-session-target session)))
- (if target (list target) nil)))))))
- ('cabal-repl
- (append (list (format "Starting inferior `cabal repl' process using %s ..."
- haskell-process-path-cabal)
- session-name
- nil)
- (apply haskell-process-wrapper-function
- (list
- (append
- (haskell-process-path-to-list haskell-process-path-cabal)
- (list "repl")
- haskell-process-args-cabal-repl
- (let ((target (haskell-session-target session)))
- (if target (list target) nil)))))))
- ('stack-ghci
- (append (list (format "Starting inferior stack GHCi process using %s" haskell-process-path-stack)
- session-name
- nil)
- (apply haskell-process-wrapper-function
- (list
- (append
- (haskell-process-path-to-list haskell-process-path-stack)
- (list "ghci")
- (let ((target (haskell-session-target session)))
- (if target (list target) nil))
- haskell-process-args-stack-ghci))))))))
-
- (defun haskell-process-path-to-list (path)
- "Convert a path (which may be a string or a list) to a list."
- (if (stringp path)
- (list path)
- path))
-
- (defun haskell-process-make (name)
- "Make an inferior Haskell process."
- (list (cons 'name name)))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Process communication
-
- (defun haskell-process-sentinel (proc event)
- "The sentinel for the process pipe."
- (let ((session (haskell-process-project-by-proc proc)))
- (when session
- (let* ((process (haskell-session-process session)))
- (unless (haskell-process-restarting process)
- (haskell-process-log
- (propertize (format "Event: %S\n" event)
- 'face '((:weight bold))))
- (haskell-process-log
- (propertize "Process reset.\n"
- 'face 'font-lock-comment-face))
- (run-hook-with-args 'haskell-process-ended-functions process))))))
-
- (defun haskell-process-filter (proc response)
- "The filter for the process pipe."
- (let ((i 0))
- (cl-loop for line in (split-string response "\n")
- do (haskell-process-log
- (concat (if (= i 0)
- (propertize "<- " 'face 'font-lock-comment-face)
- " ")
- (propertize line 'face 'haskell-interactive-face-compile-warning)))
- do (setq i (1+ i))))
- (let ((session (haskell-process-project-by-proc proc)))
- (when session
- (if (haskell-process-cmd (haskell-session-process session))
- (haskell-process-collect session
- response
- (haskell-session-process session))))))
-
- (defun haskell-process-log (msg)
- "Effective append MSG to the process log (if enabled)."
- (when haskell-process-log
- (let* ((append-to (get-buffer-create "*haskell-process-log*")))
- (with-current-buffer append-to
- ;; point should follow insertion so that it stays at the end
- ;; of the buffer
- (setq-local window-point-insertion-type t)
- (let ((buffer-read-only nil))
- (insert msg "\n"))))))
-
- (defun haskell-process-project-by-proc (proc)
- "Find project by process."
- (cl-find-if (lambda (project)
- (string= (haskell-session-name project)
- (process-name proc)))
- haskell-sessions))
-
- (defun haskell-process-collect (_session response process)
- "Collect input for the response until receives a prompt."
- (haskell-process-set-response process
- (concat (haskell-process-response process) response))
- (while (haskell-process-live-updates process))
- (when (string-match haskell-process-prompt-regex
- (haskell-process-response process))
- (haskell-command-exec-complete
- (haskell-process-cmd process)
- (replace-regexp-in-string
- haskell-process-prompt-regex
- ""
- (haskell-process-response process)))
- (haskell-process-reset process)
- (haskell-process-trigger-queue process)))
-
- (defun haskell-process-reset (process)
- "Reset the process's state, ready for the next send/reply."
- (progn (haskell-process-set-response-cursor process 0)
- (haskell-process-set-response process "")
- (haskell-process-set-cmd process nil)))
-
- (defun haskell-process-consume (process regex)
- "Consume a regex from the response and move the cursor along if succeed."
- (when (string-match regex
- (haskell-process-response process)
- (haskell-process-response-cursor process))
- (haskell-process-set-response-cursor process (match-end 0))
- t))
-
- (defun haskell-process-send-string (process string)
- "Try to send a string to the process's process. Ask to restart if it's not running."
- (let ((child (haskell-process-process process)))
- (if (equal 'run (process-status child))
- (let ((out (concat string "\n")))
- (let ((i 0))
- (cl-loop for line in (split-string out "\n")
- do (unless (string-equal "" line)
- (haskell-process-log
- (concat (if (= i 0)
- (propertize "-> " 'face 'font-lock-comment-face)
- " ")
- (propertize line 'face 'font-lock-string-face))))
- do (setq i (1+ i))))
- (process-send-string child out))
- (unless (haskell-process-restarting process)
- (run-hook-with-args 'haskell-process-ended-functions process)))))
-
- (defun haskell-process-live-updates (process)
- "Process live updates."
- (haskell-command-exec-live (haskell-process-cmd process)
- (haskell-process-response process)))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Making commands
-
- (defun haskell-process-queue-without-filters (process line)
- "Queue LINE to be sent to PROCESS without bothering to look at
- the response."
- (haskell-process-queue-command
- process
- (make-haskell-command
- :state (cons process line)
- :go (lambda (state)
- (haskell-process-send-string (car state)
- (cdr state))))))
-
-
- (defun haskell-process-queue-command (process command)
- "Add a command to the process command queue."
- (haskell-process-cmd-queue-add process command)
- (haskell-process-trigger-queue process))
-
- (defun haskell-process-trigger-queue (process)
- "Trigger the next command in the queue to be ran if there is no current command."
- (if (and (haskell-process-process process)
- (process-live-p (haskell-process-process process)))
- (unless (haskell-process-cmd process)
- (let ((cmd (haskell-process-cmd-queue-pop process)))
- (when cmd
- (haskell-process-set-cmd process cmd)
- (haskell-command-exec-go cmd))))
- (progn (haskell-process-reset process)
- (haskell-process-set process 'command-queue nil)
- (run-hook-with-args 'haskell-process-ended-functions process))))
-
- (defun haskell-process-queue-flushed-p (process)
- "Return t if command queue has been completely processed."
- (not (or (haskell-process-cmd-queue process)
- (haskell-process-cmd process))))
-
- (defun haskell-process-queue-flush (process)
- "Block till PROCESS' command queue has been completely processed.
- This uses `accept-process-output' internally."
- (while (not (haskell-process-queue-flushed-p process))
- (haskell-process-trigger-queue process)
- (accept-process-output (haskell-process-process process) 1)))
-
- (defun haskell-process-queue-sync-request (process reqstr)
- "Queue submitting REQSTR to PROCESS and return response blockingly."
- (let ((cmd (make-haskell-command
- :state (cons nil process)
- :go `(lambda (s) (haskell-process-send-string (cdr s) ,reqstr))
- :complete 'setcar)))
- (haskell-process-queue-command process cmd)
- (haskell-process-queue-flush process)
- (car-safe (haskell-command-state cmd))))
-
- (defun haskell-process-get-repl-completions (process inputstr &optional limit)
- "Query PROCESS with `:complete repl ...' for INPUTSTR.
- Give optional LIMIT arg to limit completion candidates count,
- zero, negative values, and nil means all possible completions.
- Returns NIL when no completions found."
- (let* ((mlimit (if (and limit (> limit 0))
- (concat " " (number-to-string limit) " ")
- " "))
- (reqstr (concat ":complete repl"
- mlimit
- (haskell-string-literal-encode inputstr)))
- (rawstr (haskell-process-queue-sync-request process reqstr))
- (response-status (haskell-utils-repl-response-error-status rawstr)))
- (if (eq 'unknown-command response-status)
- (error
- "GHCi lacks `:complete' support (try installing GHC 7.8+ or ghci-ng)")
- (when rawstr
- ;; parse REPL response if any
- (let* ((s1 (split-string rawstr "\r?\n" t))
- (cs (mapcar #'haskell-string-literal-decode (cdr s1)))
- (h0 (car s1))) ;; "<limit count> <all count> <unused string>"
- (unless (string-match
- "\\`\\([0-9]+\\) \\([0-9]+\\) \\(\".*\"\\)\\'"
- h0)
- (error "Invalid `:complete' response"))
- (let ((cnt1 (match-string 1 h0))
- (h1 (haskell-string-literal-decode (match-string 3 h0))))
- (unless (= (string-to-number cnt1) (length cs))
- (error "Lengths inconsistent in `:complete' reponse"))
- (cons h1 cs)))))))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Accessing the process
-
- (defun haskell-process-get (process key)
- "Get the PROCESS's KEY value.
- Returns nil if KEY not set."
- (cdr (assq key process)))
-
- (defun haskell-process-set (process key value)
- "Set the PROCESS's KEY to VALUE.
- Returns newly set VALUE."
- (if process
- (let ((cell (assq key process)))
- (if cell
- (setcdr cell value) ; modify cell in-place
- (setcdr process (cons (cons key value) (cdr process))) ; new cell
- value))
- (display-warning 'haskell-interactive
- "`haskell-process-set' called with nil process")))
-
- ;; Wrappers using haskell-process-{get,set}
-
- (defun haskell-process-set-sent-stdin (p v)
- "We've sent stdin, so let's not clear the output at the end."
- (haskell-process-set p 'sent-stdin v))
-
- (defun haskell-process-sent-stdin-p (p)
- "Did we send any stdin to the process during evaluation?"
- (haskell-process-get p 'sent-stdin))
-
- (defun haskell-process-set-suggested-imports (p v)
- "Remember what imports have been suggested, to avoid
- re-asking about the same imports."
- (haskell-process-set p 'suggested-imported v))
-
- (defun haskell-process-suggested-imports (p)
- "Get what modules have already been suggested and accepted."
- (haskell-process-get p 'suggested-imported))
-
- (defun haskell-process-set-evaluating (p v)
- "Set status of evaluating to be on/off."
- (haskell-process-set p 'evaluating v))
-
- (defun haskell-process-evaluating-p (p)
- "Get status of evaluating (on/off)."
- (haskell-process-get p 'evaluating))
-
- (defun haskell-process-set-process (p v)
- "Set the process's inferior process."
- (haskell-process-set p 'inferior-process v))
-
- (defun haskell-process-process (p)
- "Get the process child."
- (haskell-process-get p 'inferior-process))
-
- (defun haskell-process-name (p)
- "Get the process name."
- (haskell-process-get p 'name))
-
- (defun haskell-process-cmd (p)
- "Get the process's current command.
- Return nil if no current command."
- (haskell-process-get p 'current-command))
-
- (defun haskell-process-set-cmd (p v)
- "Set the process's current command."
- (haskell-process-set-evaluating p nil)
- (haskell-process-set-sent-stdin p nil)
- (haskell-process-set-suggested-imports p nil)
- (haskell-process-set p 'current-command v))
-
- (defun haskell-process-response (p)
- "Get the process's current response."
- (haskell-process-get p 'current-response))
-
- (defun haskell-process-session (p)
- "Get the process's current session."
- (haskell-process-get p 'session))
-
- (defun haskell-process-set-response (p v)
- "Set the process's current response."
- (haskell-process-set p 'current-response v))
-
- (defun haskell-process-set-session (p v)
- "Set the process's current session."
- (haskell-process-set p 'session v))
-
- (defun haskell-process-response-cursor (p)
- "Get the process's current response cursor."
- (haskell-process-get p 'current-response-cursor))
-
- (defun haskell-process-set-response-cursor (p v)
- "Set the process's response cursor."
- (haskell-process-set p 'current-response-cursor v))
-
- ;; low-level command queue operations
-
- (defun haskell-process-restarting (process)
- "Is the PROCESS restarting?"
- (haskell-process-get process 'is-restarting))
-
- (defun haskell-process-cmd-queue (process)
- "Get the PROCESS' command queue.
- New entries get added to the end of the list. Use
- `haskell-process-cmd-queue-add' and
- `haskell-process-cmd-queue-pop' to modify the command queue."
- (haskell-process-get process 'command-queue))
-
- (defun haskell-process-cmd-queue-add (process cmd)
- "Add CMD to end of PROCESS's command queue."
- (cl-check-type cmd haskell-command)
- (haskell-process-set process
- 'command-queue
- (append (haskell-process-cmd-queue process)
- (list cmd))))
-
- (defun haskell-process-cmd-queue-pop (process)
- "Pop the PROCESS' next entry from command queue.
- Returns nil if queue is empty."
- (let ((queue (haskell-process-cmd-queue process)))
- (when queue
- (haskell-process-set process 'command-queue (cdr queue))
- (car queue))))
-
-
- (defun haskell-process-unignore-file (session file)
- "
-
- Note to Windows Emacs hackers:
-
- chmod is how to change the mode of files in POSIX
- systems. This will not work on your operating
- system.
-
- There is a command a bit like chmod called \"Calcs\"
- that you can try using here:
-
- http://technet.microsoft.com/en-us/library/bb490872.aspx
-
- If it works, you can submit a patch to this
- function and remove this comment.
- "
- (shell-command (read-from-minibuffer "Permissions command: "
- (concat "chmod 700 "
- file)))
- (haskell-session-modify
- session
- 'ignored-files
- (lambda (files)
- (cl-remove-if (lambda (path)
- (string= path file))
- files))))
-
- (defun haskell-command-exec-go (command)
- "Call the command's go function."
- (let ((go-func (haskell-command-go command)))
- (when go-func
- (funcall go-func (haskell-command-state command)))))
-
- (defun haskell-command-exec-complete (command response)
- "Call the command's complete function."
- (let ((comp-func (haskell-command-complete command)))
- (when comp-func
- (condition-case-unless-debug e
- (funcall comp-func
- (haskell-command-state command)
- response)
- (quit (message "Quit"))
- (error (message "Haskell process command errored with: %S" e))))))
-
- (defun haskell-command-exec-live (command response)
- "Trigger the command's live updates callback."
- (let ((live-func (haskell-command-live command)))
- (when live-func
- (funcall live-func
- (haskell-command-state command)
- response))))
-
- (provide 'haskell-process)
-
- ;;; haskell-process.el ends here
|