diff --git a/.gitignore b/.gitignore index de38f4e..2337517 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ pom.xml.asc .lein-* .nrepl-port reports +*.cpcache diff --git a/README.md b/README.md index d5a0211..b17fe8e 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,13 @@ selectively enabled or disabled: other references in the `ns` forms at the top of your namespaces. Defaults to false. +* `:align-associative?` - + true if cljfmt should left align the values of maps and binding + special forms (let, loop, binding). This will convert + `{:foo 1\n:barbaz 2}` to `{:foo 1\n :barbaz 2}` + and `(let [foo 1\n barbaz 2])` to `(let [foo 1\n barbaz 2])`. + Defaults to false. + You can also configure the behavior of cljfmt: * `:paths` - determines which directories to include in the diff --git a/cljfmt/src/cljfmt/core.cljc b/cljfmt/src/cljfmt/core.cljc index 6a0fd7f..d6811ea 100644 --- a/cljfmt/src/cljfmt/core.cljc +++ b/cljfmt/src/cljfmt/core.cljc @@ -71,9 +71,73 @@ (not (namespaced-map? (z/up* zloc))) (element? (z/right* zloc)))) +(def ^:private binding-keywords #{"doseq" "let" "loop" "binding" "with-open" + "go-loop" "if-let" "when-some" "if-some" "for" + "with-local-vars" "with-redefs"}) + +(defn ks->max-length [ks] + (if (empty? ks) + 0 + (->> ks + (apply max-key (comp count str)) + str + count))) + +(defn- aligner [zloc max-length align?] + (cond + (zero? max-length) (z/up zloc) + (z/rightmost? zloc) (z/up zloc) + align? (let [to-add (->> zloc + z/sexpr + str + count + (- max-length)) + new-zloc (z/insert-space-right zloc to-add)] + (aligner (z/right new-zloc) max-length false)) + :else (aligner (z/right zloc) max-length true))) + +(defn- align-binding [zloc] + (let [se (z/sexpr zloc) + ks (take-nth 2 se) + max-length (ks->max-length ks) + bindings (z/down zloc)] + (if bindings + (aligner bindings max-length true) + zloc))) + +(defn- align-map [zloc] + (let [se (z/sexpr zloc) + ks (keys se) + max-length (ks->max-length ks) + kvs (z/down zloc)] + (if kvs + (aligner kvs max-length true) + zloc))) + +(defn- binding? [zloc] + (and (z/vector? zloc) + (-> zloc z/sexpr count even?) + (->> zloc + z/left + z/string + binding-keywords))) + +(defn align-map-or-binding [zloc] + (cond + (binding? zloc) (align-binding zloc) + (z/map? zloc) (align-map zloc) + :else zloc)) + +(defn- align-associative? [zloc] + (or (binding? zloc) + (z/map? zloc))) + (defn insert-missing-whitespace [form] (transform form edit-all missing-whitespace? z/insert-space-right)) +(defn align-associative [form] + (transform form edit-all align-associative? align-map-or-binding)) + (defn- space? [zloc] (= (z/tag zloc) :whitespace)) @@ -492,6 +556,7 @@ :remove-surrounding-whitespace? true :remove-trailing-whitespace? true :split-keypairs-over-multiple-lines? false + :align-associative? false :sort-ns-references? false :indents default-indents :alias-map {}}) @@ -512,6 +577,8 @@ remove-surrounding-whitespace) (cond-> (:insert-missing-whitespace? opts) insert-missing-whitespace) + (cond-> (:align-associative? opts) + align-associative) (cond-> (:remove-multiple-non-indenting-spaces? opts) remove-multiple-non-indenting-spaces) (cond-> (:indentation? opts) diff --git a/cljfmt/test/cljfmt/core_test.cljc b/cljfmt/test/cljfmt/core_test.cljc index 9bed12b..555ca39 100644 --- a/cljfmt/test/cljfmt/core_test.cljc +++ b/cljfmt/test/cljfmt/core_test.cljc @@ -1336,3 +1336,90 @@ " ^{:x 1} b" " [c]))"] {:sort-ns-references? true}))) + +(deftest test-align-associative + (testing "sanity" + (is (reformats-to? + ["(def x 1)"] + ["(def x 1)"] + {:align-associative? true}))) + (testing "no op 0" + (is (reformats-to? + ["(let [x 1" + " y 2])"] + ["(let [x 1" + " y 2])"] + {:align-associative? true}))) + (testing "no op 1" + (is (reformats-to? + ["(let [x 1])"] + ["(let [x 1])"] + {:align-associative? true}))) + (testing "empty" + (is (reformats-to? + ["(let [])"] + ["(let [])"] + {:align-associative? true}))) + (testing "simple binding" + (is (reformats-to? + ["(let [x 1" + " longer 2])"] + ["(let [x 1" + " longer 2])"] + {:align-associative? true}))) + (testing "simple map" + (is (reformats-to? + ["{:x 1" + " :longer 2}"] + ["{:x 1" + " :longer 2}"] + {:align-associative? true}))) + (testing "nested simple map" + (is (reformats-to? + ["{:x {:x 1}" + " :longer 2}"] + ["{:x {:x 1}" + " :longer 2}"] + {:align-associative? true}))) + (testing "nested align map" + (is (reformats-to? + ["{:x {:x 1" + " :longer 2}" + " :longer 2}"] + ["{:x {:x 1" + " :longer 2}" + " :longer 2}"] + {:align-associative? true}))) + (testing "nested align binding" + (is (reformats-to? + ["(let [x (let [x 1" + " longer 2])" + " longer 2])"] + ["(let [x (let [x 1" + " longer 2])" + " longer 2])"] + {:align-associative? true}))) + (testing "align many map" + (is (reformats-to? + ["{:a 1" + " :longer 2" + " :b 3}"] + ["{:a 1" + " :longer 2" + " :b 3}"] + {:align-associative? true}))) + (testing "binding align preserves comments" + (is (reformats-to? + ["(let [a 1 ;; comment" + " longer 2])"] + ["(let [a 1 ;; comment" + " longer 2])"] + {:align-associative? true} + ))) + (testing "map align preserves comments" + (is (reformats-to? + ["{:a 1 ;; comment" + " :longer 2}"] + ["{:a 1 ;; comment" + " :longer 2}"] + {:align-associative? true}))))