Skip to content

Commit a4b20f6

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

File tree

4 files changed

+293
-43
lines changed

4 files changed

+293
-43
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

clojure-ts-mode.el

+140-39
Original file line numberDiff line numberDiff line change
@@ -746,33 +746,61 @@ The possible values for this variable are
746746
((parent-is "list_lit") parent 1)
747747
((parent-is "set_lit") parent 2))))
748748

749-
(defvar clojure-ts--symbols-with-body-expressions-regexp
749+
(defvar clojure-ts--symbols-block-0-regexp
750750
(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))))
751+
(rx (and line-start
752+
(or "alt!" "alt!!"
753+
"comment"
754+
"cond"
755+
"delay"
756+
"do"
757+
"finally"
758+
"future"
759+
"go"
760+
"thread"
761+
"try"
762+
"with-out-str")
763+
line-end)))
774764
"A regex to match symbols that are functions/macros with a body argument.
775-
Taken from cljfmt:
765+
Taken from cljfmt (all symbols with [[:block 0]] rule):
766+
https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de898c3/cljfmt/resources/cljfmt/indents/clojure.clj")
767+
768+
(defun clojure-ts--match-block-0-body (node parent bol)
769+
"Match NODE if it is an argument to a PARENT expression.
770+
771+
Check if the first expression in the body is not at the same line as
772+
NODE. If there is no body, check that BOL is not at the same line."
773+
(and (clojure-ts--list-node-p parent)
774+
(let* ((first-child (clojure-ts--node-child-skip-metadata parent 0))
775+
(body-pos (if-let* ((body (treesit-node-next-sibling first-child)))
776+
(treesit-node-start body)
777+
bol)))
778+
(and (not (clojure-ts--match-with-metadata node))
779+
(clojure-ts--symbol-matches-p
780+
clojure-ts--symbols-block-0-regexp
781+
first-child)
782+
(< (line-number-at-pos (treesit-node-start first-child))
783+
(line-number-at-pos body-pos))))))
784+
785+
(defvar clojure-ts--symbols-block-1-regexp
786+
(eval-and-compile
787+
(rx (and line-start
788+
(or "defprotocol"
789+
"binding"
790+
"case" "cond->" "cond->>"
791+
"doseq" "dotimes" "doto"
792+
"extend" "extend-protocol" "extend-type"
793+
"for"
794+
"go-loop"
795+
"if" "if-let" "if-not" "if-some"
796+
"let" "letfn" "locking" "loop"
797+
"match" "ns" "struct-map"
798+
"testing"
799+
"when" "when-first" "when-let" "when-not" "when-some" "while"
800+
"with-local-vars" "with-open" "with-precision" "with-redefs")
801+
line-end)))
802+
"A regex to match symbols that are functions/macros with a body argument.
803+
Taken from cljfmt (all symbols with [[:block 1]] rule):
776804
https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de898c3/cljfmt/resources/cljfmt/indents/clojure.clj")
777805

778806
(defun clojure-ts--match-function-call-arg (node parent _bol)
@@ -787,23 +815,93 @@ https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de89
787815
(clojure-ts--keyword-node-p first-child)
788816
(clojure-ts--var-node-p first-child)))))
789817

