-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add option to sort ns reference blocks
The option results in cljfmt sorting the libspecs inside :require, :require-macros, :use, and :import blocks. Whitespace and newlines are preserved for most cases, in particular whether the first libspec element follows :require (or one of the others) or moves to the next line. Comments above and behind a libspec are considered part of that libspec, and they'll move to the new position of the libspec. This means libspecs commented out won't sorted, e.g. [c] #_[b] [a] will sort to #_[b] [a] [c] and similar with `;` comments, because it is treated as a comment for [a]. This probably is acceptable, as it is an edge case, and there shouldn't be an expectation that the comment is treated as a libspec.
- Loading branch information
Showing
4 changed files
with
257 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
(ns cljfmt.namespace | ||
(:require [rewrite-clj.node :as n] | ||
[rewrite-clj.zip :as z])) | ||
|
||
(def reference-block-keyword? | ||
#{:require | ||
:require-macros | ||
:use | ||
:import}) | ||
|
||
(def libspec? | ||
(comp not | ||
#{:comment | ||
:uneval | ||
:newline | ||
:whitespace | ||
:comma} | ||
n/tag)) | ||
|
||
(defn contain-newline? [elements] | ||
(->> elements | ||
(apply concat) | ||
(some (comp #{:newline | ||
:comment} | ||
n/tag)))) | ||
|
||
(defn partition-reference-block-children | ||
"Partition children of a reference block, considering one element to look like: | ||
(<whitespace>|<comment>)*<libspec>(<whitespace>|<comment>)* | ||
This ensures that comments above a libspec and comments behind it will be grouped | ||
and moved together with it." | ||
[zloc] | ||
(let [children (-> zloc | ||
z/node | ||
n/children) | ||
; the first element is the keyword :require, :use, etc. and surrounding whitespace, | ||
; but it shouldn't be sorted, so preserve it as is | ||
first-element? #(or (-> % n/tag #{:newline | ||
:whitespace | ||
:comma}) | ||
(and (-> % n/keyword-node?) | ||
(-> % n/sexpr reference-block-keyword?))) | ||
first-element (vec (take-while first-element? children)) | ||
relevant-children (drop-while first-element? children)] | ||
(loop [[node & remaining] relevant-children | ||
current-element [] | ||
result []] | ||
(let [t (some-> node n/tag)] | ||
(cond | ||
(nil? node) [first-element | ||
(if (seq current-element) | ||
(conj result (conj | ||
current-element | ||
(if (contain-newline? result) | ||
(n/newlines 1) | ||
(n/whitespace-node " ")))) | ||
result)] | ||
|
||
(#{:whitespace | ||
:comma | ||
:uneval} t) (recur remaining | ||
(conj current-element node) | ||
result) | ||
|
||
(#{:comment | ||
:newline} t) (if (some libspec? current-element) | ||
(recur remaining | ||
[] | ||
(conj result (conj current-element node))) | ||
(recur remaining | ||
(conj current-element node) | ||
result)) | ||
|
||
; otherwise it'll be a libspec node, which should be added if it is the first | ||
; in this element, otherwise close the current element and start a new one; an example | ||
; for this is "[a] b" without a newline, which should result in elements "[a]" and "b" | ||
:else (if (some libspec? current-element) | ||
(recur remaining | ||
[node] | ||
(conj result current-element)) | ||
(recur remaining | ||
(conj current-element node) | ||
result))))))) | ||
|
||
(defn element-sort-key [element] | ||
(let [relevant (first (filter libspec? element)) | ||
t (some-> relevant n/tag)] | ||
(cond | ||
(#{:token} t) (let [value (n/sexpr relevant)] | ||
(cond | ||
(string? value) [0 value] | ||
(symbol? value) [1 (name value)] | ||
:else [2 (str value)])) | ||
(#{:meta} t) (some-> relevant | ||
n/children | ||
;; skip over the metadata itself | ||
rest | ||
element-sort-key) | ||
(#{:vector | ||
:list} t) (element-sort-key (n/children relevant)) | ||
|
||
:else [10000 nil]))) | ||
|
||
(defn dedupe-whitespace-nodes | ||
"Remove whitespace nodes following directly behind other whitespace nodes. | ||
This can happen due to rearranging the reference block elements, which may begin and/or | ||
end with whitespace nodes. The rest of the code might get confused by two consecutive | ||
whitespace nodes, as the parser would never allow that situation. | ||
No attempt is made to combine the nodes, just keeping the first one should be fine." | ||
[nodes] | ||
(reduce | ||
(fn [result element] | ||
(if (= :whitespace | ||
(n/tag element) | ||
(some-> result | ||
last | ||
n/tag)) | ||
result | ||
(conj result element))) | ||
[] | ||
nodes)) | ||
|
||
(defn sort-reference-block [zloc] | ||
(let [[first-element | ||
elements] (partition-reference-block-children zloc) | ||
sorted-elements (->> (sort-by element-sort-key elements) | ||
(apply concat first-element) | ||
dedupe-whitespace-nodes)] | ||
(z/replace zloc (-> zloc | ||
z/node | ||
(n/replace-children sorted-elements))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters