Skip to content

Add optional column-oriented wrapping via :max-line-length#414

Open
ispandey81 wants to merge 1 commit intoweavejester:masterfrom
ispandey81:master
Open

Add optional column-oriented wrapping via :max-line-length#414
ispandey81 wants to merge 1 commit intoweavejester:masterfrom
ispandey81:master

Conversation

@ispandey81
Copy link
Copy Markdown

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.

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.
@ispandey81
Copy link
Copy Markdown
Author

Hi @weavejester , this is my first stab at #114
Let me know if this is a step in the right direction.

Comment on lines +637 to +650
(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))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +692 to +698
(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))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why there's a separate function for libspecs? Why is breaking the lines here different to other forms?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to attempt this feature in two steps:

  1. Get a basic line-breaking feature
  2. 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.

Comment on lines +770 to +771
(z/subedit-node (update-space-left zloc padding)
#(pad-inside-node % padding)))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this change made?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +726 to +737
(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)))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take this feedback and apply it for my next iteration of this PR

Comment on lines +700 to +725
(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))))))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "the first possible place" I mean the first possible place that doesn't change the semantic meaning.

@weavejester
Copy link
Copy Markdown
Owner

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)

@weavejester
Copy link
Copy Markdown
Owner

weavejester commented Apr 18, 2026

I think before any more code is written, we need to define the algorithm we're using to divide up lines. This needs to:

  1. Work with lines of arbitrary length
  2. Be able to split a line up more than once if necessary
  3. Take into account indentation
  4. Accept that some lines cannot be split up without user intervention (e.g. long string literals)

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants