Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ pom.xml.asc
reports
.cpcache
.clj-kondo
.lsp
.idea
*.iml
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,38 @@ 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`.

* `:align-forms?` -
true if cljfmt should left align the values of specified forms
This will convert:
```clojure
(let [foo 1
barbaz 2])
```
To:
```clojure
(let [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

Expand All @@ -309,9 +341,47 @@ In order to load the standard configuration file from Leiningen, add the
Paths can also be passed as command line arguments. If the path is
`-`, then the input is STDIN, and the output STDOUT.

* `:aligns` -
a map of var symbols to arguments' positions that require alignment
i.e. `{myform #{1 2}}` will align `[a 1]` and `[b 2]` forms like `(myform 0 [a 1] [b 2])`.
Argument positions 0-indexed.
See the next section for a detailed explanation.

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

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

## Compile binary locally

1. Download GraalVM for Java SDK, eg `sdk install java 24.0.2-graal`
2. Use in current shell with `sdk use java 24.0.2-graal`
3. `cd cljfmt`
4. Run `lein native-image`
5. Try binary with `./target/cljfmt --help`

## License

Copyright © 2024 James Reeves
Copyright © 2025 James Reeves

Distributed under the Eclipse Public License either version 1.0 or (at
your option) any later version.
5 changes: 4 additions & 1 deletion cljfmt.edn
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
"cljfmt/test"
"cljfmt/project.clj"
"lein-cljfmt/src"]
:sort-ns-references? true}
:sort-ns-references? true
:align-forms? true
:align-maps? true
}
9 changes: 9 additions & 0 deletions cljfmt/resources/cljfmt/align/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}}
142 changes: 139 additions & 3 deletions cljfmt/src/cljfmt/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@
(read-resource "cljfmt/indents/compojure.clj")
(read-resource "cljfmt/indents/fuzzy.clj")))


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


(def default-options
{:indentation? true
:insert-missing-whitespace? true
Expand All @@ -346,9 +351,12 @@
:split-keypairs-over-multiple-lines? false
:sort-ns-references? false
:function-arguments-indentation :community
:indents default-indents
:extra-indents {}
:alias-map {}})
:indents default-indents
:extra-indents {}
:align-maps? false
:align-forms? false
:aligns default-aligns
:alias-map {}})

(defmulti ^:private indenter-fn
(fn [_sym _context [type & _args]] type))
Expand Down Expand Up @@ -555,6 +563,130 @@
(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-right* zloc (whitespace 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- composite-node? [zloc]
(#{:map :vector :set :list} (z/tag zloc)))


(defn- pad-node [zloc width]
(let [padding-needed (- width (node-width zloc))]
(if (and (composite-node? zloc) (> padding-needed 0))
;; For composite nodes, remove existing space and insert correct amount
(let [right (z/right* zloc)]
(if (space? right)
;; Remove space - z/remove* returns zloc at previous position
;; which for a space after a composite is inside the composite
(let [after-remove (z/remove* right)
;; Navigate back up to the composite level
at-composite (if (z/up* after-remove)
(loop [loc after-remove]
(if (= (z/node (z/up* loc)) (z/node zloc))
loc
(if-let [up (z/up* loc)]
(recur up)
loc)))
after-remove)]
;; Go up one level to be at the composite node level
(if (z/up* at-composite)
(z/insert-right* (z/up* at-composite) (whitespace padding-needed))
(z/insert-right* at-composite (whitespace padding-needed))))
;; No space to remove, just insert
(z/insert-right* zloc (whitespace padding-needed))))
;; For non-composite nodes, use the standard approach
(set-spacing-right zloc padding-needed))))

(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 alignable? [aligns form]
(let [zloc form]
(when (and (not (z/whitespace-or-comment? zloc))
(z/list? (z/up zloc)))
(let [form-type (some-> zloc z/up z/down z/string symbol)]
(when-let [alignable-indices (aligns form-type)]
(contains? alignable-indices (-> zloc index-of dec)))))))

(defn align-forms [form aligns]
(transform form edit-all (partial alignable? aligns) align-form-columns))

(defn reformat-form
([form]
(reformat-form form {}))
Expand All @@ -573,6 +705,10 @@
insert-missing-whitespace)
(cond-> (:remove-multiple-non-indenting-spaces? opts)
remove-multiple-non-indenting-spaces)
(cond-> (:align-maps? opts)
align-maps)
(cond-> (:align-forms? opts)
(align-forms (:aligns opts)))
(cond-> (:indentation? opts)
(reindent (merge (:indents opts) (:extra-indents opts))
(:alias-map opts)
Expand Down
Loading