Skip to content

Avoid introducing linebreaks in template interpolations#15209

Merged
fisker merged 5 commits intoprettier:mainfrom
bakkot:template-newlines
Dec 17, 2023
Merged

Avoid introducing linebreaks in template interpolations#15209
fisker merged 5 commits intoprettier:mainfrom
bakkot:template-newlines

Conversation

@bakkot
Copy link
Collaborator

@bakkot bakkot commented Aug 5, 2023

Description

In a template string like

`this is a long message which contains an interpolation: ${format(data)} <- like this`;

avoid adding a linebreak when formatting the expression unless one is already present or it's unavoidable due to e.g. a nested function. Previously a linebreak could be introduced whenever some interpolation in the template was sufficiently "not simple":

`this is a long message which contains an interpolation: ${format(
  data,
)} <- like this`;

Now it will instead be left alone.

If a linebreak is already present within the ${...}, format as normal.

Fixes (?) #3368. It doesn't introduce a new option; instead, if you actively want a linebreak, you can add one yourself (just like for object literals). At the moment it still keeps the old "sufficiently simple" heuristic, but I'm happy to rip that out and just rely on the "is a newline already present" heuristic instead.

A possible refinement is to only apply avoid linebreaks when either the line before or after the interpolation has some non-whitespace text. That would allow introducing linebreaks in cases like this:

let items = `
  ${someComplicatedExpression()}
  ${someComplicatedExpression()}
`;

I'm happy to push a commit with that change if you'd like.

Checklist

