Skip to content

Commit eb6841e

Browse files
committed
Improve semantic indentation rules to be more consistent with cljfmt
1 parent f932fc3 commit eb6841e

File tree

6 files changed

+325
-53
lines changed

6 files changed

+325
-53
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Highlight named lambda functions properly.
1111
- Fix syntax highlighting for functions and vars with metadata on the previous
1212
line.
13+
- Improve semantic indentation rules to be more consistent with cljfmt.
1314

1415
## 0.2.3 (2025-03-04)
1516

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ Set the var `clojure-ts-indent-style` to change it.
170170
>
171171
> You can find [this article](https://metaredux.com/posts/2020/12/06/semantic-clojure-formatting.html) comparing semantic and fixed indentation useful.
172172
173+
#### Customizing semantic indentation
174+
175+
You can customize how body of different forms is indented. There is a set of
176+
default rules, which are aligned with cljfmt indent rules, but you can also set
177+
custom rules as:
178+
179+
```emacs-lisp
180+
(setopt clojure-ts-semantic-indent-rules '(("are" . (:block 1))))
181+
```
182+
183+
Custom rules have higher priority, so default rules can be overridden. Unlike
184+
cljfmt `clojure-ts-mode` does not support nested rules (it's handled
185+
differently).
186+
173187
### Font Locking
174188

175189
To highlight entire rich `comment` expression with the comment font face, set

clojure-ts-mode.el

+140-48
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,22 @@ double quotes on the third column."
125125
:type 'boolean
126126
:package-version '(clojure-ts-mode . "0.2.4"))
127127

128+
(defcustom clojure-ts-semantic-indent-rules nil
129+
"Custom rules to extend default indentation rules for `semantic' style.
130+
131+
Each rule is an alist entry which looks like `(\"symbol-name\"
132+
. (rule-type rule-value))', where rule-type is one either `:block' or
133+
`:inner' and rule-value is an integer. The semantic is similar to
134+
cljfmt indentation rules.
135+
136+
Default set of rules is defined in
137+
`clojure-ts--semantic-indent-rules-defaults'."
138+
:type '(alist :key-type string
139+
:value-type (list (choice (const :tag "Block indentation rule" :block)
140+
(const :tag "Inner indentation rule" :inner))
141+
integer))
142+
:package-version '(clojure-ts-mode . "0.2.4"))
143+
128144
(defvar clojure-ts-mode-remappings
129145
'((clojure-mode . clojure-ts-mode)
130146
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -182,7 +198,6 @@ Only intended for use at development time.")
182198
table)
183199
"Syntax table for `clojure-ts-mode'.")
184200

185-
186201
(defconst clojure-ts--builtin-dynamic-var-regexp
187202
(eval-and-compile
188203
(concat "^"
@@ -746,34 +761,129 @@ The possible values for this variable are
746761
((parent-is "list_lit") parent 1)
747762
((parent-is "set_lit") parent 2))))
748763

