Skip to content
Open
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,38 @@ In order to load the standard configuration file from Leiningen, add the
:as everything} ;; 27 spaces, falls back to 1 space
```

* `:max-column-anchor-gap` -
a positive integer that controls anchor selection during column
alignment. Instead of using the longest key as the alignment anchor,
cljfmt selects the largest key for which every shorter key would need
no more than N spaces to align. Keys longer than the chosen anchor
are not padded. Defaults to nil (no limit). **Experimental.**

This differs from `:max-column-alignment-gap` in where the limit is
applied: `:max-column-alignment-gap` checks each row individually
after the anchor is determined; `:max-column-anchor-gap` demotes the
anchor itself so that all shorter keys fit within the limit.

```clojure
;; Input:
{:a 1
:abcde 2
:abcdefg 3
:long-name 4}

;; With :align-map-columns? true and :max-column-anchor-gap 5:
;; long-name (gap 7) and abcdefg (gap 5) exceed the limit from :a;
;; anchor demotes to :abcde (gap for :a = 4 ≤ 5)
{:a 1 ;; 4-space gap, aligns
:abcde 2 ;; anchor, 1 space
:abcdefg 3 ;; longer than anchor, 1 space
:long-name 4} ;; longer than anchor, 1 space
```

Both flags can be used together. `:max-column-anchor-gap` runs first
to select the anchor; `:max-column-alignment-gap` then trims any
individual rows whose gap still exceeds its limit.

* `:blank-lines-separate-alignment?` -
true if cljfmt should treat blank lines as separators when aligning
columns. When enabled, alignment groups are separated by blank lines,
Expand Down
51 changes: 33 additions & 18 deletions cljfmt/src/cljfmt/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@
:align-single-column-lines? false
:aligned-forms default-aligned-forms
:max-column-alignment-gap nil
:max-column-anchor-gap nil
:blank-line-forms blank-line-forms
:blank-lines-separate-alignment? false
:extra-aligned-forms {}
Expand Down Expand Up @@ -670,14 +671,18 @@
(defn- count-spaces [zloc]
(if (space? zloc) (node-str-length zloc) 0))

(defn- pad-to-position [zloc start-position {max-gap :max-column-alignment-gap}]
{:pre [(or (nil? max-gap) (pos-int? max-gap))]}
(let [delta (- start-position (margin zloc))]
(if max-gap
(let [old-gap (count-spaces (z/left* zloc))
new-gap (+ old-gap delta)]
(pad-node zloc (if (> new-gap max-gap) (- 1 old-gap) delta)))
(pad-node zloc delta))))
(defn- pad-to-position [zloc start-position {max-gap :max-column-alignment-gap
anchor-gap :max-column-anchor-gap}]
{:pre [(or (nil? max-gap) (pos-int? max-gap))
(or (nil? anchor-gap) (pos-int? anchor-gap))]}
(let [old-gap (count-spaces (z/left* zloc))
delta (- start-position (margin zloc))
new-gap (+ old-gap delta)]
(cond
(< new-gap 1) (pad-node zloc (- 1 old-gap))
(and max-gap
(> new-gap max-gap)) (pad-node zloc (- 1 old-gap))
:else (pad-node zloc delta))))

(defn- edit-column [zloc column f]
(loop [zloc zloc, col 0]
Expand Down Expand Up @@ -708,16 +713,26 @@
(recur (z/right* zloc) (inc col) (f zloc col acc)))))))

(defn- column-start-position [zloc col opts]
(let [reduce-fn (if (:blank-lines-separate-alignment? opts)
reduce-column-group
reduce-columns)
maximizer (fn [zloc c max-pos]
(if (and (= c (dec col))
(or (:align-single-column-lines? opts)
(not (single-column-line? zloc))))
(max max-pos (node-end-position zloc))
max-pos))]
(inc (reduce-fn zloc maximizer 0))))
(let [reduce-fn (if (:blank-lines-separate-alignment? opts)
reduce-column-group
reduce-columns)
anchor-gap (:max-column-anchor-gap opts)
collector (fn [zloc c positions]
(if (and (= c (dec col))
(or (:align-single-column-lines? opts)
(not (single-column-line? zloc))))
(conj positions (node-end-position zloc))
positions))
positions (reduce-fn zloc collector [])
anchor (when (seq positions)
(if anchor-gap
(let [min-pos (apply min positions)
threshold (+ min-pos anchor-gap)]
(->> positions
(filter #(<= % threshold))
(apply max)))
(apply max positions)))]
(inc (or anchor 0))))

(defn- align-one-column
[zloc col {:keys [blank-lines-separate-alignment?] :as opts}]
Expand Down
41 changes: 41 additions & 0 deletions cljfmt/test/cljfmt/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -3193,6 +3193,47 @@
{:align-map-columns? true
:max-column-alignment-gap 5}))))

(deftest test-max-column-anchor-gap
(testing "anchor demoted until all shorter keys fit within gap — form columns"
(is (reformats-to?
["(let [a 1"
" abcde 2"
" abcdefg 3"
" long-name 4]"
" [a abcde abcdefg long-name])"]
["(let [a 1"
" abcde 2"
" abcdefg 3"
" long-name 4]"
" [a abcde abcdefg long-name])"]
{:align-form-columns? true
:max-column-anchor-gap 5})))
(testing "anchor demoted until all shorter keys fit within gap — map columns"
(is (reformats-to?
["{:a \"x\""
" :abcde \"y\""
" :abcdefg \"z\""
" :long-key \"w\"}"]
["{:a \"x\""
" :abcde \"y\""
" :abcdefg \"z\""
" :long-key \"w\"}"]
{:align-map-columns? true
:max-column-anchor-gap 5})))
(testing "both flags compose: anchor-gap selects anchor, alignment-gap trims rows"
(is (reformats-to?
["{:x \"a\""
" :abc \"b\""
" :abcde \"c\""
" :long-key \"d\"}"]
["{:x \"a\""
" :abc \"b\""
" :abcde \"c\""
" :long-key \"d\"}"]
{:align-map-columns? true
:max-column-anchor-gap 5
:max-column-alignment-gap 3}))))

(deftest test-realign-form
(is (= "
{:x 1
Expand Down
Loading