I haven't done these yet because I'm waiting for approval for this approach. I'll update this PR if and when it's decided this is a good idea.

  • I’ve added tests to confirm my change works.
  • (If changing the API or CLI) I’ve documented the changes I’ve made (in the docs/ directory).
  • (If the change is user-facing) I’ve added my changes to changelog_unreleased/*/XXXX.md file following changelog_unreleased/TEMPLATE.md.
  • I’ve read the contributing guidelines.

Try the playground for this PR

@bakkot bakkot force-pushed the template-newlines branch from f611481 to 2a60012 Compare August 5, 2023 20:34
@sosukesuzuki
Copy link
Contributor

I haven't fully reviewed the code yet, so I can't approve it on GitHub. However, I'd like to express my personal opinion on this approach.

I am in favor of this approach.

From my experience, template literals often contain sentences in natural language. Therefore, I believe it's hard to determine the format of expressions within template literals solely from AST.

Ideally, we wouldn't want to introduce a format that depends on user-input line breaks. However, I think this is acceptable.

If we decide to accept this PR, we'll need to update https://prettier.io/docs/en/rationale.html.

@bakkot
Copy link
Collaborator Author

bakkot commented Aug 8, 2023

I've gone ahead and updated the rationale (and the changelog).

Does anyone have opinions on whether we should still keep the old "sufficiently simple" heuristic, or just rely on the "is there a linebreak" thing?

export @decorator class Foo {}
```

### Template literals
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add code example to here same as changelog?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@thorn0
Copy link
Member

thorn0 commented Aug 28, 2023

Is the current heuristic really hopeless? Or can it probably just be updated to cover cases like format(data)?

@bakkot
Copy link
Collaborator Author

bakkot commented Aug 29, 2023

I don't think the current heuristic is salvageable. As @sosukesuzuki points out, the fundamental problem is that the appropriate formatting depends on the content of the template string, which is just not something it's practical for Prettier to reason about.

It's not just stuff like format(data) - consider console.log(`you have ${n} new message${n === 1 ? '' : 's'}.`), for example. That definitely shouldn't be split over multiple lines. But to know that you have to know that it's prose, such that a linebreak would make it unreadable; if the thing being printed were something else, a linebreak might well be acceptable. And I don't think any purely AST-based heuristic is going to be able to figure that out.

@bakkot bakkot force-pushed the template-newlines branch from 4861776 to bf9845a Compare October 3, 2023 00:30
@bakkot
Copy link
Collaborator Author

bakkot commented Oct 3, 2023

Rebased. Friendly ping for maintainers - can we get this merged?

Copy link
Contributor

@sosukesuzuki sosukesuzuki left a comment

Choose a reason for hiding this comment

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

LGTM.

@sosukesuzuki sosukesuzuki requested a review from fisker October 4, 2023 15:52
@owenallenaz
Copy link

owenallenaz commented Oct 4, 2023

I hate that prettier mangles template literals, but I have a question about your implementation. You state that you won't add linebreaks in this example:

let items = `
  ${someComplicatedExpression()}
  ${someComplicatedExpression()}
`;

But that expression would already not be split or kept as one line because prettier only splits up a line when the line break occur within a sub expression ${} as there is a functional different between \ncontent\ncontent\ncontent and content content. In your first example you correctly enumerate this. I guess with that... I'm just a bit confused on what's changing here?

@bakkot
Copy link
Collaborator Author

bakkot commented Oct 4, 2023

I hate that prettier mangles template literals, but I have a question about your implementation. You state that you won't add linebreaks in this example:

let items = `
  ${someComplicatedExpression()}
  ${someComplicatedExpression()}
`;

But that expression would already not be split or kept as one line because prettier only splits up a line when the line break occur within a sub expression ${} as there is a functional different between \ncontent\ncontent\ncontent and content content. In your first example you correctly enumerate this. I guess with that... I'm just a bit confused on what's changing here?

Suppose someComplicatedExpression() was really long, exceeding the print width, and also had an opportunity for a line break. Right now (prior to this PR), Prettier will put in linebreaks inside the ${}, like in this playground:

let items = `
  ${someComplicatedExpression(
    asdf,
  )}
  ${someComplicatedExpression(
    asdf,
  )}
`;

With this PR, it won't anymore; it just leaves the input alone, like this:

let items = `
  ${someComplicatedExpression(asdf)}
  ${someComplicatedExpression(asdf)}
`;

Does that answer your question? If not I don't understand your question.

@owenallenaz
Copy link

owenallenaz commented Oct 4, 2023

@bakkot It perfectly answers my question. That makes total sense, if the user has a line break within the sub expression within a template string, it will format it nicely... if no line breaks, it doesn't touch it. I played around with it and it behaves like I would expect, which is leaving my template strings alone.

@owenallenaz
Copy link

@sosukesuzuki Any chance we could look at this PR again? It's a blocker in our teams adopting prettier and from the checks above it seems to be mergeable pending review.

@sosukesuzuki
Copy link
Contributor

@fisker Can you review this?

@bakkot
Copy link
Collaborator Author

bakkot commented Nov 9, 2023

@sosukesuzuki Is it possible to land this without @fisker's review? This has been ready for four months.

@bakkot
Copy link
Collaborator Author

bakkot commented Dec 13, 2023

@sosukesuzuki @fisker ping?

@fisker
Copy link
Member

fisker commented Dec 13, 2023

I'm deeply sorry for the long delay.

Sometimes respects the original line break is annoying, when I delete props from object literal, I don't know if it can fit one line, I often have to delete new line after { before Prettier runs, especially when there are only 2~3 properties left.

Anyway, I agree curenttly we format the template literals really bad in some cases, I'll just remain neutral on this one.

@thorn0 also seems not happy with this solution, #15209 (comment)

@thorn0
Copy link
Member

thorn0 commented Dec 13, 2023

What about template literals like `${ ... }px`? Code like that is widespread because of the prefer-template ESLint rule. Keeping everything on one line feels unexpected here:

Prettier pr-15209
Playground link

Input:

let items = `${someComplicatedExpression(asdf) + cantileveredRhinocerosFactory / someComplicatedExpression(asdf) - transdecimalThingamabobProvider}px`;

Output:

let items = `${someComplicatedExpression(asdf) + cantileveredRhinocerosFactory / someComplicatedExpression(asdf) - transdecimalThingamabobProvider}px`;

@thorn0
Copy link
Member

thorn0 commented Dec 13, 2023

It's not just stuff like format(data) - consider console.log(`you have ${n} new message${n === 1 ? '' : 's'}.`)

That's a convincing example. But what about longer expressions, those that are longer than printWidth? Would it really ever make sense from the readability point of view to keep those long expressions on a single line?

@bakkot
Copy link
Collaborator Author

bakkot commented Dec 13, 2023

In my experience it people overwhelmingly prefer not to have linebreaks within an interpolation, even if the interpolation is long.

So I think the right default is to not introduce any linebreaks and allow users to opt in where it makes sense to do so by adding one, instead of trying to have a more complicated rule. I'm open to more complicated rules if you want to suggest them, but I think there's a lot of value in simplicity.

@fisker fisker merged commit cb6fea8 into prettier:main Dec 17, 2023
@bakkot bakkot deleted the template-newlines branch December 17, 2023 16:05
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.

5 participants