blob: 5e9a6de2aef97c365e1e7015929c89c39764ccd0 [file] [log] [blame]
;;; shm.el --- Structured Haskell Mode
;; Copyright (c) 2013 Chris Done. All rights reserved.
;; Copyright (c) 1998 Heribert Schuetz, Graeme E Moss
;; Author: Chris Done <>
;; Created: 19-Oct-2013
;; Version: 1.0.2
;; Keywords: development, haskell, structured
;; Stability: unstable
;; 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
;; 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 <>.
;;; Commentary:
;; A minor mode for adding structured editing to Haskell.
;;; Code:
(require 'shm-customizations)
(require 'shm-ast-documentation)
(require 'shm-evaporate)
(require 'shm-node)
(require 'shm-lambda)
(require 'shm-context-menu)
(require 'cl)
(require 'json)
(defvar shm-current-node-overlay nil
"Overlay to highlight the current node.")
(defvar shm-decl-asts nil
"This is partly an optimization and partly for more
functionality. We could parse the whole module, but that would be
wasteful and expensive to lookup nodes every time we want a
node. So it's cheaper to have the granularity of lookup start at
the declaration's point and the node's span.
Second it's better because a module may have unparseable content
in it, but that doesn't mean we don't want structured editing to
stop working on declarations that are fine. I've found in my use
of SHM that this is a common use-case worth taking into account.")
(defvar shm-string-node nil
"The string node that's currently being edited.")
(defvar shm-string-buffer nil
"The buffer of the string node that's currently being edited.")
(defvar shm-lighter " SHM?"
"The lighter for structured Haskell mode.")
(defvar shm-last-point 0
"When moving around, the current node overlay will update
according to where you are. But often you can shrink/expand the
scope of the current node. This variable lets us avoid the node
being reset by realising we haven't actually moved the point.")
(defvar shm-parsing-timer nil
"The timer used to re-parse every so often. The idle time can
be configured with `shm-idle-timeout'.")
(defvar shm-last-parse-start 0
"This is used to avoid unnecessary work, if the start of the
declaration hasn't changed, and the end (see
`shm-last-parse-end') since we last parsed, don't bother
(defvar shm-last-parse-end 0
"See `shm-last-parse-start' for explanation.")
(defvar shm-last-yanked (list 0 0)
"When yanking, some text will be inserted, when popping a
yank (i.e. with M-y), you need to be able to erase the previous
yank. This is simply a region.")
(defvar shm-map
(let ((map (make-sparse-keymap)))
;; Navigation
(if (eq window-system 'x)
(define-key map (kbd "M-<return>") 'shm/present-actions-for-node)
(define-key map (kbd "M-]") 'shm/present-actions-for-node))
(define-key map (kbd "C-M-f") 'shm/forward-node)
(define-key map (kbd "C-M-b") 'shm/backward-node)
(define-key map (kbd "M-a") 'shm/goto-parent)
(define-key map (kbd "M-}") 'shm/forward-paragraph)
(define-key map (kbd "M-{") 'shm/backward-paragraph)
(define-key map (kbd "C-M-SPC") 'shm/mark-node)
(define-key map (kbd "C-c C-w") 'shm/goto-where)
(define-key map (kbd "C-c C-q") 'shm/qualify-import)
(define-key map (kbd "M-p") 'shm/walk)
"Structural editing operations keymap. Any key bindings in this
map are intended to be only structural operations which operate
with the tree in mind.")
(define-minor-mode structured-haskell-mode
"Structured editing for Haskell."
:lighter shm-lighter
:keymap shm-map
(if structured-haskell-mode
(defmacro shm-with-fallback (fallback &rest body)
"Perform the given action unless we're in a comment, in which
case run the fallback function insteaad."
`(if (shm-in-comment)
(call-interactively ',fallback)
(if debug-on-error
(progn ,@body)
(condition-case e
(progn ,@body)
(message "(SHM command failed, falling back to %S. Run M-: (setq debug-on-error t) to see the error.)"
(call-interactively ',fallback))))))
(defun shm-mode-start ()
"Start the minor mode."
(set (make-local-variable 'shm-decl-asts)
(set (make-local-variable 'shm-current-node-overlay)
(add-hook 'post-self-insert-hook 'shm-post-self-insert nil t)
(unless shm-parsing-timer
(setq shm-parsing-timer
(run-with-idle-timer shm-idle-timeout t 'shm-reparsing-timer))))
(defun shm-post-self-insert ()
"Self-insertion handler."
(forward-char -1)
(shm-adjust-dependents (point) 1)))
(defun shm-mode-stop ()
"Stop the minor mode. Restore various settings and clean up any
state that will hopefully be garbage collected."
;; Kill the timer.
(cancel-timer shm-parsing-timer)
(setq shm-parsing-timer nil)
;; Delete all markers.
(mapc (lambda (pair)
(mapc #'shm-node-delete-markers
(cdr pair))
(set-marker (car pair) nil))
;; Delete all overlays.
(shm-delete-overlays (point-min) (point-max) 'shm-current-overlay)
(shm-delete-overlays (point-min) (point-max) 'shm-quarantine)
;; Reset variables.
(setq shm-decl-asts nil)
(setq shm-current-node-overlay nil)
(setq shm-last-parse-start 0)
(setq shm-last-parse-end 0)
(setq shm-last-point 0))
(defun shm-reparsing-timer ()
"Re-parse the tree on the idle timer."
(when structured-haskell-mode
(defun shm-decl-ast (&optional reparse)
"Return the AST representing the current declaration at point.
If the AST has already been loaded, that is returned immediately,
otherwise it's regenerated. See the Internal AST section below
for more information."
(let ((p (shm-decl-points)))
(when p
(shm-get-decl-ast (car p)
(cdr p)
(defun shm-set-decl-ast (point ast)
"Store the given decl AST at the given POINT. If there is
already an AST for a decl at the given point then remove that one
and instate this one."
(setq shm-decl-asts
(cons (set-marker (make-marker) point) ast)
(remove-if (lambda (pair)
(when (= (marker-position (car pair))
(set-marker (car pair) nil)
(defun shm-get-decl-ast (start end &optional reparse)
"Get the AST of the declaration starting at POINT."
(let ((pair (car (remove-if-not (lambda (pair)
(= (marker-position (car pair))
(if (and (not reparse)
(cdr pair)
(when (or (/= start shm-last-parse-start)
(/= end shm-last-parse-end))
(setq shm-last-parse-start start)
(setq shm-last-parse-end end)
(let ((ast (shm-get-nodes (shm-get-ast "decl"
(if ast
(progn (setq shm-lighter " SHM")
(when pair
(shm-delete-markers pair))
(shm-set-decl-ast start ast)
;; Delete only quarantine overlays.
(shm-delete-overlays (point-min) (point-max) 'shm-quarantine)
(when shm-display-quarantine
(shm-quarantine-overlay start end))
(setq shm-lighter " SHM!")
(defun shm-delete-markers (decl)
"Delete the markers in DECL."
(mapc #'shm-node-delete-markers
(cdr decl)))
(defun shm-get-ast (type start end)
"Get the AST for the given region at START and END. Parses with TYPE.
This currently launches a fresh process and uses this buffer
nonsense, for any parse, which sucks, but is fast enough _right
now_. Later on a possibility to make this much faster is to have
a persistent running parser server and than just send requests to
it, that should bring down the roundtrip time significantly, I'd
(let ((message-log-max nil)
(buffer (current-buffer)))
(when (> end (1+ start))
(let ((temp-buffer (current-buffer)))
(with-current-buffer buffer
(condition-case e
(call-process-region start
(error "Unable to find structured-haskell-mode executable! See README for help.")))))
(json-read-from-string (buffer-string))))))
(defun shm-lint-ast (type start end)
"Get refactor suggestions for the region of TYPE from START to END."
(let ((message-log-max nil)
(buffer (current-buffer)))
(when (> end (1+ start))
(let ((temp-buffer (current-buffer)))
(with-current-buffer buffer
(condition-case e
(call-process-region start
(error "Unable to find structured-haskell-mode executable! See README for help.")))))
(read (buffer-string))))))
(defun shm-get-nodes (ast start end)
"Get the nodes of the given AST.
We convert all the line-col numbers to Emacs points and then
create markers out of them. We also store the type of the node,
e.g. Exp, and the case of the node, e.g. Lit or Case or Let,
which is helpful for doing node-specific operations like
Any optimizations welcome."
(let* ((start-end (cons start end))
(start-column (save-excursion (goto-char start)
(cond ((vectorp ast)
(map 'vector
(lambda (node)
(elt node 0)
(elt node 1)
(progn (goto-char (car start-end))
(forward-line (1- (elt node 2)))
;; This trick is to ensure that the first
;; line's columns are offsetted for
;; regions that don't start at column
;; zero.
(goto-char (+ (if (= (elt node 2) 1)
(1- (+ (point) (elt node 3)))))
(let ((marker (set-marker (make-marker) (point))))
(progn (goto-char (car start-end))
(forward-line (1- (elt node 4)))
;; Same logic as commented above.
(goto-char (+ (if (= (elt node 4) 1)
(1- (+ (point) (elt node 5)))))
;; This avoids the case of:
(while (save-excursion (goto-char (line-beginning-position))
(or (looking-at "[ ]+-- ")
(looking-at "[ ]+$")))
(forward-line -1)
(goto-char (line-end-position)))
(let ((marker (set-marker (make-marker) (point))))
(set-marker-insertion-type marker t)
(t nil))))
(defun shm-decl-points (&optional use-line-comments)
"Get the start and end position of the current
declaration. This assumes that declarations start at column zero
and that the rest is always indented by one space afterwards, so
Template Haskell uses with it all being at column zero are not
expected to work."
;; If we're in a block comment spanning multiple lines then let's
;; see if it starts at the beginning of the line (or if any comment
;; is at the beginning of the line, we don't care to treat it as a
;; proper declaration.
((and (not use-line-comments)
(save-excursion (goto-char (line-beginning-position))
(goto-char (line-beginning-position))
(or (looking-at "^-}$")
(looking-at "^{-$")))
;; Otherwise we just do our line-based hack.
(let ((start (or (progn (goto-char (line-end-position))
(search-backward-regexp "^[^ \n]" nil t 1)
(unless (or (looking-at "^-}$")
(looking-at "^{-$"))
(end (progn (goto-char (1+ (point)))
(or (when (search-forward-regexp "[\n]+[^ \n]" nil t 1)
(forward-char -1)
(search-backward-regexp "[^\n ]" nil t)
(cons start end))))))
(defun shm-decl-node (start)
"Get the top-level node of the declaration."
(let* ((vector (save-excursion (goto-char start)
(elt vector 0)))
(defun shm/backward-kill-word ()
"Kill the word backwards."
(let ((to-be-deleted (save-excursion (backward-word)
(shm-adjust-dependents (point) (* -1 (- (point) to-be-deleted))))
(backward-kill-word 1)))
(defun shm/delete ()
"Delete the current node."
(let ((current (shm-current-node))
(inhibit-read-only t))
(delete-region (shm-node-start current)
(shm-node-end current))))
(defun shm/mark-node ()
"Set the active mark to the current node."
(let ((current (shm-current-node)))
(goto-char (shm-node-start current))
(set-mark (shm-node-end current))))
(defun shm/type-of-node ()
(let ((current (shm-current-node)))
((or (string= (shm-node-type-name current) "Exp")
(string= (shm-node-type-name current) "Decl")
(string= (shm-node-type-name current) "Pat")
(string= (shm-node-type-name current) "QOp"))
(let ((type-info (shm-node-type-info current)))
(if type-info
(shm-present-type-info current type-info)
(if (and shm-type-info-fallback-to-ghci
(fboundp 'haskell-process-do-type))
(error "Unable to get type information for that node.")))))
((and (string= (shm-node-type-name current) "Name")
(let ((parent-name (shm-node-type-name (cdr (shm-node-parent (shm-current-node-pair))))))
(or (string= parent-name "Match")
(string= parent-name "Decl"))))
(let* ((node (cdr (shm-node-parent (shm-current-node-pair))))
(type-info (shm-node-type-info node)))
(if type-info
(shm-present-type-info node type-info)
(if (and shm-type-info-fallback-to-ghci
(fboundp 'haskell-process-do-type))
(error "Unable to get type information for that node (tried the whole decl, too).")))))
(t (error "Not an expression, operator, pattern binding or declaration.")))))
(defun shm/describe-node (&optional node)
"Present a description of the current node in the minibuffer.
Very useful for debugging and also a bit useful for newbies."
(let ((node (or node (shm-current-node))))
(if node
(message "%s" (shm-node-description node))
(error "No current node."))))
(defun shm/goto-where ()
"Either make or go to a where clause of the current right-hand-side."
(let ((node-pair (shm-current-node-pair))
(vector (shm-decl-ast)))
(loop for i
downfrom (car node-pair)
to -1
until (or (= i -1)
(let ((node (elt vector i)))
(and (string= "Rhs"
(shm-node-type-name node))
(<= (shm-node-start node)
(shm-node-start (cdr node-pair)))
(>= (shm-node-end node)
(shm-node-end (cdr node-pair))))))
finally (return
(when (>= i 0)
(let ((rhs (elt vector i)))
(goto-char (shm-node-end rhs))
((looking-at "[\n ]*where")
(search-forward-regexp "where[ \n]*"))
(unless (= (line-beginning-position) (point))
(+ 2
(cdr (shm-node-parent (cons i rhs))))))
(insert "where ")))))))))
(defun shm-find-furthest-parent-on-line (current)
"Find the parent which starts nearest to column 0 on the
current line.
This is used when indenting dangling expressions."
(let ((parent (shm-node-parent current)))
(if parent
(if (= (line-beginning-position)
(save-excursion (goto-char (shm-node-start (cdr parent)))
(shm-find-furthest-parent-on-line parent)
(defun shm/forward-paragraph ()
"Go forward one declaration."
(unless (/= (point)
(goto-char (cdr (shm-decl-points t))))
(search-forward-regexp "[^\n ]" nil t 1)
(defun shm/backward-paragraph ()
"Go backward one declaration."
(unless (/= (point)
(goto-char (car (shm-decl-points t))))
(search-backward-regexp "[^\n ]" nil t 1)
(defun shm/walk ()
(defun shm/close-paren ()
"Either insert a close paren or go to the end of the node."
(if (shm-literal-insertion)
(shm-insert-string ")")
(progn (shm/reparse)
(defun shm/close-bracket ()
"Either insert a close bracket or go to the end of the node."
(if (shm-literal-insertion)
(shm-insert-string "]")
(progn (shm/reparse)
(defun shm/close-brace ()
"Either insert a close brace or go to the end of the node."
(if (shm-literal-insertion)
(shm-insert-string "}")
(progn (shm/reparse)
(defun shm/goto-parent-end ()
"Set the current node overlay to the parent node, but go to the
end rather than the start."
(shm/goto-parent nil 'end))
(defun shm/forward-node ()
"Go forward by node, i.e. go to the next of the current node. If
we're already at the end of the current node, jump to the next
(let* ((current-pair (shm-current-node-pair))
(current (cdr current-pair)))
(if (= (point) (shm-node-end current))
(let ((next-pair (shm-node-next current-pair)))
(goto-char (shm-node-start (cdr next-pair))))
(goto-char (shm-node-end current)))))
(defun shm/backward-node ()
"Go backward by node, i.e. go to the previous of the current node. If
we're already at the start of the current node, jump to the previous
(let* ((current-pair (shm-current-node-pair))
(current (cdr current-pair)))
(if (= (point) (shm-node-start current))
(let ((prev-pair (shm-node-previous current-pair)))
(goto-char (shm-node-start (cdr prev-pair))))
(goto-char (shm-node-start current)))))
(defun shm/goto-parent (&optional node-pair direction)
"Set the current node overlay to the parent node-pair"
(let ((direction (or direction 'start)))
(if shm-current-node-overlay
(let* ((o shm-current-node-overlay)
(parent-pair (shm-node-parent (or node-pair
(when parent-pair
(let ((parent (cdr parent-pair)))
(if (and o
(overlay-buffer o)
(>= (shm-node-start parent)
(overlay-start o))
(<= (shm-node-end parent)
(overlay-end o)))
(shm/goto-parent parent-pair direction)
(shm-set-node-overlay parent-pair direction)))))
(when node-pair
(shm-set-node-overlay node-pair direction)))))
(defun shm/reparse ()
"Re-parse the current node.
This is used on the reparsing timer, but also on commands that
really need accurate AST information *right now*, so this will
force a reparse immediately (if necessary)."
(shm-decl-ast t)
(when (/= shm-last-point (point))
(defun shm-current-node ()
"Return just the current node, without its index.
See `shm-current-node-pair' for what 'current' means."
(cdr (shm-current-node-pair)))
(defun shm-actual-node ()
"Return just the actual current node, without its index.
Normally node functions only care about the current workable
node. This function will return the *actual* node at point. See
`shm-current-node-pair' for what 'workable' means."
(cdr (shm-node-backwards)))
(defun shm-current-node-pair ()
"Return the current workable node at point.
Workable means that it is something that we want to be able to
For example, if we're looking at a Name,
then that is all well and good, but we don't want to edit a Name,
nor a QName (the parent), we want to edit an Exp (parent-parent)
whose constructor will be a Var."
(let ((current (shm-node-backwards)))
(when current
(if (and shm-current-node-overlay
(overlay-buffer shm-current-node-overlay)
(or (= (shm-node-start (cdr current))
(overlay-start shm-current-node-overlay))
(= (shm-node-end (cdr current))
(overlay-end shm-current-node-overlay))))
(overlay-get shm-current-node-overlay 'node-pair)
(shm-workable-node current)))))
(defun shm-current-workable-node ()
"Returns the same as `shm-current-node' but including the index."
(let ((current (shm-node-backwards)))
(when current
(shm-workable-node current))))
(defun shm-workable-node (current-pair)
"Assume that the given CURRENT node is not workable, and look
at the parent. If the parent has the same start/end position,
then the parent is the correct one to work with."
(let* ((parent-pair (shm-node-parent current-pair))
(parent (cdr parent-pair))
(current (cdr current-pair)))
(t (if parent
(if (and (= (shm-node-start current)
(shm-node-start parent))
(= (shm-node-end current)
(shm-node-end parent)))
(if (string= (shm-node-type current) (shm-node-type parent))
(shm-workable-node parent-pair))
(defun shm-node-previous (node-pair)
"Get the previous node of NODE-PAIR."
(let ((vector (shm-decl-ast)))
(loop for i
downfrom (car node-pair)
to -1
until (or (= i -1)
(let ((node (elt vector i)))
(<= (shm-node-end node)
(shm-node-start (cdr node-pair)))))
finally (return
(when (>= i 0)
(shm-workable-node (cons i
(elt vector i))))))))
(defun shm-node-delete-markers (n)
"Set the markers to NIL, which is about the best we can do for
deletion. The markers will be garbage collected eventually."
(set-marker (elt n 2) nil)
(set-marker (elt n 3) nil))
(defun shm-in-comment ()
"Are we currently in a comment?"
(or (and (eq 'font-lock-comment-delimiter-face
(get-text-property (point) 'face))
;; This is taking liberties, but I'm not too sad about it.
(not (save-excursion (goto-char (line-beginning-position))
(looking-at "{-"))))
(eq 'font-lock-doc-face
(get-text-property (point) 'face))
(and (eq 'font-lock-comment-face
(get-text-property (point) 'face))
(not (save-excursion (goto-char (line-beginning-position))
(looking-at "{-"))))
(save-excursion (goto-char (line-beginning-position))
(looking-at "^\-\- "))))
(defun shm-in-string ()
"Are we in a string?"
(or (eq 'font-lock-string-face
(get-text-property (point) 'face))))
(defun shm-find-overlay (type)
"Find overlays at point."
(remove-if-not (lambda (o) (overlay-get o type))
(overlays-in (point-min) (point-max))))
(defun shm-current-overlay (start end node-pair)
"Make the overlay for current node at START to END, setting the
NODE-PAIR in the overlay."
(let ((o (make-overlay start end nil nil t)))
(overlay-put o 'shm-current-overlay t)
(overlay-put o 'face 'shm-current-face)
(overlay-put o 'node-pair node-pair)
(overlay-put o 'priority 1)
(defun shm-quarantine-overlay (start end)
"Make a quarantine from START to END."
(let ((o (make-overlay start end nil nil t)))
(overlay-put o 'shm-quarantine t)
(overlay-put o 'face 'shm-quarantine-face)
(overlay-put o 'priority 0)
(defun shm-set-node-overlay (&optional node-pair jump-direction)
"Set the current overlay for the current node. Optionally pass
NODE-PAIR to use the specific node-pair (index + node)."
(setq shm-current-node-overlay nil)
(shm-delete-overlays (point-min)
(let* ((node-pair (or node-pair
(node (cdr node-pair)))
(when jump-direction
(if (eq jump-direction 'end)
(goto-char (shm-node-end node))
(goto-char (shm-node-start node))))
(setq shm-last-point (point))
(setq shm-current-node-overlay
(when node
(shm-current-overlay (shm-node-start node)
(shm-node-end node)
(defun shm-delete-overlays (start end type)
"Delete overlays of the given type. This is used for both
current overlay and quarantines."
(mapc (lambda (o)
(when (overlay-get o type)
(delete-overlay o)))
(overlays-in start end)))
(defun shm/init (&optional force-renew)
"Initialize the current node overlay at point.
FORCE-RENEW would be used when the buffer has changed and
therefore the current overlay should be re-initialized."
(when force-renew
(setq shm-current-node-overlay nil))
(defun shm-type-of-region (beg end)
"Get a type for the region."
(let ((types (shm-types-at-point beg)))
(loop for type
in types
do (when (and (= (elt type 0) beg)
(= (elt type 1)
(return (elt type 2))))))
(defun shm-types-at-point (point)
"Get a list of spans and types for the current point."
(goto-char point)
(let ((line (line-number-at-pos))
(col (1+ (current-column)))
(file-name (buffer-file-name)))
(call-process "hdevtools" nil t nil "type" "-g" "-fdefer-type-errors"
(number-to-string line)
(number-to-string col))
(defun shm-parse-hdevtools-type-info (string)
"Parse type information from the output of hdevtools."
(let ((lines (split-string string "\n+")))
(loop for line
in lines
while (string-match "\\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\) \"\\(.+\\)\"$"
do (goto-char (point-min))
(let ((start-line (string-to-number (match-string 1 line)))
(end-line (string-to-number (match-string 3 line))))
(vector (progn (forward-line (1- start-line))
(+ (line-beginning-position)
(1- (string-to-number (match-string 2 line)))))
(progn (when (/= start-line end-line)
(forward-line (1- (- start-line end-line))))
(+ (line-beginning-position)
(1- (string-to-number (match-string 4 line)))))
(match-string 5 line))))))
(defun shm/qualify-import ()
"Toggle the qualification of the import at point."
(let ((points (shm-decl-points)))
(goto-char (car points))
(let ((current (shm-current-node)))
(when (and current
(string= "ImportDecl"
(shm-node-type-name current)))
((looking-at "import[\n ]+qualified[ \n]+")
(search-forward-regexp "qualified" (shm-node-end current) t 1)
(delete-region (point)
(search-backward-regexp "qualified"))
(just-one-space 1))
(search-forward-regexp "import")
(shm-insert-string " qualified")
(just-one-space 1))))))))
(defun shm/modify-type-constraint ()
"Modify a type signatures constraint"
(let* ((pair (shm-current-node-pair))
(current-node (cdr pair)))
(if (shm-type-signature-with-constraint-p pair)
(shm-add-additional-type-constraint current-node)
(add-initial-type-constraint current-node))))
(provide 'shm)
;;; shm.el ends here
;; Local Variables:
;; byte-compile-warnings: (not cl-functions)
;; End: