external-reality | 8f34d23 | 2014-02-22 21:35:47 -0500 | [diff] [blame] | 1 | ;;; shm.el --- Structured Haskell Mode |
| 2 | |
| 3 | ;; Copyright (c) 2013 Chris Done. All rights reserved. |
| 4 | ;; Copyright (c) 1998 Heribert Schuetz, Graeme E Moss |
| 5 | |
| 6 | ;; Author: Chris Done <chrisdone@gmail.com> |
| 7 | ;; Created: 19-Oct-2013 |
| 8 | ;; Version: 1.0.2 |
| 9 | ;; Keywords: development, haskell, structured |
| 10 | ;; Stability: unstable |
| 11 | |
| 12 | ;; This file is free software; you can redistribute it and/or modify |
| 13 | ;; it under the terms of the GNU General Public License as published by |
| 14 | ;; the Free Software Foundation; either version 3, or (at your option) |
| 15 | ;; any later version. |
| 16 | |
| 17 | ;; This file is distributed in the hope that it will be useful, |
| 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 20 | ;; GNU General Public License for more details. |
| 21 | |
| 22 | ;; You should have received a copy of the GNU General Public License |
| 23 | ;; along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 24 | |
| 25 | ;;; Commentary: |
| 26 | |
| 27 | ;; A minor mode for adding structured editing to Haskell. |
| 28 | |
| 29 | ;;; Code: |
| 30 | |
| 31 | (require 'shm-customizations) |
| 32 | (require 'shm-ast-documentation) |
| 33 | (require 'shm-evaporate) |
| 34 | (require 'shm-node) |
| 35 | (require 'shm-lambda) |
| 36 | (require 'shm-context-menu) |
externalreality | f0eca72 | 2014-03-12 00:45:28 -0400 | [diff] [blame^] | 37 | (require 'hsq-cabal) |
external-reality | 8f34d23 | 2014-02-22 21:35:47 -0500 | [diff] [blame] | 38 | |
| 39 | (require 'cl) |
external-reality | a56f115 | 2014-03-04 21:57:05 -0500 | [diff] [blame] | 40 | (require 'json) |
external-reality | 8f34d23 | 2014-02-22 21:35:47 -0500 | [diff] [blame] | 41 | |
| 42 | (defvar shm-current-node-overlay nil |
| 43 | "Overlay to highlight the current node.") |
| 44 | |
| 45 | (defvar shm-decl-asts nil |
| 46 | "This is partly an optimization and partly for more |
| 47 | functionality. We could parse the whole module, but that would be |
| 48 | wasteful and expensive to lookup nodes every time we want a |
| 49 | node. So it's cheaper to have the granularity of lookup start at |
| 50 | the declaration's point and the node's span. |
| 51 | |
| 52 | Second it's better because a module may have unparseable content |
| 53 | in it, but that doesn't mean we don't want structured editing to |
| 54 | stop working on declarations that are fine. I've found in my use |
| 55 | of SHM that this is a common use-case worth taking into account.") |
| 56 | |
| 57 | (defvar shm-string-node nil |
| 58 | "The string node that's currently being edited.") |
| 59 | |
| 60 | (defvar shm-string-buffer nil |
| 61 | "The buffer of the string node that's currently being edited.") |
| 62 | |
| 63 | (defvar shm-lighter " SHM?" |
| 64 | "The lighter for structured Haskell mode.") |
| 65 | |
| 66 | (defvar shm-last-point 0 |
| 67 | "When moving around, the current node overlay will update |
| 68 | according to where you are. But often you can shrink/expand the |
| 69 | scope of the current node. This variable lets us avoid the node |
| 70 | being reset by realising we haven't actually moved the point.") |
| 71 | |
| 72 | (defvar shm-parsing-timer nil |
| 73 | "The timer used to re-parse every so often. The idle time can |
| 74 | be configured with `shm-idle-timeout'.") |
| 75 | |
| 76 | (defvar shm-last-parse-start 0 |
| 77 | "This is used to avoid unnecessary work, if the start of the |
| 78 | declaration hasn't changed, and the end (see |
| 79 | `shm-last-parse-end') since we last parsed, don't bother |
| 80 | re-parsing.") |
| 81 | |
| 82 | (defvar shm-last-parse-end 0 |
| 83 | "See `shm-last-parse-start' for explanation.") |
| 84 | |
| 85 | (defvar shm-last-yanked (list 0 0) |
| 86 | "When yanking, some text will be inserted, when popping a |
| 87 | yank (i.e. with M-y), you need to be able to erase the previous |
| 88 | yank. This is simply a region.") |
| 89 | |
| 90 | (defvar shm-map |
| 91 | (let ((map (make-sparse-keymap))) |
| 92 | ;; Navigation |
| 93 | (if (eq window-system 'x) |
| 94 | (define-key map (kbd "M-<return>") 'shm/present-actions-for-node) |
| 95 | (define-key map (kbd "M-]") 'shm/present-actions-for-node)) |
| 96 | (define-key map (kbd "C-M-f") 'shm/forward-node) |
| 97 | (define-key map (kbd "C-M-b") 'shm/backward-node) |
| 98 | (define-key map (kbd "M-a") 'shm/goto-parent) |
| 99 | (define-key map (kbd "M-}") 'shm/forward-paragraph) |
| 100 | (define-key map (kbd "M-{") 'shm/backward-paragraph) |
| 101 | (define-key map (kbd "C-M-SPC") 'shm/mark-node) |
| 102 | (define-key map (kbd "C-c C-w") 'shm/goto-where) |
| 103 | (define-key map (kbd "C-c C-q") 'shm/qualify-import) |
| 104 | (define-key map (kbd "M-p") 'shm/walk) |
| 105 | map) |
| 106 | "Structural editing operations keymap. Any key bindings in this |
| 107 | map are intended to be only structural operations which operate |
| 108 | with the tree in mind.") |
| 109 | |
| 110 | ;;;###autoload |
| 111 | (define-minor-mode structured-haskell-mode |
| 112 | "Structured editing for Haskell." |
| 113 | :lighter shm-lighter |
| 114 | :keymap shm-map |
| 115 | (if structured-haskell-mode |
| 116 | (shm-mode-start) |
| 117 | (shm-mode-stop))) |
| 118 | |
| 119 | (defmacro shm-with-fallback (fallback &rest body) |
| 120 | "Perform the given action unless we're in a comment, in which |
| 121 | case run the fallback function insteaad." |
| 122 | `(if (shm-in-comment) |
| 123 | (call-interactively ',fallback) |
| 124 | (if debug-on-error |
| 125 | (progn ,@body) |
| 126 | (condition-case e |
| 127 | (progn ,@body) |
| 128 | (error |
| 129 | (message "(SHM command failed, falling back to %S. Run M-: (setq debug-on-error t) to see the error.)" |
| 130 | ',fallback) |
| 131 | (call-interactively ',fallback)))))) |
| 132 | |
| 133 | (defun shm-mode-start () |
| 134 | "Start the minor mode." |
| 135 | (set (make-local-variable 'shm-decl-asts) |
| 136 | nil) |
| 137 | (set (make-local-variable 'shm-current-node-overlay) |
| 138 | nil) |
| 139 | (add-hook 'post-self-insert-hook 'shm-post-self-insert nil t) |
| 140 | (unless shm-parsing-timer |
| 141 | (setq shm-parsing-timer |
| 142 | (run-with-idle-timer shm-idle-timeout t 'shm-reparsing-timer)))) |
| 143 | |
| 144 | (defun shm-post-self-insert () |
| 145 | "Self-insertion handler." |
| 146 | (save-excursion |
| 147 | (shm-appropriate-adjustment-point) |
| 148 | (forward-char -1) |
| 149 | (shm-adjust-dependents (point) 1))) |
| 150 | |
| 151 | (defun shm-mode-stop () |
| 152 | "Stop the minor mode. Restore various settings and clean up any |
| 153 | state that will hopefully be garbage collected." |
| 154 | ;; Kill the timer. |
| 155 | (cancel-timer shm-parsing-timer) |
| 156 | (setq shm-parsing-timer nil) |
| 157 | ;; Delete all markers. |
| 158 | (mapc (lambda (pair) |
| 159 | (mapc #'shm-node-delete-markers |
| 160 | (cdr pair)) |
| 161 | (set-marker (car pair) nil)) |
| 162 | shm-decl-asts) |
| 163 | ;; Delete all overlays. |
| 164 | (shm-delete-overlays (point-min) (point-max) 'shm-current-overlay) |
| 165 | (shm-delete-overlays (point-min) (point-max) 'shm-quarantine) |
| 166 | ;; Reset variables. |
| 167 | (setq shm-decl-asts nil) |
| 168 | (setq shm-current-node-overlay nil) |
| 169 | (setq shm-last-parse-start 0) |
| 170 | (setq shm-last-parse-end 0) |
| 171 | (setq shm-last-point 0)) |
| 172 | |
| 173 | (defun shm-reparsing-timer () |
| 174 | "Re-parse the tree on the idle timer." |
| 175 | (when structured-haskell-mode |
| 176 | (shm/reparse))) |
| 177 | |
| 178 | |
| 179 | (defun shm-decl-ast (&optional reparse) |
| 180 | "Return the AST representing the current declaration at point. |
| 181 | |
| 182 | If the AST has already been loaded, that is returned immediately, |
| 183 | otherwise it's regenerated. See the Internal AST section below |
| 184 | for more information." |
| 185 | (let ((p (shm-decl-points))) |
| 186 | (when p |
| 187 | (shm-get-decl-ast (car p) |
| 188 | (cdr p) |
| 189 | reparse)))) |
| 190 | |
| 191 | (defun shm-set-decl-ast (point ast) |
| 192 | "Store the given decl AST at the given POINT. If there is |
| 193 | already an AST for a decl at the given point then remove that one |
| 194 | and instate this one." |
| 195 | (setq shm-decl-asts |
| 196 | (cons |
| 197 | (cons (set-marker (make-marker) point) ast) |
| 198 | (remove-if (lambda (pair) |
| 199 | (when (= (marker-position (car pair)) |
| 200 | point) |
| 201 | (set-marker (car pair) nil) |
| 202 | t)) |
| 203 | shm-decl-asts))) |
| 204 | ast) |
| 205 | |
| 206 | (defun shm-get-decl-ast (start end &optional reparse) |
| 207 | "Get the AST of the declaration starting at POINT." |
| 208 | (let ((pair (car (remove-if-not (lambda (pair) |
| 209 | (= (marker-position (car pair)) |
| 210 | start)) |
| 211 | shm-decl-asts)))) |
| 212 | (if (and (not reparse) |
| 213 | pair) |
| 214 | (cdr pair) |
| 215 | (progn |
| 216 | (when (or (/= start shm-last-parse-start) |
| 217 | (/= end shm-last-parse-end)) |
| 218 | (setq shm-last-parse-start start) |
| 219 | (setq shm-last-parse-end end) |
| 220 | (let ((ast (shm-get-nodes (shm-get-ast "decl" |
| 221 | start |
| 222 | end) |
| 223 | start |
| 224 | end))) |
| 225 | (if ast |
| 226 | (progn (setq shm-lighter " SHM") |
| 227 | (when pair |
| 228 | (shm-delete-markers pair)) |
| 229 | (shm-set-decl-ast start ast) |
| 230 | ;; Delete only quarantine overlays. |
| 231 | (shm-delete-overlays (point-min) (point-max) 'shm-quarantine) |
| 232 | (shm/init) |
| 233 | ast) |
| 234 | (progn |
| 235 | (when shm-display-quarantine |
| 236 | (shm-quarantine-overlay start end)) |
| 237 | (setq shm-lighter " SHM!") |
| 238 | nil)))))))) |
| 239 | |
| 240 | (defun shm-delete-markers (decl) |
| 241 | "Delete the markers in DECL." |
| 242 | (mapc #'shm-node-delete-markers |
| 243 | (cdr decl))) |
| 244 | |
| 245 | (defun shm-get-ast (type start end) |
| 246 | "Get the AST for the given region at START and END. Parses with TYPE. |
| 247 | |
| 248 | This currently launches a fresh process and uses this buffer |
| 249 | nonsense, for any parse, which sucks, but is fast enough _right |
| 250 | now_. Later on a possibility to make this much faster is to have |
| 251 | a persistent running parser server and than just send requests to |
| 252 | it, that should bring down the roundtrip time significantly, I'd |
| 253 | imagine." |
| 254 | (let ((message-log-max nil) |
| 255 | (buffer (current-buffer))) |
| 256 | (when (> end (1+ start)) |
| 257 | (with-temp-buffer |
| 258 | (let ((temp-buffer (current-buffer))) |
| 259 | (with-current-buffer buffer |
| 260 | (condition-case e |
| 261 | (call-process-region start |
| 262 | end |
| 263 | shm-program-name |
| 264 | nil |
| 265 | temp-buffer |
| 266 | nil |
| 267 | "parse" |
| 268 | "Emacs") |
| 269 | ((file-error) |
| 270 | (error "Unable to find structured-haskell-mode executable! See README for help."))))) |
external-reality | a56f115 | 2014-03-04 21:57:05 -0500 | [diff] [blame] | 271 | (json-read-from-string (buffer-string)))))) |
external-reality | 8f34d23 | 2014-02-22 21:35:47 -0500 | [diff] [blame] | 272 | |
| 273 | (defun shm-lint-ast (type start end) |
| 274 | "Get refactor suggestions for the region of TYPE from START to END." |
| 275 | (let ((message-log-max nil) |
| 276 | (buffer (current-buffer))) |
| 277 | (when (> end (1+ start)) |
| 278 | (with-temp-buffer |
| 279 | (let ((temp-buffer (current-buffer))) |
| 280 | (with-current-buffer buffer |
| 281 | (condition-case e |
| 282 | (call-process-region start |
| 283 | end |
| 284 | shm-program-name |
| 285 | nil |
| 286 | temp-buffer |
| 287 | nil |
| 288 | "hlint" |
| 289 | "Emacs") |
| 290 | ((file-error) |
| 291 | (error "Unable to find structured-haskell-mode executable! See README for help."))))) |
| 292 | (read (buffer-string)))))) |
| 293 | |
| 294 | (defun shm-get-nodes (ast start end) |
| 295 | "Get the nodes of the given AST. |
| 296 | |
| 297 | We convert all the line-col numbers to Emacs points and then |
| 298 | create markers out of them. We also store the type of the node, |
| 299 | e.g. Exp, and the case of the node, e.g. Lit or Case or Let, |
| 300 | which is helpful for doing node-specific operations like |
| 301 | indentation. |
| 302 | |
| 303 | Any optimizations welcome." |
| 304 | (let* ((start-end (cons start end)) |
| 305 | (start-column (save-excursion (goto-char start) |
| 306 | (current-column)))) |
| 307 | (cond ((vectorp ast) |
| 308 | (save-excursion |
| 309 | (map 'vector |
| 310 | (lambda (node) |
| 311 | (vector |
| 312 | (elt node 0) |
| 313 | (elt node 1) |
| 314 | (progn (goto-char (car start-end)) |
| 315 | (forward-line (1- (elt node 2))) |
| 316 | ;; This trick is to ensure that the first |
| 317 | ;; line's columns are offsetted for |
| 318 | ;; regions that don't start at column |
| 319 | ;; zero. |
| 320 | (goto-char (+ (if (= (elt node 2) 1) |
| 321 | start-column |
| 322 | 0) |
| 323 | (1- (+ (point) (elt node 3))))) |
| 324 | (let ((marker (set-marker (make-marker) (point)))) |
| 325 | marker)) |
| 326 | (progn (goto-char (car start-end)) |
| 327 | (forward-line (1- (elt node 4))) |
| 328 | ;; Same logic as commented above. |
| 329 | (goto-char (+ (if (= (elt node 4) 1) |
| 330 | start-column |
| 331 | 0) |
| 332 | (1- (+ (point) (elt node 5))))) |
| 333 | ;; This avoids the case of: |
| 334 | (while (save-excursion (goto-char (line-beginning-position)) |
| 335 | (or (looking-at "[ ]+-- ") |
| 336 | (looking-at "[ ]+$"))) |
| 337 | (forward-line -1) |
| 338 | (goto-char (line-end-position))) |
| 339 | (let ((marker (set-marker (make-marker) (point)))) |
| 340 | (set-marker-insertion-type marker t) |
| 341 | marker)))) |
| 342 | ast))) |
| 343 | (t nil)))) |
| 344 | |
| 345 | (defun shm-decl-points (&optional use-line-comments) |
| 346 | "Get the start and end position of the current |
| 347 | declaration. This assumes that declarations start at column zero |
| 348 | and that the rest is always indented by one space afterwards, so |
| 349 | Template Haskell uses with it all being at column zero are not |
| 350 | expected to work." |
| 351 | (cond |
| 352 | ;; If we're in a block comment spanning multiple lines then let's |
| 353 | ;; see if it starts at the beginning of the line (or if any comment |
| 354 | ;; is at the beginning of the line, we don't care to treat it as a |
| 355 | ;; proper declaration. |
| 356 | ((and (not use-line-comments) |
| 357 | (shm-in-comment) |
| 358 | (save-excursion (goto-char (line-beginning-position)) |
| 359 | (shm-in-comment))) |
| 360 | nil) |
| 361 | ((save-excursion |
| 362 | (goto-char (line-beginning-position)) |
| 363 | (or (looking-at "^-}$") |
| 364 | (looking-at "^{-$"))) |
| 365 | nil) |
| 366 | ;; Otherwise we just do our line-based hack. |
| 367 | (t |
| 368 | (save-excursion |
| 369 | (let ((start (or (progn (goto-char (line-end-position)) |
| 370 | (search-backward-regexp "^[^ \n]" nil t 1) |
| 371 | (unless (or (looking-at "^-}$") |
| 372 | (looking-at "^{-$")) |
| 373 | (point))) |
| 374 | 0)) |
| 375 | (end (progn (goto-char (1+ (point))) |
| 376 | (or (when (search-forward-regexp "[\n]+[^ \n]" nil t 1) |
| 377 | (forward-char -1) |
| 378 | (search-backward-regexp "[^\n ]" nil t) |
| 379 | (forward-char) |
| 380 | (point)) |
| 381 | (point-max))))) |
| 382 | (cons start end)))))) |
| 383 | |
| 384 | (defun shm-decl-node (start) |
| 385 | "Get the top-level node of the declaration." |
| 386 | (let* ((vector (save-excursion (goto-char start) |
| 387 | (shm-decl-ast)))) |
| 388 | (elt vector 0))) |
| 389 | |
| 390 | (defun shm/backward-kill-word () |
| 391 | "Kill the word backwards." |
| 392 | (interactive) |
| 393 | (let ((to-be-deleted (save-excursion (backward-word) |
| 394 | (point)))) |
| 395 | (save-excursion |
| 396 | (shm-appropriate-adjustment-point) |
| 397 | (shm-adjust-dependents (point) (* -1 (- (point) to-be-deleted)))) |
| 398 | (backward-kill-word 1))) |
| 399 | |
| 400 | (defun shm/delete () |
| 401 | "Delete the current node." |
| 402 | (interactive) |
| 403 | (let ((current (shm-current-node)) |
| 404 | (inhibit-read-only t)) |
| 405 | (delete-region (shm-node-start current) |
| 406 | (shm-node-end current)))) |
| 407 | |
| 408 | (defun shm/mark-node () |
| 409 | "Set the active mark to the current node." |
| 410 | (interactive) |
| 411 | (let ((current (shm-current-node))) |
| 412 | (goto-char (shm-node-start current)) |
| 413 | (set-mark (shm-node-end current)))) |
| 414 | |
| 415 | (defun shm/type-of-node () |
| 416 | (interactive) |
| 417 | (let ((current (shm-current-node))) |
| 418 | (cond |
| 419 | ((or (string= (shm-node-type-name current) "Exp") |
| 420 | (string= (shm-node-type-name current) "Decl") |
| 421 | (string= (shm-node-type-name current) "Pat") |
| 422 | (string= (shm-node-type-name current) "QOp")) |
| 423 | (let ((type-info (shm-node-type-info current))) |
| 424 | (if type-info |
| 425 | (shm-present-type-info current type-info) |
| 426 | (if (and shm-type-info-fallback-to-ghci |
| 427 | (fboundp 'haskell-process-do-type)) |
| 428 | (haskell-process-do-type) |
| 429 | (error "Unable to get type information for that node."))))) |
| 430 | ((and (string= (shm-node-type-name current) "Name") |
| 431 | (let ((parent-name (shm-node-type-name (cdr (shm-node-parent (shm-current-node-pair)))))) |
| 432 | (or (string= parent-name "Match") |
| 433 | (string= parent-name "Decl")))) |
| 434 | (let* ((node (cdr (shm-node-parent (shm-current-node-pair)))) |
| 435 | (type-info (shm-node-type-info node))) |
| 436 | (if type-info |
| 437 | (shm-present-type-info node type-info) |
| 438 | (if (and shm-type-info-fallback-to-ghci |
| 439 | (fboundp 'haskell-process-do-type)) |
| 440 | (haskell-process-do-type) |
| 441 | (error "Unable to get type information for that node (tried the whole decl, too)."))))) |
| 442 | (t (error "Not an expression, operator, pattern binding or declaration."))))) |
| 443 | |
| 444 | (defun shm/describe-node (&optional node) |
| 445 | "Present a description of the current node in the minibuffer. |
| 446 | |
| 447 | Very useful for debugging and also a bit useful for newbies." |
| 448 | (interactive) |
| 449 | (let ((node (or node (shm-current-node)))) |
| 450 | (if node |
| 451 | (message "%s" (shm-node-description node)) |
| 452 | (error "No current node.")))) |
| 453 | |
| 454 | |
| 455 | (defun shm/goto-where () |
| 456 | "Either make or go to a where clause of the current right-hand-side." |
| 457 | (interactive) |
| 458 | (let ((node-pair (shm-current-node-pair)) |
| 459 | (vector (shm-decl-ast))) |
| 460 | (loop for i |
| 461 | downfrom (car node-pair) |
| 462 | to -1 |
| 463 | until (or (= i -1) |
| 464 | (let ((node (elt vector i))) |
| 465 | (and (string= "Rhs" |
| 466 | (shm-node-type-name node)) |
| 467 | (<= (shm-node-start node) |
| 468 | (shm-node-start (cdr node-pair))) |
| 469 | (>= (shm-node-end node) |
| 470 | (shm-node-end (cdr node-pair)))))) |
| 471 | finally (return |
| 472 | (when (>= i 0) |
| 473 | (let ((rhs (elt vector i))) |
| 474 | (goto-char (shm-node-end rhs)) |
| 475 | (cond |
| 476 | ((looking-at "[\n ]*where") |
| 477 | (search-forward-regexp "where[ \n]*")) |
| 478 | (t |
| 479 | (unless (= (line-beginning-position) (point)) |
| 480 | (newline)) |
| 481 | (indent-to |
| 482 | (+ 2 |
| 483 | (shm-node-start-column |
| 484 | (cdr (shm-node-parent (cons i rhs)))))) |
| 485 | (insert "where "))))))))) |
| 486 | |
| 487 | |
| 488 | |
| 489 | (defun shm-find-furthest-parent-on-line (current) |
| 490 | "Find the parent which starts nearest to column 0 on the |
| 491 | current line. |
| 492 | |
| 493 | This is used when indenting dangling expressions." |
| 494 | (let ((parent (shm-node-parent current))) |
| 495 | (if parent |
| 496 | (if (= (line-beginning-position) |
| 497 | (save-excursion (goto-char (shm-node-start (cdr parent))) |
| 498 | (line-beginning-position))) |
| 499 | (shm-find-furthest-parent-on-line parent) |
| 500 | current) |
| 501 | current))) |
| 502 | |
| 503 | |
| 504 | (defun shm/forward-paragraph () |
| 505 | "Go forward one declaration." |
| 506 | (interactive) |
| 507 | (unless (/= (point) |
| 508 | (goto-char (cdr (shm-decl-points t)))) |
| 509 | (search-forward-regexp "[^\n ]" nil t 1) |
| 510 | (backward-char))) |
| 511 | |
| 512 | (defun shm/backward-paragraph () |
| 513 | "Go backward one declaration." |
| 514 | (interactive) |
| 515 | (unless (/= (point) |
| 516 | (goto-char (car (shm-decl-points t)))) |
| 517 | (search-backward-regexp "[^\n ]" nil t 1) |
| 518 | (forward-char))) |
| 519 | |
| 520 | (defun shm/walk () |
| 521 | (interactive) |
| 522 | (shm/reparse) |
| 523 | (shm/goto-parent-end)) |
| 524 | |
| 525 | (defun shm/close-paren () |
| 526 | "Either insert a close paren or go to the end of the node." |
| 527 | (interactive) |
| 528 | (shm-with-fallback |
| 529 | self-insert-command |
| 530 | (if (shm-literal-insertion) |
| 531 | (shm-insert-string ")") |
| 532 | (progn (shm/reparse) |
| 533 | (shm/goto-parent-end))))) |
| 534 | |
| 535 | (defun shm/close-bracket () |
| 536 | "Either insert a close bracket or go to the end of the node." |
| 537 | (interactive) |
| 538 | (shm-with-fallback |
| 539 | self-insert-command |
| 540 | (if (shm-literal-insertion) |
| 541 | (shm-insert-string "]") |
| 542 | (progn (shm/reparse) |
| 543 | (shm/goto-parent-end))))) |
| 544 | |
| 545 | (defun shm/close-brace () |
| 546 | "Either insert a close brace or go to the end of the node." |
| 547 | (interactive) |
| 548 | (shm-with-fallback |
| 549 | self-insert-command |
| 550 | (if (shm-literal-insertion) |
| 551 | (shm-insert-string "}") |
| 552 | (progn (shm/reparse) |
| 553 | (shm/goto-parent-end))))) |
| 554 | |
| 555 | (defun shm/goto-parent-end () |
| 556 | "Set the current node overlay to the parent node, but go to the |
| 557 | end rather than the start." |
| 558 | (interactive) |
| 559 | (shm/goto-parent nil 'end)) |
| 560 | |
| 561 | (defun shm/forward-node () |
| 562 | "Go forward by node, i.e. go to the next of the current node. If |
| 563 | we're already at the end of the current node, jump to the next |
| 564 | node." |
| 565 | (interactive) |
| 566 | (let* ((current-pair (shm-current-node-pair)) |
| 567 | (current (cdr current-pair))) |
| 568 | (if (= (point) (shm-node-end current)) |
| 569 | (let ((next-pair (shm-node-next current-pair))) |
| 570 | (goto-char (shm-node-start (cdr next-pair)))) |
| 571 | (goto-char (shm-node-end current))))) |
| 572 | |
| 573 | (defun shm/backward-node () |
| 574 | "Go backward by node, i.e. go to the previous of the current node. If |
| 575 | we're already at the start of the current node, jump to the previous |
| 576 | node." |
| 577 | (interactive) |
| 578 | (let* ((current-pair (shm-current-node-pair)) |
| 579 | (current (cdr current-pair))) |
| 580 | (if (= (point) (shm-node-start current)) |
| 581 | (let ((prev-pair (shm-node-previous current-pair))) |
| 582 | (goto-char (shm-node-start (cdr prev-pair)))) |
| 583 | (goto-char (shm-node-start current))))) |
| 584 | |
| 585 | (defun shm/goto-parent (&optional node-pair direction) |
| 586 | "Set the current node overlay to the parent node-pair" |
| 587 | (interactive) |
| 588 | (let ((direction (or direction 'start))) |
| 589 | (if shm-current-node-overlay |
| 590 | (let* ((o shm-current-node-overlay) |
| 591 | (parent-pair (shm-node-parent (or node-pair |
| 592 | (shm-current-workable-node))))) |
| 593 | (when parent-pair |
| 594 | (let ((parent (cdr parent-pair))) |
| 595 | (if (and o |
| 596 | (overlay-buffer o) |
| 597 | (>= (shm-node-start parent) |
| 598 | (overlay-start o)) |
| 599 | (<= (shm-node-end parent) |
| 600 | (overlay-end o))) |
| 601 | (shm/goto-parent parent-pair direction) |
| 602 | (shm-set-node-overlay parent-pair direction))))) |
| 603 | (when node-pair |
| 604 | (shm-set-node-overlay node-pair direction))))) |
| 605 | |
| 606 | (defun shm/reparse () |
| 607 | "Re-parse the current node. |
| 608 | |
| 609 | This is used on the reparsing timer, but also on commands that |
| 610 | really need accurate AST information *right now*, so this will |
| 611 | force a reparse immediately (if necessary)." |
| 612 | (interactive) |
| 613 | (shm-decl-ast t) |
| 614 | (when (/= shm-last-point (point)) |
| 615 | (shm-set-node-overlay))) |
| 616 | |
| 617 | (defun shm-current-node () |
| 618 | "Return just the current node, without its index. |
| 619 | |
| 620 | See `shm-current-node-pair' for what 'current' means." |
| 621 | (cdr (shm-current-node-pair))) |
| 622 | |
| 623 | (defun shm-actual-node () |
| 624 | "Return just the actual current node, without its index. |
| 625 | |
| 626 | Normally node functions only care about the current workable |
| 627 | node. This function will return the *actual* node at point. See |
| 628 | `shm-current-node-pair' for what 'workable' means." |
| 629 | (cdr (shm-node-backwards))) |
| 630 | |
| 631 | (defun shm-current-node-pair () |
| 632 | "Return the current workable node at point. |
| 633 | |
| 634 | Workable means that it is something that we want to be able to |
| 635 | parse. |
| 636 | |
| 637 | For example, if we're looking at a Name, |
| 638 | |
| 639 | foobar |
| 640 | |
| 641 | then that is all well and good, but we don't want to edit a Name, |
| 642 | nor a QName (the parent), we want to edit an Exp (parent-parent) |
| 643 | whose constructor will be a Var." |
| 644 | (let ((current (shm-node-backwards))) |
| 645 | (when current |
| 646 | (if (and shm-current-node-overlay |
| 647 | (overlay-buffer shm-current-node-overlay) |
| 648 | (or (= (shm-node-start (cdr current)) |
| 649 | (overlay-start shm-current-node-overlay)) |
| 650 | (= (shm-node-end (cdr current)) |
| 651 | (overlay-end shm-current-node-overlay)))) |
| 652 | (overlay-get shm-current-node-overlay 'node-pair) |
| 653 | (shm-workable-node current))))) |
| 654 | |
| 655 | (defun shm-current-workable-node () |
| 656 | "Returns the same as `shm-current-node' but including the index." |
| 657 | (let ((current (shm-node-backwards))) |
| 658 | (when current |
| 659 | (shm-workable-node current)))) |
| 660 | |
| 661 | (defun shm-workable-node (current-pair) |
| 662 | "Assume that the given CURRENT node is not workable, and look |
| 663 | at the parent. If the parent has the same start/end position, |
| 664 | then the parent is the correct one to work with." |
| 665 | (let* ((parent-pair (shm-node-parent current-pair)) |
| 666 | (parent (cdr parent-pair)) |
| 667 | (current (cdr current-pair))) |
| 668 | (cond |
| 669 | |
| 670 | (t (if parent |
| 671 | (if (and (= (shm-node-start current) |
| 672 | (shm-node-start parent)) |
| 673 | (= (shm-node-end current) |
| 674 | (shm-node-end parent))) |
| 675 | (if (string= (shm-node-type current) (shm-node-type parent)) |
| 676 | current-pair |
| 677 | (shm-workable-node parent-pair)) |
| 678 | current-pair) |
| 679 | current-pair))))) |
| 680 | |
| 681 | (defun shm-node-previous (node-pair) |
| 682 | "Get the previous node of NODE-PAIR." |
| 683 | (let ((vector (shm-decl-ast))) |
| 684 | (loop for i |
| 685 | downfrom (car node-pair) |
| 686 | to -1 |
| 687 | until (or (= i -1) |
| 688 | (let ((node (elt vector i))) |
| 689 | (<= (shm-node-end node) |
| 690 | (shm-node-start (cdr node-pair))))) |
| 691 | finally (return |
| 692 | (when (>= i 0) |
| 693 | (shm-workable-node (cons i |
| 694 | (elt vector i)))))))) |
| 695 | |
| 696 | (defun shm-node-delete-markers (n) |
| 697 | "Set the markers to NIL, which is about the best we can do for |
| 698 | deletion. The markers will be garbage collected eventually." |
| 699 | (set-marker (elt n 2) nil) |
| 700 | (set-marker (elt n 3) nil)) |
| 701 | |
| 702 | (defun shm-in-comment () |
| 703 | "Are we currently in a comment?" |
| 704 | (or (and (eq 'font-lock-comment-delimiter-face |
| 705 | (get-text-property (point) 'face)) |
| 706 | ;; This is taking liberties, but I'm not too sad about it. |
| 707 | (not (save-excursion (goto-char (line-beginning-position)) |
| 708 | (looking-at "{-")))) |
| 709 | (eq 'font-lock-doc-face |
| 710 | (get-text-property (point) 'face)) |
| 711 | (and (eq 'font-lock-comment-face |
| 712 | (get-text-property (point) 'face)) |
| 713 | (not (save-excursion (goto-char (line-beginning-position)) |
| 714 | (looking-at "{-")))) |
| 715 | (save-excursion (goto-char (line-beginning-position)) |
| 716 | (looking-at "^\-\- ")))) |
| 717 | |
| 718 | (defun shm-in-string () |
| 719 | "Are we in a string?" |
| 720 | (or (eq 'font-lock-string-face |
| 721 | (get-text-property (point) 'face)))) |
| 722 | |
| 723 | (defun shm-find-overlay (type) |
| 724 | "Find overlays at point." |
| 725 | (remove-if-not (lambda (o) (overlay-get o type)) |
| 726 | (overlays-in (point-min) (point-max)))) |
| 727 | |
| 728 | (defun shm-current-overlay (start end node-pair) |
| 729 | "Make the overlay for current node at START to END, setting the |
| 730 | NODE-PAIR in the overlay." |
| 731 | (let ((o (make-overlay start end nil nil t))) |
| 732 | (overlay-put o 'shm-current-overlay t) |
| 733 | (overlay-put o 'face 'shm-current-face) |
| 734 | (overlay-put o 'node-pair node-pair) |
| 735 | (overlay-put o 'priority 1) |
| 736 | o)) |
| 737 | |
| 738 | (defun shm-quarantine-overlay (start end) |
| 739 | "Make a quarantine from START to END." |
| 740 | (let ((o (make-overlay start end nil nil t))) |
| 741 | (overlay-put o 'shm-quarantine t) |
| 742 | (overlay-put o 'face 'shm-quarantine-face) |
| 743 | (overlay-put o 'priority 0) |
| 744 | o)) |
| 745 | |
| 746 | (defun shm-set-node-overlay (&optional node-pair jump-direction) |
| 747 | "Set the current overlay for the current node. Optionally pass |
| 748 | NODE-PAIR to use the specific node-pair (index + node)." |
| 749 | (setq shm-current-node-overlay nil) |
| 750 | (shm-delete-overlays (point-min) |
| 751 | (point-max) |
| 752 | 'shm-current-overlay) |
| 753 | (let* ((node-pair (or node-pair |
| 754 | (shm-current-node-pair))) |
| 755 | (node (cdr node-pair))) |
| 756 | (when jump-direction |
| 757 | (if (eq jump-direction 'end) |
| 758 | (goto-char (shm-node-end node)) |
| 759 | (goto-char (shm-node-start node)))) |
| 760 | (setq shm-last-point (point)) |
| 761 | (setq shm-current-node-overlay |
| 762 | (when node |
| 763 | (shm-current-overlay (shm-node-start node) |
| 764 | (shm-node-end node) |
| 765 | node-pair))))) |
| 766 | |
| 767 | (defun shm-delete-overlays (start end type) |
| 768 | "Delete overlays of the given type. This is used for both |
| 769 | current overlay and quarantines." |
| 770 | (mapc (lambda (o) |
| 771 | (when (overlay-get o type) |
| 772 | (delete-overlay o))) |
| 773 | (overlays-in start end))) |
| 774 | |
| 775 | (defun shm/init (&optional force-renew) |
| 776 | "Initialize the current node overlay at point. |
| 777 | |
| 778 | FORCE-RENEW would be used when the buffer has changed and |
| 779 | therefore the current overlay should be re-initialized." |
| 780 | (interactive) |
| 781 | (when force-renew |
| 782 | (setq shm-current-node-overlay nil)) |
| 783 | (shm-set-node-overlay)) |
| 784 | |
| 785 | (defun shm-type-of-region (beg end) |
| 786 | "Get a type for the region." |
| 787 | (let ((types (shm-types-at-point beg))) |
| 788 | (loop for type |
| 789 | in types |
| 790 | do (when (and (= (elt type 0) beg) |
| 791 | (= (elt type 1) |
| 792 | end)) |
| 793 | (return (elt type 2)))))) |
| 794 | |
| 795 | (defun shm-types-at-point (point) |
| 796 | "Get a list of spans and types for the current point." |
| 797 | (save-excursion |
| 798 | (goto-char point) |
| 799 | (let ((line (line-number-at-pos)) |
| 800 | (col (1+ (current-column))) |
| 801 | (file-name (buffer-file-name))) |
| 802 | (cond |
| 803 | (shm-use-hdevtools |
| 804 | (shm-parse-hdevtools-type-info |
| 805 | (with-temp-buffer |
| 806 | (call-process "hdevtools" nil t nil "type" "-g" "-fdefer-type-errors" |
| 807 | file-name |
| 808 | (number-to-string line) |
| 809 | (number-to-string col)) |
| 810 | (buffer-string)))))))) |
| 811 | |
| 812 | (defun shm-parse-hdevtools-type-info (string) |
| 813 | "Parse type information from the output of hdevtools." |
| 814 | (let ((lines (split-string string "\n+"))) |
| 815 | (loop for line |
| 816 | in lines |
| 817 | while (string-match "\\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\) \"\\(.+\\)\"$" |
| 818 | line) |
| 819 | do (goto-char (point-min)) |
| 820 | collect |
| 821 | (let ((start-line (string-to-number (match-string 1 line))) |
| 822 | (end-line (string-to-number (match-string 3 line)))) |
| 823 | (vector (progn (forward-line (1- start-line)) |
| 824 | (+ (line-beginning-position) |
| 825 | (1- (string-to-number (match-string 2 line))))) |
| 826 | (progn (when (/= start-line end-line) |
| 827 | (forward-line (1- (- start-line end-line)))) |
| 828 | (+ (line-beginning-position) |
| 829 | (1- (string-to-number (match-string 4 line))))) |
| 830 | (match-string 5 line)))))) |
| 831 | |
| 832 | |
| 833 | (defun shm/qualify-import () |
| 834 | "Toggle the qualification of the import at point." |
| 835 | (interactive) |
| 836 | (save-excursion |
| 837 | (let ((points (shm-decl-points))) |
| 838 | (goto-char (car points)) |
| 839 | (shm/reparse) |
| 840 | (let ((current (shm-current-node))) |
| 841 | (when (and current |
| 842 | (string= "ImportDecl" |
| 843 | (shm-node-type-name current))) |
| 844 | (cond |
| 845 | ((looking-at "import[\n ]+qualified[ \n]+") |
| 846 | (search-forward-regexp "qualified" (shm-node-end current) t 1) |
| 847 | (delete-region (point) |
| 848 | (search-backward-regexp "qualified")) |
| 849 | (just-one-space 1)) |
| 850 | (t |
| 851 | (search-forward-regexp "import") |
| 852 | (shm-insert-string " qualified") |
| 853 | (just-one-space 1)))))))) |
| 854 | |
| 855 | (defun shm/modify-type-constraint () |
| 856 | "Modify a type signatures constraint" |
| 857 | (interactive) |
| 858 | (let* ((pair (shm-current-node-pair)) |
| 859 | (current-node (cdr pair))) |
| 860 | (if (shm-type-signature-with-constraint-p pair) |
| 861 | (shm-add-additional-type-constraint current-node) |
| 862 | (add-initial-type-constraint current-node)))) |
| 863 | |
| 864 | (provide 'shm) |
| 865 | |
| 866 | ;;; shm.el ends here |
| 867 | ;; Local Variables: |
| 868 | ;; byte-compile-warnings: (not cl-functions) |
| 869 | ;; End: |