Skip to content

Commit e4bba07

Browse files
committed
[#16] Add support for automatic aligning forms
1 parent 605adba commit e4bba07

File tree

5 files changed

+313
-44
lines changed

5 files changed

+313
-44
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- [#16](https://github.com/clojure-emacs/clojure-ts-mode/issues/16): Introduce `clojure-ts-align`.
66
- [#11](https://github.com/clojure-emacs/clojure-ts-mode/issues/11): Enable regex syntax highlighting.
7+
- [#16](https://github.com/clojure-emacs/clojure-ts-mode/issues/16): Add support for automatic aligning forms.
78

89
## 0.3.0 (2025-04-15)
910

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ Leads to the following:
258258
:other-key 2})
259259
```
260260

261+
This can also be done automatically (as part of indentation) by turning on
262+
`clojure-ts-align-forms-automatically`. This way it will happen whenever you
263+
select some code and hit `TAB`.
264+
261265
Forms that can be aligned vertically are configured via the following variables:
262266

263267
- `clojure-ts-align-reader-conditionals` - align reader conditionals as if they

clojure-ts-mode.el

+95-42
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,22 @@ double quotes on the third column."
197197
:safe #'listp
198198
:type '(repeat string))
199199

200+
(defcustom clojure-ts-align-forms-automatically nil
201+
"If non-nil, vertically align some forms automatically.
202+
203+
Automatically means it is done as part of indenting code. This applies
204+
to binding forms (`clojure-ts-align-binding-forms'), to cond
205+
forms (`clojure-ts-align-cond-forms') and to map literals. For
206+
instance, selecting a map a hitting
207+
\\<clojure-ts-mode-map>`\\[indent-for-tab-command]' will align the
208+
values like this:
209+
210+
{:some-key 10
211+
:key2 20}"
212+
:package-version '(clojure-ts-mode . "0.4")
213+
:safe #'booleanp
214+
:type 'boolean)
215+
200216
(defvar clojure-ts-mode-remappings
201217
'((clojure-mode . clojure-ts-mode)
202218
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -1340,6 +1356,9 @@ if NODE has metadata and its parent has type NODE-TYPE."
13401356
((parent-is "vec_lit") parent 1) ;; https://guide.clojure.style/#bindings-alignment
13411357
((parent-is "map_lit") parent 1) ;; https://guide.clojure.style/#map-keys-alignment
13421358
((parent-is "set_lit") parent 2)
1359+
((parent-is "splicing_read_cond_lit") parent 4)
1360+
((parent-is "read_cond_lit") parent 3)
1361+
((parent-is "tagged_or_ctor_lit") parent 0)
13431362
;; https://guide.clojure.style/#body-indentation
13441363
(clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2)
13451364
;; https://guide.clojure.style/#threading-macros-alignment
@@ -1447,40 +1466,67 @@ Regular expression and syntax analysis code is borrowed from
14471466
14481467
BOUND bounds the whitespace search."
14491468
(unwind-protect
1450-
(when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node (point) t)))
1451-
(goto-char (treesit-node-start cur-sexp))
1452-
(if (and (string= "sym_lit" (treesit-node-type cur-sexp))
1453-
(clojure-ts--metadata-node-p (treesit-node-child cur-sexp 0 t))
1454-
(and (not (treesit-node-child-by-field-name cur-sexp "value"))
1455-
(string-empty-p (clojure-ts--named-node-text cur-sexp))))
1456-
(treesit-end-of-thing 'sexp 2 'restricted)
1457-
(treesit-end-of-thing 'sexp 1 'restrict))
1458-
(when (looking-at ",")
1459-
(forward-char))
1460-
;; Move past any whitespace or comment.
1461-
(search-forward-regexp "\\([,\s\t]*\\)\\(;+.*\\)?" bound)
1462-
(pcase (syntax-after (point))
1463-
;; End-of-line, try again on next line.
1464-
(`(12) (clojure-ts--search-whitespace-after-next-sexp root-node bound))
1465-
;; Closing paren, stop here.
1466-
(`(5 . ,_) nil)
1467-
;; Anything else is something to align.
1468-
(_ (point))))
1469+
(let ((regex "\\([,\s\t]*\\)\\(;+.*\\)?"))
1470+
;; If we're on an empty line, we should return match, otherwise
1471+
;; `clojure-ts-align-separator' setting won't work.
1472+
(if (and (bolp) (looking-at-p "[[:blank:]]*$"))
1473+
(progn
1474+
(search-forward-regexp regex bound)
1475+
(point))
1476+
(when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node (point) t)))
1477+
(goto-char (treesit-node-start cur-sexp))
1478+
(if (and (string= "sym_lit" (treesit-node-type cur-sexp))
1479+
(clojure-ts--metadata-node-p (treesit-node-child cur-sexp 0 t))
1480+
(and (not (treesit-node-child-by-field-name cur-sexp "value"))
1481+
(string-empty-p (clojure-ts--named-node-text cur-sexp))))
1482+
(treesit-end-of-thing 'sexp 2 'restricted)
1483+
(treesit-end-of-thing 'sexp 1 'restrict))
1484+
(when (looking-at ",")
1485+
(forward-char))
1486+
;; Move past any whitespace or comment.
1487+
(search-forward-regexp regex bound)
1488+
(pcase (syntax-after (point))
1489+
;; End-of-line, try again on next line.
1490+
(`(12) (progn
1491+
(forward-char 1)
1492+
(clojure-ts--search-whitespace-after-next-sexp root-node bound)))
1493+
;; Closing paren, stop here.
1494+
(`(5 . ,_) nil)
1495+
;; Anything else is something to align.
1496+
(_ (point))))))
14691497
(when (and bound (> (point) bound))
14701498
(goto-char bound))))
14711499

1472-
(defun clojure-ts--get-nodes-to-align (region-node beg end)
1500+
(defun clojure-ts--region-node (beg end)
1501+
"Return the smallest node that covers buffer positions BEG to END."
1502+
(let* ((root-node (treesit-buffer-root-node 'clojure)))
1503+
(treesit-node-descendant-for-range root-node beg end t)))
1504+
1505+
(defun clojure-ts--node-from-sexp-data (beg end sexp)
1506+
"Return updated node using SEXP data in the region between BEG and END."
1507+
(let* ((new-region-node (clojure-ts--region-node beg end))
1508+
(sexp-beg (marker-position (plist-get sexp :beg-marker)))
1509+
(sexp-end (marker-position (plist-get sexp :end-marker))))
1510+
(treesit-node-descendant-for-range new-region-node
1511+
sexp-beg
1512+
sexp-end
1513+
t)))
1514+
1515+
(defun clojure-ts--get-nodes-to-align (beg end)
14731516
"Return a plist of nodes data for alignment.
14741517
1475-
The search is limited by BEG, END and REGION-NODE.
1518+
The search is limited by BEG, END.
14761519
14771520
Possible node types are: map, bindings-vec, cond or read-cond.
14781521
14791522
The returned value is a list of property lists. Each property list
14801523
includes `:sexp-type', `:node', `:beg-marker', and `:end-marker'.
14811524
Markers are necessary to fetch the same nodes after their boundaries
14821525
have changed."
1483-
(let* ((query (treesit-query-compile 'clojure
1526+
;; By default `treesit-query-capture' captures all nodes that cross the range.
1527+
;; We need to restrict it to only nodes inside of the range.
1528+
(let* ((region-node (clojure-ts--region-node beg end))
1529+
(query (treesit-query-compile 'clojure
14841530
(append
14851531
`(((map_lit) @map)
14861532
((list_lit
@@ -1492,7 +1538,8 @@ have changed."
14921538
(:match ,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym)))
14931539
@cond))
14941540
(when clojure-ts-align-reader-conditionals
1495-
'(((read_cond_lit) @read-cond)))))))
1541+
'(((read_cond_lit) @read-cond)
1542+
((splicing_read_cond_lit) @read-cond)))))))
14961543
(thread-last (treesit-query-capture region-node query beg end)
14971544
(seq-remove (lambda (elt) (eq (car elt) 'sym)))
14981545
;; When first node is reindented, all other nodes become
@@ -1542,43 +1589,44 @@ between BEG and END."
15421589
(end (clojure-ts--end-of-defun-pos)))
15431590
(list start end)))))
15441591
(setq end (copy-marker end))
1545-
(let* ((root-node (treesit-buffer-root-node 'clojure))
1546-
;; By default `treesit-query-capture' captures all nodes that cross the
1547-
;; range. We need to restrict it to only nodes inside of the range.
1548-
(region-node (treesit-node-descendant-for-range root-node beg (marker-position end) t))
1549-
(sexps-to-align (clojure-ts--get-nodes-to-align region-node beg (marker-position end))))
1592+
(let* ((sexps-to-align (clojure-ts--get-nodes-to-align beg (marker-position end)))
1593+
;; We have to disable it here to avoid endless recursion.
1594+
(clojure-ts-align-forms-automatically nil))
15501595
(save-excursion
15511596
(indent-region beg (marker-position end))
15521597
(dolist (sexp sexps-to-align)
15531598
;; After reindenting a node, all other nodes in the `sexps-to-align'
15541599
;; list become outdated, so we need to fetch updated nodes for every
15551600
;; iteration.
1556-
(let* ((new-root-node (treesit-buffer-root-node 'clojure))
1557-
(new-region-node (treesit-node-descendant-for-range new-root-node
1558-
beg
1559-
(marker-position end)
1560-
t))
1561-
(sexp-beg (marker-position (plist-get sexp :beg-marker)))
1562-
(sexp-end (marker-position (plist-get sexp :end-marker)))
1563-
(node (treesit-node-descendant-for-range new-region-node
1564-
sexp-beg
1565-
sexp-end
1566-
t))
1601+
(let* ((node (clojure-ts--node-from-sexp-data beg (marker-position end) sexp))
15671602
(sexp-type (plist-get sexp :sexp-type))
15681603
(node-end (treesit-node-end node)))
15691604
(clojure-ts--point-to-align-position sexp-type node)
1570-
(align-region (point) node-end nil
1605+
(align-region (point) node-end t
15711606
`((clojure-align (regexp . ,(lambda (&optional bound _noerror)
1572-
(clojure-ts--search-whitespace-after-next-sexp node bound)))
1607+
(let ((updated-node (clojure-ts--node-from-sexp-data beg (marker-position end) sexp)))
1608+
(clojure-ts--search-whitespace-after-next-sexp updated-node bound))))
15731609
(group . 1)
15741610
(separate . ,clojure-ts-align-separator)
15751611
(repeat . t)))
15761612
nil)
15771613
;; After every iteration we have to re-indent the s-expression,
15781614
;; otherwise some can be indented inconsistently.
15791615
(indent-region (marker-position (plist-get sexp :beg-marker))
1580-
(marker-position (plist-get sexp :end-marker))))))))
1616+
(marker-position (plist-get sexp :end-marker)))))
1617+
;; If `clojure-ts-align-separator' is used, `align-region' leaves trailing
1618+
;; whitespaces on empty lines.
1619+
(delete-trailing-whitespace beg (marker-position end)))))
1620+
1621+
(defun clojure-ts-indent-region (beg end)
1622+
"Like `indent-region', but also maybe align forms.
15811623
1624+
Forms between BEG and END are aligned according to
1625+
`clojure-ts-align-forms-automatically'."
1626+
(prog1 (let ((indent-region-function #'treesit-indent-region))
1627+
(indent-region beg end))
1628+
(when clojure-ts-align-forms-automatically
1629+
(clojure-ts-align beg end))))
15821630

15831631
(defvar clojure-ts-mode-map
15841632
(let ((map (make-sparse-keymap)))
@@ -1717,6 +1765,11 @@ REGEX-AVAILABLE."
17171765

17181766
(treesit-major-mode-setup)
17191767

1768+
;; We should assign this after calling `treesit-major-mode-setup',
1769+
;; otherwise it will be owerwritten.
1770+
(when clojure-ts-align-forms-automatically
1771+
(setq-local indent-region-function #'clojure-ts-indent-region))
1772+
17201773
;; Initial indentation rules cache calculation.
17211774
(setq clojure-ts--semantic-indent-rules-cache
17221775
(clojure-ts--compute-semantic-indentation-rules-cache clojure-ts-semantic-indent-rules))

0 commit comments

Comments
 (0)