Skip to content

Commit ec50b30

Browse files
committed
Support nested indentation rules
1 parent 9ae1f42 commit ec50b30

File tree

5 files changed

+192
-126
lines changed

5 files changed

+192
-126
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [#61](https://github.com/clojure-emacs/clojure-ts-mode/issues/61): Fix issue with indentation of collection items with metadata.
1616
- Proper syntax highlighting for expressions with metadata.
1717
- Add basic support for dynamic indentation via `clojure-ts-get-indent-function`.
18+
- Add support for nested indentation rules.
1819

1920
## 0.2.3 (2025-03-04)
2021

README.md

+11-10
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ your personal config. Let's assume you want to indent `->>` and `->` like this:
188188
You can do so by putting the following in your config:
189189

190190
```emacs-lisp
191-
(setopt clojure-ts-semantic-indent-rules '(("->" . (:block 1))
192-
("->>" . (:block 1))))
191+
(setopt clojure-ts-semantic-indent-rules '(("->" . ((:block 1)))
192+
("->>" . ((:block 1)))))
193193
```
194194

195195
This means that the body of the `->`/`->>` is after the first argument.
@@ -198,16 +198,17 @@ The default set of rules is defined as
198198
`clojure-ts--semantic-indent-rules-defaults`, any rule can be overridden using
199199
customization option.
200200

201-
There are 2 types of rules supported: `:block` and `:inner`, similarly to
202-
cljfmt. If rule is defined as `:block n`, `n` means a number of arguments after
203-
which begins the body. If rule is defined as `:inner n`, each form in the body
204-
is indented with 2 spaces regardless of `n` value (currently all default rules
205-
has 0 value).
201+
Two types of rules are supported: `:block` and `:inner`, mirroring those in
202+
cljfmt. When a rule is defined as `:block n`, `n` represents the number of
203+
arguments preceding the body. When a rule is defined as `:inner n`, each form
204+
within the expression's body, nested `n` levels deep, is indented by two
205+
spaces. These rule definitions fully reflect the [cljfmt rules](https://github.com/weavejester/cljfmt/blob/0.13.0/docs/INDENTS.md).
206206

207207
For example:
208-
- `do` has a rule `:block 0`.
209-
- `when` has a rule `:block 1`.
210-
- `defn` and `fn` have a rule `:inner 0`.
208+
- `do` has a rule `((:block 0))`.
209+
- `when` has a rule `((:block 1))`.
210+
- `defn` and `fn` have a rule `((:inner 0))`.
211+
- `letfn` has a rule `((:block 1) (:inner 2 0))`.
211212

212213
### Font Locking
213214

clojure-ts-mode.el

+151-106
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,12 @@ Default set of rules is defined in
137137
`clojure-ts--semantic-indent-rules-defaults'."
138138
:safe #'listp
139139
:type '(alist :key-type string
140-
:value-type (list (choice (const :tag "Block indentation rule" :block)
141-
(const :tag "Inner indentation rule" :inner))
142-
integer))
140+
:value-type (repeat (choice (list (choice (const :tag "Block indentation rule" :block)
141+
(const :tag "Inner indentation rule" :inner))
142+
integer)
143+
(list (const :tag "Inner indentation rule" :inner)
144+
integer
145+
integer))))
143146
:package-version '(clojure-ts-mode . "0.2.4"))
144147

145148
(defvar clojure-ts-mode-remappings
@@ -769,74 +772,73 @@ The possible values for this variable are
769772
((parent-is "set_lit") parent 2))))
770773

771774
(defvar clojure-ts--semantic-indent-rules-defaults
772-
'(("alt!" . (:block 0))
773-
("alt!!" . (:block 0))
774-
("comment" . (:block 0))
775-
("cond" . (:block 0))
776-
("delay" . (:block 0))
777-
("do" . (:block 0))
778-
("finally" . (:block 0))
779-
("future" . (:block 0))
780-
("go" . (:block 0))
781-
("thread" . (:block 0))
782-
("try" . (:block 0))
783-
("with-out-str" . (:block 0))
784-
("defprotocol" . (:block 1))
785-
("binding" . (:block 1))
786-
("defprotocol" . (:block 1))
787-
("binding" . (:block 1))
788-
("case" . (:block 1))
789-
("cond->" . (:block 1))
790-
("cond->>" . (:block 1))
791-
("doseq" . (:block 1))
792-
("dotimes" . (:block 1))
793-
("doto" . (:block 1))
794-
("extend" . (:block 1))
795-
("extend-protocol" . (:block 1))
796-
("extend-type" . (:block 1))
797-
("for" . (:block 1))
798-
("go-loop" . (:block 1))
799-
("if" . (:block 1))
800-
("if-let" . (:block 1))
801-
("if-not" . (:block 1))
802-
("if-some" . (:block 1))
803-
("let" . (:block 1))
804-
("letfn" . (:block 1))
805-
("locking" . (:block 1))
806-
("loop" . (:block 1))
807-
("match" . (:block 1))
808-
("ns" . (:block 1))
809-
("struct-map" . (:block 1))
810-
("testing" . (:block 1))
811-
("when" . (:block 1))
812-
("when-first" . (:block 1))
813-
("when-let" . (:block 1))
814-
("when-not" . (:block 1))
815-
("when-some" . (:block 1))
816-
("while" . (:block 1))
817-
("with-local-vars" . (:block 1))
818-
("with-open" . (:block 1))
819-
("with-precision" . (:block 1))
820-
("with-redefs" . (:block 1))
821-
("defrecord" . (:block 2))
822-
("deftype" . (:block 2))
823-
("are" . (:block 2))
824-
("as->" . (:block 2))
825-
("catch" . (:block 2))
826-
("condp" . (:block 2))
827-
("bound-fn" . (:inner 0))
828-
("def" . (:inner 0))
829-
("defmacro" . (:inner 0))
830-
("defmethod" . (:inner 0))
831-
("defmulti" . (:inner 0))
832-
("defn" . (:inner 0))
833-
("defn-" . (:inner 0))
834-
("defonce" . (:inner 0))
835-
("deftest" . (:inner 0))
836-
("fdef" . (:inner 0))
837-
("fn" . (:inner 0))
838-
("reify" . (:inner 0))
839-
("use-fixtures" . (:inner 0)))
775+
'(("alt!" . ((:block 0)))
776+
("alt!!" . ((:block 0)))
777+
("comment" . ((:block 0)))
778+
("cond" . ((:block 0)))
779+
("delay" . ((:block 0)))
780+
("do" . ((:block 0)))
781+
("finally" . ((:block 0)))
782+
("future" . ((:block 0)))
783+
("go" . ((:block 0)))
784+
("thread" . ((:block 0)))
785+
("try" . ((:block 0)))
786+
("with-out-str" . ((:block 0)))
787+
("defprotocol" . ((:block 1) (:inner 1)))
788+
("binding" . ((:block 1)))
789+
("case" . ((:block 1)))
790+
("cond->" . ((:block 1)))
791+
("cond->>" . ((:block 1)))
792+
("doseq" . ((:block 1)))
793+
("dotimes" . ((:block 1)))
794+
("doto" . ((:block 1)))
795+
("extend" . ((:block 1)))
796+
("extend-protocol" . ((:block 1) (:inner 1)))
797+
("extend-type" . ((:block 1) (:inner 1)))
798+
("for" . ((:block 1)))
799+
("go-loop" . ((:block 1)))
800+
("if" . ((:block 1)))
801+
("if-let" . ((:block 1)))
802+
("if-not" . ((:block 1)))
803+
("if-some" . ((:block 1)))
804+
("let" . ((:block 1)))
805+
("letfn" . ((:block 1) (:inner 2 0)))
806+
("locking" . ((:block 1)))
807+
("loop" . ((:block 1)))
808+
("match" . ((:block 1)))
809+
("ns" . ((:block 1)))
810+
("struct-map" . ((:block 1)))
811+
("testing" . ((:block 1)))
812+
("when" . ((:block 1)))
813+
("when-first" . ((:block 1)))
814+
("when-let" . ((:block 1)))
815+
("when-not" . ((:block 1)))
816+
("when-some" . ((:block 1)))
817+
("while" . ((:block 1)))
818+
("with-local-vars" . ((:block 1)))
819+
("with-open" . ((:block 1)))
820+
("with-precision" . ((:block 1)))
821+
("with-redefs" . ((:block 1)))
822+
("defrecord" . ((:block 2) (:inner 1)))
823+
("deftype" . ((:block 2) (:inner 1)))
824+
("are" . ((:block 2)))
825+
("as->" . ((:block 2)))
826+
("catch" . ((:block 2)))
827+
("condp" . ((:block 2)))
828+
("bound-fn" . ((:inner 0)))
829+
("def" . ((:inner 0)))
830+
("defmacro" . ((:inner 0)))
831+
("defmethod" . ((:inner 0)))
832+
("defmulti" . ((:inner 0)))
833+
("defn" . ((:inner 0)))
834+
("defn-" . ((:inner 0)))
835+
("defonce" . ((:inner 0)))
836+
("deftest" . ((:inner 0)))
837+
("fdef" . ((:inner 0)))
838+
("fn" . ((:inner 0)))
839+
("reify" . ((:inner 0) (:inner 1)))
840+
("proxy" . ((:block 2) (:inner 1)))
841+
("use-fixtures" . ((:inner 0))))
840842
"Default semantic indentation rules.
841843
842844
The format reflects cljfmt indentation rules. All the default rules are
@@ -882,22 +884,87 @@ The returned value is expected to be the same as
882884
`clojure-get-indent-function' from `clojure-mode' for compatibility
883885
reasons.")
884886

887+
(defun clojure-ts--unwrap-dynamic-spec (spec current-depth)
888+
"Recursively unwrap SPEC, incrementally increasing the CURRENT-DEPTH.
889+
890+
This function accepts a list SPEC, like ((:defn)) and produce a proper
891+
indent rule. For example, ((:defn)) is converted to (:inner 2),
892+
and (:defn) is converted to (:inner 1)."
893+
(if (consp spec)
894+
(clojure-ts--unwrap-dynamic-spec (car spec) (1+ current-depth))
895+
(cond
896+
((equal spec :defn) (list :inner current-depth))
897+
(t nil))))
898+
885899
(defun clojure-ts--dynamic-indent-for-symbol (symbol-name)
886-
"Return dynamic indentation spec for SYMBOL-NAME if found.
900+
"Returns the dynamic indentation specification for SYMBOL-NAME, if found.
901+
902+
If the function `clojure-ts-get-indent-function' is defined, call it and
903+
produce a valid indentation specification from its return value.
887904
888-
If function `clojure-ts-get-indent-function' is not nil, call it and
889-
produce a valid indentation spec from the returned value.
905+
The `clojure-ts-get-indent-function' should return an indentation
906+
specification compatible with `clojure-mode', which will then be
907+
converted to a suitable `clojure-ts-mode' specification.
890908
891-
The indentation rules for `clojure-ts-mode' are simpler than for
892-
`clojure-mode' so we only take the first integer N and produce `(:block
893-
N)' rule. If an integer cannot be found, this function returns nil and
894-
the default rule is used."
909+
For example, (1 ((:defn)) nil) is converted to ((:block 1) (:inner 2))."
895910
(when (functionp clojure-ts-get-indent-function)
896911
(let ((spec (funcall clojure-ts-get-indent-function symbol-name)))
897-
(if (consp spec)
898-
`(:block ,(car spec))
899-
(when (integerp spec)
900-
`(:block ,spec))))))
912+
(if (integerp spec)
913+
(list (list :block spec))
914+
(when (sequencep spec)
915+
(thread-last spec
916+
(seq-map (lambda (el)
917+
(cond
918+
((integerp el) (list :block el))
919+
((equal el :defn) (list :inner 0))
920+
((consp el) (clojure-ts--unwrap-dynamic-spec el 0))
921+
(t nil))))
922+
(seq-remove #'null)
923+
;; Always put `:block' to the beginning.
924+
(seq-sort (lambda (spec1 _spec2)
925+
(equal (car spec1) :block)))))))))
926+
927+
(defun clojure-ts--find-semantic-rule (node parent current-depth)
928+
"Returns a suitable indentation rule for NODE, considering the CURRENT-DEPTH.
929+
930+
Attempts to find an indentation rule by examining the symbol name of the
931+
PARENT's first child. If a rule is not found, it navigates up the
932+
syntax tree and recursively attempts to find a rule, incrementally
933+
increasing the CURRENT-DEPTH. If a rule is not found upon reaching the
934+
root of the syntax tree, it returns nil. A rule is considered a match
935+
only if the CURRENT-DEPTH matches the rule's required depth."
936+
(let* ((first-child (clojure-ts--node-child-skip-metadata parent 0))
937+
(symbol-name (clojure-ts--named-node-text first-child))
938+
(idx (- (treesit-node-index node) 2)))
939+
(if-let* ((rule-set (or (clojure-ts--dynamic-indent-for-symbol symbol-name)
940+
(alist-get symbol-name
941+
(seq-union clojure-ts-semantic-indent-rules
942+
clojure-ts--semantic-indent-rules-defaults
943+
(lambda (e1 e2) (equal (car e1) (car e2))))
944+
nil
945+
nil
946+
#'equal))))
947+
(if (zerop current-depth)
948+
(let ((rule (car rule-set)))
949+
(if (equal (car rule) :block)
950+
rule
951+
(pcase-let ((`(,_ ,rule-depth ,rule-idx) rule))
952+
(when (and (equal rule-depth current-depth)
953+
(or (null rule-idx)
954+
(equal rule-idx idx)))
955+
rule))))
956+
(thread-last rule-set
957+
(seq-filter (lambda (rule)
958+
(pcase-let ((`(,rule-type ,rule-depth ,rule-idx) rule))
959+
(and (equal rule-type :inner)
960+
(equal rule-depth current-depth)
961+
(or (null rule-idx)
962+
(equal rule-idx idx))))))
963+
(seq-first)))
964+
(when-let* ((new-parent (treesit-node-parent parent)))
965+
(clojure-ts--find-semantic-rule parent
966+
new-parent
967+
(1+ current-depth))))))
901968

902969
(defun clojure-ts--match-form-body (node parent bol)
903970
"Match if NODE has to be indented as a for body.
@@ -907,16 +974,8 @@ indentation rule in `clojure-ts--semantic-indent-rules-defaults' or
907974
`clojure-ts-semantic-indent-rules' check if NODE should be indented
908975
according to the rule. If NODE is nil, use next node after BOL."
909976
(and (clojure-ts--list-node-p parent)
910-
(let* ((first-child (clojure-ts--node-child-skip-metadata parent 0))
911-
(symbol-name (clojure-ts--named-node-text first-child)))
912-
(when-let* ((rule (or (clojure-ts--dynamic-indent-for-symbol symbol-name)
913-
(alist-get symbol-name
914-
(seq-union clojure-ts-semantic-indent-rules
915-
clojure-ts--semantic-indent-rules-defaults
916-
(lambda (e1 e2) (equal (car e1) (car e2))))
917-
nil
918-
nil
919-
#'equal))))
977+
(let* ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
978+
(when-let* ((rule (clojure-ts--find-semantic-rule node parent 0)))
920979
(and (not (clojure-ts--match-with-metadata node))
921980
(let ((rule-type (car rule))
922981
(rule-value (cadr rule)))
@@ -940,19 +999,6 @@ according to the rule. If NODE is nil, use next node after BOL."
940999
(clojure-ts--keyword-node-p first-child)
9411000
(clojure-ts--var-node-p first-child)))))
9421001

943-
(defun clojure-ts--match-method-body (_node parent _bol)
944-
"Matches a `NODE' in the body of a `PARENT' method implementation.
945-
A method implementation referes to concrete implementations being defined in
946-
forms like deftype, defrecord, reify, proxy, etc."
947-
(and
948-
(clojure-ts--list-node-p parent)
949-
(let* ((grandparent (treesit-node-parent parent))
950-
;; auncle: gender neutral sibling of parent, aka child of grandparent
951-
(first-auncle (treesit-node-child grandparent 0 t)))
952-
(and (clojure-ts--list-node-p grandparent)
953-
(clojure-ts--symbol-matches-p clojure-ts--type-symbol-regexp
954-
first-auncle)))))
955-
9561002
(defvar clojure-ts--threading-macro
9571003
(eval-and-compile
9581004
(rx (and "->" (? ">") line-end)))
@@ -1043,7 +1089,6 @@ if NODE has metadata and its parent has type NODE-TYPE."
10431089
((parent-is "source") parent-bol 0)
10441090
(clojure-ts--match-docstring parent 0)
10451091
;; https://guide.clojure.style/#body-indentation
1046-
(clojure-ts--match-method-body parent 2)
10471092
(clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2)
10481093
;; https://guide.clojure.style/#threading-macros-alignment
10491094
(clojure-ts--match-threading-macro-arg prev-sibling 0)

test/clojure-ts-mode-indentation-test.el

+10-6
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ DESCRIPTION is a string with the description of the spec."
9696
(when (stringp symbol-name)
9797
(cond
9898
((string-equal symbol-name "my-with-in-str") 1)
99-
((string-equal symbol-name "my-letfn") '(1 ((:defn) (:form)))))))
99+
((string-equal symbol-name "my-letfn") '(1 ((:defn)) :form)))))
100100

101101

102102
(describe "indentation"
@@ -242,7 +242,7 @@ DESCRIPTION is a string with the description of the spec."
242242
2 3
243243
4 5
244244
6 6)"
245-
(setopt clojure-ts-semantic-indent-rules '(("are" . (:block 1))))
245+
(setopt clojure-ts-semantic-indent-rules '(("are" . ((:block 1)))))
246246
(indent-region (point-min) (point-max))
247247
(expect (buffer-string) :to-equal "
248248
(are [x y]
@@ -305,8 +305,10 @@ DESCRIPTION is a string with the description of the spec."
305305
[fnspecs & body]
306306
~@body)
307307
308-
(my-letfn [(twice [x] (* x 2))
309-
(six-times [y] (* (twice y) 3))]
308+
(my-letfn [(twice [x]
309+
(* x 2))
310+
(six-times [y]
311+
(* (twice y) 3))]
310312
(println \"Twice 15 =\" (twice 15))
311313
(println \"Six times 15 =\" (six-times 15)))"
312314
(setq-local clojure-ts-get-indent-function #'cider--get-symbol-indent-mock)
@@ -318,7 +320,9 @@ DESCRIPTION is a string with the description of the spec."
318320
[fnspecs & body]
319321
~@body)
320322
321-
(my-letfn [(twice [x] (* x 2))
322-
(six-times [y] (* (twice y) 3))]
323+
(my-letfn [(twice [x]
324+
(* x 2))
325+
(six-times [y]
326+
(* (twice y) 3))]
323327
(println \"Twice 15 =\" (twice 15))
324328
(println \"Six times 15 =\" (six-times 15)))"))))

0 commit comments

Comments
 (0)