Skip to content

Fix the indent spec conversions and match cljfmt's specs - #709

Merged
bbatsov merged 4 commits into
masterfrom
fix-indent-spec-conversion
Nov 1, 2026
Merged

bbatsov merged 4 commits into
masterfrom
fix-indent-spec-conversion

Conversation

@bbatsov

@bbatsov bbatsov commented Nov 1, 2026

Copy link
Copy Markdown
Member

The conversions between the legacy and modern indent spec formats dropped the positions of :inner rules and got their depths wrong, and a modern spec like ((:inner 1 0)) made indentation signal an error. With those fixed, the built-in specs now match the ones clojure-ts-mode and cljfmt use. extend-protocol and friends get (:inner 1), since their (:inner 0) only worked through the old conversion, so specs copied from the 5.23 README need the same change.

clojure--indent-spec-to-modern turned each element of a positional
spec into a rule on its own, dropping the position an element stands
for and counting the depth of a nested spec from the wrong level:

    (1 ((:defn)) nil)   letfn             became ((:block 1) (:inner 2))
    (:defn (1))         reify             became ((:inner 0))
    (1 :defn)           extend-protocol   became ((:block 1) (:inner 0))

Read the elements the way the backtracking indentation does instead:
the first one is the spec of the form itself and the rest those of its
arguments, the last one also covering the arguments after it.  That
gives ((:block 1) (:inner 2 0)), ((:inner 0) (:inner 1)) and
((:block 1) (:inner 1)), the rules clojure-ts-mode and cljfmt use for
those forms.  clojure-ts-mode has used the same conversion for the
specs it gets from CIDER since clojure-emacs/clojure-ts-mode#162.
clojure--indent-spec-to-legacy, which turns a modern spec into one the
backtracking indentation can use, got a few things wrong:

- An :inner rule with a position went to that position in the list
  when there was no :block rule, but position 0 is the form itself, so
  ((:inner 1 0)) became ((:defn) nil) and indenting such a form
  signaled an error.
- An (:inner 0) rule went at the end, where it applied to the
  arguments, so ((:block 1) (:inner 0)) indented the methods of
  extend-protocol like (:inner 1) would.  The built-in specs of
  extend-protocol, extend-type, specify and specify! relied on that,
  and clojure-ts-mode and cljfmt read them differently.
- Several :inner rules without a position each took a position of
  their own, and only the last applied to the arguments after them.

Reserve position 0 for the :block rule or (:inner 0), put the rules
with a position after it, and end with the shallowest rule without
one.  Give the four forms above the (:inner 1) rule they mean.
cljfmt indents clojure.spec's fdef with [[:inner 0]], so a name on a
line of its own gets 2 spaces, where the (:block 1) rule gave it 4.  It
also has let* as [[:block 1]], which clojure-mode only got through its
rule for symbols that start with let.  clojure-ts-mode has both.
cljfmt has rules for match, struct-map, defstruct, the with- forms and
a few def forms that clojure-mode had no spec for.  The def and with-
forms got body indentation from the rule for symbols with those
prefixes, so a with-open whose bindings were on a line of their own
indented them as a body, unlike let.  match and struct-map got none
and were indented like function calls.  Use cljfmt's rules, which
clojure-ts-mode uses too.
@bbatsov
bbatsov force-pushed the fix-indent-spec-conversion branch 2 times, most recently from 42b6fc4 to 4ca3026 Compare November 1, 2026 09:35
bbatsov added a commit to clojure-emacs/cider that referenced this pull request Nov 1, 2026
cider--indent-spec-to-legacy turns the modern :style/indent specs from
cider-nrepl into the legacy format, which both clojure-mode and
clojure-ts-mode accept.  It got a few things wrong:

- An :inner rule with a position went to that position in the list
  when there was no :block rule, but position 0 is the form itself, so
  [[:inner 1 0]] became ((:defn) nil).  clojure-mode signaled an error
  indenting such a form, and clojure-ts-mode found no rules in it.
- An (:inner 0) rule went at the end, where it applied to the
  arguments, so [[:block 1] [:inner 0]] indented them like [:inner 1]
  would.  cljfmt ignores [:inner 0] next to a :block rule.
- Several :inner rules without a position each took a position of
  their own, and only the last applied to the arguments after them.

Reserve position 0 for the :block rule or (:inner 0), put the rules
with a position after it, and end with the shallowest rule without
one.  clojure-mode's own copy of this conversion gets the same fix in
clojure-emacs/clojure-mode#709.
@bbatsov
bbatsov merged commit 8060fc8 into master Nov 1, 2026
7 checks passed
bbatsov added a commit to clojure-emacs/cider that referenced this pull request Nov 1, 2026
cider--indent-spec-to-legacy turns the modern :style/indent specs from
cider-nrepl into the legacy format, which both clojure-mode and
clojure-ts-mode accept.  It got a few things wrong:

- An :inner rule with a position went to that position in the list
  when there was no :block rule, but position 0 is the form itself, so
  [[:inner 1 0]] became ((:defn) nil).  clojure-mode signaled an error
  indenting such a form, and clojure-ts-mode found no rules in it.
- An (:inner 0) rule went at the end, where it applied to the
  arguments, so [[:block 1] [:inner 0]] indented them like [:inner 1]
  would.  cljfmt ignores [:inner 0] next to a :block rule.
- Several :inner rules without a position each took a position of
  their own, and only the last applied to the arguments after them.

Reserve position 0 for the :block rule or (:inner 0), put the rules
with a position after it, and end with the shallowest rule without
one.  clojure-mode's own copy of this conversion gets the same fix in
clojure-emacs/clojure-mode#709.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant