Skip to content

Commit cb39f7a

Browse files
committed
Add option to lign maps and bindings
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.
1 parent 3418b7f commit cb39f7a

File tree

4 files changed

+303
-1
lines changed

4 files changed

+303
-1
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ selectively enabled or disabled:
154154
other references in the `ns` forms at the top of your namespaces.
155155
Defaults to false.
156156

157+
* `:align-maps?` -
158+
true if cljfmt should left align the values of maps
159+
This will convert `{:foo 1\n:barbaz 2}` to `{:foo 1\n :barbaz 2}`
160+
Defaults to false.
161+
162+
* `:align-bindings?` -
163+
true if cljfmt should left align the values of bindings
164+
This will convert `(let [foo 1\n barbaz 2])` to `(let [foo 1\n barbaz 2])`.
165+
166+
Defaults to false.
167+
157168
You can also configure the behavior of cljfmt:
158169

159170
* `:paths` - determines which directories to include in the
@@ -193,6 +204,35 @@ You can also configure the behavior of cljfmt:
193204
:cljfmt {:indents ^:replace {#".*" [[:inner 0]]}}
194205
```
195206

207+
* `:align-bindings-args` -
208+
a map of var symbols to arguments positions that require binding alignment
209+
i.e. `{symbol #{1 2}`. Argument positions start at 0.
210+
See the next section for a detailed explanation.
211+
212+
Unqualified symbols in the indents map will apply to any symbol with a
213+
matching "name" - so `foo` would apply to both `org.me/foo` and
214+
`com.them/foo`. If you want finer-grained control, you can use a fully
215+
qualified symbol in the align-bindings-args map to configure binding alignment that
216+
applies only to `org.me/foo`:
217+
218+
```clojure
219+
:cljfmt {:align-bindings-args {org.me/foo #{2 3}}
220+
```
221+
222+
Configured this way, `org.me/foo` will align only argument positions 2 3 (starting from 0).
223+
224+
Note that `cljfmt` currently doesn't resolve symbols brought into a
225+
namespace using `:refer` or `:use` - they can only be controlled by an
226+
unqualified align rule.
227+
228+
As with Leiningen profiles, you can add metadata hints. If you want to
229+
override all existing aligns, instead of just supplying new aligns
230+
that are merged with the defaults, you can use the `:replace` hint:
231+
232+
```clojure
233+
:cljfmt {:align-bindings-args ^:replace {#".*" #{0}}
234+
```
235+
196236
* `:alias-map` -
197237
a map of namespace alias strings to fully qualified namespace
198238
names. This option is unnecessary in almost all cases, because
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{let #{0}
2+
doseq #{0}
3+
go-loop #{0}
4+
binding #{0}
5+
with-open #{0}
6+
loop #{0}
7+
for #{0}
8+
with-local-vars #{0}
9+
with-redefs #{0}}

cljfmt/src/cljfmt/core.cljc

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,90 @@
7171
(not (namespaced-map? (z/up* zloc)))
7272
(element? (z/right* zloc))))
7373

74+
(def ^:private default-align-bindings-args
75+
(read-resource "cljfmt/align_bindings/clojure.clj"))
76+
77+
(defn- ks->max-length [ks]
78+
(if (empty? ks)
79+
0
80+
(->> ks
81+
(apply max-key (comp count str))
82+
str
83+
count)))
84+
85+
86+
(defn- aligner [zloc max-length align?]
87+
(cond
88+
(zero? max-length) (z/up zloc)
89+
(z/rightmost? zloc) (z/up zloc)
90+
align? (let [clean-zloc (-> zloc
91+
z/right*
92+
(z/replace (n/whitespace-node " "))
93+
z/left)
94+
to-add (->> clean-zloc
95+
z/sexpr
96+
str
97+
count
98+
(- max-length))
99+
new-zloc (z/insert-space-right clean-zloc to-add)]
100+
(aligner (z/right new-zloc) max-length false))
101+
:else (aligner (z/right zloc) max-length true)))
102+
103+
(defn- align-binding [zloc]
104+
(let [se (z/sexpr zloc)
105+
ks (take-nth 2 se)
106+
max-length (ks->max-length ks)
107+
bindings (z/down zloc)]
108+
(if bindings
109+
(aligner bindings max-length true)
110+
zloc)))
111+
112+
(defn- align-map [zloc]
113+
(let [se (z/sexpr zloc)
114+
ks (keys se)
115+
max-length (ks->max-length ks)
116+
kvs (z/down zloc)]
117+
(if kvs
118+
(aligner kvs max-length true)
119+
zloc)))
120+
121+
(defn- sibling-distance [left right]
122+
(if (= left right)
123+
0
124+
(if (z/rightmost? left)
125+
nil
126+
(when-let [d (sibling-distance (z/right left) right)]
127+
(+ 1 d)))))
128+
129+
(defn- binding? [zloc align-bindings-args]
130+
(and (z/vector? zloc)
131+
(-> zloc z/sexpr count even?)
132+
(let [sexpr-type (-> zloc
133+
z/leftmost
134+
z/value)
135+
zloc-pos (-> zloc
136+
z/leftmost
137+
(sibling-distance zloc)
138+
dec)
139+
align-args (align-bindings-args sexpr-type)]
140+
(and align-args
141+
(align-args zloc-pos)))))
142+
143+
(defn- align-map? [zloc]
144+
(z/map? zloc))
145+
74146
(defn insert-missing-whitespace [form]
75147
(transform form edit-all missing-whitespace? z/insert-space-right))
76148

149+
(defn- align-bindings
150+
([form]
151+
(align-bindings form default-align-bindings-args))
152+
([form align-bindings-args]
153+
(transform form edit-all #(binding? % align-bindings-args) align-binding)))
154+
155+
(defn- align-maps [form]
156+
(transform form edit-all align-map? align-map))
157+
77158
(defn- space? [zloc]
78159
(= (z/tag zloc) :whitespace))
79160

@@ -493,7 +574,10 @@
493574
:remove-trailing-whitespace? true
494575
:split-keypairs-over-multiple-lines? false
495576
:sort-ns-references? false
496-
:indents default-indents
577+
:align-bindings? false
578+
:align-maps? false
579+
:indents default-indents
580+
:align-bindings-args default-align-bindings-args
497581
:alias-map {}})
498582

499583
(defn reformat-form
@@ -512,6 +596,10 @@
512596
remove-surrounding-whitespace)
513597
(cond-> (:insert-missing-whitespace? opts)
514598
insert-missing-whitespace)
599+
(cond-> (:align-maps? opts)
600+
align-maps)
601+
(cond-> (:align-bindings? opts)
602+
(align-bindings (:align-bindings-args opts)))
515603
(cond-> (:remove-multiple-non-indenting-spaces? opts)
516604
remove-multiple-non-indenting-spaces)
517605
(cond-> (:indentation? opts)

cljfmt/test/cljfmt/core_test.cljc

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,3 +1336,168 @@
13361336
" ^{:x 1} b"
13371337
" [c]))"]
13381338
{:sort-ns-references? true})))
1339+
1340+
(deftest test-align-bindings
1341+
(testing "straightforward test cases"
1342+
(testing "sanity"
1343+
(is (reformats-to?
1344+
["(def x 1)"]
1345+
["(def x 1)"]
1346+
{:align-bindings? true})))
1347+
(testing "no op 2"
1348+
(is (reformats-to?
1349+
["(let [x 1"
1350+
" y 2])"]
1351+
["(let [x 1"
1352+
" y 2])"]
1353+
{:align-bindings? true})))
1354+
(testing "no op 1"
1355+
(is (reformats-to?
1356+
["(let [x 1])"]
1357+
["(let [x 1])"]
1358+
{:align-bindings? true})))
1359+
(testing "empty"
1360+
(is (reformats-to?
1361+
["(let [])"]
1362+
["(let [])"]
1363+
{:align-bindings? true})))
1364+
(testing "simple"
1365+
(is (reformats-to?
1366+
["(let [x 1"
1367+
" longer 2])"]
1368+
["(let [x 1"
1369+
" longer 2])"]
1370+
{:align-bindings? true})))
1371+
(testing "nested align"
1372+
(is (reformats-to?
1373+
["(let [x (let [x 1"
1374+
" longer 2])"
1375+
" longer 2])"]
1376+
["(let [x (let [x 1"
1377+
" longer 2])"
1378+
" longer 2])"]
1379+
{:align-bindings? true})))
1380+
(testing "preserves comments"
1381+
(is (reformats-to?
1382+
["(let [a 1 ;; comment"
1383+
" longer 2])"]
1384+
["(let [a 1 ;; comment"
1385+
" longer 2])"]
1386+
{:align-bindings? true})))
1387+
(testing "align args"
1388+
(testing "simple"
1389+
(is (reformats-to?
1390+
["(special something [a 1"
1391+
" longer 2])"]
1392+
["(special something [a 1"
1393+
" longer 2])"]
1394+
{:align-bindings? true
1395+
:align-bindings-args {'special #{1}}})))
1396+
(testing "don't mixup args"
1397+
(is (reformats-to?
1398+
["(special [a 1"
1399+
" longer 2]"
1400+
" [a 1"
1401+
" longer 2])"]
1402+
["(special [a 1"
1403+
" longer 2]"
1404+
" [a 1"
1405+
" longer 2])"]
1406+
{:align-bindings? true
1407+
:align-bindings-args {'special #{1}}}))))))
1408+
1409+
(deftest test-align-maps
1410+
(testing "straightforward test cases"
1411+
(testing "sanity"
1412+
(is (reformats-to?
1413+
["(def x 1)"]
1414+
["(def x 1)"]
1415+
{:align-maps? true})))
1416+
(testing "no op 1"
1417+
(is (reformats-to?
1418+
["{:a 1}"]
1419+
["{:a 1}"]
1420+
{:align-maps? true})))
1421+
(testing "no op 2"
1422+
(is (reformats-to?
1423+
["{:a 1"
1424+
" :b 2}"]
1425+
["{:a 1"
1426+
" :b 2}"]
1427+
{:align-maps? true})))
1428+
(testing "empty"
1429+
(is (reformats-to?
1430+
["{}"]
1431+
["{}"]
1432+
{:align-maps? true})))
1433+
(testing "simple"
1434+
(is (reformats-to?
1435+
["{:x 1"
1436+
" :longer 2}"]
1437+
["{:x 1"
1438+
" :longer 2}"]
1439+
{:align-maps? true})))
1440+
(testing "nested simple"
1441+
(is (reformats-to?
1442+
["{:x {:x 1}"
1443+
" :longer 2}"]
1444+
["{:x {:x 1}"
1445+
" :longer 2}"]
1446+
{:align-maps? true})))
1447+
(testing "nested align"
1448+
(is (reformats-to?
1449+
["{:x {:x 1"
1450+
" :longer 2}"
1451+
" :longer 2}"]
1452+
["{:x {:x 1"
1453+
" :longer 2}"
1454+
" :longer 2}"]
1455+
{:align-maps? true})))
1456+
(testing "align many"
1457+
(is (reformats-to?
1458+
["{:a 1"
1459+
" :longer 2"
1460+
" :b 3}"]
1461+
["{:a 1"
1462+
" :longer 2"
1463+
" :b 3}"]
1464+
{:align-maps? true})))
1465+
(testing "preserves comments"
1466+
(is (reformats-to?
1467+
["{:a 1 ;; comment"
1468+
" :longer 2}"]
1469+
["{:a 1 ;; comment"
1470+
" :longer 2}"]
1471+
{:align-maps? true})))))
1472+
1473+
(deftest test-align-associative-abnormal
1474+
(testing "abnormal test cases"
1475+
(testing "indentation off #1"
1476+
(is (reformats-to?
1477+
["{ :a 1"
1478+
" :longer 2}"]
1479+
["{:a 1"
1480+
" :longer 2}"]
1481+
{:align-maps? true})))
1482+
(testing "indentation off #2"
1483+
(is (reformats-to?
1484+
["{ :a 1"
1485+
" :longer 2}"]
1486+
["{:a 1"
1487+
" :longer 2}"]
1488+
{:align-maps? true})))
1489+
(testing "indentation off #3"
1490+
(is (reformats-to?
1491+
["{:a 1"
1492+
" :longer 2}"]
1493+
["{:a 1"
1494+
" :longer 2}"]
1495+
{:align-maps? true})))
1496+
(testing "future effort?"
1497+
(testing "multi-value line"
1498+
(is (reformats-to?
1499+
["{:a 1 :b 2"
1500+
" :longer 2}"]
1501+
["{:a 1 :b 2"
1502+
" :longer 2}"]
1503+
{:align-maps? true}))))))

0 commit comments

Comments
 (0)