Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
### Changes

* [#705](https://github.com/clojure-emacs/clojure-mode/pull/705): Rename the `#_` toggle commands to `clojure-toggle-discard`, `clojure-toggle-discard-surrounding-form` and `clojure-toggle-discard-defun`, matching Clojure's "discard" terminology (the old `clojure-toggle-ignore*` names remain as obsolete aliases).
* [#709](https://github.com/clojure-emacs/clojure-mode/pull/709): Give `extend-protocol`, `extend-type`, `specify` and `specify!` the `((:block 1) (:inner 1))` indent spec that `clojure-ts-mode` and cljfmt use, instead of an `(:inner 0)` rule that only worked because of how it was converted to the legacy format (specs copied from it need `(:inner 1)` too).
* [#709](https://github.com/clojure-emacs/clojure-mode/pull/709): Indent `fdef` like cljfmt does, as `((:inner 0))`, and list `let*` in the built-in indent specs, so that `clojure-get-indent-spec` returns its spec.
* [#709](https://github.com/clojure-emacs/clojure-mode/pull/709): Add the indent specs that cljfmt and `clojure-ts-mode` have and `clojure-mode` lacked, which indents the bodies of `match` and `struct-map`, and the bindings of `with-open`, `with-redefs` and the like as special arguments.

### Bugs fixed

* [#709](https://github.com/clojure-emacs/clojure-mode/pull/709): Fix `clojure-get-indent-spec` and `put-clojure-indent` converting legacy indent specs, like those of `letfn`, `reify` or `extend-protocol`, to the wrong modern rules.
* [#709](https://github.com/clojure-emacs/clojure-mode/pull/709): Fix indenting with a modern indent spec that has an `:inner` rule with a position but no `:block` rule, like `((:inner 1 0))`, which signaled an error.

## 5.23.0 (2026-03-25)

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ nested indentation. For example, `letfn` uses `'((:block 1) (:inner 2 0))`:

```clojure
(letfn [(twice [x] ;; (:block 1) → 1 special arg (the binding vector)
(* x 2)) ;; (:inner 2 0) → at depth 2, position 0 in the binding
(thrice [x] ;; vector, use body-style indentation
(* x 2)) ;; (:inner 2 0) → at depth 2, inside argument 0 (the
(thrice [x] ;; binding vector), use body-style indentation
(* x 3))]
(+ (twice 5) ;; after the block arg → body indentation
(thrice 5)))
Expand All @@ -296,10 +296,10 @@ Here are the built-in multi-rule specs:
(definterface '((:block 1) (:inner 1)))
(reify '((:inner 0) (:inner 1)))
(proxy '((:block 2) (:inner 1)))
(extend-protocol '((:block 1) (:inner 0)))
(extend-type '((:block 1) (:inner 0)))
(specify '((:block 1) (:inner 0)))
(specify! '((:block 1) (:inner 0))))
(extend-protocol '((:block 1) (:inner 1)))
(extend-type '((:block 1) (:inner 1)))
(specify '((:block 1) (:inner 1)))
(specify! '((:block 1) (:inner 1))))
```

This format is shared with `clojure-ts-mode`. It also follows the same
Expand Down
164 changes: 85 additions & 79 deletions clojure-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -1670,7 +1670,7 @@ a string. This name will be exactly as it appears in the buffer,
so it might start with a namespace alias.

Return values can be in either the modern format (e.g.,
\\='((:block 1) (:inner 0))) or the legacy format (e.g., 1 or
\\='((:block 1) (:inner 1))) or the legacy format (e.g., 1 or
:defn). Modern-format specs are automatically converted to
legacy format for the backtracking indent engine.

Expand Down Expand Up @@ -1942,8 +1942,11 @@ This function also returns nil meaning don't specify the indentation."
;; Legacy format (to be removed in clojure-mode 6):
;; - Integer N: N special args, then body
;; - :defn: body-style indentation
;; - Quoted positional list: '(1 ((:defn)) nil) where each element
;; controls a specific argument position
;; - Quoted positional list: '(1 ((:defn)) nil), where the first
;; element is the spec of the form itself and the rest are the specs
;; of its arguments, the last one also covering the arguments after
;; it. A list holding just another list applies that to every
;; argument.
;;
;; Modern format (shared with clojure-ts-mode):
;; - ((:block N)): N special args, then body
Expand All @@ -1962,14 +1965,36 @@ A modern spec is a list of rules like ((:block N)) or
(memq (car rule) '(:block :inner))))
spec)))

(defun clojure--unwrap-legacy-spec (spec depth)
"Recursively unwrap legacy SPEC at DEPTH to produce an :inner rule.
For example, ((:defn)) at depth 0 produces (:inner 2),
and (:defn) at depth 0 produces (:inner 1)."
(if (consp spec)
(clojure--unwrap-legacy-spec (car spec) (1+ depth))
(when (eq spec :defn)
(list :inner depth))))
(defun clojure--legacy-spec-to-rules (spec depth idx)
"Return the modern rules for a form with the legacy indent SPEC.

DEPTH is how many levels the form is below the form whose head has the
spec, and IDX is the position of the argument of that form it is in, or
nil for any argument.

The rules can't express everything legacy specs can. The spec of the
last argument applies to all the arguments, a position inside an
argument counts for nothing, and an integer below the form itself means
body indentation."
(let ((inner (if idx (list :inner depth idx) (list :inner depth))))
(pcase spec
((pred integerp)
(list (if (zerop depth) (list :block spec) inner)))
((or :defn 'defun) (list inner))
(`(,(and arg-spec (pred consp)))
(clojure--legacy-spec-to-rules arg-spec (1+ depth) idx))
(`(,own-spec . ,arg-specs)
(let ((last-pos (1- (length arg-specs))))
(apply #'append
(unless (consp own-spec)
(clojure--legacy-spec-to-rules own-spec depth idx))
(seq-map-indexed (lambda (arg-spec pos)
(clojure--legacy-spec-to-rules
arg-spec
(1+ depth)
(cond ((> depth 0) idx)
((< pos last-pos) pos))))
arg-specs)))))))

(defun clojure--indent-spec-to-modern (spec)
"Convert a legacy indent SPEC to modern tuple format.
Expand All @@ -1978,29 +2003,13 @@ function.

Integer N becomes ((:block N)).
:defn becomes ((:inner 0)).
A positional list is converted element by element.
A positional list is converted with `clojure--legacy-spec-to-rules'.

The legacy format will be removed in clojure-mode 6."
(cond
((clojure--modern-indent-spec-p spec) spec)
((functionp spec) spec)
((integerp spec) (list (list :block spec)))
((eq spec :defn) '((:inner 0)))
((listp spec)
(let (rules)
(dolist (el spec)
(cond
((integerp el) (push (list :block el) rules))
((eq el :defn) (push (list :inner 0) rules))
((consp el)
(let ((rule (clojure--unwrap-legacy-spec el 0)))
(when rule (push rule rules))))
;; nil elements are positional padding — skip
(t nil)))
;; Put :block rules first, matching clojure-ts-mode convention
(sort (nreverse rules)
(lambda (a _b) (eq (car a) :block)))))
(t spec)))
(t (clojure--legacy-spec-to-rules spec 0 nil))))

(defun clojure--wrap-defn (depth)
"Wrap :defn in DEPTH layers of lists.
Expand All @@ -2017,55 +2026,37 @@ Returns SPEC unchanged if it is not in modern format.

\((:block N)) becomes N.
\((:inner 0)) becomes :defn.
Complex multi-rule specs are converted to positional lists.
Other specs become positional lists: an argument spec of
\(:defn) is an (:inner 1) rule, ((:defn)) an (:inner 2) rule, and so
on.

The legacy format can't express everything the modern one can. Only
the shallowest of several :inner rules without a position is kept, and
a rule with a position replaces the others for that argument. Like in
cljfmt, (:inner 0) does nothing next to a :block rule. Rules with
negative numbers are ignored.

The legacy format will be removed in clojure-mode 6."
(if (not (clojure--modern-indent-spec-p spec))
spec
;; Extract components
(let ((block-n nil)
(inner-no-idx nil) ; list of depths without position index
(inner-with-idx nil)) ; list of (depth . index) with position index
(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)))
;; Position 0: block-n or first non-indexed inner
(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 (clojure--wrap-defn depth)))
;; Pad with nil to reach position
(while (<= (length result) pos)
(setq result (append result (list nil))))
(setf (nth pos result) wrapped)))
;; Append non-indexed :inner rules as tail
;; (applies to all remaining positions)
;; Sort ascending so shallower depths come first.
(dolist (depth (sort inner-no-idx #'<))
(let ((wrapped (clojure--wrap-defn depth)))
(setq result (append result (list wrapped)))))
;; Append trailing nil if there are indexed rules
;; (so the backtracking engine sees the end of the spec)
(when inner-with-idx
(setq result (append result (list nil))))
result))))))
(let* ((block (let ((n (cadr (assq :block spec)))) (and (natnump n) n)))
(inner (seq-filter (lambda (rule) (eq (car rule) :inner)) spec))
(own (or block (and (member '(:inner 0) inner) :defn)))
(depths (sort (seq-filter (lambda (depth) (> depth 0))
(mapcar #'cadr (seq-remove #'cddr inner)))
#'<))
(tail (and depths (clojure--wrap-defn (car depths))))
(indexed (seq-filter (lambda (rule)
(and (natnump (caddr rule)) (> (cadr rule) 0)))
inner)))
(if (not (or tail indexed))
own
;; The spec of the form itself, then those of its arguments.
(let* ((args (if indexed (+ 2 (apply #'max (mapcar #'caddr indexed))) 1))
(result (cons own (make-list args tail))))
(pcase-dolist (`(:inner ,depth ,idx) indexed)
(setf (nth (1+ idx) result) (clojure--wrap-defn depth)))
result)))))

