Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
- [#165](https://github.com/clojure-emacs/clojure-ts-mode/pull/165): Fix `clojure-ts-comment-macro-font-lock-body` and `clojure-ts-extra-def-forms` being ignored in `.dir-locals.el` and when set after the package loads, and a directory-local `clojure-ts-extra-def-forms` applying to other buffers instead.
- [#165](https://github.com/clojure-emacs/clojure-ts-mode/pull/165): Fix `clojure-ts-indent-style` and `clojure-ts-align-forms-automatically` being ignored in `.dir-locals.el`, and `setopt` of `clojure-ts-indent-style` not reaching open buffers.
- [#166](https://github.com/clojure-emacs/clojure-ts-mode/pull/166): Indent function literals, reader conditionals and tagged literals in the `fixed` indentation style, and lists whose head symbol has metadata, which got 1 space instead of 2.
- [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Indent forms like `if`, `when` or `let` as function calls when their body starts on the first line, like cljfmt and `clojure-mode` do, instead of indenting the rest of the body by 2 spaces.
- [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Count forms discarded with `#_` as arguments of forms like `when` or `let` when indenting them, like `clojure-mode` does.
- [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Add the `let*` semantic indentation rule from cljfmt.

## 0.6.0 (2025-12-02)

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,10 @@ customization option.

Two types of rules are supported: `:block` and `:inner`, mirroring those in
cljfmt. When a rule is defined as `:block n`, `n` represents the number of
arguments preceding the body. When a rule is defined as `:inner n`, each form
within the expression's body, nested `n` levels deep, is indented by two
spaces. These rule definitions fully reflect the [cljfmt rules](https://github.com/weavejester/cljfmt/blob/0.13.0/docs/INDENTS.md).
arguments preceding the body. If the body doesn't start on a line of its own,
the form is indented like a function call instead. When a rule is
defined as `:inner n`, each form within the expression's body, nested `n`
levels deep, is indented by two spaces. These rule definitions fully reflect the [cljfmt rules](https://github.com/weavejester/cljfmt/blob/0.13.0/docs/INDENTS.md).

For example:

Expand Down
84 changes: 42 additions & 42 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -1025,15 +1025,23 @@ If there is no namespace, returns nil."
(and (clojure-ts--symbol-node-p node)
(string-equal expected-symbol-name (clojure-ts--named-node-text node))))

(defun clojure-ts--node-child-skip-metadata (node n)
"Return the Nth value-field child of NODE, skipping metadata children."
(defun clojure-ts--counted-child-p (node count-discarded)
"Return non-nil if NODE is a value child of its parent.
If COUNT-DISCARDED is non-nil, a form discarded with #_ counts too."
(or (equal "value" (treesit-node-field-name node))
(and count-discarded (equal "dis_expr" (treesit-node-type node)))))

(defun clojure-ts--node-child-skip-metadata (node n &optional count-discarded)
"Return the Nth value-field child of NODE, skipping metadata children.
If COUNT-DISCARDED is non-nil, forms discarded with #_ count as children
too."
;; Walks siblings directly via C-level tree-sitter calls to avoid
;; allocating intermediate lists, as this is called frequently on hot paths.
(let ((child (treesit-node-child-by-field-name node "value"))
(remaining n))
(while (and child (> remaining 0))
(setq child (treesit-node-next-sibling child t))
(when (and child (equal "value" (treesit-node-field-name child)))
(when (and child (clojure-ts--counted-child-p child count-discarded))
(setq remaining (1- remaining))))
child))

Expand Down Expand Up @@ -1336,6 +1344,7 @@ on the line after the prefix, it is indented the way PREFIXED would be."
("if-not" . ((:block 1)))
("if-some" . ((:block 1)))
("let" . ((:block 1)))
("let*" . ((:block 1)))
("letfn" . ((:block 1) (:inner 2 0)))
("locking" . ((:block 1)))
("loop" . ((:block 1)))
Expand Down Expand Up @@ -1432,46 +1441,43 @@ changes."
clojure-ts-semantic-indent-rules))))
(cdr clojure-ts--semantic-indent-rules-cache))

(defun clojure-ts--match-block-0-body (bol first-child)
"Match if expression body is not at the same line as FIRST-CHILD.

If there is no body, check that BOL is not at the same line. Metadata
and comments after FIRST-CHILD are not the body."
(let* ((body-pos (if-let* ((body (clojure-ts--node-child-skip-metadata
(treesit-node-parent first-child) 1)))
(treesit-node-start body)
bol)))
(< (line-number-at-pos (treesit-node-start first-child))
(line-number-at-pos body-pos))))

(defun clojure-ts--value-position (node)
(defun clojure-ts--value-position (node &optional count-discarded)
"Return how many value children of NODE's parent come before NODE.

Only children in the value field count, so metadata, comments and the
parens don't: in (when ^boolean x y), y is at position 2, after the
symbol (0) and x (1). For a metadata node this is the position of the
value it belongs to."
value it belongs to. If COUNT-DISCARDED is non-nil, forms discarded
with #_ count too."
(let ((pos 0)
(sibling (treesit-node-prev-sibling node)))
(while sibling
(when (equal "value" (treesit-node-field-name sibling))
(when (clojure-ts--counted-child-p sibling count-discarded)
(setq pos (1+ pos)))
(setq sibling (treesit-node-prev-sibling sibling)))
pos))

(defun clojure-ts--node-pos-match-block (node parent bol block)
"Return non-nil if NODE index in the PARENT matches requested BLOCK.

NODE might be nil (when we insert an empty line for example), in this
case we look for next available child node in the PARENT after BOL
position.

The first value in the expression is the matched symbol, and the next
BLOCK values are its special arguments, so NODE is in the body when
more than BLOCK values come before it. Metadata and comments are not
counted (see `clojure-ts--value-position')."
(when-let* ((node (or node (treesit-node-first-child-for-pos parent bol))))
(> (clojure-ts--value-position node) block)))
(defun clojure-ts--match-block-body (node parent bol block)
"Return non-nil if NODE is in the body of PARENT.

PARENT is a form with BLOCK special arguments. NODE might be nil (when
we insert an empty line for example), in this case we look for next
available child node in the PARENT after BOL position.

The first value in PARENT is the matched symbol and the next BLOCK
values are its special arguments, so NODE is in the body when more than
BLOCK values come before it. Like in cljfmt and `clojure-mode', a form
whose body doesn't start on a line of its own is indented like a call
instead. Forms discarded with #_ count as values here, like in
`clojure-mode'."
(when-let* ((node (or node (treesit-node-first-child-for-pos parent bol)))
((> (clojure-ts--value-position node t) block)))
(let ((body (clojure-ts--node-child-skip-metadata parent (1+ block) t)))
(or (null body)
(save-excursion
(goto-char (clojure-ts--node-start-with-metadata body))
(skip-chars-backward " \t,")
(bolp))))))

(defvar clojure-ts-get-indent-function nil
"Function to get the indent spec of a symbol.
Expand Down Expand Up @@ -1624,17 +1630,11 @@ indentation rule in `clojure-ts--semantic-indent-rules-defaults' or
according to the rule. If NODE is nil, use next node after BOL."
(and (or (clojure-ts--list-node-p parent)
(clojure-ts--anon-fn-node-p parent))
(let* ((first-child (clojure-ts--first-value-child parent)))
(when-let* ((rule (clojure-ts--find-semantic-rule node parent 0)))
(let ((rule-type (car rule))
(rule-value (cadr rule)))
(if (equal rule-type :block)
(if (zerop rule-value)
;; Special treatment for block 0 rule.
(clojure-ts--match-block-0-body bol first-child)
(clojure-ts--node-pos-match-block node parent bol rule-value))
;; Return true for any inner rule.
t))))))
(when-let* ((rule (clojure-ts--find-semantic-rule node parent 0)))
(pcase rule
(`(:block ,block . ,_) (clojure-ts--match-block-body node parent bol block))
;; Return true for any inner rule.
(_ t)))))

(defun clojure-ts--first-call-arg (node)
"Return the first argument of the call NODE.
Expand Down
4 changes: 2 additions & 2 deletions test/clojure-ts-mode-cycling-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@

(when-refactoring-with-point-it "should move metadata along with its branch past a comment"
"(if |a ^String ;; comment
b c)"
b c)"
"(if-not |a c ^String ;; comment
b)"
b)"

(clojure-ts-cycle-conditional))

Expand Down
51 changes: 46 additions & 5 deletions test/clojure-ts-mode-indentation-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ DESCRIPTION is a string with the description of the spec."
b 2]
(+ a b))"

"
(let* [a 1]
a)"

"
(if (pos? x)
(inc x)
Expand Down Expand Up @@ -697,17 +701,49 @@ DESCRIPTION is a string with the description of the spec."
(str x)
\"a\" 1)")

(when-indenting-it "should not take metadata or a comment for the body of block 0 forms"
(when-indenting-it "should not take a comment for the body of block 0 forms"
"
(comment ;; scratch
(start-server)
(stop-server))"
(stop-server))")

(when-indenting-it "should indent a block form like a call when its body starts on the first line"
"
(if (pos? x) :pos
:neg)"

"
(let [a 1] (foo a)
(bar a))"

"
(defprotocol P (foo [this])
(bar [this]))"

;; The metadata starts the body.
"
(do ^:foo
(a))")
(a))")

(when-indenting-it "should count discarded forms as elements of block forms"
"
(when #_debug c
a)"

(when-indenting-it "should not count metadata and comments for inner rules with an index"
"
(let [a 1] #_x
a)"

"
(do #_x
a)"

"
(when c
#_x
a)")

(when-indenting-it "should not count metadata, comments or discarded forms for inner rules with an index"
"
(letfn ;; local fns
[(f [x]
Expand All @@ -718,7 +754,12 @@ DESCRIPTION is a string with the description of the spec."
(letfn ^:foo
[(f [x]
x)]
(f 1))")
(f 1))"

"
(letfn #_[(g [] 1)] [(f [x]
x)]
(f 1))")

(it "should indent a form on the line after its reader prefix"
;; No rule used to match these lines, so they kept whatever indentation
Expand Down
Loading