Skip to content

Commit 95d38cd

Browse files
committed
Merge pull request #347 from clojure-emacs/form-alignment
Form alignment
2 parents 0c14631 + 64d3098 commit 95d38cd

File tree

4 files changed

+256
-0
lines changed

4 files changed

+256
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### New features
66

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

README.md

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

116+
### Vertical aligment
117+
118+
You can vertically align sexps with `C-c SPC`. For instance, typing
119+
this combo on the following form:
120+
121+
```clj
122+
(def my-map
123+
{:a-key 1
124+
:other-key 2})
125+
```
126+
127+
Leads to the following:
128+
129+
```clj
130+
(def my-map
131+
{:a-key 1
132+
:other-key 2})
133+
```
134+
135+
This can also be done automatically (as part of indentation) by
136+
turning on `clojure-align-forms-automatically`. This way it will
137+
happen whenever you select some code and hit `TAB`.
138+
116139
## Related packages
117140

118141
* [clojure-mode-extra-font-locking][] provides additional font-locking

clojure-mode.el

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
(require 'cl-lib)
6868
(require 'imenu)
6969
(require 'newcomment)
70+
(require 'align)
7071

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

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

709+
;;; Vertical alignment
710+
(defcustom clojure-align-forms-automatically nil
711+
"If non-nil, vertically align some forms automatically.
712+
Automatically means it is done as part of indenting code. This
713+
applies to binding forms (`clojure-align-binding-forms'), to cond
714+
forms (`clojure-align-cond-forms') and to map literals. For
715+
instance, selecting a map a hitting \\<clojure-mode-map>`\\[indent-for-tab-command]' will align the values
716+
like this:
717+
{:some-key 10
718+
:key2 20}"
719+
:package-version '(clojure-mode . "5.1")
720+
:type 'boolean)
721+
722+
(defcustom clojure-align-binding-forms '("let" "when-let" "if-let" "binding" "loop" "with-open")
723+
"List of strings matching forms that have binding forms."
724+
:package-version '(clojure-mode . "5.1")
725+
:type '(repeat string))
726+
727+
(defcustom clojure-align-cond-forms '("condp" "cond" "cond->" "cond->>" "case")
728+
"List of strings identifying cond-like forms."
729+
:package-version '(clojure-mode . "5.1")
730+
:type '(repeat string))
731+
732+
(defun clojure--position-for-alignment ()
733+
"Non-nil if the sexp around point should be automatically aligned.
734+
This function expects to be called immediately after an
735+
open-brace or after the function symbol in a function call.
736+
737+
First check if the sexp around point is a map literal, or is a
738+
call to one of the vars listed in `clojure-align-cond-forms'. If
739+
it isn't, return nil. If it is, return non-nil and place point
740+
immediately before the forms that should be aligned.
741+
742+
For instance, in a map literal point is left immediately before
743+
the first key; while, in a let-binding, point is left inside the
744+
binding vector and immediately before the first binding
745+
construct."
746+
;; Are we in a map?
747+
(or (and (eq (char-before) ?{)
748+
(not (eq (char-before (1- (point))) ?\#)))
749+
;; Are we in a cond form?
750+
(let* ((fun (car (member (thing-at-point 'symbol) clojure-align-cond-forms)))
751+
(method (and fun (clojure--get-indent-method fun)))
752+
;; The number of special arguments in the cond form is
753+
;; the number of sexps we skip before aligning.
754+
(skip (cond ((numberp method) method)
755+
((sequencep method) (elt method 0)))))
756+
(when (numberp skip)
757+
(clojure-forward-logical-sexp skip)
758+
(comment-forward (point-max))
759+
fun)) ; Return non-nil (the var name).
760+
;; Are we in a let-like form?
761+
(when (member (thing-at-point 'symbol)
762+
clojure-align-binding-forms)
763+
;; Position inside the binding vector.
764+
(clojure-forward-logical-sexp)
765+
(backward-sexp)
766+
(when (eq (char-after) ?\[)
767+
(forward-char 1)
768+
(comment-forward (point-max))
769+
;; Return non-nil.
770+
t))))
771+
772+
(defun clojure--find-sexp-to-align (end)
773+
"Non-nil if there's a sexp ahead to be aligned before END.
774+
Place point as in `clojure--position-for-alignment'."
775+
;; Look for a relevant sexp.
776+
(let ((found))
777+
(while (and (not found)
778+
(search-forward-regexp
779+
(concat "{\\|(" (regexp-opt
780+
(append clojure-align-binding-forms
781+
clojure-align-cond-forms)
782+
'symbols))
783+
end 'noerror))
784+
785+
(let ((ppss (syntax-ppss)))
786+
;; If we're in a string or comment.
787+
(unless (or (elt ppss 3)
788+
(elt ppss 4))
789+
;; Only stop looking if we successfully position
790+
;; the point.
791+
(setq found (clojure--position-for-alignment)))))
792+
found))
793+
794+
(defun clojure--search-whitespace-after-next-sexp (&optional bound _noerror)
795+
"Move point after all whitespace after the next sexp.
796+
Set the match data group 1 to be this region of whitespace and
797+
return point."
798+
(unwind-protect
799+
(ignore-errors
800+
(clojure-forward-logical-sexp 1)
801+
(search-forward-regexp "\\( *\\)" bound)
802+
(pcase (syntax-after (point))
803+
;; End-of-line, try again on next line.
804+
(`(12) (clojure--search-whitespace-after-next-sexp bound))
805+
;; Closing paren, stop here.
806+
(`(5 . ,_) nil)
807+
;; Anything else is something to align.
808+
(_ (point))))
809+
(when (and bound (> (point) bound))
810+
(goto-char bound))))
811+
812+
(defun clojure-align (beg end)
813+
"Vertically align the contents of the sexp around point.
814+
If region is active, align it. Otherwise, align everything in the
815+
current top-level sexp.
816+
When called from lisp code align everything between BEG and END."
817+
(interactive (if (use-region-p)
818+
(list (region-beginning) (region-end))
819+
(save-excursion
820+
(let ((end (progn (end-of-defun)
821+
(point))))
822+
(clojure-backward-logical-sexp)
823+
(list (point) end)))))
824+
(save-excursion
825+
(goto-char beg)
826+
(while (clojure--find-sexp-to-align end)
827+
(align-region (point)
828+
(save-excursion
829+
(backward-up-list)
830+
(forward-sexp 1)
831+
(point))
832+
nil
833+
'((clojure-align (regexp . clojure--search-whitespace-after-next-sexp)
834+
(group . 1)
835+
(repeat . t)))
836+
nil))))
837+
838+
;;; Indentation
839+
(defun clojure-indent-region (beg end)
840+
"Like `indent-region', but also maybe align forms.
841+
Forms between BEG and END are aligned according to
842+
`clojure-align-forms-automatically'."
843+
(prog1 (let ((indent-region-function nil))
844+
(indent-region beg end))
845+
(when clojure-align-forms-automatically
846+
(condition-case er
847+
(clojure-align beg end)
848+
(scan-error nil)))))
849+
706850
(defun clojure-indent-line ()
707851
"Indent current line as Clojure code."
708852
(if (clojure-in-docstring-p)
@@ -1191,6 +1335,7 @@ Sexps that don't represent code are ^metadata or #reader.macros."
11911335
This will skip over sexps that don't represent objects, so that ^hints and
11921336
#reader.macros are considered part of the following sexp."
11931337
(interactive "p")
1338+
(unless n (setq n 1))
11941339
(if (< n 0)
11951340
(clojure-backward-logical-sexp (- n))
11961341
(let ((forward-sexp-function nil))
@@ -1206,6 +1351,7 @@ This will skip over sexps that don't represent objects, so that ^hints and
12061351
This will skip over sexps that don't represent objects, so that ^hints and
12071352
#reader.macros are considered part of the following sexp."
12081353
(interactive "p")
1354+
(unless n (setq n 1))
12091355
(if (< n 0)
12101356
(clojure-forward-logical-sexp (- n))
12111357
(let ((forward-sexp-function nil))

test/clojure-mode-indentation-test.el

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,92 @@ x
385385
2
386386
3))")
387387

388+
;;; Alignment
389+
(defmacro def-full-align-test (name &rest forms)
390+
"Verify that all FORMs correspond to a properly indented sexps."
391+
(declare (indent defun))
392+
`(ert-deftest ,(intern (format "test-align-%s" name)) ()
393+
(let ((clojure-align-forms-automatically t))
394+
,@(mapcar (lambda (form)
395+
`(with-temp-buffer
396+
(clojure-mode)
397+
(insert "\n" ,(replace-regexp-in-string " +" " " form))
398+
(indent-region (point-min) (point-max))
399+
(should (equal (buffer-substring-no-properties (point-min) (point-max))
400+
,(concat "\n" form)))))
401+
forms))
402+
(let ((clojure-align-forms-automatically nil))
403+
,@(mapcar (lambda (form)
404+
`(with-temp-buffer
405+
(clojure-mode)
406+
(insert "\n" ,(replace-regexp-in-string " +" " " form))
407+
(indent-region (point-min) (point-max))
408+
(should (equal (buffer-substring-no-properties
409+
(point-min) (point-max))
410+
,(concat "\n" (replace-regexp-in-string
411+
"\\([a-z]\\) +" "\\1 " form))))))
412+
forms))))
413+
414+
(def-full-align-test basic
415+
"{:this-is-a-form b
416+
c d}"
417+
"{:this-is b
418+
c d}"
419+
"{:this b
420+
c d}"
421+
"{:a b
422+
c d}"
423+
424+
"(let [this-is-a-form b
425+
c d])"
426+
"(let [this-is b
427+
c d])"
428+
"(let [this b
429+
c d])"
430+
"(let [a b
431+
c d])")
432+
433+
(def-full-align-test basic-reversed
434+
"{c d
435+
:this-is-a-form b}"
436+
"{c d
437+
:this-is b}"
438+
"{c d
439+
:this b}"
440+
"{c d
441+
:a b}"
442+
443+
"(let [c d
444+
this-is-a-form b])"
445+
"(let [c d
446+
this-is b])"
447+
"(let [c d
448+
this b])"
449+
"(let [c d
450+
a b])")
451+
452+
(def-full-align-test incomplete-sexp
453+
"(cond aa b
454+
casodkas )"
455+
"(cond aa b
456+
casodkas)"
457+
"(cond aa b
458+
casodkas "
459+
"(cond aa b
460+
casodkas"
461+
"(cond aa b
462+
casodkas a)"
463+
"(cond casodkas a
464+
aa b)"
465+
"(cond casodkas
466+
aa b)")
467+
468+
(def-full-align-test multiple-words
469+
"(cond this is just
470+
a test of
471+
how well
472+
multiple words will work)")
473+
388474

389475
;;; Misc
390476

0 commit comments

Comments
 (0)