From 28393ede94e1a25c96d8bb5dfb1b4a1b3f448a3e Mon Sep 17 00:00:00 2001 From: Roman Rudakov Date: Mon, 22 Jun 2026 13:13:39 +0200 Subject: [PATCH] Improve performance of clojure-ts-align Cache semantic indentation rules for a single clojure-ts-align run to avoid lookup for every repeating symbol. --- CHANGELOG.md | 1 + clojure-ts-mode.el | 37 +++++++++++++---- test/clojure-ts-mode-indentation-test.el | 53 ++++++++++++++++-------- 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b24bb6..238bf36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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..31e360f 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -1442,17 +1442,34 @@ 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 clojure-ts--dynamic-indent-for-symbol-cache + (make-hash-table :test 'equal)) + +(defvar clojure-ts--dynamic-indent-for-symbol-cache-p 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 nil nil - #'equal)))) + #'equal) + :not-found))) + +(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)) + (symbol-full-name (treesit-node-text first-child))) + (if clojure-ts--dynamic-indent-for-symbol-cache-p + (with-memoization (gethash symbol-full-name clojure-ts--dynamic-indent-for-symbol-cache) + (clojure-ts--find-semantic-rules-for-symbol first-child)) + (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 +1481,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 +1917,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 +1938,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--dynamic-indent-for-symbol-cache-p 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 c62ff5c..7c76bb4 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -43,17 +43,17 @@ represents the expected position of point. DESCRIPTION is a string with the description of the spec." (declare (indent 1)) `(it ,description - (let* ((after ,after) - (expected-cursor-pos (1+ (clojure-ts--s-index-of "|" after))) - (expected-state (delete ?| after))) - (with-clojure-ts-buffer ,before - (goto-char (point-min)) - (search-forward "|") - (delete-char -1) - (font-lock-ensure) - (indent-according-to-mode) - (expect (buffer-string) :to-equal expected-state) - (expect (point) :to-equal expected-cursor-pos))))) + (let* ((after ,after) + (expected-cursor-pos (1+ (clojure-ts--s-index-of "|" after))) + (expected-state (delete ?| after))) + (with-clojure-ts-buffer ,before + (goto-char (point-min)) + (search-forward "|") + (delete-char -1) + (font-lock-ensure) + (indent-according-to-mode) + (expect (buffer-string) :to-equal expected-state) + (expect (point) :to-equal expected-cursor-pos))))) @@ -698,7 +698,7 @@ DESCRIPTION is a string with the description of the spec." (with-clojure-ts-buffer-point " (let [a-long-name 10 b |20])" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " (let [a-long-name 10 b 20])")) @@ -713,7 +713,7 @@ b |20])" :a #long \"1234\" :b {:this \"is\" :nested \"map\"}}])" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " (let [^long my-map {:hello \"World\" ;Hello :foo @@ -730,7 +730,7 @@ b |20])" |123 \"Hello\" 99999 \"World\" 234 nil)" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " (condp = 2 123 \"Hello\" @@ -741,7 +741,7 @@ b |20])" (with-clojure-ts-buffer-point " #?(:clj 2 |:cljs 2)" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " #?(:clj 2 :cljs 2)"))) @@ -750,7 +750,7 @@ b |20])" (with-clojure-ts-buffer-point " #?(:clj 2 |:cljs 2)" - (setq-local clojure-ts-align-reader-conditionals t) + (setq-local clojure-ts-align-reader-conditionals t) (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " #?(:clj 2 @@ -758,8 +758,25 @@ 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}")))) + (call-interactively #'clojure-ts-align) + (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'