Skip to content

Commit e7bb46b

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 an 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 e7bb46b

File tree

5 files changed

+303
-1
lines changed

5 files changed

+303
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ pom.xml.asc
88
.lein-*
99
.nrepl-port
1010
reports
11+
*.cpcache

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: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,89 @@
7171
(not (namespaced-map? (z/up* zloc)))
7272
(element? (z/right* zloc))))
7373

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

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

@@ -493,7 +573,10 @@
493573
:remove-trailing-whitespace? true
494574
:split-keypairs-over-multiple-lines? false
495575
:sort-ns-references? false
496-
:indents default-indents
576+
:align-bindings? false
577+
:align-maps? false
578+
:indents default-indents
579+
:align-bindings-args default-align-bindings-args
497580
:alias-map {}})
498581

499582
(defn reformat-form
@@ -512,6 +595,10 @@
512595
remove-surrounding-whitespace)
513596
(cond-> (:insert-missing-whitespace? opts)
514597
insert-missing-whitespace)
598+
(cond-> (:align-maps? opts)
599+
align-maps)
600+
(cond-> (:align-bindings? opts)
601+
(align-bindings (:align-bindings-args opts)))
515602
(cond-> (:remove-multiple-non-indenting-spaces? opts)
516603
remove-multiple-non-indenting-spaces)
517604
(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)