Add optional column-oriented wrapping via :max-line-length#414
Add optional column-oriented wrapping via :max-line-length#414ispandey81 wants to merge 1 commit intoweavejester:masterfrom
Conversation
Refs weavejester#114 Apply :max-line-length as a structural pass: break only between direct children of eligible single-line forms using existing width accounting. Cover lists (including ns :import / :use), vectors, maps, sets, fn literals, namespaced maps, and inner collections in #? / #?@ reader conditionals; skip layout changes inside other reader macros and inside ns :require libspec vectors (row breaks only between those vectors). When :max-line-length is set, gate map keypair newlines on line width; keep unconditional keypair splitting when only :split-keypairs-over-multiple-lines? is enabled. Document behavior and split/max interaction in README; extend core tests accordingly.
|
Hi @weavejester , this is my first stab at #114 |
| (defn- map-key-needs-split-for-layout? [zloc opts] | ||
| (when (map-key-without-line-break? zloc) | ||
| (let [split? (:split-keypairs-over-multiple-lines? opts) | ||
| max-len (:max-line-length opts) | ||
| max-on? (pos-int? max-len)] | ||
| (cond | ||
| (and split? (not max-on?)) true | ||
| max-on? (> (node-end-position zloc) max-len) | ||
| :else false)))) | ||
|
|
||
| (defn- split-map-keypairs-for-layout [form opts] | ||
| (transform form edit-all | ||
| #(map-key-needs-split-for-layout? % opts) | ||
| insert-newline-left)) |
There was a problem hiding this comment.
Can you explain why the split-keypairs-over-multiple-lines function needs to be changed in this way? I think there needs to be a very good reason to add complexity here.
There was a problem hiding this comment.
If we keep the old behavior when both are on, the keypair pass always explodes maps onto many lines before the line-length pass runs. That makes :max-line-length almost meaningless for maps (it all stems from my approach being dependent on the outer form which I agree is not generic)
My intent with this change: with a positive max and keypair, breaks on maps are width-gated; split-only (no max) stays exactly the old “always split qualifying keys” behavior.
| (defn- wrap-ns-require-libspec-rows-to-max-length [form max-len] | ||
| (transform form edit-all | ||
| (fn [zloc] | ||
| (and (libspec-vector-not-first-after-require? zloc) | ||
| (single-line-direct-children? (z/up zloc)) | ||
| (> (node-end-position zloc) max-len))) | ||
| insert-newline-left)) |
There was a problem hiding this comment.
Can you explain why there's a separate function for libspecs? Why is breaking the lines here different to other forms?
There was a problem hiding this comment.
General structural wrap inserts newlines between direct children of lists/vectors. Breaking “at the first child boundary” inside [my.ns :as m] splits the same require entry across lines, which is not ideal. If we later get configurable break rules (#413), this could become one configured rule among others.
There was a problem hiding this comment.
I think we want to attempt this feature in two steps:
- Get a basic line-breaking feature
- Create some sort of per-form syntax for preferring line break locations
I don't think we should try to hard code specific behavior, as (a) we'll have to eventually remove it in favor of a configurable system, and (b) we don't want to break things when we switch over systems.
| (z/subedit-node (update-space-left zloc padding) | ||
| #(pad-inside-node % padding))) |
There was a problem hiding this comment.
my bad, this should be reset to the way it was.. Initial drafts of my change were reporting an error for this when I ran bb lint
| (defn- apply-max-line-length-structural-wrap [form opts] | ||
| (let [max-len (:max-line-length opts)] | ||
| (if (pos-int? max-len) | ||
| (-> form | ||
| (wrap-ns-require-libspec-rows-to-max-length max-len) | ||
| (transform edit-all | ||
| (fn [zloc] | ||
| (and (max-len-structural-wrap-target? zloc) | ||
| (single-line-direct-children? zloc) | ||
| (not (structural-wrap-under-forbidden-reader-macro? zloc)))) | ||
| #(wrap-max-line-length-at-child-boundaries % max-len))) | ||
| form))) |
There was a problem hiding this comment.
I think we should be aiming for a function like:
(defn limit-line-length [form {:keys [max-line-length]}]
(transform edit-all #(needs-line-break? % max-line-length) add-line-break))There was a problem hiding this comment.
I will take this feedback and apply it for my next iteration of this PR
| (defn- wrap-max-line-length-at-child-boundaries [zloc max-len] | ||
| (cond | ||
| (= :namespaced-map (z/tag zloc)) | ||
| (if-let [inner (some-> zloc z/down z/right)] | ||
| (if (= :map (z/tag inner)) | ||
| (let [wrapped (wrap-max-line-length-at-child-boundaries inner max-len)] | ||
| (or (some-> wrapped z/up) zloc)) | ||
| zloc) | ||
| zloc) | ||
| (not (max-len-structural-wrap-target? zloc)) | ||
| zloc | ||
| (not (single-line-direct-children? zloc)) | ||
| zloc | ||
| :else | ||
| (let [first-el (-> zloc z/down (skip-whitespace-and-commas z/right*))] | ||
| (if-not first-el | ||
| zloc | ||
| (let [last-zloc | ||
| (loop [current first-el] | ||
| (if-some [nxt (-> current z/right* (skip-whitespace-and-commas z/right*))] | ||
| (recur (if (> (node-end-position nxt) max-len) | ||
| (insert-newline-left nxt) | ||
| nxt)) | ||
| current))] | ||
| (z/up last-zloc)))))) | ||
|
|
There was a problem hiding this comment.
This is looks like the logic for line breaks depends on the form, but why is that the case? Why not add line breaks at the first possible place prior to the max-line-length?
If certain forms are going to need specific line break, that sounds like something that should be configurable (see #413).
There was a problem hiding this comment.
My thinking was hard wrap at max-line-length is going to be unsafe e.g.
very-long-namespace/foo or keyword-with-dashes must stay intact.
String literals cannot be broken without changing semantics etc
I will think about not being dependent on the form and make this more generic
There was a problem hiding this comment.
By "the first possible place" I mean the first possible place that doesn't change the semantic meaning.
|
Thanks for the PR, I have a few comments which I've made inline. Generally I think this needs to be simpler and more general. Where the line break happens should be very predictable, and shouldn't depend on what the outer form is. I also note that the PR doesn't seem to include calls to the indentation functions, unless I've missed that somehow? You need to know how a line will be indented in order to know whether a line break is in an appropriate place. For instance: ;; too long a line
(aaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccccccccccccccccccc)
;; breaking it up here also leads to too long a line due to indent
(aaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccccccccccccccccccccccccccc)
;; so it needs to be split like so:
(aaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccccccccccccccccccccccccccc)
;; however, if the indentation rules were different, this might be valid:
(aaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccccccccccccccccccccccccccc) |
|
I think before any more code is written, we need to define the algorithm we're using to divide up lines. This needs to:
Splitting one line into two seems relatively straightforward: we start from the line break and step left until we get a line break position that works. However, splitting one line into three or more seems more complex. Consider a situation where we wrap at 10 characters: ;123456789
(aa bb (cc dd ee ff))How should this be wrapped? Presumably we want to try to minimize the number of new lines under most circumstances, but are we trying every permutation, or relying on some greedy matching algorithm that works most of the time? |
Refs #114
Apply :max-line-length as a structural pass: break only between direct children of eligible single-line forms using existing width accounting. Cover lists (including ns :import / :use), vectors, maps, sets, fn literals, namespaced maps, and inner collections in #? / #?@ reader conditionals; skip layout changes inside other reader macros and inside ns :require libspec vectors (row breaks only between those vectors). When :max-line-length is set, gate map keypair newlines on line width; keep unconditional keypair splitting when only
:split-keypairs-over-multiple-lines? is enabled. Document behavior and split/max interaction in README; extend core tests accordingly.