branch: elpa/cider
commit cb910a0dcd07e83cbb9e364e34950502f7c57a9f
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>

    Convert modern indent specs to legacy for clojure-mode compatibility
    
    orchard's indent inference tables now emit modern tuple-format
    specs (e.g., [[:block 1] [:inner 0]]) which arrive via nREPL as
    the :style/indent metadata. Convert these to legacy format before
    passing to clojure-mode's clojure-get-indent-function hook, so
    older clojure-mode versions that don't understand the modern format
    still work correctly.
    
    New functions:
    - cider--modern-indent-spec-p: detect modern tuple format
    - cider--indent-spec-to-legacy: convert modern → legacy
    
    The conversion happens in cider--get-symbol-indent after reading
    and vector-to-list conversion.
---
 CHANGELOG.md             |  3 +--
 lisp/cider-mode.el       | 12 +++++++--
 lisp/cider-util.el       | 64 ++++++++++++++++++++++++++++++++++++++++++++++++
 test/cider-util-tests.el | 34 +++++++++++++++++++++++++
 4 files changed, 109 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab36ab7473..62311f44e6 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/lisp/cider-mode.el b/lisp/cider-mode.el
index 1c5ce529e5..83367188a1 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 3f40697213..b044fb7143 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 18ad76e33c..78b51ab545 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"

Reply via email to