diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b9fed..050ccc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Fix font-lock issues: duplicate query, missing `definline` metadata docstring. - Fix missing builtin symbols in font-lock regexp. - Improve performance for imenu, font-lock and indentation. +- Improve performance for `clojure-ts-align` by caching indentation rules. ## 0.6.0 (2025-12-02) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 6244349..980d21d 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -1442,11 +1442,18 @@ If NS is defined, then the fully qualified symbol is passed to (seq-sort (lambda (spec1 _spec2) (equal (car spec1) :block))))))))) -(defun clojure-ts--find-semantic-rules-for-node (node) - "Return a list of semantic rules for NODE." - (let* ((first-child (clojure-ts--node-child-skip-metadata node 0)) - (symbol-name (clojure-ts--named-node-text first-child)) - (symbol-namespace (clojure-ts--node-namespace-text first-child))) +(defvar-local clojure-ts--dynamic-indent-for-symbol-cache + (make-hash-table :test 'equal)) + +(defvar-local clojure-ts--use-dynamic-indent-cache nil + "If set to nil, do not use cache for dynamic indentation rules.") + +(defun clojure-ts--find-semantic-rules-for-symbol (node) + "Return a list of semantic rules for symbol NODE. + +If rules are not found return :not-found symbol." + (let ((symbol-name (clojure-ts--named-node-text node)) + (symbol-namespace (clojure-ts--node-namespace-text node))) (or (clojure-ts--dynamic-indent-for-symbol symbol-name symbol-namespace) (alist-get symbol-name clojure-ts--semantic-indent-rules-cache @@ -1454,6 +1461,20 @@ If NS is defined, then the fully qualified symbol is passed to nil #'equal)))) +(defun clojure-ts--find-semantic-rules-for-node (node) + "Return a list of semantic rules for NODE." + (let ((first-child (clojure-ts--first-value-child node))) + (if clojure-ts--use-dynamic-indent-cache + ;; `with-memoization' can't cache nil, so a sentinel stands in for "no + ;; rules" and is translated back before returning. + (let ((rules (with-memoization + (gethash (treesit-node-text first-child) + clojure-ts--dynamic-indent-for-symbol-cache) + (or (clojure-ts--find-semantic-rules-for-symbol first-child) + :not-found)))) + (unless (eq rules :not-found) rules)) + (clojure-ts--find-semantic-rules-for-symbol first-child)))) + (defun clojure-ts--find-semantic-rule (node parent current-depth) "Return a suitable indentation rule for NODE, considering the CURRENT-DEPTH. @@ -1464,7 +1485,8 @@ 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 (- (treesit-node-index node) 2))) - (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node parent))) + (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node parent)) + ((not (equal rule-set :not-found)))) (if (zerop current-depth) (let ((rule (car rule-set))) (if (equal (car rule) :block) @@ -1899,6 +1921,7 @@ subsequent special arguments based on block indentation rules." ;; indentation rules. First node to skip is the symbol itself. (when (equal sexp-type 'cond) (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node node)) + ((not (equal rule-set :not-found))) (rule (car rule-set)) ((equal (car rule) :block))) (treesit-beginning-of-thing 'sexp (1- (- (cadr rule))) 'restrict) @@ -1919,9 +1942,11 @@ between BEG and END." (end (clojure-ts--end-of-defun-pos))) (list start end)))))) (setq end (copy-marker end)) + (clrhash clojure-ts--dynamic-indent-for-symbol-cache) (let* ((sexps-to-align (clojure-ts--get-nodes-to-align beg (marker-position end))) ;; We have to disable it here to avoid endless recursion. - (clojure-ts-align-forms-automatically nil)) + (clojure-ts-align-forms-automatically nil) + (clojure-ts--use-dynamic-indent-cache t)) (save-excursion (indent-region beg (marker-position end)) (dolist (sexp sexps-to-align) diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index cd1e68f..0835f91 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -759,7 +759,24 @@ b |20])" (it "should remove extra commas" (with-clojure-ts-buffer-point "{|:a 2, ,:c 4}" (call-interactively #'clojure-ts-align) - (expect (buffer-string) :to-equal "{:a 2, :c 4}")))) + (expect (buffer-string) :to-equal "{:a 2, :c 4}"))) + + (it "should not fail to align a cond form without a semantic indentation rule" + ;; A symbol added to `clojure-ts-align-cond-forms' need not have a matching + ;; semantic indentation rule. When it has none, alignment must fall back + ;; gracefully instead of raising `(wrong-type-argument listp :not-found)'. + (with-clojure-ts-buffer-point " +(my-cond + |:a 1 + :bbbb 2 + :cc 3)" + (setq-local clojure-ts-align-cond-forms (cons "my-cond" clojure-ts-align-cond-forms)) + (call-interactively #'clojure-ts-align) + (expect (buffer-string) :to-equal " +(my-cond + :a 1 + :bbbb 2 + :cc 3)")))) (describe "clojure-ts-align-forms-automatically" ;; Copied from `clojure-mode'