From 684bd52073b838ac592cf99369506d280c96ae13 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sat, 7 Nov 2026 10:33:43 +0200 Subject: [PATCH 1/3] Tell the value children of a form apart by their type Getting the field of a node walks the siblings before it, and clojure-ts--value-position got the field of every sibling before a line, so the time to indent a form grew with the cube of its length: indent-region took 13 seconds on a comment form holding 400 functions, and 4.7 on cljs/core.cljs. In a collection, the field follows from the node's type, except for the prefix of a namespaced map, which comes right after its #. --- CHANGELOG.md | 1 + clojure-ts-mode.el | 18 +++++++++++++++--- test/clojure-ts-mode-indentation-test.el | 9 ++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9b908..70cc02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - [#169](https://github.com/clojure-emacs/clojure-ts-mode/pull/169): Add `clojure-ts-remove-unused-keys` (`C-c C-r k`), which removes the names the top-level form at point destructures from maps but never uses, along with their `:or` defaults. - [#164](https://github.com/clojure-emacs/clojure-ts-mode/pull/164): Support `setopt-local` for `clojure-ts-indent-style`, `clojure-ts-extra-def-forms` and `clojure-ts-comment-macro-font-lock-body`, which Emacs 31 refused to set locally and Emacs 32 warned about in `.dir-locals.el`. - [#168](https://github.com/clojure-emacs/clojure-ts-mode/pull/168): Indent docstrings like `clojure-mode`, moving the lines indented less than `clojure-ts-docstring-fill-prefix-width` to that column and keeping deeper ones, instead of leaving them alone with `clojure-ts-use-markdown-inline` and flattening them without it. +- [#174](https://github.com/clojure-emacs/clojure-ts-mode/pull/174): Speed up indentation of forms with many children, like a big `comment` form, which slowed down with the cube of their length. ## 0.6.0 (2025-12-02) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index e7288de..0db0364 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -981,9 +981,21 @@ If there is no namespace, returns nil." (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))))) +The parent has to be a collection, like a list, a map or a reader +conditional. If COUNT-DISCARDED is non-nil, a form discarded with #_ +counts too." + ;; Getting the field walks the siblings before NODE, which adds up in a + ;; form with many children, so it's told from the type where it can be. + (pcase (treesit-node-type node) + ("dis_expr" count-discarded) + ((or "comment" "meta_lit" "old_meta_lit" "ERROR") nil) + ;; The prefix of a namespaced map comes right after its #, or after an + ;; error in broken code, and only then is the field worth getting. + ((or "kwd_lit" "auto_res_mark") + (or (not (member (treesit-node-type (treesit-node-prev-sibling node)) + '("#" "ERROR"))) + (equal "value" (treesit-node-field-name node)))) + (_ (treesit-node-check node 'named)))) (defun clojure-ts--value-children (node) "Return the value children of NODE. diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index 0983064..796fa75 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -1475,4 +1475,11 @@ c) (when-aligning-it "should support namespaced maps" "#:hello {:world true :foo \"bar\" - :some-very-long \"value\"}")) + :some-very-long \"value\"}") + + (when-aligning-it "should support auto-resolved namespaced maps" + "#::{:world true + :foo \"bar\"}" + + "#::alias{:a 1 + :bbb 2}")) From ce61d698110a3b57fbed83353951a30cd5ea8052 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sat, 7 Nov 2026 10:51:19 +0200 Subject: [PATCH 2/3] Count the forms before a line only as far as needed Finding the indentation rule counted the forms before a line at every level it climbed, though only a rule for a given argument needs the count, and only to tell whether the line is at that argument. A block rule likewise only compares it with its number of special arguments. In a form with many children, each line paid for counting all the forms before it. --- clojure-ts-mode.el | 47 +++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 0db0364..b7fea8e 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -1438,17 +1438,18 @@ changes." clojure-ts-semantic-indent-rules)))) (cdr clojure-ts--semantic-indent-rules-cache)) -(defun clojure-ts--value-position (node &optional count-discarded) +(defun clojure-ts--value-position (node &optional count-discarded limit) "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. If COUNT-DISCARDED is non-nil, forms discarded -with #_ count too." +with #_ count too. If LIMIT is non-nil, stop counting once past it: the +result then only tells whether the position is greater than LIMIT." (let ((pos 0) (sibling (treesit-node-prev-sibling node))) - (while sibling + (while (and sibling (or (null limit) (<= pos limit))) (when (clojure-ts--counted-child-p sibling count-discarded) (setq pos (1+ pos))) (setq sibling (treesit-node-prev-sibling sibling))) @@ -1468,7 +1469,7 @@ 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))) + ((> (clojure-ts--value-position node t block) block))) (let ((body (clojure-ts--node-child-skip-metadata parent (1+ block) t))) (or (null body) (save-excursion @@ -1600,23 +1601,27 @@ syntax tree and recursively attempts to find a rule, incrementally increasing the CURRENT-DEPTH. If a rule is not found upon reaching the root of the syntax tree, it returns nil. A rule is considered a match only if the CURRENT-DEPTH matches the rule's required depth." - (let* ((idx (1- (clojure-ts--value-position node)))) - (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node parent)) - ((not (equal rule-set :not-found)))) - (seq-find (lambda (rule) - (pcase-let ((`(,rule-type ,rule-depth ,rule-idx) rule)) - (if (equal rule-type :block) - (zerop current-depth) - (and (equal rule-depth current-depth) - (or (null rule-idx) - (equal rule-idx idx)))))) - rule-set) - ;; Let's go no more than 3 levels up to avoid performance degradation. - (when-let* (((< current-depth 3)) - (new-parent (treesit-node-parent parent))) - (clojure-ts--find-semantic-rule parent - new-parent - (1+ current-depth)))))) + (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node parent)) + ((not (equal rule-set :not-found)))) + (seq-find (lambda (rule) + (pcase-let ((`(,rule-type ,rule-depth ,rule-idx) rule)) + (if (equal rule-type :block) + (zerop current-depth) + (and (equal rule-depth current-depth) + (or (null rule-idx) + ;; Only counted when a rule needs it, and only + ;; as far as needed, as it takes longer the more + ;; forms come before NODE. + (equal rule-idx + (1- (clojure-ts--value-position + node nil (1+ rule-idx))))))))) + rule-set) + ;; Let's go no more than 3 levels up to avoid performance degradation. + (when-let* (((< current-depth 3)) + (new-parent (treesit-node-parent parent))) + (clojure-ts--find-semantic-rule parent + new-parent + (1+ current-depth))))) (defun clojure-ts--match-form-body (node parent bol) "Match if NODE has to be indented as a form body. From ee879d0a7245752e8d6d689f9f7134cdce287f9b Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sat, 7 Nov 2026 10:51:19 +0200 Subject: [PATCH 3/3] Tell an opening paren by its type clojure-ts--anchor-parent-opening-paren searches the parent for its paren, and the search tests the parent itself first, so getting the node's text copied the whole form for every line in its body. --- clojure-ts-mode.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index b7fea8e..17049c7 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -933,7 +933,7 @@ literals with regex grammar." (defun clojure-ts--opening-paren-node-p (node) "Return non-nil if NODE is an opening paren." - (string-equal "(" (treesit-node-text node))) + (string-equal "(" (treesit-node-type node))) (defun clojure-ts--symbol-node-p (node) "Return non-nil if NODE is a Clojure symbol."