;;; Setting indentation
(defun put-clojure-indent (sym indent)
Expand Down Expand Up @@ -2205,6 +2196,12 @@ work). To set it from Lisp code, use
(fn '((:inner 0)))
(def '((:inner 0)))
(defn '((:inner 0)))
(defn- '((:inner 0)))
(defmacro '((:inner 0)))
(defmulti '((:inner 0)))
(defonce '((:inner 0)))
(defstruct '((:block 1)))
(struct-map '((:block 1)))
(bound-fn '((:inner 0)))
(if '((:block 1)))
(if-not '((:block 1)))
Expand All @@ -2223,27 +2220,33 @@ work). To set it from Lisp code, use
(comment '((:block 0)))
(doto '((:block 1)))
(locking '((:block 1)))
(with-open '((:block 1)))
(with-local-vars '((:block 1)))
(with-precision '((:block 1)))
(with-redefs '((:block 1)))
(with-out-str '((:block 0)))
(proxy '((:block 2) (:inner 1)))
(as-> '((:block 2)))
(fdef '((:block 1)))
(fdef '((:inner 0)))

(reify '((:inner 0) (:inner 1)))
(deftype '((:block 2) (:inner 1)))
(defrecord '((:block 2) (:inner 1)))
(defprotocol '((:block 1) (:inner 1)))
(definterface '((:block 1) (:inner 1)))
(extend '((:block 1)))
(extend-protocol '((:block 1) (:inner 0)))
(extend-type '((:block 1) (:inner 0)))
(extend-protocol '((:block 1) (:inner 1)))
(extend-type '((:block 1) (:inner 1)))
;; specify and specify! are from ClojureScript
(specify '((:block 1) (:inner 0)))
(specify! '((:block 1) (:inner 0)))
(specify '((:block 1) (:inner 1)))
(specify! '((:block 1) (:inner 1)))
(try '((:block 0)))
(catch '((:block 2)))
(finally '((:block 0)))

;; binding forms
(let '((:block 1)))
(let* '((:block 1)))
(letfn '((:block 1) (:inner 2 0)))
(binding '((:block 1)))
(loop '((:block 1)))
Expand All @@ -2270,6 +2273,9 @@ work). To set it from Lisp code, use
(run* '((:inner 0)))
(fresh '((:inner 0)))

;; core.match
(match '((:block 1)))

;; core.async
(alt! '((:block 0)))
(alt!! '((:block 0)))
Expand Down
Loading
Loading