Good explanation of how (some) pretty printers work.
There is one snag, at least on my browser (Brave on MacOS).
The final example with line width 120 is marred by having some lines wrapped, no matter how I resize my browser window or resize the text:
See also [0] for description of Dart's auto-formatter for how (some) other pretty printers work.
P.S. "unicode-width is to get accurate string widths for monospaced terminal fonts including full-width Unicode characters. I’m pretty sure this is an impossible problem" ― it's not impossible, it's just that different terminals (and different terminal fonts) have different ideas on the amount of screen cells occupied by the Unicode codepoints and the chances that they will coincide with the ideas of the Unicode library your app uses are pretty slim. For instance, '\N{WOMAN}\N{ZWJ}\N{HEAVY BLACK HEART}\N{VARIATION SELECTOR-16}\N{ZWJ}\N{MAN}' could be either 2 cells if your terminal and font support the single combined 'Couple with Heart: Woman, Man' emoji; or 6 cells if they fall back to displaying it as a sequence of 'Woman', 'Red Heart', 'Man' emojis; or 5 cells if your terminal fails to recognize that '\N{HEAVY BLACK HEART}\N{VARIATION SELECTOR-16}' actually takes 2 cells, not 1: GNOME Terminal 3.28.2 for instance, does that although it still paints the 2-cells wide 'Red Heart' emoji which leads to visual overlap ― how do you even account for that?!
> There’s another softer rule, which is that if you have a choice x | y then x should not contain forced newlines.
Line comments will make this rule hard to follow.
I've found that line comments (which must end in a newline, regardless of where they appear) are one of the major sources of complexity in writing a good formatter.
> I've found that line comments (which must end in a newline, regardless of where they appear) are one of the major sources of complexity in writing a good formatter.
Indeed they are!
Most line comments appear in places that already have forced line breaks (and no choices), and thus aren't a problem. E.g. comments in function bodies or at the top level.
But then there's nasty cases like:
[ 1, // one
2]
and
let x = 5; // long comment that doesn't fit on the line
An important thing to remember is that you're writing code to construct a Notation, and you can do whatever you like in that code. E.g. for the first example, disable the single-line layout if there's a `//` comment inside, and for the second example, move the comment to the line above if it's too long.
There's also a small modification to the printing algorithm which can help, that I didn't describe in this post. The "softer rule" can be enforced by the printer. When picking between `x | y`, it can pick `x` if it fits on the line and does not contain a newline inside of a flat. (EDIT: switched to better modification.)
EDIT: munificent, how does dart-fmt handle that second example with the long end-of-line comment?
We're in the middle of a rewrite of the formatter, so my answer will be sort of hand-wavey to try to cover how both of them work.
Basically, we take the incoming tree and convert it to an internal representation that has some nesting structuring and understands places where line splitting choices can be made.
When a mandatory newline (like from a line comment or multi-line string) occurs, we walk outwards to the surrounding IR nodes ("chunks" in the old formatter, "pieces" in the new one) and tell them that a newline has occurred.
In the old formatter, that tells the surrounding rules to "harden", which turns them into the maximally split form. In the new formatter, each piece can choose whether or not it allows newlines in different contexts. If it gets word that a newline occurs where not allowed, that solution is invalidated and the solver goes looking for a better one.
7 comments
[ 6.9 ms ] story [ 43.7 ms ] threadThere is one snag, at least on my browser (Brave on MacOS). The final example with line width 120 is marred by having some lines wrapped, no matter how I resize my browser window or resize the text:
Which runs rather counter to all the pretty printer's efforts to make things fit on a line.The page source shows it embedded in <pre><code><small><small> </small></small></code></pre>
How could they have prevented the browser from introducing line breaks? E.g. by making it use scrollbars?
P.S. "unicode-width is to get accurate string widths for monospaced terminal fonts including full-width Unicode characters. I’m pretty sure this is an impossible problem" ― it's not impossible, it's just that different terminals (and different terminal fonts) have different ideas on the amount of screen cells occupied by the Unicode codepoints and the chances that they will coincide with the ideas of the Unicode library your app uses are pretty slim. For instance, '\N{WOMAN}\N{ZWJ}\N{HEAVY BLACK HEART}\N{VARIATION SELECTOR-16}\N{ZWJ}\N{MAN}' could be either 2 cells if your terminal and font support the single combined 'Couple with Heart: Woman, Man' emoji; or 6 cells if they fall back to displaying it as a sequence of 'Woman', 'Red Heart', 'Man' emojis; or 5 cells if your terminal fails to recognize that '\N{HEAVY BLACK HEART}\N{VARIATION SELECTOR-16}' actually takes 2 cells, not 1: GNOME Terminal 3.28.2 for instance, does that although it still paints the 2-cells wide 'Red Heart' emoji which leads to visual overlap ― how do you even account for that?!
[0] https://journal.stuffwithstuff.com/2015/09/08/the-hardest-pr...
Fixed it with:
Line comments will make this rule hard to follow.
I've found that line comments (which must end in a newline, regardless of where they appear) are one of the major sources of complexity in writing a good formatter.
Indeed they are!
Most line comments appear in places that already have forced line breaks (and no choices), and thus aren't a problem. E.g. comments in function bodies or at the top level.
But then there's nasty cases like:
and An important thing to remember is that you're writing code to construct a Notation, and you can do whatever you like in that code. E.g. for the first example, disable the single-line layout if there's a `//` comment inside, and for the second example, move the comment to the line above if it's too long.There's also a small modification to the printing algorithm which can help, that I didn't describe in this post. The "softer rule" can be enforced by the printer. When picking between `x | y`, it can pick `x` if it fits on the line and does not contain a newline inside of a flat. (EDIT: switched to better modification.)
EDIT: munificent, how does dart-fmt handle that second example with the long end-of-line comment?
Basically, we take the incoming tree and convert it to an internal representation that has some nesting structuring and understands places where line splitting choices can be made.
When a mandatory newline (like from a line comment or multi-line string) occurs, we walk outwards to the surrounding IR nodes ("chunks" in the old formatter, "pieces" in the new one) and tell them that a newline has occurred.
In the old formatter, that tells the surrounding rules to "harden", which turns them into the maximally split form. In the new formatter, each piece can choose whether or not it allows newlines in different contexts. If it gets word that a newline occurs where not allowed, that solution is invalidated and the solver goes looking for a better one.