From 920045f4e9b67ba0a3d9f30179d311afbcdd7e45 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:16:51 +0200 Subject: [PATCH 01/13] Add indentation tests for defmethod, multi-arity defn, nested letfn, and try/catch/finally These forms all have indent specs but lacked dedicated tests verifying their indentation output: - defmethod with dispatch values (:defn style) - Multi-arity defn with and without docstrings/metadata - Nested letfn verifying the complex '(1 ((:defn)) nil) spec propagates correctly through multiple nesting levels - try/catch/finally verifying specs 0/2/0 --- test/clojure-mode-indentation-test.el | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 9537dd54..bf2de6f0 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -512,6 +512,60 @@ DESCRIPTION is a string with the description of the spec." (with-out-binding [out messages] (.flush out))))") + (when-indenting-it "should handle defmethod" + " +(defmethod foo :bar + [x] + (println x))" + + " +(defmethod foo :default + [x y] + (+ x y))") + + (when-indenting-it "should handle multi-arity defn" + " +(defn foo + ([x] + (foo x 1)) + ([x y] + (+ x y)))" + + " +(defn foo + \"docstring\" + ([x] + (foo x 1)) + ([x y] + (+ x y)))" + + " +(defn ^:private foo + ([x] + (foo x 1)) + ([x y] + (+ x y)))") + + (when-indenting-it "should handle nested letfn" + " +(letfn [(foo [x] + (let [y (inc x)] + (* y y))) + (bar [a] + (letfn [(baz [b] + (+ a b))] + (baz 1)))] + (foo (bar 10)))") + + (when-indenting-it "should handle try/catch/finally" + " +(try + (dangerous) + (catch Exception e + (println e)) + (finally + (cleanup)))") + (when-indenting-it "should handle reader conditionals" "#?@ (:clj [] :cljs [])") From 6e773a2e80301e309b2885efde52e25f76090676 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:17:24 +0200 Subject: [PATCH 02/13] Add indentation tests for as->, with-* fallback, and def* fallback Test the as-> form (spec 2) and two implicit indentation behaviors in clojure-indent-function: - with-* forms get body-style indentation even without explicit indent specs (e.g., with-custom-thing) - Unknown def* forms (defwhatever, ns/defwhatever) also get body-style indentation via the fallback regex match --- test/clojure-mode-indentation-test.el | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index bf2de6f0..d2d9ee7c 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -566,6 +566,36 @@ DESCRIPTION is a string with the description of the spec." (finally (cleanup)))") + (when-indenting-it "should handle as->" + " +(as-> x $ + (inc $) + (* $ 2))") + + (when-indenting-it "should indent with-* forms like body even without explicit specs" + " +(with-open [f (io/reader \"x\")] + (slurp f))" + + " +(with-redefs [foo bar] + (test-stuff))" + + " +(with-custom-thing [x y] + (body x y))") + + (when-indenting-it "should indent unknown def forms like body" + " +(defwhatever my-thing + :some-option true + :another false)" + + " +(my.ns/defwhatever my-thing + :some-option true + :another false)") + (when-indenting-it "should handle reader conditionals" "#?@ (:clj [] :cljs [])") From 21e229b16dc9a6ecfe489adebae060e0ed937f57 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:17:54 +0200 Subject: [PATCH 03/13] Add indentation tests for condp and namespace-qualified special forms Test condp (spec 2, two special args before body) and verify that namespace-qualified versions of special forms (clojure.core/let, clojure.core/when, clojure.core/defn) indent correctly via the backtracking mechanism. --- test/clojure-mode-indentation-test.el | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index d2d9ee7c..ee1c5656 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -585,6 +585,27 @@ DESCRIPTION is a string with the description of the spec." (with-custom-thing [x y] (body x y))") + (when-indenting-it "should handle condp" + " +(condp = x + 1 \"one\" + 2 \"two\" + \"other\")") + + (when-indenting-it "should handle namespace-qualified special forms" + " +(clojure.core/let [x 1] + (inc x))" + + " +(clojure.core/when true + (do-stuff))" + + " +(clojure.core/defn foo + [x] + (inc x))") + (when-indenting-it "should indent unknown def forms like body" " (defwhatever my-thing From 6c7ece93b6c18fa09ed007a6c6143a33c22a7a26 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:21:29 +0200 Subject: [PATCH 04/13] Add indentation tests for fn, def, and bound-fn These :defn-style forms had indent specs but no dedicated tests verifying body indentation. Includes fn with name, multi-arity fn, and def with metadata. --- test/clojure-mode-indentation-test.el | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index ee1c5656..72179907 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -617,6 +617,36 @@ DESCRIPTION is a string with the description of the spec." :some-option true :another false)") + (when-indenting-it "should handle fn" + " +(fn [x] + (inc x))" + + " +(fn my-fn [x] + (inc x))" + + " +(fn + ([x] + (inc x)) + ([x y] + (+ x y)))") + + (when-indenting-it "should handle def" + " +(def x + (+ 1 2))" + + " +(def ^:dynamic *x* + 42)") + + (when-indenting-it "should handle bound-fn" + " +(bound-fn [x] + (inc x))") + (when-indenting-it "should handle reader conditionals" "#?@ (:clj [] :cljs [])") From e279bdde18ed43aabfb01177e130a37cf6a27ff7 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:21:53 +0200 Subject: [PATCH 05/13] Add indentation tests for control flow and body-0 forms Test if, if-not, case, when, when-not, when-first, while (spec 1), and do, delay, future, comment (spec 0). --- test/clojure-mode-indentation-test.el | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 72179907..6abb126b 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -647,6 +647,68 @@ DESCRIPTION is a string with the description of the spec." (bound-fn [x] (inc x))") + (when-indenting-it "should handle if" + " +(if (even? x) + (inc x) + (dec x))") + + (when-indenting-it "should handle if-not" + " +(if-not (nil? x) + (use x) + (default))") + + (when-indenting-it "should handle case" + " +(case x + :a 1 + :b 2 + 3)") + + (when-indenting-it "should handle when" + " +(when (pos? x) + (println x) + (inc x))") + + (when-indenting-it "should handle when-not" + " +(when-not (nil? x) + (println x))") + + (when-indenting-it "should handle when-first" + " +(when-first [x xs] + (println x))") + + (when-indenting-it "should handle while" + " +(while (pos? @counter) + (swap! counter dec))") + + (when-indenting-it "should handle do" + " +(do + (println 1) + (println 2))") + + (when-indenting-it "should handle delay" + " +(delay + (expensive-computation))") + + (when-indenting-it "should handle future" + " +(future + (long-running-task))") + + (when-indenting-it "should handle comment" + " +(comment + (foo 1 2) + (bar 3 4))") + (when-indenting-it "should handle reader conditionals" "#?@ (:clj [] :cljs [])") From f72578f1c0f146fc87f8391692728255a04a856a Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:22:18 +0200 Subject: [PATCH 06/13] Add indentation tests for binding, iteration, and special-arg forms Test binding, loop, for, doseq, dotimes, when-let, if-let, when-some, if-some, doto, locking, fdef, and this-as. All use spec 1 (one special arg before body). --- test/clojure-mode-indentation-test.el | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 6abb126b..8021d729 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -709,6 +709,77 @@ DESCRIPTION is a string with the description of the spec." (foo 1 2) (bar 3 4))") + (when-indenting-it "should handle binding" + " +(binding [*out* writer] + (println \"hello\"))") + + (when-indenting-it "should handle loop" + " +(loop [i 0] + (when (< i 10) + (recur (inc i))))") + + (when-indenting-it "should handle for" + " +(for [x (range 10) + :when (even? x)] + (* x x))") + + (when-indenting-it "should handle doseq" + " +(doseq [x xs] + (println x))") + + (when-indenting-it "should handle dotimes" + " +(dotimes [i 10] + (println i))") + + (when-indenting-it "should handle when-let" + " +(when-let [x (foo)] + (bar x))") + + (when-indenting-it "should handle if-let" + " +(if-let [x (foo)] + (bar x) + (baz))") + + (when-indenting-it "should handle when-some" + " +(when-some [x (foo)] + (bar x))") + + (when-indenting-it "should handle if-some" + " +(if-some [x (foo)] + (bar x) + (baz))") + + (when-indenting-it "should handle doto" + " +(doto (java.util.HashMap.) + (.put \"a\" 1) + (.put \"b\" 2))") + + (when-indenting-it "should handle locking" + " +(locking obj + (alter-state! obj))") + + (when-indenting-it "should handle fdef" + " +(fdef my-fn + :args (s/cat :x int?) + :ret int?)") + + (when-indenting-it "should handle this-as" + " +(this-as self + (.method self))") + (when-indenting-it "should handle reader conditionals" "#?@ (:clj [] :cljs [])") From 30524274cd0c6da274e5d13e2c0d7e8545471f19 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:22:44 +0200 Subject: [PATCH 07/13] Add indentation tests for clojure.test, core.logic, and core.async forms Test library-specific indent specs: - clojure.test: testing (1), deftest (:defn), are (2), use-fixtures (:defn), async (1) - core.logic: run (:defn), run* (:defn), fresh (:defn) - core.async: go (0), go-loop (1), thread (0), alt! (0), alt!! (0) --- test/clojure-mode-indentation-test.el | 75 +++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 8021d729..957070fb 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -780,6 +780,81 @@ DESCRIPTION is a string with the description of the spec." (this-as self (.method self))") + ;; clojure.test + (when-indenting-it "should handle testing" + " +(testing \"some feature\" + (is (= 1 1)))") + + (when-indenting-it "should handle deftest" + " +(deftest my-test + (is (= 1 1)))") + + (when-indenting-it "should handle are" + " +(are [x y] (= x y) + 1 1 + 2 2)") + + (when-indenting-it "should handle use-fixtures" + " +(use-fixtures :each + my-fixture)") + + (when-indenting-it "should handle async" + " +(async done + (do-stuff) + (done))") + + ;; core.logic + (when-indenting-it "should handle run" + " +(run [q] + (== q 1))") + + (when-indenting-it "should handle run*" + " +(run* [q] + (== q 1))") + + (when-indenting-it "should handle fresh" + " +(fresh [a b] + (== a 1) + (== b 2))") + + ;; core.async + (when-indenting-it "should handle go" + " +(go + (! ch x) + (recur (inc x)))") + + (when-indenting-it "should handle thread" + " +(thread + (blocking-op))") + + (when-indenting-it "should handle alt!" + " +(alt! + ch1 ([v] (println v)) + ch2 ([v] (println v)))") + + (when-indenting-it "should handle alt!!" + " +(alt!! + ch1 ([v] (println v)) + ch2 ([v] (println v)))") + (when-indenting-it "should handle reader conditionals" "#?@ (:clj [] :cljs [])") From 2007e530899cde4d258f602df73bc18a5d1820d2 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:49:16 +0200 Subject: [PATCH 08/13] Improve documentation for backtracking indentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backtracking indent mechanism was poorly documented — the README showed incorrect specs and didn't explain how positional indexing works, and the docstrings were minimal. README changes: - Replace the vague "more sophisticated indent specifications" section with a detailed explanation of how backtracking works, including a table of spec elements and annotated examples for letfn and defrecord - Update the built-in specs to match the actual code - Rewrite the performance section with specifics about which forms break when backtracking is disabled Docstring changes: - clojure-use-backtracking-indent: explain what it does and what breaks when disabled - clojure-max-backtracking: clarify relationship to backtracking - clojure--find-indent-spec-backtracking: document the algorithm and positional indexing with an example - clojure-indent-function: document list specs and the def*/with-* fallback behavior - put-clojure-indent: document all allowed INDENT values with examples --- README.md | 88 ++++++++++++++++++++++++++++++++++++++----------- clojure-mode.el | 55 +++++++++++++++++++++++++------ 2 files changed, 114 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index d9259f00..9c96146e 100644 --- a/README.md +++ b/README.md @@ -239,28 +239,75 @@ prefixed with some ns (or ns alias): (put-clojure-indent 'my-ns/do 1) ``` -The bodies of certain more complicated macros and special forms -(e.g. `letfn`, `deftype`, `extend-protocol`, etc) are indented using -a contextual backtracking indentation method, require more sophisticated -indent specifications. Here are a few examples: +##### Backtracking (contextual) indentation + +Certain macros and special forms (e.g. `letfn`, `deftype`, +`extend-protocol`) contain *nested* sub-forms that each need their +own indentation style. For these, `clojure-mode` uses **backtracking +indentation**: when indenting a line, it walks up the sexp tree to +find a parent form with an indent spec, then uses the current +position within that spec to decide how to indent. + +A backtracking indent spec is a **quoted list** where each element +controls indentation at the corresponding argument position +(0-indexed). The allowed elements are: + +| Element | Meaning | +|---------|---------| +| An integer N | First N args are "special" (indented further); rest are body | +| `:defn` | Indent like a function/macro body | +| `:form` | Indent like a regular form | +| `nil` | Use default indentation rules | +| A list `(SPEC)` | This position holds a **list of forms**, each indented according to SPEC | + +For example, `letfn` uses `'(1 ((:defn)) nil)`: + +```clojure +(letfn [(twice [x] ;; pos 0 → spec 1 (1 special arg = the binding vector) + (* x 2)) ;; inside binding → spec ((:defn)) applies: + (thrice [x] ;; each binding is a list of :defn-style forms + (* x 3))] ;; so function bodies get :defn indentation + (+ (twice 5) ;; pos 1+ → spec nil (default → body indentation) + (thrice 5))) +``` + +And `defrecord` uses `'(2 nil nil (:defn))`: + +```clojure +(defrecord MyRecord ;; pos 0 → spec 2 (2 special args: name + fields) + [field1 field2] ;; pos 1 → spec nil (within special args zone) + SomeProtocol ;; pos 2 → spec nil + (some-method [this] ;; pos 3+ → spec (:defn) — each method gets :defn-style + (do-stuff this))) +``` + +Here are the built-in backtracking specs: ```el (define-clojure-indent - (implement '(1 (1))) - (letfn '(1 ((:defn)) nil)) - (proxy '(2 nil nil (1))) - (reify '(:defn (1))) - (deftype '(2 nil nil (1))) - (defrecord '(2 nil nil (1))) - (specify '(1 (1))) - (specify '(1 (1)))) + (letfn '(1 ((:defn)) nil)) + (deftype '(2 nil nil (:defn))) + (defrecord '(2 nil nil (:defn))) + (defprotocol '(1 (:defn))) + (definterface '(1 (:defn))) + (reify '(:defn (1))) + (proxy '(2 nil nil (:defn))) + (extend-protocol '(1 :defn)) + (extend-type '(1 :defn)) + (specify '(1 :defn)) + (specify! '(1 :defn))) ``` These follow the same rules as the `:style/indent` metadata specified by [cider-nrepl][]. -For instructions on how to write these specifications, see +For more details on writing indent specifications, see [this document](https://docs.cider.mx/cider/indent_spec.html). The only difference is that you're allowed to use lists instead of vectors. +Backtracking is controlled by `clojure-use-backtracking-indent` +(default `t`) and limited to `clojure-max-backtracking` levels +(default 3). Disabling backtracking will break indentation for +all forms with list-based specs. + The indentation of [special arguments](https://docs.cider.mx/cider/indent_spec.html#special-arguments) is controlled by `clojure-special-arg-indent-factor`, which by default indents special arguments a further `lisp-body-indent` when compared to ordinary arguments. @@ -724,13 +771,16 @@ See [this ticket](https://github.com/clojure-emacs/clojure-mode/issues/270) for ### Indentation Performance -`clojure-mode`'s indentation engine is a bit slow. You can speed things up -significantly by disabling `clojure-use-backtracking-indent`, but this will -break the indentation of complex forms like `deftype`, `defprotocol`, `reify`, -`letfn`, etc. +`clojure-mode`'s indentation engine is a bit slow due to the +[backtracking indentation](#backtracking-contextual-indentation) logic +that walks up the sexp tree for context. You can speed things up +significantly by setting `clojure-use-backtracking-indent` to `nil`, +but this will break the indentation of forms with list-based specs +(`deftype`, `defrecord`, `defprotocol`, `definterface`, `reify`, +`proxy`, `letfn`, `extend-type`, `extend-protocol`, `specify`, +`specify!`). Simple integer and `:defn` specs will continue to work. -We should look into ways to optimize the performance of the backtracking -indentation logic. See [this ticket](https://github.com/clojure-emacs/clojure-mode/issues/606) for more +See [this ticket](https://github.com/clojure-emacs/clojure-mode/issues/606) for more details. ### Font-locking Implementation diff --git a/clojure-mode.el b/clojure-mode.el index ed8d49c9..adceb4a4 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -197,12 +197,19 @@ to indent keyword invocation forms. :package-version '(clojure-mode . "5.19.0")) (defcustom clojure-use-backtracking-indent t - "When non-nil, enable context sensitive indentation." + "When non-nil, enable context-sensitive indentation. +When indenting a line, walk up the sexp tree to find a parent +form with an indent spec, then use the current position within +that spec to determine indentation. This is required for forms +with list-based indent specs like `letfn', `deftype', `defrecord', +`reify', `proxy', etc. Disabling this speeds up indentation but +breaks those forms." :type 'boolean :safe 'booleanp) (defcustom clojure-max-backtracking 3 - "Maximum amount to backtrack up a list to check for context." + "Maximum number of levels to walk up the sexp tree for indent context. +Only relevant when `clojure-use-backtracking-indent' is non-nil." :type 'integer :safe 'integerp) @@ -1694,8 +1701,15 @@ symbol properties." (defvar clojure--current-backtracking-depth 0) (defun clojure--find-indent-spec-backtracking () - "Return the indent sexp that applies to the sexp at point. -Implementation function for `clojure--find-indent-spec'." + "Return the indent spec that applies to the sexp at point. +Walk up the sexp tree (up to `clojure-max-backtracking' levels) +to find a parent form with an indent spec, then use the current +position within that parent to index into its spec. + +For a list spec like (1 ((:defn)) nil), position 0 yields 1, +position 1 yields ((:defn)), and position 2+ yields nil. A +sub-spec wrapped in a list like ((:defn)) means \"this position +holds a list of forms, each indented with :defn style\"." (when (and (>= clojure-max-backtracking clojure--current-backtracking-depth) (not (looking-at "^"))) (let ((clojure--current-backtracking-depth (1+ clojure--current-backtracking-depth)) @@ -1836,14 +1850,23 @@ the indentation. The property value can be -- `:defn', meaning indent `defn'-style; +- `:defn', meaning indent `defn'-style (body indentation); - an integer N, meaning indent the first N arguments specially - like ordinary function arguments and then indent any further - arguments like a body; + (further indented) and then indent any further arguments like + a body; - a function to call just as this function was called. If that function returns nil, that means it doesn't specify - the indentation. -- a list, which is used by `clojure-backtracking-indent'. + the indentation; +- a list, used for backtracking indentation of complex forms. + Each element controls indentation at the corresponding argument + position. Elements can be integers, `:defn', `:form', nil, or + a nested list like ((:defn)) meaning \"a list of :defn-style + forms\". See `clojure--find-indent-spec-backtracking' for + details. + +When no indent spec is found, forms starting with `def' or `with-' +get body-style indentation, and forms starting with `:' use +`clojure-indent-keyword-style'. This function also returns nil meaning don't specify the indentation." ;; Goto to the open-paren. @@ -1904,7 +1927,19 @@ This function also returns nil meaning don't specify the indentation." ;;; Setting indentation (defun put-clojure-indent (sym indent) - "Instruct `clojure-indent-function' to indent the body of SYM by INDENT." + "Set the indentation spec of SYM to INDENT. + +INDENT can be: +- `:defn' — indent like a function/macro body +- an integer N — N special args, then body +- a function — custom indentation function +- a quoted list — positional backtracking spec (see + `clojure--find-indent-spec-backtracking') + +Examples: + (put-clojure-indent \\='when 1) + (put-clojure-indent \\='>defn :defn) + (put-clojure-indent \\='letfn \\='(1 ((:defn)) nil))" (put sym 'clojure-indent-function indent)) (defun clojure--maybe-quoted-symbol-p (x) From 2cc7ed7516409484bde8a62376f87a10efe59f7f Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:54:18 +0200 Subject: [PATCH 09/13] Add tests for clojure-indent-keyword-style Test all three values (always-align, always-indent, align-arguments) with :require forms inside ns, covering both case A (arg on same line as keyword) and case B (no args on same line). --- test/clojure-mode-indentation-test.el | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 957070fb..bfb4f828 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1214,6 +1214,40 @@ x '(put-clojure-indent 'foo "bar")) :to-throw))) +(describe "clojure-indent-keyword-style" + (it "should align keyword forms with always-align (default)" + (let ((clojure-indent-keyword-style 'always-align)) + ;; Case A: arg on same line → align with it + (with-clojure-buffer "\n(ns foo\n(:require\n[bar]))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(ns foo\n (:require\n [bar]))")) + ;; Case B: no arg on same line → align with keyword + (with-clojure-buffer "\n(ns foo\n(:require [bar]\n[baz]))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(ns foo\n (:require [bar]\n [baz]))")))) + + (it "should indent keyword forms with always-indent" + (let ((clojure-indent-keyword-style 'always-indent)) + ;; Case A: arg on same line → still indented like body + (with-clojure-buffer "\n(ns foo\n(:require [bar]\n[baz]))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(ns foo\n (:require [bar]\n [baz]))")) + ;; Case B: no arg on same line → indented like body + (with-clojure-buffer "\n(ns foo\n(:require\n[bar]))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(ns foo\n (:require\n [bar]))")))) + + (it "should indent keyword forms with align-arguments" + (let ((clojure-indent-keyword-style 'align-arguments)) + ;; Case A: arg on same line → align with first arg + (with-clojure-buffer "\n(ns foo\n(:require [bar]\n[baz]))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(ns foo\n (:require [bar]\n [baz]))")) + ;; Case B: no arg on same line → indented like body + (with-clojure-buffer "\n(ns foo\n(:require\n[bar]))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(ns foo\n (:require\n [bar]))"))))) + (provide 'clojure-mode-indentation-test) ;;; clojure-mode-indentation-test.el ends here From 9a1a8e1fbd4f5b3ab9a0602eaf135a9919db4e0d Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:54:43 +0200 Subject: [PATCH 10/13] Add tests for clojure-use-backtracking-indent Verify that when backtracking is disabled: - Simple specs (integer, :defn) still work correctly - Complex list-based specs (letfn) lose nested context --- test/clojure-mode-indentation-test.el | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index bfb4f828..de7e6916 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1248,6 +1248,31 @@ x (indent-region (point-min) (point-max)) (expect (buffer-string) :to-equal "\n(ns foo\n (:require\n [bar]))"))))) +(describe "clojure-use-backtracking-indent" + (it "should still indent simple specs correctly when disabled" + (let ((clojure-use-backtracking-indent nil)) + ;; Integer spec (when has spec 1) + (with-clojure-buffer "\n(when true\nbody)" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(when true\n body)")) + ;; :defn spec + (with-clojure-buffer "\n(defn foo\n[x]\nx)" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(defn foo\n [x]\n x)")))) + + (it "should lose context for complex specs when disabled" + (let ((clojure-use-backtracking-indent nil)) + ;; Without backtracking, the body of a letfn binding won't get + ;; :defn-style indentation because the backtracking that walks + ;; up to letfn's spec (1 ((:defn)) nil) is disabled. + (with-clojure-buffer "\n(letfn [(foo [x]\n(+ x 1))]\n(foo 1))" + (indent-region (point-min) (point-max)) + ;; The body of foo should NOT get :defn-style (2-space) indent + ;; relative to foo — instead it gets default alignment. + (let ((result (buffer-string))) + ;; Just verify it differs from the backtracking result + (expect result :not :to-equal "\n(letfn [(foo [x]\n (+ x 1))]\n (foo 1))")))))) + (provide 'clojure-mode-indentation-test) ;;; clojure-mode-indentation-test.el ends here From 8c18bc592a4260fdae35d96b2206e25513b7f81c Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:55:05 +0200 Subject: [PATCH 11/13] Add tests for clojure-max-backtracking Verify that setting max-backtracking to 0 prevents context lookup for nested forms, while the default depth of 3 allows it. --- test/clojure-mode-indentation-test.el | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index de7e6916..8671bf69 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1273,6 +1273,23 @@ x ;; Just verify it differs from the backtracking result (expect result :not :to-equal "\n(letfn [(foo [x]\n (+ x 1))]\n (foo 1))")))))) +(describe "clojure-max-backtracking" + (it "should limit how far up the sexp tree backtracking goes" + ;; With max-backtracking = 0, even one level of backtracking is + ;; disabled, so letfn bindings lose :defn-style indentation. + (let ((clojure-max-backtracking 0)) + (with-clojure-buffer "\n(letfn [(foo [x]\n(+ x 1))]\n(foo 1))" + (indent-region (point-min) (point-max)) + (let ((result (buffer-string))) + (expect result :not :to-equal "\n(letfn [(foo [x]\n (+ x 1))]\n (foo 1))"))))) + + (it "should indent correctly with sufficient backtracking depth" + ;; With the default depth (3), letfn works fine. + (let ((clojure-max-backtracking 3)) + (with-clojure-buffer "\n(letfn [(foo [x]\n(+ x 1))]\n(foo 1))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(letfn [(foo [x]\n (+ x 1))]\n (foo 1))"))))) + (provide 'clojure-mode-indentation-test) ;;; clojure-mode-indentation-test.el ends here From 5c42d7711007f35729e7f1f96b124ac41f6a728f Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:55:33 +0200 Subject: [PATCH 12/13] Add tests for clojure-enable-indent-specs Verify that when indent specs are disabled: - Forms like let and when get uniform (function-call) indentation instead of their special body-style indentation - The def*/with-* fallback still applies regardless, since it's handled outside the spec lookup --- test/clojure-mode-indentation-test.el | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 8671bf69..91c3c3bd 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1290,6 +1290,27 @@ x (indent-region (point-min) (point-max)) (expect (buffer-string) :to-equal "\n(letfn [(foo [x]\n (+ x 1))]\n (foo 1))"))))) +(describe "clojure-enable-indent-specs" + (it "should use uniform indentation when disabled" + (let ((clojure-enable-indent-specs nil)) + ;; let normally gets spec 1, but with specs disabled it should + ;; indent like a regular function call. + (with-clojure-buffer "\n(let [x 1]\nx)" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(let [x 1]\n x)")) + ;; when normally gets spec 1 + (with-clojure-buffer "\n(when true\nbody)" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(when true\n body)")))) + + (it "should still indent def*/with-* forms like body when specs are disabled" + ;; The def*/with-* fallback in clojure-indent-function fires + ;; regardless of clojure-enable-indent-specs. + (let ((clojure-enable-indent-specs nil)) + (with-clojure-buffer "\n(defn foo\n[x]\nx)" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(defn foo\n [x]\n x)"))))) + (provide 'clojure-mode-indentation-test) ;;; clojure-mode-indentation-test.el ends here From 40c9c76d846edd313a9d2a33598740ba956eb0ae Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 23 Mar 2026 09:55:54 +0200 Subject: [PATCH 13/13] Add tests for clojure-get-indent-function Verify that the custom indent spec lookup hook works: - A custom function can provide specs for unknown forms - When the custom function returns nil, built-in specs still apply --- test/clojure-mode-indentation-test.el | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 91c3c3bd..d4a49199 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1311,6 +1311,27 @@ x (indent-region (point-min) (point-max)) (expect (buffer-string) :to-equal "\n(defn foo\n [x]\n x)"))))) +(describe "clojure-get-indent-function" + (it "should use custom function to look up indent specs" + (let ((clojure-get-indent-function + (lambda (name) + (when (string= name "my-custom-macro") + 1)))) + ;; my-custom-macro has no built-in spec, but our custom function + ;; provides spec 1 (one special arg, then body). + (with-clojure-buffer "\n(my-custom-macro binding\nbody)" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(my-custom-macro binding\n body)")))) + + (it "should fall back to built-in specs when custom function returns nil" + (let ((clojure-get-indent-function + (lambda (_name) nil))) + ;; let has a built-in spec, and the custom function returns nil, + ;; so the built-in spec should still apply. + (with-clojure-buffer "\n(let [x 1]\nx)" + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "\n(let [x 1]\n x)"))))) + (provide 'clojure-mode-indentation-test) ;;; clojure-mode-indentation-test.el ends here