From 22fcd0aadf3339e5a445d5e6f1677446640fb88b Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 11:15:49 +0200 Subject: [PATCH 1/5] Add modern indent spec format with conversion functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce the modern tuple-based indent spec format shared with clojure-ts-mode, alongside conversion functions between modern and legacy formats. Modern format uses explicit rule tuples: - ((:block N)) — N special args, then body - ((:inner D)) — body-style at nesting depth D - ((:inner D I)) — body-style at depth D, only at position I The legacy positional format will be removed in clojure-mode 6. New functions: - clojure--modern-indent-spec-p: detect modern format - clojure--indent-spec-to-modern: legacy → modern conversion - clojure--indent-spec-to-legacy: modern → legacy conversion - clojure--unwrap-legacy-spec: helper for nested spec unwrapping --- clojure-mode.el | 141 ++++++++++++++++++++++++++ test/clojure-mode-indentation-test.el | 85 ++++++++++++++++ 2 files changed, 226 insertions(+) diff --git a/clojure-mode.el b/clojure-mode.el index adceb4a4..45d780f0 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -1925,6 +1925,147 @@ This function also returns nil meaning don't specify the indentation." ;; preference. (t (clojure--normal-indent last-sexp clojure-indent-style)))))))) +;;; Indent spec format conversion +;; +;; clojure-mode supports two indent spec formats: +;; +;; 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 +;; +;; Modern format (shared with clojure-ts-mode): +;; - ((:block N)): N special args, then body +;; - ((:inner D)): body-style at nesting depth D +;; - ((:inner D I)): body-style at depth D, only at position I +;; - Multiple rules: ((:block 1) (:inner 2 0)) + +(defun clojure--modern-indent-spec-p (spec) + "Return non-nil if SPEC uses the modern tuple-based indent format. +A modern spec is a list of rules like ((:block N)) or +\((:inner D) (:inner D I))." + (and (listp spec) + spec ; not nil + (cl-every (lambda (rule) + (and (listp rule) + (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--indent-spec-to-modern (spec) + "Convert a legacy indent SPEC to modern tuple format. +Returns SPEC unchanged if it is already in modern format or is a +function. + +Integer N becomes ((:block N)). +:defn becomes ((:inner 0)). +A positional list is converted element by element. + +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))) + +(defun clojure--indent-spec-to-legacy (spec) + "Convert a modern indent SPEC to legacy positional format. +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. + +The legacy format will be removed in clojure-mode 6." + (cond + ((not (clojure--modern-indent-spec-p spec)) spec) + ;; Simple cases + ((equal spec '((:inner 0))) :defn) + ((and (= 1 (length spec)) + (eq :block (caar spec))) + (cadar spec)) + ;; Complex multi-rule specs + (t + (let ((block-n nil) + (inner-rules nil)) + (dolist (rule spec) + (pcase rule + (`(:block ,n) (setq block-n n)) + (`(:inner ,d) (push (cons d nil) inner-rules)) + (`(:inner ,d ,i) (push (cons d i) inner-rules)))) + ;; Build a positional list. + ;; The :block N determines how many special args there are. + ;; :inner rules become nested (:defn) wrappers at appropriate positions. + (let* ((result nil) + ;; Find max position we need to fill + (inner-with-idx (cl-remove-if-not #'cdr inner-rules)) + (inner-no-idx (cl-remove-if #'cdr inner-rules)) + ;; An :inner D without index goes in the last position + ;; (applies to all remaining args at that depth) + (tail-spec (when inner-no-idx + (let ((depth (caar inner-no-idx))) + ;; Wrap :defn in depth-1 layers of parens + (let ((s :defn)) + (dotimes (_ (max 0 (1- depth))) + (setq s (list s))) + s))))) + ;; Start with block-n or first element + (when block-n + (push block-n result)) + ;; Add indexed :inner rules at their positions + (dolist (ir inner-with-idx) + (let* ((depth (car ir)) + (idx (cdr ir)) + ;; Pad result to reach position idx+1 (accounting for block-n at pos 0) + (target-len (+ (if block-n 1 0) idx 1)) + (s :defn)) + ;; Wrap :defn in depth-1 layers of parens + (dotimes (_ (max 0 (1- depth))) + (setq s (list s))) + ;; Pad with nil + (while (< (length result) target-len) + (push nil (cdr (last result)))) + (setf (nth (+ (if block-n 1 0) idx) result) s))) + ;; Append tail spec (non-indexed :inner) or nil padding + (when tail-spec + (let ((current-len (length result))) + ;; Ensure tail goes after block positions + (when block-n + (while (< (length result) (+ block-n 1)) + (push nil (cdr (last result))))) + ;; Only append if not already covered + (when (or (null result) + (>= (length result) current-len)) + (nconc result (list tail-spec))))) + ;; Add trailing nil for specs that need it (like letfn) + (when (and inner-with-idx tail-spec) + (nconc result (list nil))) + result))))) + ;;; Setting indentation (defun put-clojure-indent (sym indent) "Set the indentation spec of SYM to INDENT. diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index d4a49199..23e5306d 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1332,6 +1332,91 @@ x (indent-region (point-min) (point-max)) (expect (buffer-string) :to-equal "\n(let [x 1]\n x)"))))) +(describe "clojure--modern-indent-spec-p" + (it "should recognize modern specs" + (expect (clojure--modern-indent-spec-p '((:block 1))) :to-be-truthy) + (expect (clojure--modern-indent-spec-p '((:inner 0))) :to-be-truthy) + (expect (clojure--modern-indent-spec-p '((:block 1) (:inner 2 0))) :to-be-truthy) + (expect (clojure--modern-indent-spec-p '((:block 2) (:inner 1))) :to-be-truthy) + (expect (clojure--modern-indent-spec-p '((:inner 0) (:inner 1))) :to-be-truthy)) + + (it "should reject legacy and non-spec values" + (expect (clojure--modern-indent-spec-p 1) :not :to-be-truthy) + (expect (clojure--modern-indent-spec-p :defn) :not :to-be-truthy) + (expect (clojure--modern-indent-spec-p nil) :not :to-be-truthy) + (expect (clojure--modern-indent-spec-p '(1 (:defn))) :not :to-be-truthy) + (expect (clojure--modern-indent-spec-p '(1 ((:defn)) nil)) :not :to-be-truthy) + (expect (clojure--modern-indent-spec-p '(:defn (1))) :not :to-be-truthy))) + +(describe "clojure--indent-spec-to-modern" + (it "should convert integer specs" + (expect (clojure--indent-spec-to-modern 0) :to-equal '((:block 0))) + (expect (clojure--indent-spec-to-modern 1) :to-equal '((:block 1))) + (expect (clojure--indent-spec-to-modern 2) :to-equal '((:block 2)))) + + (it "should convert :defn" + (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))) + + (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)))) + + (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))) + + (it "should convert extend-protocol spec" + ;; '(1 :defn) → ((:block 1) (:inner 0)) + (expect (clojure--indent-spec-to-modern '(1 :defn)) + :to-equal '((:block 1) (:inner 0)))) + + (it "should convert defprotocol spec" + ;; '(1 (:defn)) → ((:block 1) (:inner 1)) + (expect (clojure--indent-spec-to-modern '(1 (:defn))) + :to-equal '((:block 1) (:inner 1)))) + + (it "should return modern specs unchanged" + (expect (clojure--indent-spec-to-modern '((:block 1) (:inner 2 0))) + :to-equal '((:block 1) (:inner 2 0)))) + + (it "should return functions unchanged" + (let ((f (lambda (_p _s) 0))) + (expect (clojure--indent-spec-to-modern f) :to-equal f)))) + +(describe "clojure--indent-spec-to-legacy" + (it "should convert simple block specs" + (expect (clojure--indent-spec-to-legacy '((:block 0))) :to-equal 0) + (expect (clojure--indent-spec-to-legacy '((:block 1))) :to-equal 1) + (expect (clojure--indent-spec-to-legacy '((:block 2))) :to-equal 2)) + + (it "should convert simple inner-0 specs" + (expect (clojure--indent-spec-to-legacy '((:inner 0))) :to-equal :defn)) + + (it "should return non-modern specs unchanged" + (expect (clojure--indent-spec-to-legacy 1) :to-equal 1) + (expect (clojure--indent-spec-to-legacy :defn) :to-equal :defn)) + + (it "should round-trip simple specs" + (dolist (spec '(0 1 2 :defn)) + (expect (clojure--indent-spec-to-legacy + (clojure--indent-spec-to-modern spec)) + :to-equal spec)))) + (provide 'clojure-mode-indentation-test) ;;; clojure-mode-indentation-test.el ends here From bd251eb5ad2d6af2a8f7a6b21f6a93e1910b09e9 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 11:16:53 +0200 Subject: [PATCH 2/5] Wire up put-clojure-indent and validation to accept modern specs put-clojure-indent now: - Accepts both modern and legacy indent spec formats - Stores the modern format canonically on 'clojure-indent-spec - Stores the legacy format on 'clojure-indent-function for the backtracking engine Validation functions updated to accept modern tuple rules ((:block N), (:inner D), (:inner D I)) for safe-local-eval. clojure--get-indent-method converts modern-format specs from clojure-get-indent-function to legacy format before returning them to the backtracking engine. --- clojure-mode.el | 67 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/clojure-mode.el b/clojure-mode.el index 45d780f0..dd10f9b8 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -1681,9 +1681,16 @@ the part after the `/'. Look for a spec using `clojure-get-indent-function', then try the `clojure-indent-function' and `clojure-backtracking-indent' -symbol properties." - (or (when (functionp clojure-get-indent-function) - (funcall clojure-get-indent-function function-name)) +symbol properties. + +The return value is always in legacy format for consumption by the +backtracking indent engine. Modern-format specs from +`clojure-get-indent-function' are converted automatically." + (or (let ((spec (when (functionp clojure-get-indent-function) + (funcall clojure-get-indent-function function-name)))) + (if (clojure--modern-indent-spec-p spec) + (clojure--indent-spec-to-legacy spec) + spec)) (get (intern-soft function-name) 'clojure-indent-function) (get (intern-soft function-name) 'clojure-backtracking-indent) (when (string-match "/\\([^/]+\\)\\'" function-name) @@ -2070,18 +2077,36 @@ The legacy format will be removed in clojure-mode 6." (defun put-clojure-indent (sym indent) "Set the indentation spec of SYM to INDENT. -INDENT can be: +INDENT can be in either the modern or legacy format. + +Modern format (preferred, shared with clojure-ts-mode): +- \\='((:block N)) — N special args, then body +- \\='((:inner D)) — body-style at nesting depth D +- \\='((:inner D I)) — depth D, only at position I +- \\='((:block N) (:inner D)) — combination + +Legacy format (to be removed in clojure-mode 6): - `:defn' — indent like a function/macro body - an integer N — N special args, then body -- a function — custom indentation function -- a quoted list — positional backtracking spec (see - `clojure--find-indent-spec-backtracking') +- a quoted positional list — see `clojure--find-indent-spec-backtracking' + +A function can also be used as a custom indentation function. Examples: + (put-clojure-indent \\='when \\='((:block 1))) + (put-clojure-indent \\='defn \\='((:inner 0))) + (put-clojure-indent \\='letfn \\='((:block 1) (:inner 2 0))) + ;; Legacy forms also accepted: (put-clojure-indent \\='when 1) - (put-clojure-indent \\='>defn :defn) - (put-clojure-indent \\='letfn \\='(1 ((:defn)) nil))" - (put sym 'clojure-indent-function indent)) + (put-clojure-indent \\='>defn :defn)" + ;; Store the modern format canonically. + (put sym 'clojure-indent-spec + (clojure--indent-spec-to-modern indent)) + ;; Store the legacy format for the backtracking engine. + (put sym 'clojure-indent-function + (if (clojure--modern-indent-spec-p indent) + (clojure--indent-spec-to-legacy indent) + indent))) (defun clojure--maybe-quoted-symbol-p (x) "Check that X is either a symbol or a quoted symbol like :foo or \\='foo." @@ -2091,14 +2116,26 @@ Examples: (eq 'quote (car x)) (symbolp (cadr x))))) +(defun clojure--valid-modern-indent-rule-p (rule) + "Check that RULE is a valid modern indent rule. +A valid rule is (:block N), (:inner D), or (:inner D I) where +N, D, and I are non-negative integers." + (pcase rule + (`(:block ,(pred integerp)) t) + (`(:inner ,(pred integerp)) t) + (`(:inner ,(pred integerp) ,(pred integerp)) t) + (_ nil))) + (defun clojure--valid-unquoted-indent-spec-p (spec) "Check that the indentation SPEC is valid. -Validate it with respect to -https://docs.cider.mx/cider/indent_spec.html e.g. (2 :form -:form (1)))." +Accepts both modern tuple format and legacy positional format." (or (null spec) (integerp spec) (memq spec '(:form :defn)) + ;; Modern tuple format + (and (clojure--modern-indent-spec-p spec) + (cl-every #'clojure--valid-modern-indent-rule-p spec)) + ;; Legacy positional format (and (listp spec) (or (integerp (car spec)) (memq (car spec) '(:form :defn)) @@ -2107,9 +2144,7 @@ https://docs.cider.mx/cider/indent_spec.html e.g. (2 :form (defun clojure--valid-indent-spec-p (spec) "Check that the indentation SPEC (quoted if a list) is valid. -Validate it with respect to -https://docs.cider.mx/cider/indent_spec.html e.g. (2 :form -:form (1)))." +Accepts both modern tuple format and legacy positional format." (or (integerp spec) (and (keywordp spec) (memq spec '(:form :defn))) (and (listp spec) From 6555532f5e8ddcb516062ba50117b89ee7041e0e Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 11:29:52 +0200 Subject: [PATCH 3/5] Convert built-in indent specs to modern format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite all define-clojure-indent specs using the modern tuple format. The put-clojure-indent function automatically converts to legacy format for the backtracking engine, so indentation behavior is unchanged. Notable conversion details: - reify: (:defn (1)) → ((:inner 0) (:inner 1)), legacy becomes (:defn (:defn)) which produces identical indentation - deftype/defrecord: (2 nil nil (:defn)) → ((:block 2) (:inner 1)), legacy becomes (2 (:defn)) — equivalent since the backtracking engine uses the last element for overflow positions --- clojure-mode.el | 243 +++++++++++++------------- test/clojure-mode-indentation-test.el | 26 ++- 2 files changed, 142 insertions(+), 127 deletions(-) diff --git a/clojure-mode.el b/clojure-mode.el index dd10f9b8..de3158d1 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -1999,6 +1999,15 @@ The legacy format will be removed in clojure-mode 6." (lambda (a _b) (eq (car a) :block))))) (t spec))) +(defun clojure--wrap-defn (depth) + "Wrap :defn in DEPTH layers of lists. +Depth 0 produces :defn, depth 1 produces (:defn), depth 2 +produces ((:defn)), etc." + (let ((s :defn)) + (dotimes (_ depth) + (setq s (list s))) + s)) + (defun clojure--indent-spec-to-legacy (spec) "Convert a modern indent SPEC to legacy positional format. Returns SPEC unchanged if it is not in modern format. @@ -2008,70 +2017,52 @@ Returns SPEC unchanged if it is not in modern format. Complex multi-rule specs are converted to positional lists. The legacy format will be removed in clojure-mode 6." - (cond - ((not (clojure--modern-indent-spec-p spec)) spec) - ;; Simple cases - ((equal spec '((:inner 0))) :defn) - ((and (= 1 (length spec)) - (eq :block (caar spec))) - (cadar spec)) - ;; Complex multi-rule specs - (t + (if (not (clojure--modern-indent-spec-p spec)) + spec + ;; Extract components (let ((block-n nil) - (inner-rules 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 (cons d nil) inner-rules)) - (`(:inner ,d ,i) (push (cons d i) inner-rules)))) - ;; Build a positional list. - ;; The :block N determines how many special args there are. - ;; :inner rules become nested (:defn) wrappers at appropriate positions. - (let* ((result nil) - ;; Find max position we need to fill - (inner-with-idx (cl-remove-if-not #'cdr inner-rules)) - (inner-no-idx (cl-remove-if #'cdr inner-rules)) - ;; An :inner D without index goes in the last position - ;; (applies to all remaining args at that depth) - (tail-spec (when inner-no-idx - (let ((depth (caar inner-no-idx))) - ;; Wrap :defn in depth-1 layers of parens - (let ((s :defn)) - (dotimes (_ (max 0 (1- depth))) - (setq s (list s))) - s))))) - ;; Start with block-n or first element - (when block-n - (push block-n result)) - ;; Add indexed :inner rules at their positions - (dolist (ir inner-with-idx) - (let* ((depth (car ir)) - (idx (cdr ir)) - ;; Pad result to reach position idx+1 (accounting for block-n at pos 0) - (target-len (+ (if block-n 1 0) idx 1)) - (s :defn)) - ;; Wrap :defn in depth-1 layers of parens - (dotimes (_ (max 0 (1- depth))) - (setq s (list s))) - ;; Pad with nil - (while (< (length result) target-len) - (push nil (cdr (last result)))) - (setf (nth (+ (if block-n 1 0) idx) result) s))) - ;; Append tail spec (non-indexed :inner) or nil padding - (when tail-spec - (let ((current-len (length result))) - ;; Ensure tail goes after block positions - (when block-n - (while (< (length result) (+ block-n 1)) - (push nil (cdr (last result))))) - ;; Only append if not already covered - (when (or (null result) - (>= (length result) current-len)) - (nconc result (list tail-spec))))) - ;; Add trailing nil for specs that need it (like letfn) - (when (and inner-with-idx tail-spec) - (nconc result (list nil))) - result))))) + (`(: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)))))) ;;; Setting indentation (defun put-clojure-indent (sym indent) @@ -2195,81 +2186,81 @@ work). To set it from Lisp code, use (define-clojure-indent ;; built-ins - (ns 1) - (fn :defn) - (def :defn) - (defn :defn) - (bound-fn :defn) - (if 1) - (if-not 1) - (case 1) - (cond 0) - (condp 2) - (cond-> 1) - (cond->> 1) - (when 1) - (while 1) - (when-not 1) - (when-first 1) - (do 0) - (delay 0) - (future 0) - (comment 0) - (doto 1) - (locking 1) - (proxy '(2 nil nil (:defn))) - (as-> 2) - (fdef 1) - - (reify '(:defn (1))) - (deftype '(2 nil nil (:defn))) - (defrecord '(2 nil nil (:defn))) - (defprotocol '(1 (:defn))) - (definterface '(1 (:defn))) - (extend 1) - (extend-protocol '(1 :defn)) - (extend-type '(1 :defn)) + (ns '((:block 1))) + (fn '((:inner 0))) + (def '((:inner 0))) + (defn '((:inner 0))) + (bound-fn '((:inner 0))) + (if '((:block 1))) + (if-not '((:block 1))) + (case '((:block 1))) + (cond '((:block 0))) + (condp '((:block 2))) + (cond-> '((:block 1))) + (cond->> '((:block 1))) + (when '((:block 1))) + (while '((:block 1))) + (when-not '((:block 1))) + (when-first '((:block 1))) + (do '((:block 0))) + (delay '((:block 0))) + (future '((:block 0))) + (comment '((:block 0))) + (doto '((:block 1))) + (locking '((:block 1))) + (proxy '((:block 2) (:inner 1))) + (as-> '((:block 2))) + (fdef '((:block 1))) + + (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))) ;; specify and specify! are from ClojureScript - (specify '(1 :defn)) - (specify! '(1 :defn)) - (try 0) - (catch 2) - (finally 0) + (specify '((:block 1) (:inner 0))) + (specify! '((:block 1) (:inner 0))) + (try '((:block 0))) + (catch '((:block 2))) + (finally '((:block 0))) ;; binding forms - (let 1) - (letfn '(1 ((:defn)) nil)) - (binding 1) - (loop 1) - (for 1) - (doseq 1) - (dotimes 1) - (when-let 1) - (if-let 1) - (when-some 1) - (if-some 1) - (this-as 1) ; ClojureScript - - (defmethod :defn) + (let '((:block 1))) + (letfn '((:block 1) (:inner 2 0))) + (binding '((:block 1))) + (loop '((:block 1))) + (for '((:block 1))) + (doseq '((:block 1))) + (dotimes '((:block 1))) + (when-let '((:block 1))) + (if-let '((:block 1))) + (when-some '((:block 1))) + (if-some '((:block 1))) + (this-as '((:block 1))) ; ClojureScript + + (defmethod '((:inner 0))) ;; clojure.test - (testing 1) - (deftest :defn) - (are 2) - (use-fixtures :defn) - (async 1) + (testing '((:block 1))) + (deftest '((:inner 0))) + (are '((:block 2))) + (use-fixtures '((:inner 0))) + (async '((:block 1))) ;; core.logic - (run :defn) - (run* :defn) - (fresh :defn) + (run '((:inner 0))) + (run* '((:inner 0))) + (fresh '((:inner 0))) ;; core.async - (alt! 0) - (alt!! 0) - (go 0) - (go-loop 1) - (thread 0)) + (alt! '((:block 0))) + (alt!! '((:block 0))) + (go '((:block 0))) + (go-loop '((:block 1))) + (thread '((:block 0)))) diff --git a/test/clojure-mode-indentation-test.el b/test/clojure-mode-indentation-test.el index 23e5306d..0e5a6b2f 100644 --- a/test/clojure-mode-indentation-test.el +++ b/test/clojure-mode-indentation-test.el @@ -1415,7 +1415,31 @@ x (dolist (spec '(0 1 2 :defn)) (expect (clojure--indent-spec-to-legacy (clojure--indent-spec-to-modern spec)) - :to-equal spec)))) + :to-equal spec))) + + (it "should convert complex multi-rule specs" + ;; letfn: ((:block 1) (:inner 2 0)) → (1 ((:defn)) nil) + (expect (clojure--indent-spec-to-legacy '((:block 1) (:inner 2 0))) + :to-equal '(1 ((:defn)) nil)) + ;; deftype: ((:block 2) (:inner 1)) → (2 (:defn)) + (expect (clojure--indent-spec-to-legacy '((:block 2) (:inner 1))) + :to-equal '(2 (:defn))) + ;; 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)) + ;; reify: ((:inner 0) (:inner 1)) → (:defn (:defn)) + (expect (clojure--indent-spec-to-legacy '((:inner 0) (:inner 1))) + :to-equal '(:defn (:defn)))) + + (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)")))) (provide 'clojure-mode-indentation-test) From feb2e5bf2c885d0810059e5f86a4be814114d507 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 11:31:12 +0200 Subject: [PATCH 4/5] Update documentation for modern indent spec format Update README to present the modern tuple format as primary, with legacy format noted for backward compatibility (to be removed in clojure-mode 6). Rewrite backtracking section examples using modern format. Update clojure-get-indent-function docstring to note that both formats are accepted and converted automatically. --- README.md | 98 +++++++++++++++++++++++++------------------------ clojure-mode.el | 11 ++++-- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 9c96146e..36c2b060 100644 --- a/README.md +++ b/README.md @@ -196,10 +196,21 @@ Similarly we have the `clojure-indent-keyword-style`, which works in the followi #### Indentation of macro forms The indentation of special forms and macros with bodies is controlled via -`put-clojure-indent`, `define-clojure-indent` and `clojure-backtracking-indent`. +`put-clojure-indent` and `define-clojure-indent`. Nearly all special forms and built-in macros with bodies have special indentation settings in `clojure-mode`. You can add/alter the indentation settings in your -personal config. Let's assume you want to indent `->>` and `->` like this: +personal config. + +Indent specs use a **modern tuple format** shared with `clojure-ts-mode`: + +| Spec | Meaning | +|------|---------| +| `'((:block N))` | First N args are special, rest indented as body | +| `'((:inner 0))` | All args indented as body (like `defn`) | +| `'((:inner D))` | Body-style indent at nesting depth D | +| `'((:inner D I))` | Body-style at depth D, only at position I | + +You can combine multiple rules. For example, let's say you want to indent `->>` and `->` like this: ```clojure (->> something @@ -211,8 +222,8 @@ personal config. Let's assume you want to indent `->>` and `->` like this: You can do so by putting the following in your config: ```el -(put-clojure-indent '-> 1) -(put-clojure-indent '->> 1) +(put-clojure-indent '-> '((:block 1))) +(put-clojure-indent '->> '((:block 1))) ``` This means that the body of the `->/->>` is after the first argument. @@ -221,24 +232,28 @@ A more compact way to do the same thing is: ```el (define-clojure-indent - (-> 1) - (->> 1)) + (-> '((:block 1))) + (->> '((:block 1)))) ``` To indent something like a definition (`defn`) you can do something like: ``` el -(put-clojure-indent '>defn :defn) +(put-clojure-indent '>defn '((:inner 0))) ``` You can also specify different indentation settings for symbols prefixed with some ns (or ns alias): ```el -(put-clojure-indent 'do 0) -(put-clojure-indent 'my-ns/do 1) +(put-clojure-indent 'do '((:block 0))) +(put-clojure-indent 'my-ns/do '((:block 1))) ``` +**Note:** A legacy format using integers (e.g., `1`), keywords (`:defn`), and +positional lists (e.g., `'(1 ((:defn)) nil)`) is also accepted for backward +compatibility. It will be removed in clojure-mode 6. + ##### Backtracking (contextual) indentation Certain macros and special forms (e.g. `letfn`, `deftype`, @@ -248,65 +263,54 @@ indentation**: when indenting a line, it walks up the sexp tree to find a parent form with an indent spec, then uses the current position within that spec to decide how to indent. -A backtracking indent spec is a **quoted list** where each element -controls indentation at the corresponding argument position -(0-indexed). The allowed elements are: - -| Element | Meaning | -|---------|---------| -| An integer N | First N args are "special" (indented further); rest are body | -| `:defn` | Indent like a function/macro body | -| `:form` | Indent like a regular form | -| `nil` | Use default indentation rules | -| A list `(SPEC)` | This position holds a **list of forms**, each indented according to SPEC | - -For example, `letfn` uses `'(1 ((:defn)) nil)`: +Multi-rule specs combine `:block` and `:inner` rules to control +nested indentation. For example, `letfn` uses `'((:block 1) (:inner 2 0))`: ```clojure -(letfn [(twice [x] ;; pos 0 → spec 1 (1 special arg = the binding vector) - (* x 2)) ;; inside binding → spec ((:defn)) applies: - (thrice [x] ;; each binding is a list of :defn-style forms - (* x 3))] ;; so function bodies get :defn indentation - (+ (twice 5) ;; pos 1+ → spec nil (default → body indentation) +(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 3))] + (+ (twice 5) ;; after the block arg → body indentation (thrice 5))) ``` -And `defrecord` uses `'(2 nil nil (:defn))`: +And `defrecord` uses `'((:block 2) (:inner 1))`: ```clojure -(defrecord MyRecord ;; pos 0 → spec 2 (2 special args: name + fields) - [field1 field2] ;; pos 1 → spec nil (within special args zone) - SomeProtocol ;; pos 2 → spec nil - (some-method [this] ;; pos 3+ → spec (:defn) — each method gets :defn-style +(defrecord MyRecord ;; (:block 2) → 2 special args (name + fields) + [field1 field2] + SomeProtocol ;; (:inner 1) → nested sub-forms at depth 1 + (some-method [this] ;; get body-style indentation (do-stuff this))) ``` -Here are the built-in backtracking specs: +Here are the built-in multi-rule specs: ```el (define-clojure-indent - (letfn '(1 ((:defn)) nil)) - (deftype '(2 nil nil (:defn))) - (defrecord '(2 nil nil (:defn))) - (defprotocol '(1 (:defn))) - (definterface '(1 (:defn))) - (reify '(:defn (1))) - (proxy '(2 nil nil (:defn))) - (extend-protocol '(1 :defn)) - (extend-type '(1 :defn)) - (specify '(1 :defn)) - (specify! '(1 :defn))) + (letfn '((:block 1) (:inner 2 0))) + (deftype '((:block 2) (:inner 1))) + (defrecord '((:block 2) (:inner 1))) + (defprotocol '((:block 1) (:inner 1))) + (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)))) ``` -These follow the same rules as the `:style/indent` metadata specified by [cider-nrepl][]. +This format is shared with `clojure-ts-mode`. It also follows the same +rules as the `:style/indent` metadata specified by [cider-nrepl][]. For more details on writing indent specifications, see [this document](https://docs.cider.mx/cider/indent_spec.html). -The only difference is that you're allowed to use lists instead of vectors. Backtracking is controlled by `clojure-use-backtracking-indent` (default `t`) and limited to `clojure-max-backtracking` levels (default 3). Disabling backtracking will break indentation for -all forms with list-based specs. +all forms with multi-rule specs. The indentation of [special arguments](https://docs.cider.mx/cider/indent_spec.html#special-arguments) is controlled by `clojure-special-arg-indent-factor`, which by default indents special arguments diff --git a/clojure-mode.el b/clojure-mode.el index de3158d1..9a4b406f 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -1669,10 +1669,13 @@ This function should take one argument, the name of the symbol as a string. This name will be exactly as it appears in the buffer, so it might start with a namespace alias. -This function is analogous to the `clojure-indent-function' -symbol property, and its return value should match one of the -allowed values of this property. See `clojure-indent-function' -for more information.") +Return values can be in either the modern format (e.g., +\\='((:block 1) (:inner 0))) or the legacy format (e.g., 1 or +:defn). Modern-format specs are automatically converted to +legacy format for the backtracking indent engine. + +This is typically set by CIDER to provide runtime-aware +indentation specs.") (defun clojure--get-indent-method (function-name) "Return the indent spec for the symbol named FUNCTION-NAME. From 53d11c8bcf58a756d942a9a34ed998a640f48391 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 24 Mar 2026 11:31:52 +0200 Subject: [PATCH 5/5] Add clojure-get-indent-spec public API Returns the modern-format indent spec for a symbol. If only a legacy-format spec exists, it is converted automatically. This provides a clean API for tools that want to inspect indent specs in the modern format. --- clojure-mode.el | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/clojure-mode.el b/clojure-mode.el index 9a4b406f..221cf3c8 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -2102,6 +2102,18 @@ Examples: (clojure--indent-spec-to-legacy indent) indent))) +(defun clojure-get-indent-spec (sym) + "Return the modern-format indent spec for the symbol SYM. +SYM is a symbol or a string. Returns nil if no spec is found. + +If only a legacy-format spec exists, it is converted to modern format." + (let* ((sym-name (if (stringp sym) sym (symbol-name sym))) + (sym-obj (intern-soft sym-name))) + (or (and sym-obj (get sym-obj 'clojure-indent-spec)) + (let ((legacy (and sym-obj (get sym-obj 'clojure-indent-function)))) + (when (and legacy (not (functionp legacy))) + (clojure--indent-spec-to-modern legacy)))))) + (defun clojure--maybe-quoted-symbol-p (x) "Check that X is either a symbol or a quoted symbol like :foo or \\='foo." (or (symbolp x)