Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Import formatting utilities.

  • Loading branch information...
commit 5dbe5015aa66a5328b27db43d58f27ec841d2996 1 parent 0b3afb8
@chrisdone chrisdone authored
View
2  Makefile
@@ -13,6 +13,8 @@ ELFILES = \
haskell-checkers.el \
haskell-mode.el \
haskell-simple-indent.el \
+ haskell-sort-imports.el \
+ haskell-align-imports.el \
ghc-core.el \
inf-haskell.el
View
173 haskell-align-imports.el
@@ -0,0 +1,173 @@
+;; haskell-align-imports.el — Align the import lines in a Haskell file.
+;; Copyright (C) 2010 Chris Done <chrisdone@gmail.com>
+
+;; Consider the following imports list:
+;;
+;; import One
+;; import Two as A
+;; import qualified Three
+;; import qualified Four as PRELUDE
+;; import Five (A)
+;; import Six (A,B)
+;; import qualified Seven (A,B)
+;; import "abc" Eight
+;; import "abc" Nine as TWO
+;; import qualified "abc" Ten
+;; import qualified "defg" Eleven as PRELUDE
+;; import "barmu" Twelve (A)
+;; import "zotconpop" Thirteen (A,B)
+;; import qualified "z" Fourteen (A,B)
+;; import Fifteen hiding (A)
+;; import Sixteen as TWO hiding (A)
+;; import qualified Seventeen hiding (A)
+;; import qualified Eighteen as PRELUDE hiding (A)
+;; import "abc" Nineteen hiding (A)
+;; import "abc" Twenty as TWO hiding (A)
+;;
+;; When haskell-align-imports is run within the same buffer, the
+;; import list is transformed to:
+;;
+;; import One
+;; import Two as A
+;; import qualified Three
+;; import qualified Four as PRELUDE
+;; import Five (A)
+;; import Six (A,B)
+;; import qualified Seven (A,B)
+;; import "abc" Eight
+;; import "abc" Nine as TWO
+;; import qualified "abc" Ten
+;; import qualified "defg" Eleven as PRELUDE
+;; import "barmu" Twelve (A)
+;; import "zotconpop" Thirteen (A,B)
+;; import qualified "z" Fourteen (A,B)
+;; import Fifteen hiding (A)
+;; import Sixteen as TWO hiding (A)
+;; import qualified Seventeen hiding (A)
+;; import qualified Eighteen as PRELUDE hiding (A)
+;; import "abc" Nineteen hiding (A)
+;; import "abc" Twenty as TWO hiding (A)
+
+;; This program 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 of
+;; the License, or (at your option) any later version.
+
+;; This program 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/>.
+
+(require 'cl)
+
+(defvar haskell-align-imports-regexp
+ (concat "^\\(import[ ]+\\)"
+ "\\(qualified \\)?"
+ "[ ]*\\(\"[^\"]*\" \\)?"
+ "[ ]*\\([A-Za-z0-9_.']*.*\\)"))
+
+;;;###autoload
+(defun haskell-align-imports ()
+ "Align all the imports in the buffer."
+ (interactive)
+ (when (haskell-align-imports-line-match)
+ (save-excursion
+ (goto-char (point-min))
+ (let* ((imports (haskell-align-imports-collect))
+ (padding (haskell-align-imports-padding imports)))
+ (mapc (lambda (x)
+ (goto-char (cdr x))
+ (delete-region (point) (line-end-position))
+ (insert (haskell-align-imports-chomp
+ (haskell-align-imports-fill padding (car x)))))
+ imports))))
+ nil)
+
+(defun haskell-align-imports-line-match ()
+ "Try to match the current line as a regexp."
+ (let ((line (buffer-substring-no-properties (line-beginning-position)
+ (line-end-position))))
+ (if (string-match "^import " line)
+ line
+ nil)))
+
+(defun haskell-align-imports-collect ()
+ "Collect a list of mark / import statement pairs."
+ (let ((imports '()))
+ (while (not (or (equal (point) (point-max)) (haskell-align-imports-after-imports-p)))
+ (let ((line (haskell-align-imports-line-match-it)))
+ (when line
+ (let ((match
+ (haskell-align-imports-merge-parts
+ (loop for i from 1 to 8
+ collect (haskell-align-imports-chomp (match-string i line))))))
+ (setq imports (cons (cons match (line-beginning-position))
+ imports)))))
+ (forward-line))
+ imports))
+
+(defun haskell-align-imports-merge-parts (l)
+ "Merge together parts of an import statement that shouldn't be separated."
+ (let ((parts (apply #'vector l))
+ (join (lambda (ls)
+ (reduce (lambda (a b)
+ (concat a
+ (if (and (> (length a) 0)
+ (> (length b) 0))
+ " "
+ "")
+ b))
+ ls))))
+ (list (funcall join (list (aref parts 0)
+ (aref parts 1)
+ (aref parts 2)))
+ (aref parts 3)
+ (funcall join (list (aref parts 4)
+ (aref parts 5)
+ (aref parts 6)))
+ (aref parts 7))))
+
+(defun haskell-align-imports-chomp (str)
+ "Chomp leading and tailing whitespace from STR."
+ (if str
+ (replace-regexp-in-string "\\(^[[:space:]\n]*\\|[[:space:]\n]*$\\)" ""
+ str)
+ ""))
+
+(defun haskell-align-imports-padding (imports)
+ "Find the padding for each part of the import statements."
+ (reduce (lambda (a b) (mapcar* #'max a b))
+ (mapcar (lambda (x) (mapcar #'length (car x)))
+ imports)))
+
+(defun haskell-align-imports-fill (padding line)
+ "Fill an import line using the padding worked out from all statements."
+ (mapconcat #'identity
+ (mapcar* (lambda (pad part)
+ (if (> (length part) 0)
+ (concat part (make-string (- pad (length part)) ? ))
+ (make-string pad ? )))
+ padding
+ line)
+ " "))
+
+(defun haskell-align-imports-line-match-it ()
+ "Try to match the current line as a regexp."
+ (let ((line (buffer-substring-no-properties (line-beginning-position)
+ (line-end-position))))
+ (if (string-match haskell-align-imports-regexp line)
+ line
+ nil)))
+
+(defun haskell-align-imports-after-imports-p ()
+ "Are we after the imports list?"
+ (save-excursion
+ (goto-char (line-beginning-position))
+ (not (not (search-forward-regexp "\\( = \\|\\<instance\\>\\| :: \\)"
+ (line-end-position) t 1)))))
+
+(provide 'haskell-align-imports)
View
11 haskell-mode.el
@@ -245,6 +245,8 @@ be set to the preferred literate style."
(define-key map (kbd "C-c C-i") 'inferior-haskell-info)
(define-key map (kbd "C-c M-.") 'inferior-haskell-find-definition)
(define-key map (kbd "C-c C-d") 'inferior-haskell-find-haddock)
+
+ (define-key map (kbd "C-c C-.") 'haskell-mode-format-imports)
@hvr Owner
hvr added a note

While going through the various key-maps to make sure the various haskell-mode components follow Emacs' keymap guidelines and don't conflict with each other, I detected this unfortunate choice as C-c C-. conflicts with haskell-indent-mode's bindings (which already existed before). Otoh, as the haskell-mode-format-imports binding is rather new, the risk is rather low to just move it to somewhere else (possibly with a better mnemonic to it as well)

@chrisdone Owner

Perhaps it can be renamed to haskell-mode-align, which will invoke the right command in the right context. Because C-c C-. is “align stuff”, which is partly what haskell-mode-format-imports does (and is why it's bound as C-c C-.), in practice you rarely align imports without re-ordering them, hence the keybinding as is now. You can mess with it if you want, I'm happy to re-bind it in my local configuration.

@hvr Owner
hvr added a note

Now that you mention it, I wonder if it'd be sensible to integrate the haskell-align-imports function with @jwiegley's align.el somehow...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
(define-key map [?\C-c ?\C-v] 'haskell-check)
@@ -667,6 +669,15 @@ This function will be called with no arguments.")
(when unindent-line-function
(funcall unindent-line-function)))
+(defun haskell-mode-format-imports ()
+ "Format the imports by aligning and sorting them."
+ (interactive)
+ (let ((col (current-column)))
+ (hs-sort-imports)
+ (hs-align-imports)
+ (goto-char (+ (line-beginning-position)
+ col))))
+
(eval-after-load "flymake"
'(add-to-list 'flymake-allowed-file-name-masks
'("\\.l?hs\\'" haskell-flymake-init)))
View
78 haskell-sort-imports.el
@@ -0,0 +1,78 @@
+;; haskell-sort-imports.el — Sort the list of Haskell imports at the point alphabetically.
+;; Copyright (C) 2010 Chris Done <chrisdone@gmail.com>
+
+;; If the region is active it sorts the imports within the
+;; region.
+
+;; This will align and sort the columns of the current import
+;; list. It's more or less the coolest thing on the planet.
+
+;; This program 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 of
+;; the License, or (at your option) any later version.
+
+;; This program 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/>.
+
+(defvar haskell-sort-imports-regexp
+ (concat "^\\(import[ ]+\\)"
+ "\\(qualified \\)?"
+ "[ ]*\\(\"[^\"]*\" \\)?"
+ "[ ]*\\([A-Za-z0-9_.']*.*\\)"))
+
+;;;###autoload
+(defun haskell-sort-imports ()
+ "Sort the import list at the point."
+ (interactive)
+ (when (haskell-sort-imports-line-match)
+ (let ((current-line (buffer-substring-no-properties
+ (line-beginning-position)
+ (line-end-position)))
+ (col (current-column)))
+ (if (use-region-p)
+ (haskell-sort-imports-sort-imports-at (region-beginning)
+ (region-end)
+ t)
+ (haskell-sort-imports-sort-imports-at
+ (save-excursion (haskell-sort-imports-goto-modules-start/end
+ 'previous-line)
+ (point))
+ (save-excursion (haskell-sort-imports-goto-modules-start/end
+ 'next-line)
+ (point))
+ nil)))))
+
+(defun haskell-sort-imports-sort-imports-at (begin end region)
+ (save-excursion
+ (sort-regexp-fields nil
+ haskell-sort-imports-regexp
+ "\\4"
+ begin end))
+ (when (not region)
+ (let ((line (save-excursion (goto-char end)
+ (search-backward current-line))))
+ (goto-char (+ line col)))))
+
+(defun haskell-sort-imports-line-match ()
+ "Try to match the current line as a regexp."
+ (let ((line (buffer-substring-no-properties (line-beginning-position)
+ (line-end-position))))
+ (if (string-match "^import " line)
+ line
+ nil)))
+
+(defun haskell-sort-imports-goto-modules-start/end (direction)
+ "Skip a bunch of consequtive import lines up/down."
+ (while (not (or (equal (point)
+ (point-max))
+ (not (haskell-sort-imports-line-match))))
+ (funcall direction)))
+
+(provide 'haskell-sort-imports)
Please sign in to comment.
Something went wrong with that request. Please try again.