From 99daa0d001e6d729ecd2d60b13fa759089ea64ec Mon Sep 17 00:00:00 2001 From: "reut.sharabani" Date: Wed, 22 Mar 2023 22:11:21 +0200 Subject: [PATCH 1/6] align maps --- .gitignore | 1 + README.md | 17 +++ cljfmt/src/cljfmt/core.cljc | 87 +++++++++++++++ cljfmt/test/cljfmt/core_test.cljc | 174 ++++++++++++++++++++++++++++++ install.sh | 1 + 5 files changed, 280 insertions(+) diff --git a/.gitignore b/.gitignore index 02ca798d..a01a3da4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ pom.xml.asc reports .cpcache .clj-kondo +.lsp diff --git a/README.md b/README.md index bdebb86c..32598949 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,23 @@ In order to load the standard configuration file from Leiningen, add the Defaults to `:community` +* `:align-maps?` - + True if cljfmt should left align the values of maps. + + This will convert: + ```clojure + {:foo 1 + :barbaz 2} + ``` + To: + ```clojure + {:foo 1 + :barbaz 2} + ``` + Defaults to `false`. + +You can also configure the behavior of cljfmt: + [indents.md]: docs/INDENTS.md [community style recommendation]: https://guide.clojure.style/#one-space-indent diff --git a/cljfmt/src/cljfmt/core.cljc b/cljfmt/src/cljfmt/core.cljc index d22d75f0..628d67e9 100644 --- a/cljfmt/src/cljfmt/core.cljc +++ b/cljfmt/src/cljfmt/core.cljc @@ -348,6 +348,7 @@ :function-arguments-indentation :community :indents default-indents :extra-indents {} + :align-maps? false :alias-map {}}) (defmulti ^:private indenter-fn @@ -555,6 +556,90 @@ (defn sort-ns-references [form] (transform form edit-all ns-reference? sort-arguments)) +(defn- node-width [zloc] + (-> zloc z/node n/string count)) + +(defn- node-column [zloc] + (loop [zloc (z/left* zloc), n 0] + (if (or (nil? zloc) (line-break? zloc)) + n + (recur (z/left* zloc) + (if (clojure-whitespace? zloc) n (inc n)))))) + +(defn- group-separator? [zloc] + (= (z/string zloc) "\n\n")) + +(defn- node-group [zloc] + (loop [zloc (z/left* zloc), n 0] + (if (nil? zloc) + n + (recur (z/left* zloc) + (if (group-separator? zloc) (inc n) n))))) + +(defn- comma-after? [zloc] + (let [right (z/right* zloc)] + (or (comma? right) + (and (z/whitespace? right) (comma? (z/right* right)))))) + +(defn- max-group-column-widths [zloc] + (loop [zloc (z/down zloc), max-widths {}] + (if (nil? zloc) + max-widths + (let [width (if (comma-after? zloc) + (inc (node-width zloc)) + (node-width zloc)) + column (node-column zloc) + group (node-group zloc)] + (recur (z/right zloc) + (update-in max-widths [group column] (fnil max 0) width)))))) + +(defn- quote? [zloc] + (-> zloc + z/node + n/tag + (= :quote))) + +(defn- remove-space-right [zloc] + (let [right (z/right* zloc)] + (if (space? right) + (if (quote? zloc) + (z/up (z/remove* right)) + (z/remove* right)) + zloc))) + +(defn- insert-space-right [zloc n] + (let [right (z/right* zloc)] + (if (comma? right) + (insert-space-right (remove-space-right right) (dec n)) + (z/insert-space-right zloc n)))) + +(defn- set-spacing-right [zloc n] + (-> zloc (remove-space-right) (insert-space-right n))) + +(defn- map-children [zloc f] + (if-let [zloc (z/down zloc)] + (loop [zloc zloc] + (let [zloc (f zloc)] + (if-let [zloc (z/right zloc)] + (recur zloc) + (z/up zloc)))) + zloc)) + +(defn- pad-node [zloc width] + (set-spacing-right zloc (- width (node-width zloc)))) + +(defn- end-of-line? [zloc] + (line-break? (skip-whitespace-and-commas (z/right* zloc)))) + +(defn- align-form-columns [zloc] + (let [max-widths (max-group-column-widths zloc)] + (map-children zloc #(cond-> % + (and (z/right %) (not (end-of-line? %))) + (pad-node (inc (get-in max-widths [(node-group %) (node-column %)]))))))) + +(defn align-maps [form] + (transform form edit-all z/map? align-form-columns)) + (defn reformat-form ([form] (reformat-form form {})) @@ -573,6 +658,8 @@ insert-missing-whitespace) (cond-> (:remove-multiple-non-indenting-spaces? opts) remove-multiple-non-indenting-spaces) + (cond-> (:align-maps? opts) + align-maps) (cond-> (:indentation? opts) (reindent (merge (:indents opts) (:extra-indents opts)) (:alias-map opts) diff --git a/cljfmt/test/cljfmt/core_test.cljc b/cljfmt/test/cljfmt/core_test.cljc index 404cbe87..8dd935d6 100644 --- a/cljfmt/test/cljfmt/core_test.cljc +++ b/cljfmt/test/cljfmt/core_test.cljc @@ -1845,3 +1845,177 @@ (deftest test-clojure-12-syntax (is (reformats-to? ["^Long/1 a"] ["^Long/1 a"]))) + +(deftest test-align-maps + (testing "straightforward test cases" + (testing "sanity" + (is (reformats-to? + ["(def x 1)"] + ["(def x 1)"] + {:align-maps? true}))) + (testing "no op 1" + (is (reformats-to? + ["{:a 1}"] + ["{:a 1}"] + {:align-maps? true}))) + (testing "no op 2" + (is (reformats-to? + ["{:a 1" + " :b 2}"] + ["{:a 1" + " :b 2}"] + {:align-maps? true}))) + (testing "empty" + (is (reformats-to? + ["{}"] + ["{}"] + {:align-maps? true}))) + (testing "simple" + (is (reformats-to? + ["{:x 1" + " :longer 2}"] + ["{:x 1" + " :longer 2}"] + {:align-maps? true}))) + (testing "nested simple" + (is (reformats-to? + ["{:x {:x 1}" + " :longer 2}"] + ["{:x {:x 1}" + " :longer 2}"] + {:align-maps? true}))) + (testing "nested align" + (is (reformats-to? + ["{:x {:x 1" + " :longer 2}" + " :longer 2}"] + ["{:x {:x 1" + " :longer 2}" + " :longer 2}"] + {:align-maps? true}))) + (testing "align many" + (is (reformats-to? + ["{:a 1" + " :longer 2" + " :b 3}"] + ["{:a 1" + " :longer 2" + " :b 3}"] + {:align-maps? true}))) + (testing "preserves comments" + (is (reformats-to? + ["{:a 1 ;; comment" + " :longer 2}"] + ["{:a 1 ;; comment" + " :longer 2}"] + {:align-maps? true})))) + (testing "non-trivial test cases" + (testing "idnentation after align" + (is (reformats-to? + ["(def m {{:a 1" + ":b 2} [x" + "y]" + ":d [z]})"] + ["(def m {{:a 1" + " :b 2} [x" + " y]" + " :d [z]})"]))) + (testing "cljs map values" + (is (reformats-to? + ["{:indents {'thing.core/defthing [[:inner 0]]" + "'let [[:inner 0]]}" + "#?@(:cljs [:alias-map {}])}"] + ["{:indents {'thing.core/defthing [[:inner 0]]" + " 'let [[:inner 0]]}" + " #?@(:cljs [:alias-map {}])}"] + {:align-maps? true}))) + (testing "indentation off #1" + (is (reformats-to? + ["{ :a 1" + " :longer 2}"] + ["{:a 1" + " :longer 2}"] + {:align-maps? true}))) + (testing "indentation off #2" + (is (reformats-to? + ["{ :a 1" + " :longer 2}"] + ["{:a 1" + " :longer 2}"] + {:align-maps? true}))) + (testing "indentation off #3" + (is (reformats-to? + ["{:a 1" + " :longer 2}"] + ["{:a 1" + " :longer 2}"] + {:align-maps? true}))) + (testing "columns" + (testing "multi-value line" + (is (reformats-to? + ["{:a 1 :b 2" + " :longer 3}"] + ["{:a 1 :b 2" + " :longer 3}"] + {:align-maps? true}))) + (testing "multi-value line" + (is (reformats-to? + ["{:a 1 :longer-a 2" + " :longer-b 3 :c 4}"] + ["{:a 1 :longer-a 2" + " :longer-b 3 :c 4}"] + {:align-maps? true}))) + (testing "multi-value commas" + (is (reformats-to? + ["{:a 1, :longer-a 2" + " :longer-b 3 , :c 4}"] + ["{:a 1, :longer-a 2" + " :longer-b 3, :c 4}"] + {:align-maps? true}))) + (testing "multi-value uneven" + (is (reformats-to? + ["{:a 1 :longer-a 2 :c 3" + " :longer-b 4 :d 5}"] + ["{:a 1 :longer-a 2 :c 3" + " :longer-b 4 :d 5}"] + {:align-maps? true}))) + (testing "multi-value groups 1" + (is (reformats-to? + ["{:a 1 :longer-a 2" + " :longer-b 3 :c 4" + "" + " :d 5 :e 6" + " :fg 7 :h 8}"] + ["{:a 1 :longer-a 2" + " :longer-b 3 :c 4" + "" + " :d 5 :e 6" + " :fg 7 :h 8}"] + {:align-maps? true}))) + (testing "multi-value groups 2" + (is (reformats-to? + ["{:a 1 :longer-a 2" + " :longer-b 3 :c 4" + "" + "" + " :d 5 :e 6" + " :fg 7 :h 8" + "" + " :i 9 :jklmno 10" + " :p 11 :q :value}"] + ["{:a 1 :longer-a 2" + " :longer-b 3 :c 4" + "" + " :d 5 :e 6" + " :fg 7 :h 8" + "" + " :i 9 :jklmno 10" + " :p 11 :q :value}"] + {:align-maps? true}))) + (testing "multi-value partial commas" + (is (reformats-to? + ["{:a 1 :longer-a 2" + " :longer-b 3 , :c 4}"] + ["{:a 1 :longer-a 2" + " :longer-b 3, :c 4}"] + {:align-maps? true})))))) diff --git a/install.sh b/install.sh index 576a8cca..73a77528 100755 --- a/install.sh +++ b/install.sh @@ -37,6 +37,7 @@ echo -n "Downloading cljfmt binaries... " curl -o /tmp/cljfmt.tar.gz -sL "$URL" echo "Done!" +sudo mkdir -p /usr/local/bin sudo tar -xzf /tmp/cljfmt.tar.gz -C /usr/local/bin echo "Extracted cljfmt into /usr/local/bin" From 50dade1daf4697adab42aaca4c9aeec14e3b9b40 Mon Sep 17 00:00:00 2001 From: "reut.sharabani" Date: Wed, 22 Mar 2023 22:11:21 +0200 Subject: [PATCH 2/6] align maps --- cljfmt/test/cljfmt/core_test.cljc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cljfmt/test/cljfmt/core_test.cljc b/cljfmt/test/cljfmt/core_test.cljc index 8dd935d6..32e52257 100644 --- a/cljfmt/test/cljfmt/core_test.cljc +++ b/cljfmt/test/cljfmt/core_test.cljc @@ -4,8 +4,7 @@ :cljs (cljs.test :refer-macros)) [deftest testing is are]] [cljfmt.core :refer [reformat-string default-line-separator normalize-newlines find-line-separator - replace-newlines wrap-normalize-newlines]] - [cljfmt.test-util.common :as common]) + replace-newlines wrap-normalize-newlines]]) #?(:cljs (:require-macros [cljfmt.test-util.cljs]))) (deftest test-indent From 02cc481a39a8ae63e29d286adaf03f87809d196b Mon Sep 17 00:00:00 2001 From: "reut.sharabani" Date: Wed, 22 Mar 2023 03:56:07 +0200 Subject: [PATCH 3/6] Add option to align non-map forms --- README.md | 47 ++++++++++++++- cljfmt/resources/cljfmt/align/clojure.clj | 9 +++ cljfmt/src/cljfmt/core.cljc | 23 +++++++- cljfmt/test/cljfmt/core_test.cljc | 72 ++++++++++++++++++++++- 4 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 cljfmt/resources/cljfmt/align/clojure.clj diff --git a/README.md b/README.md index 32598949..2b214670 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,21 @@ In order to load the standard configuration file from Leiningen, add the ``` Defaults to `false`. +* `:align-forms?` - + true if cljfmt should left align the values of specified forms + This will convert: + ```clojure + (let [foo 1 + barbaz 2]) + ``` + To: + ```clojure + (let [foo 1 + barbaz 2]) + ``` + + Defaults to false. + You can also configure the behavior of cljfmt: [indents.md]: docs/INDENTS.md @@ -326,9 +341,39 @@ You can also configure the behavior of cljfmt: Paths can also be passed as command line arguments. If the path is `-`, then the input is STDIN, and the output STDOUT. +* `:aligns` - + a map of var symbols to arguments' positions that require alignment + i.e. `{myform #{1 2}}` will align `[a 1]` and `[b 2]` forms like `(myform 0 [a 1] [b 2])`. + Argument positions 0-indexed. + See the next section for a detailed explanation. + + Unqualified symbols in the align map will apply to any symbol with a + matching "name" - so `foo` would apply to both `org.me/foo` and + `com.them/foo`. If you want finer-grained control, you can use a fully + qualified symbol in the aligns map to configure form alignment that + applies only to `org.me/foo`: + + ```clojure + :cljfmt {:aligns {org.me/foo #{2 3}} + ``` + + Configured this way, `org.me/foo` will align only argument positions 2 3 (starting from 0). + + Note that `cljfmt` currently doesn't resolve symbols brought into a + namespace using `:refer` or `:use` - they can only be controlled by an + unqualified align rule. + + As with Leiningen profiles, you can add metadata hints. If you want to + override all existing aligns, instead of just supplying new aligns + that are merged with the defaults, you can use the `:replace` hint: + + ```clojure + :cljfmt {:aligns ^:replace {#".*" #{0}} + ``` + ## License -Copyright © 2024 James Reeves +Copyright © 2025 James Reeves Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. diff --git a/cljfmt/resources/cljfmt/align/clojure.clj b/cljfmt/resources/cljfmt/align/clojure.clj new file mode 100644 index 00000000..627e52bd --- /dev/null +++ b/cljfmt/resources/cljfmt/align/clojure.clj @@ -0,0 +1,9 @@ +{let #{0} + doseq #{0} + go-loop #{0} + binding #{0} + with-open #{0} + loop #{0} + for #{0} + with-local-vars #{0} + with-redefs #{0}} diff --git a/cljfmt/src/cljfmt/core.cljc b/cljfmt/src/cljfmt/core.cljc index 628d67e9..a1535273 100644 --- a/cljfmt/src/cljfmt/core.cljc +++ b/cljfmt/src/cljfmt/core.cljc @@ -346,10 +346,11 @@ :split-keypairs-over-multiple-lines? false :sort-ns-references? false :function-arguments-indentation :community - :indents default-indents - :extra-indents {} + :indents default-indents + :extra-indents {} :align-maps? false - :alias-map {}}) + :align-forms? false + :alias-map {}}) (defmulti ^:private indenter-fn (fn [_sym _context [type & _args]] type)) @@ -640,6 +641,20 @@ (defn align-maps [form] (transform form edit-all z/map? align-form-columns)) +(def ^:private default-aligns + (read-resource "cljfmt/align/clojure.clj")) + +(defn alignable? [aligns form] + (let [zloc (-> form z/of-node first)] + (when (and (not (z/whitespace-or-comment? zloc)) + (z/list? (z/up zloc))) + (let [form-type (some-> zloc z/up z/down z/string symbol)] + (when-let [alignable-indices (aligns form-type)] + (contains? alignable-indices (-> zloc index-of dec))))))) + +(defn align-forms [form aligns] + (transform form edit-all (partial alignable? aligns) align-form-columns)) + (defn reformat-form ([form] (reformat-form form {})) @@ -660,6 +675,8 @@ remove-multiple-non-indenting-spaces) (cond-> (:align-maps? opts) align-maps) + (cond-> (:align-forms? opts) + (align-forms (:aligns opts))) (cond-> (:indentation? opts) (reindent (merge (:indents opts) (:extra-indents opts)) (:alias-map opts) diff --git a/cljfmt/test/cljfmt/core_test.cljc b/cljfmt/test/cljfmt/core_test.cljc index 32e52257..6e3f76b2 100644 --- a/cljfmt/test/cljfmt/core_test.cljc +++ b/cljfmt/test/cljfmt/core_test.cljc @@ -4,7 +4,8 @@ :cljs (cljs.test :refer-macros)) [deftest testing is are]] [cljfmt.core :refer [reformat-string default-line-separator normalize-newlines find-line-separator - replace-newlines wrap-normalize-newlines]]) + replace-newlines wrap-normalize-newlines]] + [cljfmt.test-util.common :as common]) #?(:cljs (:require-macros [cljfmt.test-util.cljs]))) (deftest test-indent @@ -1845,6 +1846,75 @@ (deftest test-clojure-12-syntax (is (reformats-to? ["^Long/1 a"] ["^Long/1 a"]))) +(deftest test-align-forms + (testing "straightforward test cases" + (testing "sanity" + (is (reformats-to? + ["(def x 1)"] + ["(def x 1)"] + {:align-forms? true}))) + (testing "no op 2" + (is (reformats-to? + ["(let [x 1" + " y 2])"] + ["(let [x 1" + " y 2])"] + {:align-forms? true}))) + (testing "no op 1" + (is (reformats-to? + ["(let [x 1])"] + ["(let [x 1])"] + {:align-forms? true}))) + (testing "empty" + (is (reformats-to? + ["(let [])"] + ["(let [])"] + {:align-forms? true}))) + (testing "simple" + (is (reformats-to? + ["(let [x 1" + " longer 2])"] + ["(let [x 1" + " longer 2])"] + {:align-forms? true}))) + (testing "nested align" + (is (reformats-to? + ["(let [x (let [x 1" + " longer 2])" + " longer 2])"] + ["(let [x (let [x 1" + " longer 2])" + " longer 2])"] + {:align-forms? true}))) + (testing "preserves comments" + (is (reformats-to? + ["(let [a 1 ;; comment" + " longer 2])"] + ["(let [a 1 ;; comment" + " longer 2])"] + {:align-forms? true}))) + (testing "align args" + (testing "simple" + (is (reformats-to? + ["(special something [a 1" + " longer 2])"] + ["(special something [a 1" + " longer 2])"] + {:align-forms? true + :aligns {'special #{1}}}))) + (testing "don't mixup args" + (is (reformats-to? + ["(special [a 1" + " longer 2]" + " [a 1" + " longer 2])"] + ["(special [a 1" + " longer 2]" + " [a 1" + " longer 2])"] + {:align-forms? true + :aligns {'special #{1}}})))))) + (deftest test-align-maps (testing "straightforward test cases" (testing "sanity" From d3b700678e08149aadafba591d31295d31ab2d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Bo=C3=ABthius=20Lissheim?= Date: Fri, 10 Oct 2025 13:04:06 +0200 Subject: [PATCH 4/6] use alignment --- cljfmt.edn | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cljfmt.edn b/cljfmt.edn index a1403530..bca5f209 100644 --- a/cljfmt.edn +++ b/cljfmt.edn @@ -3,4 +3,7 @@ "cljfmt/test" "cljfmt/project.clj" "lein-cljfmt/src"] - :sort-ns-references? true} + :sort-ns-references? true + :align-forms? true + :align-maps? true + } From 87a4ec6f80474370c8dec79ac05c92641b447469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Bo=C3=ABthius=20Lissheim?= Date: Fri, 10 Oct 2025 14:48:36 +0200 Subject: [PATCH 5/6] fix null pointer exception when `:align-forms?` enabled --- README.md | 8 ++++++++ cljfmt/src/cljfmt/core.cljc | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b214670..abd701df 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,14 @@ You can also configure the behavior of cljfmt: :cljfmt {:aligns ^:replace {#".*" #{0}} ``` +## Compile binary locally + +1. Download GraalVM for Java SDK, eg `sdk install java 24.0.2-graal` +2. Use in current shell with `sdk use java 24.0.2-graal` +3. `cd cljfmt` +4. Run `lein native-image` +5. Try binary with `./target/cljfmt --help` + ## License Copyright © 2025 James Reeves diff --git a/cljfmt/src/cljfmt/core.cljc b/cljfmt/src/cljfmt/core.cljc index a1535273..3edf87c6 100644 --- a/cljfmt/src/cljfmt/core.cljc +++ b/cljfmt/src/cljfmt/core.cljc @@ -336,6 +336,11 @@ (read-resource "cljfmt/indents/compojure.clj") (read-resource "cljfmt/indents/fuzzy.clj"))) + +(def ^:private default-aligns + (read-resource "cljfmt/align/clojure.clj")) + + (def default-options {:indentation? true :insert-missing-whitespace? true @@ -350,6 +355,7 @@ :extra-indents {} :align-maps? false :align-forms? false + :aligns default-aligns :alias-map {}}) (defmulti ^:private indenter-fn @@ -641,8 +647,6 @@ (defn align-maps [form] (transform form edit-all z/map? align-form-columns)) -(def ^:private default-aligns - (read-resource "cljfmt/align/clojure.clj")) (defn alignable? [aligns form] (let [zloc (-> form z/of-node first)] From 25e1f17181e89154fdbc21e71bceec1d9b1ca461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Bo=C3=ABthius=20Lissheim?= Date: Fri, 10 Oct 2025 15:25:26 +0200 Subject: [PATCH 6/6] fix whitespace issues for complex binding forms like for map- or vector-destructuring; add whitespace between the top forms and not inside the vector (ie in `[[a b] (foo)]` or `[{:keys [a b]} (baz)]`) --- .gitignore | 2 ++ cljfmt/src/cljfmt/core.cljc | 34 ++++++++++++++++++++++++++--- cljfmt/test/cljfmt/core_test.cljc | 36 ++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index a01a3da4..e7b78371 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ reports .cpcache .clj-kondo .lsp +.idea +*.iml diff --git a/cljfmt/src/cljfmt/core.cljc b/cljfmt/src/cljfmt/core.cljc index 3edf87c6..530e5796 100644 --- a/cljfmt/src/cljfmt/core.cljc +++ b/cljfmt/src/cljfmt/core.cljc @@ -618,7 +618,7 @@ (let [right (z/right* zloc)] (if (comma? right) (insert-space-right (remove-space-right right) (dec n)) - (z/insert-space-right zloc n)))) + (z/insert-right* zloc (whitespace n))))) (defn- set-spacing-right [zloc n] (-> zloc (remove-space-right) (insert-space-right n))) @@ -632,8 +632,36 @@ (z/up zloc)))) zloc)) +(defn- composite-node? [zloc] + (#{:map :vector :set :list} (z/tag zloc))) + + (defn- pad-node [zloc width] - (set-spacing-right zloc (- width (node-width zloc)))) + (let [padding-needed (- width (node-width zloc))] + (if (and (composite-node? zloc) (> padding-needed 0)) + ;; For composite nodes, remove existing space and insert correct amount + (let [right (z/right* zloc)] + (if (space? right) + ;; Remove space - z/remove* returns zloc at previous position + ;; which for a space after a composite is inside the composite + (let [after-remove (z/remove* right) + ;; Navigate back up to the composite level + at-composite (if (z/up* after-remove) + (loop [loc after-remove] + (if (= (z/node (z/up* loc)) (z/node zloc)) + loc + (if-let [up (z/up* loc)] + (recur up) + loc))) + after-remove)] + ;; Go up one level to be at the composite node level + (if (z/up* at-composite) + (z/insert-right* (z/up* at-composite) (whitespace padding-needed)) + (z/insert-right* at-composite (whitespace padding-needed)))) + ;; No space to remove, just insert + (z/insert-right* zloc (whitespace padding-needed)))) + ;; For non-composite nodes, use the standard approach + (set-spacing-right zloc padding-needed)))) (defn- end-of-line? [zloc] (line-break? (skip-whitespace-and-commas (z/right* zloc)))) @@ -649,7 +677,7 @@ (defn alignable? [aligns form] - (let [zloc (-> form z/of-node first)] + (let [zloc form] (when (and (not (z/whitespace-or-comment? zloc)) (z/list? (z/up zloc))) (let [form-type (some-> zloc z/up z/down z/string symbol)] diff --git a/cljfmt/test/cljfmt/core_test.cljc b/cljfmt/test/cljfmt/core_test.cljc index 6e3f76b2..8ffbfa84 100644 --- a/cljfmt/test/cljfmt/core_test.cljc +++ b/cljfmt/test/cljfmt/core_test.cljc @@ -1913,7 +1913,41 @@ " [a 1" " longer 2])"] {:align-forms? true - :aligns {'special #{1}}})))))) + :aligns {'special #{1}}})))) + (testing "binding forms" + (testing "simple symbol binding forms" + (is (reformats-to? + ["(let [is-user? (node/type? node/user entry)" + " admin? true])"] + ["(let [is-user? (node/type? node/user entry)" + " admin? true])"] + {:align-forms? true}))) + (testing "map binding forms" + (is (reformats-to? + ["(let [{:keys [id]} @(simplechat.tx/create-chat cluster org account)])"] + ["(let [{:keys [id]} @(simplechat.tx/create-chat cluster org account)])"] + {:align-forms? true}))) + (testing "vector binding forms" + (is (reformats-to? + ["(let [[value set-value!] (ls/use-state \"foo\" \"\")" + " [other thing] (something-else)])"] + ["(let [[value set-value!] (ls/use-state \"foo\" \"\")" + " [other thing] (something-else)])"] + {:align-forms? true}))) + (testing "mixed binding forms with complex case" + (is (reformats-to? + ["(let [user (get-user)" + " [value set-value!] (ls/use-state \"foo\" \"\")" + " {:keys [id name]} (get-data)" + " [a b] (something)" + " simple-var 42])"] + ["(let [user (get-user)" + " [value set-value!] (ls/use-state \"foo\" \"\")" + " {:keys [id name]} (get-data)" + " [a b] (something)" + " simple-var 42])"] + {:align-forms? true})))))) + (deftest test-align-maps (testing "straightforward test cases"