diff --git a/CHANGELOG.md b/CHANGELOG.md index f06d07e..d3de2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) 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..bd75cb5 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)) @@ -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))) @@ -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. @@ -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. 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..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) @@ -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] @@ -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