749-
(defvar clojure-ts--symbols-with-body-expressions-regexp
750-
(eval-and-compile
751-
(rx (or
752-
;; Match def* symbols,
753-
;; we also explicitly do not match symbols beginning with
754-
;; "default" "deflate" and "defer", like cljfmt
755-
(and line-start "def")
756-
;; Match with-* symbols
757-
(and line-start "with-")
758-
;; Exact matches
759-
(and line-start
760-
(or "alt!" "alt!!" "are" "as->"
761-
"binding" "bound-fn"
762-
"case" "catch" "comment" "cond" "condp" "cond->" "cond->>"
763-
"delay" "do" "doseq" "dotimes" "doto"
764-
"extend" "extend-protocol" "extend-type"
765-
"fdef" "finally" "fn" "for" "future"
766-
"go" "go-loop"
767-
"if" "if-let" "if-not" "if-some"
768-
"let" "letfn" "locking" "loop"
769-
"match" "ns" "proxy" "reify" "struct-map"
770-
"testing" "thread" "try"
771-
"use-fixtures"
772-
"when" "when-first" "when-let" "when-not" "when-some" "while")
773-
line-end))))
774-
"A regex to match symbols that are functions/macros with a body argument.
775-
Taken from cljfmt:
776-
https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de898c3/cljfmt/resources/cljfmt/indents/clojure.clj")
764+
(defvar clojure-ts--semantic-indent-rules-defaults
765+
'(("alt!" . (:block 0))
766+
("alt!!" . (:block 0))
767+
("comment" . (:block 0))
768+
("cond" . (:block 0))
769+
("delay" . (:block 0))
770+
("do" . (:block 0))
771+
("finally" . (:block 0))
772+
("future" . (:block 0))
773+
("go" . (:block 0))
774+
("thread" . (:block 0))
775+
("try" . (:block 0))
776+
("with-out-str" . (:block 0))
777+
("defprotocol" . (:block 1))
778+
("binding" . (:block 1))
779+
("defprotocol" . (:block 1))
780+
("binding" . (:block 1))
781+
("case" . (:block 1))
782+
("cond->" . (:block 1))
783+
("cond->>" . (:block 1))
784+
("doseq" . (:block 1))
785+
("dotimes" . (:block 1))
786+
("doto" . (:block 1))
787+
("extend" . (:block 1))
788+
("extend-protocol" . (:block 1))
789+
("extend-type" . (:block 1))
790+
("for" . (:block 1))
791+
("go-loop" . (:block 1))
792+
("if" . (:block 1))
793+
("if-let" . (:block 1))
794+
("if-not" . (:block 1))
795+
("if-some" . (:block 1))
796+
("let" . (:block 1))
797+
("letfn" . (:block 1))
798+
("locking" . (:block 1))
799+
("loop" . (:block 1))
800+
("match" . (:block 1))
801+
("ns" . (:block 1))
802+
("struct-map" . (:block 1))
803+
("testing" . (:block 1))
804+
("when" . (:block 1))
805+
("when-first" . (:block 1))
806+
("when-let" . (:block 1))
807+
("when-not" . (:block 1))
808+
("when-some" . (:block 1))
809+
("while" . (:block 1))
810+
("with-local-vars" . (:block 1))
811+
("with-open" . (:block 1))
812+
("with-precision" . (:block 1))
813+
("with-redefs" . (:block 1))
814+
("defrecord" . (:block 2))
815+
("deftype" . (:block 2))
816+
("are" . (:block 2))
817+
("as->" . (:block 2))
818+
("catch" . (:block 2))
819+
("condp" . (:block 2))
820+
("bound-fn" . (:inner 0))
821+
("def" . (:inner 0))
822+
("defmacro" . (:inner 0))
823+
("defmethod" . (:inner 0))
824+
("defmulti" . (:inner 0))
825+
("defn" . (:inner 0))
826+
("defn-" . (:inner 0))
827+
("defonce" . (:inner 0))
828+
("deftest" . (:inner 0))
829+
("fdef" . (:inner 0))
830+
("fn" . (:inner 0))
831+
("reify" . (:inner 0))
832+
("use-fixtures" . (:inner 0)))
833+
"Default semantic indentation rules.
834+
835+
The format reflects cljfmt indentation rules. All the default rules are
836+
aligned with
837+
https://github.com/weavejester/cljfmt/blob/0.13.0/cljfmt/resources/cljfmt/indents/clojure.clj")
838+
839+
(defun clojure-ts--match-block-0-body (bol first-child)
840+
"Match if expression body is not at the same line as FIRST-CHILD.
841+
842+
If there is no body, check that BOL is not at the same line."
843+
(let* ((body-pos (if-let* ((body (treesit-node-next-sibling first-child)))
844+
(treesit-node-start body)
845+
bol)))
846+
(< (line-number-at-pos (treesit-node-start first-child))
847+
(line-number-at-pos body-pos))))
848+
849+
(defun clojure-ts--node-pos-match-block (node parent bol block)
850+
"Return TRUE if NODE index in the PARENT matches requested BLOCK.
851+
852+
NODE might be nil (when we insert an empty line for example), in this
853+
case we look for next available child node in the PARENT after BOL
854+
position.
855+
856+
The first node in the expression is usually an opening paren, the last
857+
node is usually a closing paren (unless some automatic parens mode is
858+
not enabled). If requested BLOCK is 1, the NODE index should be at
859+
least 3 (first node is opening paren, second node is matched symbol,
860+
third node is first argument, and the rest is body which should be
861+
indented.)"
862+
(if node
863+
(> (treesit-node-index node) (1+ block))
864+
(when-let* ((node-after-bol (treesit-node-first-child-for-pos parent bol)))
865+
(> (treesit-node-index node-after-bol) (1+ block)))))
866+
867+
(defun clojure-ts--match-form-body (node parent bol)
868+
(and (clojure-ts--list-node-p parent)
869+
(let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
870+
(when-let* ((rule (alist-get (clojure-ts--named-node-text first-child)
871+
(seq-union clojure-ts-semantic-indent-rules
872+
clojure-ts--semantic-indent-rules-defaults
873+
(lambda (e1 e2) (equal (car e1) (car e2))))
874+
nil
875+
nil
876+
#'equal)))
877+
(and (not (clojure-ts--match-with-metadata node))
878+
(let ((rule-type (car rule))
879+
(rule-value (cadr rule)))
880+
(if (equal rule-type :block)
881+
(if (zerop rule-value)
882+
;; Special treatment for block 0 rule.
883+
(clojure-ts--match-block-0-body bol first-child)
884+
(clojure-ts--node-pos-match-block node parent bol rule-value))
885+
;; Return true for any inner rule.
886+
t)))))))
777887

778888
(defun clojure-ts--match-function-call-arg (node parent _bol)
779889
"Match NODE if PARENT is a list expressing a function or macro call."
@@ -787,24 +897,6 @@ https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de89
787897
(clojure-ts--keyword-node-p first-child)
788898
(clojure-ts--var-node-p first-child)))))
789899

790-
(defun clojure-ts--match-expression-in-body (node parent _bol)
791-
"Match NODE if it is an expression used in a body argument.
792-
PARENT is expected to be a list literal.
793-
See `treesit-simple-indent-rules'."
794-
(and
795-
(clojure-ts--list-node-p parent)
796-
(let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
797-
(and
798-
(not
799-
(clojure-ts--symbol-matches-p
800-
;; Symbols starting with this are false positives
801-
(rx line-start (or "default" "deflate" "defer"))
802-
first-child))
803-
(not (clojure-ts--match-with-metadata node))
804-
(clojure-ts--symbol-matches-p
805-
clojure-ts--symbols-with-body-expressions-regexp
806-
first-child)))))
807-
808900
(defun clojure-ts--match-method-body (_node parent _bol)
809901
"Matches a `NODE' in the body of a `PARENT' method implementation.
810902
A method implementation referes to concrete implementations being defined in
@@ -885,7 +977,7 @@ forms like deftype, defrecord, reify, proxy, etc."
885977
(clojure-ts--match-docstring parent 0)
886978
;; https://guide.clojure.style/#body-indentation
887979
(clojure-ts--match-method-body parent 2)
888-
(clojure-ts--match-expression-in-body parent 2)
980+
(clojure-ts--match-form-body parent 2)
889981
;; https://guide.clojure.style/#threading-macros-alignment
890982
(clojure-ts--match-threading-macro-arg prev-sibling 0)
891983
;; https://guide.clojure.style/#vertically-align-fn-args

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

+100-1
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,103 @@ DESCRIPTION is a string with the description of the spec."
140140
(when-indenting-it "should support function calls via vars"
141141
"
142142
(#'foo 5
143-
6)"))
143+
6)")
144+
145+
(when-indenting-it "should support block-0 expressions"
146+
"
147+
(do (aligned)
148+
(vertically))"
149+
150+
"
151+
(do
152+
(indented)
153+
(with-2-spaces))"
154+
155+
"
156+
(future
157+
(body is indented))"
158+
159+
"
160+
(try
161+
(something)
162+
;; A bit of block 2 rule
163+
(catch Exception e
164+
\"Third argument is indented with 2 spaces.\")
165+
(catch ExceptionInfo
166+
e-info
167+
\"Second argument is aligned vertically with the first one.\"))")
168+
169+
(when-indenting-it "should support block-1 expressions"
170+
"
171+
(case x
172+
2 (print 2)
173+
3 (print 3)
174+
(print \"Default\"))"
175+
176+
"
177+
(cond-> {}
178+
:always (assoc :hello \"World\")
179+
false (do nothing))"
180+
181+
"
182+
(with-precision 32
183+
(/ (bigdec 20) (bigdec 30)))"
184+
185+
"
186+
(testing \"Something should work\"
187+
(is (something-working?)))")
188+
189+
(when-indenting-it "should support block-2 expressions"
190+
"
191+
(are [x y]
192+
(= x y)
193+
2 3
194+
4 5
195+
6 6)"
196+
197+
"
198+
(as-> {} $
199+
(assoc $ :hello \"World\"))"
200+
201+
"
202+
(as-> {}
203+
my-map
204+
(assoc my-map :hello \"World\"))"
205+
206+
"
207+
(defrecord MyThingR []
208+
IProto
209+
(foo [this x] x))")
210+
211+
(when-indenting-it "should support inner-0 expressions"
212+
"
213+
(fn named-lambda [x]
214+
(+ x x))"
215+
216+
"
217+
(defmethod hello :world
218+
[arg1 arg2]
219+
(+ arg1 arg2))"
220+
221+
"
222+
(reify
223+
AutoCloseable
224+
(close
225+
[this]
226+
(is properly indented)))")
227+
228+
(it "should prioritize custom semantic indentation rules"
229+
(with-clojure-ts-buffer "
230+
(are [x y]
231+
(= x y)
232+
2 3
233+
4 5
234+
6 6)"
235+
(setopt clojure-ts-semantic-indent-rules '(("are" . (:block 1))))
236+
(indent-region (point-min) (point-max))
237+
(expect (buffer-string) :to-equal "
238+
(are [x y]
239+
(= x y)
240+
2 3
241+
4 5
242+
6 6)"))))

0 commit comments

Comments
 (0)