From ea15090a432810facb8db0845186de9cc12bb238 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:24:25 +0200 Subject: [PATCH 1/4] Convert legacy indent specs to modern ones the way they're read 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. --- CHANGELOG.md | 4 ++ clojure-mode.el | 65 +++++++++++++++------------ test/clojure-mode-indentation-test.el | 34 +++++++------- 3 files changed, 56 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7626700..ac291cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * [#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). +### 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. + ## 5.23.0 (2026-03-25) ### New features diff --git a/clojure-mode.el b/clojure-mode.el index 6cb40a2b..dde91316 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -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 @@ -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. @@ -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. diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 0e5a6b2f..8186d9a4 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1358,32 +1358,28 @@ x (expect (clojure--indent-spec-to-modern :defn) :to-equal '((:inner 0)))) (it "should convert letfn spec" - ;; '(1 ((:defn)) nil) → ((:block 1) (:inner 2 0)) - ;; The nil is positional padding and gets skipped. - ;; ((:defn)) unwraps: depth 0 → cons, depth 1 → cons, depth 2 → :defn → (:inner 2) - ;; But the position index (0) is not preserved by the simple converter. - ;; The result should at minimum have (:block 1) and (:inner 2). - (let ((result (clojure--indent-spec-to-modern '(1 ((:defn)) nil)))) - (expect (assq :block result) :to-equal '(:block 1)) - (expect (cl-find-if (lambda (r) (eq (car r) :inner)) result) :not :to-be nil) - (expect (cadr (cl-find-if (lambda (r) (eq (car r) :inner)) result)) :to-equal 2))) + ;; The bindings vector is the first argument, and the functions in it + ;; are lists two levels down. + (expect (clojure--indent-spec-to-modern '(1 ((:defn)) nil)) + :to-equal '((:block 1) (:inner 2 0)))) (it "should convert deftype/defrecord spec" - ;; '(2 nil nil (:defn)) → ((:block 2) (:inner 1)) - (let ((result (clojure--indent-spec-to-modern '(2 nil nil (:defn))))) - (expect (assq :block result) :to-equal '(:block 2)) - (expect (cl-find-if (lambda (r) (eq (car r) :inner)) result) - :to-equal '(:inner 1)))) + (expect (clojure--indent-spec-to-modern '(2 nil nil (:defn))) + :to-equal '((:block 2) (:inner 1)))) (it "should convert reify spec" - ;; '(:defn (1)) → ((:inner 0) ...) — :defn becomes (:inner 0) - (let ((result (clojure--indent-spec-to-modern '(:defn (1))))) - (expect (member '(:inner 0) result) :not :to-be nil))) + ;; A nested integer means body indentation. + (expect (clojure--indent-spec-to-modern '(:defn (1))) + :to-equal '((:inner 0) (:inner 1)))) (it "should convert extend-protocol spec" - ;; '(1 :defn) → ((:block 1) (:inner 0)) + ;; :defn as the spec of the arguments indents them like defn. (expect (clojure--indent-spec-to-modern '(1 :defn)) - :to-equal '((:block 1) (:inner 0)))) + :to-equal '((:block 1) (:inner 1)))) + + (it "should apply a list holding just a list to every argument" + (expect (clojure--indent-spec-to-modern '((:defn))) + :to-equal '((:inner 1)))) (it "should convert defprotocol spec" ;; '(1 (:defn)) → ((:block 1) (:inner 1)) From 316c9f2bdb6ddc8ff1bb20a09b1a6de927059767 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:24:53 +0200 Subject: [PATCH 2/4] Put modern indent rules at the right legacy positions 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. --- CHANGELOG.md | 2 + README.md | 12 ++-- clojure-mode.el | 82 +++++++++++---------------- test/clojure-mode-indentation-test.el | 40 +++++++++++-- 4 files changed, 76 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac291cd7..2b513a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,12 @@ ### 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). ### 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) diff --git a/README.md b/README.md index b4b73d59..542c6ad6 100644 --- a/README.md +++ b/README.md @@ -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))) @@ -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 diff --git a/clojure-mode.el b/clojure-mode.el index dde91316..ed7f1331 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -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. @@ -2026,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) @@ -2242,11 +2224,11 @@ work). To set it from Lisp code, use (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))) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 8186d9a4..77e54291 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1423,19 +1423,51 @@ x ;; defprotocol: ((:block 1) (:inner 1)) → (1 (:defn)) (expect (clojure--indent-spec-to-legacy '((:block 1) (:inner 1))) :to-equal '(1 (:defn))) - ;; extend-protocol: ((:block 1) (:inner 0)) → (1 :defn) - (expect (clojure--indent-spec-to-legacy '((:block 1) (:inner 0))) - :to-equal '(1 :defn)) + ;; extend-protocol: ((:block 1) (:inner 1)) → (1 (:defn)) + (expect (clojure--indent-spec-to-legacy '((:block 1) (:inner 1))) + :to-equal '(1 (:defn))) ;; reify: ((:inner 0) (:inner 1)) → (:defn (:defn)) (expect (clojure--indent-spec-to-legacy '((:inner 0) (:inner 1))) :to-equal '(:defn (:defn)))) + (it "should leave the spec of the form itself empty without a :block or (:inner 0) rule" + (expect (clojure--indent-spec-to-legacy '((:inner 1 0))) + :to-equal '(nil (:defn) nil)) + (expect (clojure--indent-spec-to-legacy '((:inner 2))) + :to-equal '(nil ((:defn))))) + + (it "should ignore rules with negative numbers" + (expect (clojure--indent-spec-to-legacy '((:block 1) (:inner 1 -1))) + :to-equal 1) + (expect (clojure--indent-spec-to-legacy '((:block 1) (:inner -1))) + :to-equal 1) + (expect (clojure--indent-spec-to-legacy '((:block -1))) + :to-equal nil)) + + (it "should drop an (:inner 0) rule next to a :block rule, as it does nothing there" + (expect (clojure--indent-spec-to-legacy '((:block 1) (:inner 0))) + :to-equal 1)) + + (it "should round-trip the built-in specs with several rules" + (dolist (sym '(letfn reify deftype defrecord defprotocol definterface + extend-protocol extend-type specify specify! proxy)) + (expect (clojure--indent-spec-to-modern + (clojure--indent-spec-to-legacy (clojure-get-indent-spec sym))) + :to-equal (clojure-get-indent-spec sym)))) + (it "should produce working specs for put-clojure-indent with modern format" ;; Verify that a form set with modern format indents correctly. (put-clojure-indent 'test-modern-let '((:block 1))) (with-clojure-buffer "\n(test-modern-let [x 1]\nbody)" (indent-region (point-min) (point-max)) - (expect (buffer-string) :to-equal "\n(test-modern-let [x 1]\n body)")))) + (expect (buffer-string) :to-equal "\n(test-modern-let [x 1]\n body)"))) + + (it "should indent with a modern spec whose :inner rule has a position" + (put-clojure-indent 'test-modern-impl '((:inner 1 0))) + (with-clojure-buffer "\n(test-modern-impl (foo [x]\nx)\n(bar [y]\ny))" + (indent-region (point-min) (point-max)) + (expect (buffer-string) + :to-equal "\n(test-modern-impl (foo [x]\n x)\n (bar [y]\n y))")))) (provide 'clojure-mode-indentation-test) From 827c43ab3e62ed2e73ba0e80352eeca82ea5f1b3 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:25:04 +0200 Subject: [PATCH 3/4] Indent fdef like cljfmt and add let* 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. --- CHANGELOG.md | 1 + clojure-mode.el | 3 ++- test/clojure-mode-indentation-test.el | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b513a74..7974a839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * [#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. ### Bugs fixed diff --git a/clojure-mode.el b/clojure-mode.el index ed7f1331..e9b87bb9 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -2216,7 +2216,7 @@ work). To set it from Lisp code, use (locking '((:block 1))) (proxy '((:block 2) (:inner 1))) (as-> '((:block 2))) - (fdef '((:block 1))) + (fdef '((:inner 0))) (reify '((:inner 0) (:inner 1))) (deftype '((:block 2) (:inner 1))) @@ -2235,6 +2235,7 @@ work). To set it from Lisp code, use ;; binding forms (let '((:block 1))) + (let* '((:block 1))) (letfn '((:block 1) (:inner 2 0))) (binding '((:block 1))) (loop '((:block 1))) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 77e54291..ac616275 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -773,7 +773,12 @@ DESCRIPTION is a string with the description of the spec." " (fdef my-fn :args (s/cat :x int?) - :ret int?)") + :ret int?)" + + " +(fdef + my-fn + :args (s/cat :x int?))") (when-indenting-it "should handle this-as" " From 4ca3026d3099d499cd00ff04310d307c4286dc36 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:32:54 +0200 Subject: [PATCH 4/4] Add the indent specs cljfmt has and clojure-mode lacked 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. --- CHANGELOG.md | 1 + clojure-mode.el | 14 ++++++++++++++ test/clojure-mode-indentation-test.el | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7974a839..b9bb1d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#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 diff --git a/clojure-mode.el b/clojure-mode.el index e9b87bb9..00ba442f 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -2196,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))) @@ -2214,6 +2220,11 @@ 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 '((:inner 0))) @@ -2262,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))) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index ac616275..feeeae38 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -769,6 +769,24 @@ DESCRIPTION is a string with the description of the spec." (locking obj (alter-state! obj))") + (when-indenting-it "should handle match" + " +(match x + 1 :one + :else :other)") + + (when-indenting-it "should handle struct-map" + " +(struct-map person + :name \"Ann\" + :age 30)") + + (when-indenting-it "should handle with-open with the bindings on their own line" + " +(with-open + [r (reader f)] + (slurp r))") + (when-indenting-it "should handle fdef" " (fdef my-fn