Skip to content

Commit

Permalink
Add option to lign maps and bindings
Browse files Browse the repository at this point in the history
This commit adds the option to allow maps and bindings.
It does so by adding two separate configurable options:

 - align-maps?

 - align-bindings?

Bindings specifically are further configurable by a cljfmt option:

 - align-bindings-args

This is a map of the form `{form #{1 2}}` that maps the form's expected
binding positions. An example is `{let #{0}}` implying that a `let`
symbol is immediately followed by a binding. The binding is expected
to be the first argument.
  • Loading branch information
rsh-blip committed Mar 13, 2023
1 parent 3418b7f commit cb39f7a
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 1 deletion.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,17 @@ selectively enabled or disabled:
other references in the `ns` forms at the top of your namespaces.
Defaults to false.

* `:align-maps?` -
true if cljfmt should left align the values of maps
This will convert `{:foo 1\n:barbaz 2}` to `{:foo 1\n :barbaz 2}`
Defaults to false.

* `:align-bindings?` -
true if cljfmt should left align the values of bindings
This will convert `(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
Expand Down Expand Up @@ -193,6 +204,35 @@ You can also configure the behavior of cljfmt:
:cljfmt {:indents ^:replace {#".*" [[:inner 0]]}}
```

* `:align-bindings-args` -
a map of var symbols to arguments positions that require binding alignment
i.e. `{symbol #{1 2}`. Argument positions start at 0.
See the next section for a detailed explanation.

Unqualified symbols in the indents 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 align-bindings-args map to configure binding alignment that
applies only to `org.me/foo`:

```clojure
:cljfmt {:align-bindings-args {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 {:align-bindings-args ^:replace {#".*" #{0}}
```

* `:alias-map` -
a map of namespace alias strings to fully qualified namespace
names. This option is unnecessary in almost all cases, because
Expand Down
9 changes: 9 additions & 0 deletions cljfmt/resources/cljfmt/align_bindings/clojure.clj
Original file line number Diff line number Diff line change
@@ -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}}
90 changes: 89 additions & 1 deletion cljfmt/src/cljfmt/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,90 @@
(not (namespaced-map? (z/up* zloc)))
(element? (z/right* zloc))))

(def ^:private default-align-bindings-args
(read-resource "cljfmt/align_bindings/clojure.clj"))

(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 [clean-zloc (-> zloc
z/right*
(z/replace (n/whitespace-node " "))
z/left)
to-add (->> clean-zloc
z/sexpr
str
count
(- max-length))
new-zloc (z/insert-space-right clean-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- sibling-distance [left right]
(if (= left right)
0
(if (z/rightmost? left)
nil
(when-let [d (sibling-distance (z/right left) right)]
(+ 1 d)))))

(defn- binding? [zloc align-bindings-args]
(and (z/vector? zloc)
(-> zloc z/sexpr count even?)
(let [sexpr-type (-> zloc
z/leftmost
z/value)
zloc-pos (-> zloc
z/leftmost
(sibling-distance zloc)
dec)
align-args (align-bindings-args sexpr-type)]
(and align-args
(align-args zloc-pos)))))

(defn- align-map? [zloc]
(z/map? zloc))

(defn insert-missing-whitespace [form]
(transform form edit-all missing-whitespace? z/insert-space-right))

(defn- align-bindings
([form]
(align-bindings form default-align-bindings-args))
([form align-bindings-args]
(transform form edit-all #(binding? % align-bindings-args) align-binding)))

(defn- align-maps [form]
(transform form edit-all align-map? align-map))

(defn- space? [zloc]
(= (z/tag zloc) :whitespace))

Expand Down Expand Up @@ -493,7 +574,10 @@
:remove-trailing-whitespace? true
:split-keypairs-over-multiple-lines? false
:sort-ns-references? false
:indents default-indents
:align-bindings? false
:align-maps? false
:indents default-indents
:align-bindings-args default-align-bindings-args
:alias-map {}})

(defn reformat-form
Expand All @@ -512,6 +596,10 @@
remove-surrounding-whitespace)
(cond-> (:insert-missing-whitespace? opts)
insert-missing-whitespace)
(cond-> (:align-maps? opts)
align-maps)
(cond-> (:align-bindings? opts)
(align-bindings (:align-bindings-args opts)))
(cond-> (:remove-multiple-non-indenting-spaces? opts)
remove-multiple-non-indenting-spaces)
(cond-> (:indentation? opts)
Expand Down
165 changes: 165 additions & 0 deletions cljfmt/test/cljfmt/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1336,3 +1336,168 @@
" ^{:x 1} b"
" [c]))"]
{:sort-ns-references? true})))

(deftest test-align-bindings
(testing "straightforward test cases"
(testing "sanity"
(is (reformats-to?
["(def x 1)"]
["(def x 1)"]
{:align-bindings? true})))
(testing "no op 2"
(is (reformats-to?
["(let [x 1"
" y 2])"]
["(let [x 1"
" y 2])"]
{:align-bindings? true})))
(testing "no op 1"
(is (reformats-to?
["(let [x 1])"]
["(let [x 1])"]
{:align-bindings? true})))
(testing "empty"
(is (reformats-to?
["(let [])"]
["(let [])"]
{:align-bindings? true})))
(testing "simple"
(is (reformats-to?
["(let [x 1"
" longer 2])"]
["(let [x 1"
" longer 2])"]
{:align-bindings? 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-bindings? true})))
(testing "preserves comments"
(is (reformats-to?
["(let [a 1 ;; comment"
" longer 2])"]
["(let [a 1 ;; comment"
" longer 2])"]
{:align-bindings? true})))
(testing "align args"
(testing "simple"
(is (reformats-to?
["(special something [a 1"
" longer 2])"]
["(special something [a 1"
" longer 2])"]
{:align-bindings? true
:align-bindings-args {'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-bindings? true
:align-bindings-args {'special #{1}}}))))))

(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})))))

(deftest test-align-associative-abnormal
(testing "abnormal test cases"
(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 "future effort?"
(testing "multi-value line"
(is (reformats-to?
["{:a 1 :b 2"
" :longer 2}"]
["{:a 1 :b 2"
" :longer 2}"]
{:align-maps? true}))))))

0 comments on commit cb39f7a

Please sign in to comment.