From 391ebebf3acd8578a29f41b9339f9b0b2a0fc730 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:00:00 +0200 Subject: [PATCH 1/3] Indent a block form as a call when its body starts on the first line cljfmt indents a form with a :block rule as a body only when the first form of the body starts its own line. When it doesn't, the whole form is indented like a function call, and clojure-mode does the same: (if (pos? x) :pos :neg) The :block rules indented every form past the special arguments by 2 regardless, so the else branch above ended up under the `f' of `if'. Check where the body starts, for (:block 0) too, which had its own version of this check that didn't treat metadata on the first line as the start of the body. --- CHANGELOG.md | 1 + README.md | 7 +-- clojure-ts-mode.el | 61 ++++++++++-------------- test/clojure-ts-mode-cycling-test.el | 4 +- test/clojure-ts-mode-indentation-test.el | 20 ++++++-- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06d07e..cb5eeb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - [#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. ## 0.6.0 (2025-12-02) diff --git a/README.md b/README.md index 30ce713..3d90b7b 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index f9b09fc..4ee663c 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -1432,18 +1432,6 @@ 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) "Return how many value children of NODE's parent come before NODE. @@ -1459,19 +1447,26 @@ value it belongs to." (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." + (when-let* ((node (or node (treesit-node-first-child-for-pos parent bol))) + ((> (clojure-ts--value-position node) block))) + (let ((body (clojure-ts--node-child-skip-metadata parent (1+ block)))) + (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. @@ -1624,17 +1619,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. diff --git a/test/clojure-ts-mode-cycling-test.el b/test/clojure-ts-mode-cycling-test.el index 2ee4698..9d533a5 100644 --- a/test/clojure-ts-mode-cycling-test.el +++ b/test/clojure-ts-mode-cycling-test.el @@ -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)) diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index 64a6c29..f9e33bf 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -697,15 +697,29 @@ 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 not count metadata and comments for inner rules with an index" " From 4b327c4a0e0ad7d0406de9dc6b4ba4652f194dc4 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:00:00 +0200 Subject: [PATCH 2/3] Count discarded forms as values of block forms The :block rules skipped forms discarded with #_ when counting special arguments, while the call argument rule aligns with them. clojure-mode counts them everywhere, and so does cljfmt when deciding where the body starts and what to align with, so a #_ form on the first line made these come out differently in clojure-ts-mode: (when #_debug ready? (let [a 1] #_(prn a) (go)) a) Count them for the :block rules, like clojure-mode. cljfmt doesn't count them for the position of the line being indented, which looks accidental (it came with the support for #_ between map keys and values), so the body of (do #_x still indents as in clojure-mode. The index of an :inner rule still counts values only, as in cljfmt, which keeps the functions of (letfn #_[...] [...] indented as bodies. --- CHANGELOG.md | 1 + clojure-ts-mode.el | 28 ++++++++++++++++-------- test/clojure-ts-mode-indentation-test.el | 27 +++++++++++++++++++++-- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb5eeb0..cdd4fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - [#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. ## 0.6.0 (2025-12-02) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 4ee663c..d8e82d2 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -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)) @@ -1432,17 +1440,18 @@ changes." clojure-ts-semantic-indent-rules)))) (cdr clojure-ts--semantic-indent-rules-cache)) -(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)) @@ -1458,10 +1467,11 @@ 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." +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) block))) - (let ((body (clojure-ts--node-child-skip-metadata parent (1+ block)))) + ((> (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)) diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index f9e33bf..bb66f80 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -721,7 +721,25 @@ DESCRIPTION is a string with the description of the spec." (do ^:foo (a))") - (when-indenting-it "should not count metadata and comments for inner rules with an index" + (when-indenting-it "should count discarded forms as elements of block forms" + " +(when #_debug c + a)" + + " +(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] @@ -732,7 +750,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 From 14a9a7f8b76b825dfd27e2e4aa8a1d94a813d4ac Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:00:00 +0200 Subject: [PATCH 3/3] Add the let* indentation rule from cljfmt cljfmt indents let* like let, as [[:block 1]], and so does clojure-mode, through the rule for symbols that start with let. --- CHANGELOG.md | 1 + clojure-ts-mode.el | 1 + test/clojure-ts-mode-indentation-test.el | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd4fdd..d3de2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - [#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) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index d8e82d2..bd75cb5 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -1344,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))) diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index bb66f80..c7d2392 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -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)