diff --git a/.gitignore b/.gitignore index 02ca798..a01a3da 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 bdebb86..3259894 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 d22d75f..628d67e 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 404cbe8..8dd935d 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 576a8cc..73a7752 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"