Skip to content

Commit

Permalink
Merge pull request #347 from clojure-emacs/form-alignment
Browse files Browse the repository at this point in the history
Form alignment
  • Loading branch information
bbatsov committed Jan 1, 2016
2 parents 0c14631 + 64d3098 commit 95d38cd
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### New features

* Vertically align sexps with `C-c SPC`. This can also be done automatically (as part of indentation) by turning on `clojure-align-forms-automatically`.
* Indent and font-lock forms that start with `let-`, `while-` or `when-` like their counterparts.
* Apply the `font-lock-comment-face` to code commented out with `#_`.

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,29 @@ For instructions on how to write these specifications, see
[this document](https://github.com/clojure-emacs/cider/blob/master/doc/Indent-Spec.md#indent-specification).
The only difference is that you're allowed to use lists instead of vectors.

### Vertical aligment

You can vertically align sexps with `C-c SPC`. For instance, typing
this combo on the following form:

```clj
(def my-map
{:a-key 1
:other-key 2})
```

Leads to the following:

```clj
(def my-map
{:a-key 1
:other-key 2})
```

This can also be done automatically (as part of indentation) by
turning on `clojure-align-forms-automatically`. This way it will
happen whenever you select some code and hit `TAB`.

## Related packages

* [clojure-mode-extra-font-locking][] provides additional font-locking
Expand Down
146 changes: 146 additions & 0 deletions clojure-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
(require 'cl-lib)
(require 'imenu)
(require 'newcomment)
(require 'align)

(declare-function lisp-fill-paragraph "lisp-mode" (&optional justify))

Expand Down Expand Up @@ -147,6 +148,7 @@ Out-of-the box clojure-mode understands lein, boot and gradle."
(defvar clojure-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-:") #'clojure-toggle-keyword-string)
(define-key map (kbd "C-c SPC") #'clojure-align)
(easy-menu-define clojure-mode-menu map "Clojure Mode Menu"
'("Clojure"
["Toggle between string & keyword" clojure-toggle-keyword-string]
Expand Down Expand Up @@ -266,6 +268,7 @@ instead of to `clojure-mode-map'."
(setq-local comment-start-skip
"\\(\\(^\\|[^\\\\\n]\\)\\(\\\\\\\\\\)*\\)\\(;+\\|#|\\) *")
(setq-local indent-line-function #'clojure-indent-line)
(setq-local indent-region-function #'clojure-indent-region)
(setq-local lisp-indent-function #'clojure-indent-function)
(setq-local lisp-doc-string-elt-property 'clojure-doc-string-elt)
(setq-local parse-sexp-ignore-comments t)
Expand Down Expand Up @@ -703,6 +706,147 @@ point) to check."
(put 'definline 'clojure-doc-string-elt 2)
(put 'defprotocol 'clojure-doc-string-elt 2)

;;; Vertical alignment
(defcustom clojure-align-forms-automatically nil
"If non-nil, vertically align some forms automatically.
Automatically means it is done as part of indenting code. This
applies to binding forms (`clojure-align-binding-forms'), to cond
forms (`clojure-align-cond-forms') and to map literals. For
instance, selecting a map a hitting \\<clojure-mode-map>`\\[indent-for-tab-command]' will align the values
like this:
{:some-key 10
:key2 20}"
:package-version '(clojure-mode . "5.1")
:type 'boolean)

(defcustom clojure-align-binding-forms '("let" "when-let" "if-let" "binding" "loop" "with-open")
"List of strings matching forms that have binding forms."
:package-version '(clojure-mode . "5.1")
:type '(repeat string))

(defcustom clojure-align-cond-forms '("condp" "cond" "cond->" "cond->>" "case")
"List of strings identifying cond-like forms."
:package-version '(clojure-mode . "5.1")
:type '(repeat string))

(defun clojure--position-for-alignment ()
"Non-nil if the sexp around point should be automatically aligned.
This function expects to be called immediately after an
open-brace or after the function symbol in a function call.
First check if the sexp around point is a map literal, or is a
call to one of the vars listed in `clojure-align-cond-forms'. If
it isn't, return nil. If it is, return non-nil and place point
immediately before the forms that should be aligned.
For instance, in a map literal point is left immediately before
the first key; while, in a let-binding, point is left inside the
binding vector and immediately before the first binding
construct."
;; Are we in a map?
(or (and (eq (char-before) ?{)
(not (eq (char-before (1- (point))) ?\#)))
;; Are we in a cond form?
(let* ((fun (car (member (thing-at-point 'symbol) clojure-align-cond-forms)))
(method (and fun (clojure--get-indent-method fun)))
;; The number of special arguments in the cond form is
;; the number of sexps we skip before aligning.
(skip (cond ((numberp method) method)
((sequencep method) (elt method 0)))))
(when (numberp skip)
(clojure-forward-logical-sexp skip)
(comment-forward (point-max))
fun)) ; Return non-nil (the var name).
;; Are we in a let-like form?
(when (member (thing-at-point 'symbol)
clojure-align-binding-forms)
;; Position inside the binding vector.
(clojure-forward-logical-sexp)
(backward-sexp)
(when (eq (char-after) ?\[)
(forward-char 1)
(comment-forward (point-max))
;; Return non-nil.
t))))

(defun clojure--find-sexp-to-align (end)
"Non-nil if there's a sexp ahead to be aligned before END.
Place point as in `clojure--position-for-alignment'."
;; Look for a relevant sexp.
(let ((found))
(while (and (not found)
(search-forward-regexp
(concat "{\\|(" (regexp-opt
(append clojure-align-binding-forms
clojure-align-cond-forms)
'symbols))
end 'noerror))

(let ((ppss (syntax-ppss)))
;; If we're in a string or comment.
(unless (or (elt ppss 3)
(elt ppss 4))
;; Only stop looking if we successfully position
;; the point.
(setq found (clojure--position-for-alignment)))))
found))

(defun clojure--search-whitespace-after-next-sexp (&optional bound _noerror)
"Move point after all whitespace after the next sexp.
Set the match data group 1 to be this region of whitespace and
return point."
(unwind-protect
(ignore-errors
(clojure-forward-logical-sexp 1)
(search-forward-regexp "\\( *\\)" bound)
(pcase (syntax-after (point))
;; End-of-line, try again on next line.
(`(12) (clojure--search-whitespace-after-next-sexp bound))
;; Closing paren, stop here.
(`(5 . ,_) nil)
;; Anything else is something to align.
(_ (point))))
(when (and bound (> (point) bound))
(goto-char bound))))

(defun clojure-align (beg end)
"Vertically align the contents of the sexp around point.
If region is active, align it. Otherwise, align everything in the
current top-level sexp.
When called from lisp code align everything between BEG and END."
(interactive (if (use-region-p)
(list (region-beginning) (region-end))
(save-excursion
(let ((end (progn (end-of-defun)
(point))))
(clojure-backward-logical-sexp)
(list (point) end)))))
(save-excursion
(goto-char beg)
(while (clojure--find-sexp-to-align end)
(align-region (point)
(save-excursion
(backward-up-list)
(forward-sexp 1)
(point))
nil
'((clojure-align (regexp . clojure--search-whitespace-after-next-sexp)
(group . 1)
(repeat . t)))
nil))))

;;; Indentation
(defun clojure-indent-region (beg end)
"Like `indent-region', but also maybe align forms.
Forms between BEG and END are aligned according to
`clojure-align-forms-automatically'."
(prog1 (let ((indent-region-function nil))
(indent-region beg end))
(when clojure-align-forms-automatically
(condition-case er
(clojure-align beg end)
(scan-error nil)))))

(defun clojure-indent-line ()
"Indent current line as Clojure code."
(if (clojure-in-docstring-p)
Expand Down Expand Up @@ -1191,6 +1335,7 @@ Sexps that don't represent code are ^metadata or #reader.macros."
This will skip over sexps that don't represent objects, so that ^hints and
#reader.macros are considered part of the following sexp."
(interactive "p")
(unless n (setq n 1))
(if (< n 0)
(clojure-backward-logical-sexp (- n))
(let ((forward-sexp-function nil))
Expand All @@ -1206,6 +1351,7 @@ This will skip over sexps that don't represent objects, so that ^hints and
This will skip over sexps that don't represent objects, so that ^hints and
#reader.macros are considered part of the following sexp."
(interactive "p")
(unless n (setq n 1))
(if (< n 0)
(clojure-forward-logical-sexp (- n))
(let ((forward-sexp-function nil))
Expand Down
86 changes: 86 additions & 0 deletions test/clojure-mode-indentation-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,92 @@ x
2
3))")

;;; Alignment
(defmacro def-full-align-test (name &rest forms)
"Verify that all FORMs correspond to a properly indented sexps."
(declare (indent defun))
`(ert-deftest ,(intern (format "test-align-%s" name)) ()
(let ((clojure-align-forms-automatically t))
,@(mapcar (lambda (form)
`(with-temp-buffer
(clojure-mode)
(insert "\n" ,(replace-regexp-in-string " +" " " form))
(indent-region (point-min) (point-max))
(should (equal (buffer-substring-no-properties (point-min) (point-max))
,(concat "\n" form)))))
forms))
(let ((clojure-align-forms-automatically nil))
,@(mapcar (lambda (form)
`(with-temp-buffer
(clojure-mode)
(insert "\n" ,(replace-regexp-in-string " +" " " form))
(indent-region (point-min) (point-max))
(should (equal (buffer-substring-no-properties
(point-min) (point-max))
,(concat "\n" (replace-regexp-in-string
"\\([a-z]\\) +" "\\1 " form))))))
forms))))

(def-full-align-test basic
"{:this-is-a-form b
c d}"
"{:this-is b
c d}"
"{:this b
c d}"
"{:a b
c d}"

"(let [this-is-a-form b
c d])"
"(let [this-is b
c d])"
"(let [this b
c d])"
"(let [a b
c d])")

(def-full-align-test basic-reversed
"{c d
:this-is-a-form b}"
"{c d
:this-is b}"
"{c d
:this b}"
"{c d
:a b}"

"(let [c d
this-is-a-form b])"
"(let [c d
this-is b])"
"(let [c d
this b])"
"(let [c d
a b])")

(def-full-align-test incomplete-sexp
"(cond aa b
casodkas )"
"(cond aa b
casodkas)"
"(cond aa b
casodkas "
"(cond aa b
casodkas"
"(cond aa b
casodkas a)"
"(cond casodkas a
aa b)"
"(cond casodkas
aa b)")

(def-full-align-test multiple-words
"(cond this is just
a test of
how well
multiple words will work)")


;;; Misc

Expand Down

0 comments on commit 95d38cd

Please sign in to comment.