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

* `:max-column-alignment-width` -
a positive integer that limits which keys participate in column
alignment. Keys whose end column is at or within N are aligned
together; keys that end past column N are not padded and receive a
single space before their value. Defaults to nil (no limit).
**Experimental.**

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

;; With :align-map-columns? true and :max-column-alignment-width 7:
;; :a and :abcde end at or within column 7 and align together;
;; :abcdefg and :long-name end past column 7 and are not padded
{:a 1 ;; ends within column 7, padded to value column
:abcde 2 ;; ends at column 7, sets the value column
:abcdefg 3 ;; ends past column 7, 1 space
:long-name 4} ;; ends past column 7, 1 space
```

* `:break-on-max-column-alignment-width?` -
true if cljfmt should move a value onto its own line, indented under
the keys, when its key ends past the column set by
`:max-column-alignment-width`, rather than falling back to a single
space on the same line. Has no effect unless
`:max-column-alignment-width` is also set. Defaults to false.
**Experimental.**

```clojure
;; Input:
{:a 1
:bb 2
:ccccccc 3}

;; With :align-map-columns? true, :max-column-alignment-width 4,
;; and :break-on-max-column-alignment-width? true:
{:a 1 ;; ends within column 4, padded to value column
:bb 2 ;; ends at column 4, sets the value column
:ccccccc ;; ends past column 4, value moves to its own line
3}
```

* `: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
54 changes: 41 additions & 13 deletions cljfmt/src/cljfmt/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,10 @@
:align-single-column-lines? false
:aligned-forms default-aligned-forms
:max-column-alignment-gap nil
:max-column-alignment-width nil
:blank-line-forms blank-line-forms
:blank-lines-separate-alignment? false
:break-on-max-column-alignment-width? false
:extra-aligned-forms {}
:extra-blank-line-forms {}
:extra-indents {}
Expand Down Expand Up @@ -670,13 +672,31 @@
(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}]
(defn- break-to-own-line [zloc]
(let [indent (coll-indent zloc)]
(-> zloc
z/left*
(z/replace* (n/newlines 1))
z/right*
(z/insert-left* (whitespace indent)))))

(defn- pad-to-position [zloc {:keys [position threshold]}
{max-gap :max-column-alignment-gap
break? :break-on-max-column-alignment-width?}]
{: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)))
(let [old-gap (count-spaces (z/left* zloc))
delta (- position (margin zloc))
new-gap (+ old-gap delta)
key-end (- (margin zloc) old-gap)]
(cond
(and break? threshold (> key-end threshold))
(break-to-own-line zloc)

(or (< new-gap 1)
(and max-gap (> new-gap max-gap)))
(pad-node zloc (- 1 old-gap))

:else
(pad-node zloc delta))))

(defn- edit-column [zloc column f]
Expand Down Expand Up @@ -707,17 +727,25 @@
(recur (z/right* zloc) 0 acc)
(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)
(defn- column-start-position
[zloc col {align-width :max-column-alignment-width
align-singles? :align-single-column-lines?
separate? :blank-lines-separate-alignment?}]
(let [reduce-fn (if separate?
reduce-column-group
reduce-columns)
maximizer (fn [zloc c max-pos]
collector (fn [zloc c positions]
(if (and (= c (dec col))
(or (:align-single-column-lines? opts)
(or align-singles?
(not (single-column-line? zloc))))
(max max-pos (node-end-position zloc))
max-pos))]
(inc (reduce-fn zloc maximizer 0))))
(conj positions (node-end-position zloc))
positions))
positions (reduce-fn zloc collector [])
threshold (when align-width (+ (apply min 0 positions) align-width))]
{:position (inc (apply max 0 (if threshold
(filter #(<= % threshold) positions)
positions)))
:threshold threshold}))

(defn- align-one-column
[zloc col {:keys [blank-lines-separate-alignment?] :as opts}]
Expand Down
3 changes: 3 additions & 0 deletions cljfmt/src/cljfmt/main.clj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
[nil "--[no-]blank-lines-separate-alignment"
:default (:blank-lines-separate-alignment? defaults)
:id :blank-lines-separate-alignment?]
[nil "--[no-]break-on-max-column-alignment-width"
:default (:break-on-max-column-alignment-width? defaults)
:id :break-on-max-column-alignment-width?]
[nil "--max-column-alignment-gap"
:default (:max-column-alignment-gap defaults)
:parse-fn #(cond-> % (string? %) parse-long)
Expand Down
91 changes: 91 additions & 0 deletions cljfmt/test/cljfmt/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -3193,6 +3193,97 @@
{:align-map-columns? true
:max-column-alignment-gap 5}))))

(deftest test-max-column-alignment-width
(testing "form: anchor demoted until all keys fit within width"
(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-alignment-width 11})))
(testing "map: anchor demoted until all keys fit within width"
(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-alignment-width 7})))
(testing "composing both flags refines alignment further"
(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-alignment-width 7
:max-column-alignment-gap 3}))))

(deftest test-break-on-max-column-alignment-width
(testing "form: the one outlier value is moved to its own line, indented under the bindings"
(is (reformats-to?
["(let [a 1"
" bb 2"
" ccccccc 3]"
" body)"]
["(let [a 1"
" bb 2"
" ccccccc"
" 3]"
" body)"]
{:align-form-columns? true
:max-column-alignment-width 8
:break-on-max-column-alignment-width? true})))
(testing "map: the one outlier value is moved to its own line, indented under the keys"
(is (reformats-to?
["{:a \"x\""
" :bb \"y\""
" :ccccccc \"z\"}"]
["{:a \"x\""
" :bb \"y\""
" :ccccccc"
" \"z\"}"]
{:align-map-columns? true
:max-column-alignment-width 4
:break-on-max-column-alignment-width? true})))
(testing "no effect when :max-column-alignment-width is not set"
(is (reformats-to?
["{:x 1"
" :longer 2}"]
["{:x 1"
" :longer 2}"]
{:align-map-columns? true
:max-column-alignment-gap 5
:break-on-max-column-alignment-width? true})))
(testing "value already on its own line is left alone (idempotent)"
(is (reformats-to?
["{:a 1"
" :bb 2"
" :ccccccc"
" 3}"]
["{:a 1"
" :bb 2"
" :ccccccc"
" 3}"]
{:align-map-columns? true
:max-column-alignment-width 4
:break-on-max-column-alignment-width? true}))))

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