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/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..b44f90c52 100644 --- a/doc/modules/ROOT/pages/indent_spec.adoc +++ b/doc/modules/ROOT/pages/indent_spec.adoc @@ -15,7 +15,88 @@ 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 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 when on their own line); +remaining arguments get body-style indentation (2 spaces). + +| `[:inner D]` +| 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 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: + +* `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 only (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 +114,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,77 +126,125 @@ 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) + (something) ;; body (2-space indent) (quick)) +---- + +The `when-let` macro has one special argument (the binding vector). +Its indent spec is `1` (shorthand for `[[:block 1]]`). -(do (whatever) - (you) - (want)) +[source,clojure] +---- +(when-let [x (foo)] ;; position 0 — special arg + (bar x)) ;; position 1+ — body ---- -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 regardless of position. -''' +[source,clojure] +---- +(defn my-fn + [x] ;; depth 0 — body-indented + (inc x)) ;; depth 0 — body-indented +---- -Let's see something more sophisticated. If the `defrecord` indent spec used by -`clojure-mode` is `[2 :form :form [1]]`. This is saying: +=== Multi-rule specs -* `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). +`defrecord` uses `[[:block 2] [:inner 1]]`. This means: + +* `[: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!!")) ---- -For something even more complicated: `letfn` is `+[1 [[:defn]] :form]+`. This means +`letfn` uses `[[:block 1] [:inner 2 0]]`. This is the most complex built-in +spec: -* `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. +* `[: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 ---- +==== 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: + +[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 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))) ---- @@ -130,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: @@ -144,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 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"