From a125380b86b56cd40707c5f1e1689484a5353924 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 16:04:34 +0200 Subject: [PATCH 1/5] Convert modern indent specs to legacy for clojure-mode compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit orchard's indent inference tables now emit modern tuple-format specs (e.g., [[:block 1] [:inner 0]]) which arrive via nREPL as the :style/indent metadata. Convert these to legacy format before passing to clojure-mode's clojure-get-indent-function hook, so older clojure-mode versions that don't understand the modern format still work correctly. New functions: - cider--modern-indent-spec-p: detect modern tuple format - cider--indent-spec-to-legacy: convert modern → legacy The conversion happens in cider--get-symbol-indent after reading and vector-to-list conversion. --- CHANGELOG.md | 3 +- lisp/cider-mode.el | 12 ++++++-- lisp/cider-util.el | 64 ++++++++++++++++++++++++++++++++++++++++ test/cider-util-tests.el | 34 +++++++++++++++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab36ab747..62311f44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,7 @@ ### Changes - Bump the injected nREPL version to 1.6. -### Changes - +- Convert modern tuple-format indent specs (e.g. `[[:block 1] [:inner 0]]`) to legacy format for compatibility with older clojure-mode versions. - Rename `cider-eval-spinner-type`, `cider-show-eval-spinner`, and `cider-eval-spinner-delay` to `cider-spinner-type`, `cider-show-spinner`, and `cider-spinner-delay`. The old names are kept as obsolete aliases. ## 1.21.0 (2026-02-07) diff --git a/lisp/cider-mode.el b/lisp/cider-mode.el index 1c5ce529e..83367188a 100644 --- a/lisp/cider-mode.el +++ b/lisp/cider-mode.el @@ -603,7 +603,11 @@ re-visited." :group 'cider) (defun cider--get-symbol-indent (symbol-name) - "Return the indent metadata for SYMBOL-NAME in the current namespace." + "Return the indent metadata for SYMBOL-NAME in the current namespace. +The return value is always in legacy format (integers, :defn, +positional lists) for compatibility with all clojure-mode versions. +Modern tuple-format specs from the nREPL backend are converted +automatically." (let* ((ns (let ((clojure-cache-ns t)) ; we force ns caching here for performance reasons ;; silence bytecode warning of unused lexical var (ignore clojure-cache-ns) @@ -612,7 +616,11 @@ re-visited." (indent (or (nrepl-dict-get meta "style/indent") (nrepl-dict-get meta "indent")))) (condition-case-unless-debug err - (cider--deep-vector-to-list (read indent)) + (let ((spec (cider--deep-vector-to-list (read indent)))) + ;; Convert modern tuple specs to legacy format so that + ;; older clojure-mode versions (without modern format + ;; support) still work correctly. + (cider--indent-spec-to-legacy spec)) (error (message ":indent metadata on `%s' is unreadable!\nERROR: %s" symbol-name (error-message-string err)) nil)) diff --git a/lisp/cider-util.el b/lisp/cider-util.el index 3f4069721..b044fb714 100644 --- a/lisp/cider-util.el +++ b/lisp/cider-util.el @@ -543,6 +543,70 @@ Any other value is just returned." (mapcar #'cider--deep-vector-to-list x) x)) +(defun cider--modern-indent-spec-p (spec) + "Return non-nil if SPEC is a modern tuple-based indent spec. +Modern specs are lists of rules like ((:block N)) or ((:inner D))." + (and (listp spec) + spec + (cl-every (lambda (rule) + (and (listp rule) + (memq (car rule) '(:block :inner)))) + spec))) + +(defun cider--indent-spec-to-legacy (spec) + "Convert a modern indent SPEC to legacy format for older clojure-mode. +Returns SPEC unchanged if it is not in modern format. + +Modern format uses ((:block N)), ((:inner D)), ((:inner D I)). +Legacy format uses integers, :defn, and positional lists. + +This ensures compatibility with clojure-mode versions that don't +understand the modern format." + (if (not (cider--modern-indent-spec-p spec)) + spec + (let ((block-n nil) + (inner-no-idx nil) + (inner-with-idx nil)) + (dolist (rule spec) + (pcase rule + (`(:block ,n) (setq block-n n)) + (`(:inner ,d) (push d inner-no-idx)) + (`(:inner ,d ,i) (push (cons d i) inner-with-idx)))) + (cond + ;; Simple: only (:block N) + ((and block-n (null inner-no-idx) (null inner-with-idx)) + block-n) + ;; Simple: only (:inner 0) + ((and (null block-n) (null inner-with-idx) + (equal inner-no-idx '(0))) + :defn) + ;; Complex: build positional list + (t + (let ((result (list)) + (wrap-defn (lambda (depth) + (let ((s :defn)) + (dotimes (_ depth) + (setq s (list s))) + s)))) + (when block-n + (setq result (list block-n))) + ;; Place indexed :inner rules at their positions + (dolist (ir inner-with-idx) + (let* ((depth (car ir)) + (idx (cdr ir)) + (pos (+ (if block-n 1 0) idx)) + (wrapped (funcall wrap-defn depth))) + (while (<= (length result) pos) + (setq result (append result (list nil)))) + (setf (nth pos result) wrapped))) + ;; Append non-indexed :inner rules (ascending depth) + (dolist (depth (sort inner-no-idx #'<)) + (setq result (append result (list (funcall wrap-defn depth))))) + ;; Trailing nil for specs with indexed rules + (when inner-with-idx + (setq result (append result (list nil)))) + result)))))) + ;;; Help mode diff --git a/test/cider-util-tests.el b/test/cider-util-tests.el index 18ad76e33..78b51ab54 100644 --- a/test/cider-util-tests.el +++ b/test/cider-util-tests.el @@ -283,6 +283,40 @@ buffer." (expect (cider--deep-vector-to-list '[bug]) :to-equal '(bug)) (expect (cider--deep-vector-to-list '(bug)) :to-equal '(bug)))) +(describe "cider--modern-indent-spec-p" + (it "recognizes modern specs" + (expect (cider--modern-indent-spec-p '((:block 1))) :to-be-truthy) + (expect (cider--modern-indent-spec-p '((:inner 0))) :to-be-truthy) + (expect (cider--modern-indent-spec-p '((:block 1) (:inner 2 0))) :to-be-truthy)) + + (it "rejects legacy and non-spec values" + (expect (cider--modern-indent-spec-p 1) :not :to-be-truthy) + (expect (cider--modern-indent-spec-p :defn) :not :to-be-truthy) + (expect (cider--modern-indent-spec-p '(1 (:defn))) :not :to-be-truthy))) + +(describe "cider--indent-spec-to-legacy" + (it "converts simple modern specs" + (expect (cider--indent-spec-to-legacy '((:block 0))) :to-equal 0) + (expect (cider--indent-spec-to-legacy '((:block 1))) :to-equal 1) + (expect (cider--indent-spec-to-legacy '((:inner 0))) :to-equal :defn)) + + (it "converts complex multi-rule specs" + (expect (cider--indent-spec-to-legacy '((:block 1) (:inner 2 0))) + :to-equal '(1 ((:defn)) nil)) + (expect (cider--indent-spec-to-legacy '((:block 2) (:inner 1))) + :to-equal '(2 (:defn))) + (expect (cider--indent-spec-to-legacy '((:block 1) (:inner 1))) + :to-equal '(1 (:defn))) + (expect (cider--indent-spec-to-legacy '((:block 1) (:inner 0))) + :to-equal '(1 :defn)) + (expect (cider--indent-spec-to-legacy '((:inner 0) (:inner 1))) + :to-equal '(:defn (:defn)))) + + (it "returns legacy specs unchanged" + (expect (cider--indent-spec-to-legacy 1) :to-equal 1) + (expect (cider--indent-spec-to-legacy :defn) :to-equal :defn) + (expect (cider--indent-spec-to-legacy '(1 (:defn))) :to-equal '(1 (:defn))))) + (describe "cider-version-sans-patch" :var (cider-version) (it "returns the version sans the patch" From ecbf17725bc4179f649ed940e615ad7f2f743a39 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 16:07:16 +0200 Subject: [PATCH 2/5] Document the modern indent spec tuple format Update indent_spec.adoc: - Present modern tuple format ([:block N], [:inner D], [:inner D I]) as the preferred format with a reference table - Reorganize examples to show modern format first - Add legacy format reference section with deprecation note Update indentation.adoc: - Use modern format in static and dynamic indentation examples - Add notes about legacy format backward compatibility --- .../ROOT/pages/config/indentation.adoc | 14 ++- doc/modules/ROOT/pages/indent_spec.adoc | 95 ++++++++++++++----- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/doc/modules/ROOT/pages/config/indentation.adoc b/doc/modules/ROOT/pages/config/indentation.adoc index 28876f876..8bb878aba 100644 --- a/doc/modules/ROOT/pages/config/indentation.adoc +++ b/doc/modules/ROOT/pages/config/indentation.adoc @@ -93,14 +93,17 @@ To get `clojure-mode` to indent it properly you'll need to add the following cod [source,lisp] ---- -(put-clojure-indent 'with-in-str 1) +(put-clojure-indent 'with-in-str '((:block 1))) ;; or (define-clojure-indent - (with-in-str 1) + (with-in-str '((:block 1)))) ---- +NOTE: The legacy shorthand `(put-clojure-indent 'with-in-str 1)` is also +accepted for backward compatibility but will be removed in clojure-mode 6. + TIP: You can find more details https://github.com/clojure-emacs/clojure-mode#indentation-of-macro-forms[here]. == Dynamic Indentation @@ -128,13 +131,13 @@ they've written (using an example in core): (baz)) ---- -And here's a more complex one: +And here's a more complex one using the modern tuple format: [source,clojure] ---- (defmacro letfn "[DOCSTRING]" - {:style/indent [1 [[:defn]] :form]} + {:style/indent [[:block 1] [:inner 2 0]]} [fnspecs & body] ...cut for brevity...) @@ -152,6 +155,9 @@ be either just a number, or one of the keywords `:defn` or `:form`. A full description of the spec is provided in the xref:indent_spec.adoc[indent spec section of the manual]. +NOTE: The legacy positional format (e.g., `[1 [[:defn]] :form]`) is also +accepted but will be removed in clojure-mode 6. + If you _don't_ want to use this feature, you can disable it by setting `cider-dynamic-indentation` to `nil` in your Emacs init file. diff --git a/doc/modules/ROOT/pages/indent_spec.adoc b/doc/modules/ROOT/pages/indent_spec.adoc index 9915eeb94..0f8bea5aa 100644 --- a/doc/modules/ROOT/pages/indent_spec.adoc +++ b/doc/modules/ROOT/pages/indent_spec.adoc @@ -15,7 +15,44 @@ under the `:style/indent` key. ...cut for brevity...) ---- -It can take one of 3 forms: +There are two supported formats for indent specs: the **modern tuple format** +(preferred) and the **legacy positional format** (deprecated, will be removed in +clojure-mode 6). + +=== Modern format (preferred) + +The modern format uses explicit rule tuples and is shared across `clojure-mode`, +`clojure-ts-mode`, and `cljfmt`. Each rule is a vector of the form +`[:block N]` or `[:inner D]` or `[:inner D I]`: + +[cols="1,3"] +|=== +| Rule | Meaning + +| `[:block N]` +| The first N arguments are "special" (indented further); remaining arguments are body. + +| `[:inner D]` +| At nesting depth D inside the form, use body-style indentation. + +| `[:inner D I]` +| Like `:inner`, but only applies at position I within the enclosing form. +|=== + +Rules are combined in a vector. For example: + +* `1` or `[[:block 1]]` — one special arg, then body (e.g., `when`, `let`) +* `:defn` or `[[:inner 0]]` — all args are body (e.g., `defn`, `fn`) +* `[[:block 2] [:inner 1]]` — two special args, nested sub-forms get body indent (e.g., `defrecord`, `deftype`) +* `[[:block 1] [:inner 2 0]]` — one special arg, depth-2 nesting at position 0 (e.g., `letfn`) + +Simple specs (an integer or `:defn`) are shorthand for the corresponding +single-rule vector. + +=== Legacy format (deprecated) + +The legacy format uses positional lists where each element controls indentation +at the corresponding argument position. It takes one of these forms: * Absent, meaning _"indent like a regular function call"_. * An integer or a keyword `x`, which is shorthand for the list `[x]`. @@ -33,6 +70,9 @@ internally (if it's not a form the spec is irrelevant). ** If the function/macro has more arguments than the list has elements, the last element of the list applies to all remaining arguments. +NOTE: The legacy positional format will be removed in clojure-mode 6. New code +should use the modern tuple format. + ''' == Examples @@ -42,34 +82,36 @@ Here we go into several examples using some well-known macros and forms from don't need to specify them. They are just examples to guide you when writing indent specs for your own macros, or for macros from third party libs. -One very simple example is the `do` form. All of its arguments get the same -indentation, and none of them are special. So its indent spec is simply `[0]`, -or `0` for short. +=== Simple specs + +The `do` form has no special arguments — all args get body indentation. +Its indent spec is simply `0` (shorthand for `[[:block 0]]`). [source,clojure] ---- (do (something) (quick)) +---- -(do (whatever) - (you) - (want)) +The `when-let` macro has one special argument (the binding vector). +Its indent spec is `1` (shorthand for `[[:block 1]]`). + +[source,clojure] +---- +(when-let [x (foo)] + (bar x)) ---- -Sticking to simplicity, the `when-let*` macro has one special argument (the -binding vector) and there's no out-of-the-ordinary internal structure -involved. So the indent spec is just `1` (which is shorthand for `[1]`). +The `defn` macro uses `:defn` (shorthand for `[[:inner 0]]`), meaning all +arguments get body-style indentation. -''' +=== Multi-rule specs -Let's see something more sophisticated. If the `defrecord` indent spec used by -`clojure-mode` is `[2 :form :form [1]]`. This is saying: +`defrecord` uses `[[:block 2] [:inner 1]]`. This means: -* `defrecord` has 2 special arguments (the name and the arglist). -* The first two arguments have no special internal structure. -* All remaining arguments have an internal indent spec of `[1]` (which means -only the arglist is indented specially and the rest is the body). +* 2 special arguments (the name and the fields vector) via `[:block 2]`. +* Nested sub-forms (protocol method bodies) get body-style indentation via `[:inner 1]`. [source,clojure] ---- @@ -82,12 +124,11 @@ only the arglist is indented specially and the rest is the body). "My very own thing!!")) ---- -For something even more complicated: `letfn` is `+[1 [[:defn]] :form]+`. This means +`letfn` uses `[[:block 1] [:inner 2 0]]`. This means: -* `letfn` has one special argument (the bindings list). -* The first arg has an indent spec of `+[[:defn]]+`, which means all forms -_inside_ the first arg have an indent spec of `+[:defn]+`. -* The second argument, and all other arguments, are regular forms. +* 1 special argument (the bindings vector) via `[:block 1]`. +* At nesting depth 2, position 0 (i.e., inside each binding form), use +body-style indentation via `[:inner 2 0]`. [source,clojure] ---- @@ -98,6 +139,16 @@ _inside_ the first arg have an indent spec of `+[:defn]+`. (six-times 15)) ---- +=== Legacy format examples + +For reference, the same specs in the legacy positional format: + +* `do`: `0` +* `when-let`: `1` +* `defn`: `:defn` +* `defrecord`: `[2 nil nil [:defn]]` +* `letfn`: `[1 [[:defn]] :form]` + == Special Arguments Many macros have a number of "special" arguments, followed by an arbitrary From 59d9e0a0aff2b6dfc9e14f3c231ff018316be8d2 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 16:13:25 +0200 Subject: [PATCH 3/5] Define key indentation concepts and explain rule semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add clear definitions for body-style indentation, special arguments, depth, and position — terms that were used throughout the docs but never explicitly defined. Expand the rule type descriptions with concrete explanations of what each depth level means. Explicitly call out cljfmt format compatibility with a link. --- doc/modules/ROOT/pages/indent_spec.adoc | 56 ++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/doc/modules/ROOT/pages/indent_spec.adoc b/doc/modules/ROOT/pages/indent_spec.adoc index 0f8bea5aa..3d7b18d60 100644 --- a/doc/modules/ROOT/pages/indent_spec.adoc +++ b/doc/modules/ROOT/pages/indent_spec.adoc @@ -22,21 +22,65 @@ clojure-mode 6). === Modern format (preferred) The modern format uses explicit rule tuples and is shared across `clojure-mode`, -`clojure-ts-mode`, and `cljfmt`. Each rule is a vector of the form -`[:block N]` or `[:inner D]` or `[:inner D I]`: +`clojure-ts-mode`, and https://github.com/weavejester/cljfmt[cljfmt]. The +format is identical in all three tools, so indent specs are portable across +editors and formatters. + +==== Key concepts + +Before diving into the rules, here are the terms used throughout: + +**Body-style indentation**:: Arguments are indented by 2 spaces relative to the +enclosing form. This is the standard indentation for macro bodies: ++ +[source,clojure] +---- +(when true + (foo) ;; body — indented 2 spaces + (bar)) +---- + +**Special arguments**:: Arguments that precede the body. When placed on their +own line, they get additional indentation to visually distinguish them from +the body: ++ +[source,clojure] +---- +(defrecord TheNameOfTheRecord + [a pretty long argument list] ;; special arg — extra indentation + SomeType ;; body — standard 2-space indentation + (method [this] ...)) +---- + +**Depth**:: The nesting level within the form, counting from 0. Depth 0 is the +form's direct arguments, depth 1 is the arguments _inside_ those arguments, +depth 2 is one level deeper, and so on. + +**Position**:: The 0-indexed argument position within the enclosing form +(excluding the form name itself). For example, in `(let [x 1] body)`, the +binding vector `[x 1]` is at position 0 and `body` is at position 1. + +==== Rule types + +Each rule is a vector of the form `[:block N]`, `[:inner D]`, or `[:inner D I]`: [cols="1,3"] |=== | Rule | Meaning | `[:block N]` -| The first N arguments are "special" (indented further); remaining arguments are body. +| The first N arguments are "special" (indented further when on their own line); +remaining arguments get body-style indentation (2 spaces). | `[:inner D]` -| At nesting depth D inside the form, use body-style indentation. +| At nesting depth D inside the form, all sub-forms get body-style indentation. +`[:inner 0]` means direct arguments are body-indented (like `defn`). +`[:inner 1]` means forms _inside_ the arguments are body-indented (like method +bodies in `defprotocol`). | `[:inner D I]` -| Like `:inner`, but only applies at position I within the enclosing form. +| Like `[:inner D]`, but restricted to position I within the enclosing form. +Used when only some arguments at a given depth need body-style indentation. |=== Rules are combined in a vector. For example: @@ -44,7 +88,7 @@ Rules are combined in a vector. For example: * `1` or `[[:block 1]]` — one special arg, then body (e.g., `when`, `let`) * `:defn` or `[[:inner 0]]` — all args are body (e.g., `defn`, `fn`) * `[[:block 2] [:inner 1]]` — two special args, nested sub-forms get body indent (e.g., `defrecord`, `deftype`) -* `[[:block 1] [:inner 2 0]]` — one special arg, depth-2 nesting at position 0 (e.g., `letfn`) +* `[[:block 1] [:inner 2 0]]` — one special arg, depth-2 nesting at position 0 only (e.g., `letfn`) Simple specs (an integer or `:defn`) are shorthand for the corresponding single-rule vector. From e92b94b0a3c4156a8ba2f7600f2e57032a294301 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 16:13:59 +0200 Subject: [PATCH 4/5] Improve examples with depth/position annotations and rule selection guide Annotate all code examples with depth and position comments so readers can see exactly how rules map to indentation. Add a detailed explanation of why letfn needs [:inner 2 0] with a position restriction. Add a "Choosing the right rule" section with heuristics for when to use :block, :inner, and combinations. Replace the plain legacy format list with a side-by-side comparison table mapping modern to legacy specs for all complex built-in forms. --- doc/modules/ROOT/pages/indent_spec.adoc | 80 ++++++++++++++++++------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/doc/modules/ROOT/pages/indent_spec.adoc b/doc/modules/ROOT/pages/indent_spec.adoc index 3d7b18d60..c75ccaf4a 100644 --- a/doc/modules/ROOT/pages/indent_spec.adoc +++ b/doc/modules/ROOT/pages/indent_spec.adoc @@ -134,7 +134,7 @@ Its indent spec is simply `0` (shorthand for `[[:block 0]]`). [source,clojure] ---- (do - (something) + (something) ;; body (2-space indent) (quick)) ---- @@ -143,55 +143,91 @@ Its indent spec is `1` (shorthand for `[[:block 1]]`). [source,clojure] ---- -(when-let [x (foo)] - (bar x)) +(when-let [x (foo)] ;; position 0 — special arg + (bar x)) ;; position 1+ — body ---- The `defn` macro uses `:defn` (shorthand for `[[:inner 0]]`), meaning all -arguments get body-style indentation. +arguments get body-style indentation regardless of position. + +[source,clojure] +---- +(defn my-fn + [x] ;; depth 0 — body-indented + (inc x)) ;; depth 0 — body-indented +---- === Multi-rule specs `defrecord` uses `[[:block 2] [:inner 1]]`. This means: -* 2 special arguments (the name and the fields vector) via `[:block 2]`. -* Nested sub-forms (protocol method bodies) get body-style indentation via `[:inner 1]`. +* `[:block 2]` — 2 special arguments (the name and the fields vector). +* `[:inner 1]` — at depth 1 (inside the protocol method forms), use body-style + indentation. This is what makes method bodies indent correctly. [source,clojure] ---- -(defrecord Thing [a] - FileNameMap - (getContentTypeFor [_ file-name] +(defrecord Thing [a] ;; depth 0, pos 0-1: special args ([:block 2]) + FileNameMap ;; depth 0, pos 2+: body + (getContentTypeFor [_ file-name] ;; depth 1: body-indented ([:inner 1]) (str a "-" file-name)) Object (toString [_] "My very own thing!!")) ---- -`letfn` uses `[[:block 1] [:inner 2 0]]`. This means: +`letfn` uses `[[:block 1] [:inner 2 0]]`. This is the most complex built-in +spec: -* 1 special argument (the bindings vector) via `[:block 1]`. -* At nesting depth 2, position 0 (i.e., inside each binding form), use -body-style indentation via `[:inner 2 0]`. +* `[:block 1]` — 1 special argument (the bindings vector). +* `[:inner 2 0]` — at depth 2, *only at position 0*, use body-style +indentation. Why position 0? Because inside the bindings vector (depth 1), +each binding is a list like `(twice [x] body)`. Position 0 in each binding +is the function name+arglist, and the body follows. The position restriction +ensures that only the function definitions inside the bindings get body-style +indentation. [source,clojure] ---- -(letfn [(twice [x] - (* x 2)) +(letfn [(twice [x] ;; depth 0, pos 0: special arg ([:block 1]) + (* x 2)) ;; depth 2, pos 0: body-indented ([:inner 2 0]) (six-times [y] (* (twice y) 3))] - (six-times 15)) + (six-times 15)) ;; depth 0, pos 1+: body ---- -=== Legacy format examples +==== Choosing the right rule + +As a rule of thumb: + +* **Use `[:block N]`** for macros with N special arguments before a body — this + is the most common case. Examples: `when` (1), `let` (1), `condp` (2), + `catch` (2). +* **Use `[:inner 0]`** for def-like forms where all arguments are body. Examples: + `defn`, `fn`, `deftest`, `defmethod`. +* **Use `[:inner 1]`** when the form contains sub-forms whose bodies need + body-style indentation (like protocol method definitions). Examples: + `defprotocol`, `deftype`, `defrecord`. +* **Combine rules** when a form has both special args _and_ nested structure. + Examples: `defrecord` = `[:block 2]` + `[:inner 1]`. + +=== Legacy format equivalents For reference, the same specs in the legacy positional format: -* `do`: `0` -* `when-let`: `1` -* `defn`: `:defn` -* `defrecord`: `[2 nil nil [:defn]]` -* `letfn`: `[1 [[:defn]] :form]` +[cols="1,1,1"] +|=== +| Form | Modern | Legacy + +| `do` | `0` or `[[:block 0]]` | `0` +| `when-let` | `1` or `[[:block 1]]` | `1` +| `defn` | `:defn` or `[[:inner 0]]` | `:defn` +| `defrecord` | `[[:block 2] [:inner 1]]` | `[2 nil nil [:defn]]` +| `defprotocol` | `[[:block 1] [:inner 1]]` | `[1 [:defn]]` +| `extend-protocol` | `[[:block 1] [:inner 0]]` | `[1 :defn]` +| `reify` | `[[:inner 0] [:inner 1]]` | `[:defn [:defn]]` +| `letfn` | `[[:block 1] [:inner 2 0]]` | `[1 [[:defn]] :form]` +|=== == Special Arguments From de0b960e2f7fc8635d6dda8cc6d2eb579b3c45f7 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 16:14:34 +0200 Subject: [PATCH 5/5] Update Special Arguments and Internal Indentation sections Rewrite the Special Arguments section to reference [:block N] rule and add depth annotations to examples. Rewrite the Internal Indentation section to explain [:inner 1] in terms of depth rather than legacy positional semantics. Add a new Namespace-Qualified Symbols section documenting clojure-mode's support for per-namespace indent overrides and the automatic fallback to unqualified specs. --- doc/modules/ROOT/pages/indent_spec.adoc | 48 +++++++++++++++++-------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/doc/modules/ROOT/pages/indent_spec.adoc b/doc/modules/ROOT/pages/indent_spec.adoc index c75ccaf4a..b44f90c52 100644 --- a/doc/modules/ROOT/pages/indent_spec.adoc +++ b/doc/modules/ROOT/pages/indent_spec.adoc @@ -233,17 +233,18 @@ For reference, the same specs in the legacy positional format: Many macros have a number of "special" arguments, followed by an arbitrary number of "non-special" arguments (sometimes called the body). The "non-special" -arguments have a small indentation (usually 2 spaces). The special arguments +arguments have body-style indentation (2 spaces). The special arguments are usually on the same line as the macro name, but, when necessary, they are placed on a separate line with additional indentation. -For instance, `defrecord` has two special arguments, and here's how it might be indented: +This is controlled by the `[:block N]` rule. For instance, `defrecord` has +`[:block 2]` (two special arguments), and here's how it might be indented: [source,clojure] ---- (defrecord TheNameOfTheRecord - [a pretty long argument list] - SomeType + [a pretty long argument list] ;; special arg — extra indent + SomeType ;; body — 2 spaces (assoc [_ x] (.assoc pretty x 10))) ---- @@ -261,11 +262,11 @@ Here's another way one could do it: _The point of the indent spec is *not* to specify how many spaces to use._ -The point is just to say "a defrecord has *2* special arguments", and then let -the editor and the user come to an agreement on how many spaces they like to use -for special and non-special arguments. +The point is just to say "a defrecord has *2* special arguments" (via +`[:block 2]`), and then let the editor and the user come to an agreement on how +many spaces they like to use for special and non-special arguments. -== Internal indentation +== Internal Indentation (Depth) The issue goes a bit deeper. Note the last argument in that `defrecord`. A regular function form would be internally indented as: @@ -275,18 +276,37 @@ regular function form would be internally indented as: (.assoc pretty x 10)) ---- -But this is not a regular function call, it's a definition. So we want to -specify that this form internally has 1 special argument (the arglist vector), -so that it will be indented like this: +But this is not a regular function call, it's a method definition. So we want +the method body to get body-style indentation: ---- (assoc [_ x] (.assoc pretty x 10)) ---- -The indent spec does this as well. It lets you specify that, for each argument -beyond the 2nd, if it is a form, it should be internally indented as having 1 -special argument. +This is what the `[:inner 1]` rule does — it says "at depth 1 (inside the +arguments of this form), use body-style indentation." Combined with +`[:block 2]`, the full spec `[[:block 2] [:inner 1]]` gives defrecord both +special argument handling and correct method body indentation. + +== Namespace-Qualified Symbols + +In `clojure-mode`, you can specify different indentation for the same symbol +depending on its namespace. This is useful when a library redefines a form +with different semantics: + +[source,lisp] +---- +;; Default indentation for `do` +(put-clojure-indent 'do '((:block 0))) + +;; Custom indentation for `my-ns/do` +(put-clojure-indent 'my-ns/do '((:block 1))) +---- + +When a namespace-qualified symbol has no explicit spec, `clojure-mode` +automatically falls back to the unqualified symbol's spec. For example, +`clojure.core/let` uses the same spec as `let`. == Indentation inference