790-
(defun clojure-ts--match-expression-in-body (node parent _bol)
818+
(defun clojure-ts--node-pos-match-block (node parent bol block)
819+
"Return TRUE if NODE index in the PARENT matches requested BLOCK.
820+
821+
NODE might be nil (when we insert an empty line for example), in this
822+
case we look for next available child node in the PARENT after BOL
823+
position.
824+
825+
The first node in the expression is usually an opening paren, the last
826+
node is usually a closing paren (unless some automatic parens mode is
827+
not enabled). If requested BLOCK is 1, the NODE index should be at
828+
least 3 (first node is opening paren, second node is matched symbol,
829+
third node is first argument, and the rest is body which should be
830+
indented.)"
831+
(if node
832+
(> (treesit-node-index node) (1+ block))
833+
(when-let* ((node-after-bol (treesit-node-first-child-for-pos parent bol)))
834+
(> (treesit-node-index node-after-bol) (1+ block)))))
835+
836+
(defun clojure-ts--match-block-1-body (node parent bol)
791837
"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'."
838+
839+
The NODE has to be at least second argument of the expression. This
840+
rule matches [[:block 1]] rule of cljfmt. If NODE is nil check the next
841+
found node after BOL. PARENT is expected to be a list literal. See
842+
`treesit-simple-indent-rules'."
794843
(and
795844
(clojure-ts--list-node-p parent)
796845
(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)))))
846+
(and (not (clojure-ts--match-with-metadata node))
847+
(clojure-ts--symbol-matches-p
848+
clojure-ts--symbols-block-1-regexp
849+
first-child)
850+
(clojure-ts--node-pos-match-block node parent bol 1)))))
851+
852+
(defvar clojure-ts--symbols-block-2-regexp
853+
(eval-and-compile
854+
(rx (and line-start
855+
(or "defrecord" "deftype" "are" "as->" "catch" "condp")
856+
line-end)))
857+
"A regex to match symbols that are functions/macros with a body argument.
858+
Taken from cljfmt (all symbols with [[:block 2]] rule):
859+
https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de898c3/cljfmt/resources/cljfmt/indents/clojure.clj")
860+
861+
(defun clojure-ts--match-block-2-body (node parent bol)
862+
"Match NODE if it is an argument to a PARENT expression.
863+
864+
The NODE has to be at least third argument of the expression. This rule
865+
matches [[:block 2]] rule of cljfmt. If NODE is nil check the next
866+
found node after BOL."
867+
(and (clojure-ts--list-node-p parent)
868+
(let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
869+
(and (not (clojure-ts--match-with-metadata node))
870+
(clojure-ts--symbol-matches-p
871+
clojure-ts--symbols-block-2-regexp
872+
first-child)
873+
(clojure-ts--node-pos-match-block node parent bol 2)))))
874+
875+
(defvar clojure-ts--symbols-inner-0-regexp
876+
(eval-and-compile
877+
(rx (and line-start
878+
(or "bound-fn"
879+
"def"
880+
"defmacro"
881+
"defmethod"
882+
"defmulti"
883+
"defn"
884+
"defn-"
885+
"defonce"
886+
"deftest"
887+
"fdef"
888+
"fn"
889+
"reify"
890+
"use-fixtures")
891+
line-end)))
892+
"A regex to match symbols that match [[:inner 0]] cljfmt rule.")
893+
894+
(defun clojure-ts--match-inner-0-body (node parent _bol)
895+
"Match NODE if it is an argument to a PARENT expression.
896+
897+
The NODE has to be a child of an epression that matches rule [[:inner
898+
0]] of cljfmt."
899+
(and (clojure-ts--list-node-p parent)
900+
(let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
901+
(and (not (clojure-ts--match-with-metadata node))
902+
(clojure-ts--symbol-matches-p
903+
clojure-ts--symbols-inner-0-regexp
904+
first-child)))))
807905

808906
(defun clojure-ts--match-method-body (_node parent _bol)
809907
"Matches a `NODE' in the body of a `PARENT' method implementation.
@@ -885,7 +983,10 @@ forms like deftype, defrecord, reify, proxy, etc."
885983
(clojure-ts--match-docstring parent 0)
886984
;; https://guide.clojure.style/#body-indentation
887985
(clojure-ts--match-method-body parent 2)
888-
(clojure-ts--match-expression-in-body parent 2)
986+
(clojure-ts--match-block-0-body parent 2)
987+
(clojure-ts--match-block-1-body parent 2)
988+
(clojure-ts--match-block-2-body parent 2)
989+
(clojure-ts--match-inner-0-body parent 2)
889990
;; https://guide.clojure.style/#threading-macros-alignment
890991
(clojure-ts--match-threading-macro-arg prev-sibling 0)
891992
;; https://guide.clojure.style/#vertically-align-fn-args

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

+84-1
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,87 @@ 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)))"))

test/samples/indentation.clj

+68-3
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@
7979
:another-keyword 2}
8080
"default value")
8181

82-
83-
8482
(defprotocol IProto
8583
(foo [this x]
8684
"`this` is a docstring.")
@@ -121,7 +119,6 @@
121119
([a b]
122120
b))})
123121

124-
125122
^:foo
126123
(def a 1)
127124

@@ -145,3 +142,71 @@
145142
"hello"
146143
[_foo]
147144
(+ 1 1))
145+
146+
;;; Block 0 rule
147+
148+
(do (aligned)
149+
(vertically))
150+
151+
(do
152+
(indented)
153+
(with-2-spaces))
154+
155+
(future
156+
(body is indented))
157+
158+
(try
159+
(something)
160+
;; A bit of block 2 rule
161+
(catch Exception e
162+
"Third argument is indented with 2 spaces.")
163+
(catch ExceptionInfo
164+
e-info
165+
"Second argument is aligned vertically with the first one."))
166+
167+
;;; Block 1 rule
168+
169+
(case x
170+
2 (print 2)
171+
3 (print 3)
172+
(print "Default"))
173+
174+
(cond-> {}
175+
:always (assoc :hello "World")
176+
false (do nothing))
177+
178+
(with-precision 32
179+
(/ (bigdec 20) (bigdec 30)))
180+
181+
(testing "Something should work"
182+
(is (something-working?)))
183+
184+
;;; Block 2 rule
185+
186+
(are [x y]
187+
(= x y)
188+
2 3
189+
4 5
190+
6 6)
191+
192+
(as-> {} $
193+
(assoc $ :hello "World"))
194+
195+
(as-> {}
196+
my-map
197+
(assoc my-map :hello "World"))
198+
199+
;;; Inner 0 rule
200+
201+
(fn named-lambda [x]
202+
(+ x x))
203+
204+
(defmethod hello :world
205+
[arg1 arg2]
206+
(+ arg1 arg2))
207+
208+
(reify
209+
AutoCloseable
210+
(close
211+
[this]
212+
(is properly indented)))

0 commit comments

Comments
 (0)