Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router: implement newlines.inInterpolation logic #3080

Merged
merged 2 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,46 @@ align.allowOverflow

It's also enabled by default in `align.preset = most`.

### `align.inInterpolation`

If this flag is set, and breaks within interpolated code are allowed
(see [`newlines.inInterpolation`](#newlinesininterpolation), then the
interpolated code and the closing `}` will be indented relative to the opening `${`.

> Since v3.4.0.

```scala mdoc:defaults
align.inInterpolation
```

Keep in mind that this option might lead to line overflow via "stacking":

```scala mdoc:scalafmt
maxColumn = 30
align.inInterpolation = true
newlines.inInterpolation = oneline
---
object a {
s"""
|foo1 ${quxQux(bazBaz, barBar)} foo2 ${quxQux(bazBaz, barBar)} foo3 ${quxQux(bazBaz, barBar)} foo4
|""".stripMargin
}
```

vs

```scala mdoc:scalafmt
maxColumn = 30
align.inInterpolation = false
newlines.inInterpolation = oneline
---
object a {
s"""
|foo1 ${quxQux(bazBaz, barBar)} foo2 ${quxQux(bazBaz, barBar)} foo3 ${quxQux(bazBaz, barBar)} foo4
|""".stripMargin
}
```

## Newlines

The `newlines.*` options are used to configure when and where `scalafmt` should
Expand Down Expand Up @@ -2134,6 +2174,23 @@ It takes the same values as [newlines.source](#newlinessource); use `null`
- `classic` (i.e., `null` and `newlines.source` is not specified):
see [Classic select chains](#classic-select-chains).

### `newlines.inInterpolation`

This parameter controls how to format spliced scala code within string constants
(e.g., `s"..."`, etc). Also see [`align.inInterpolation`](#alignininterpolation).

```scala mdoc:defaults
newlines.inInterpolation
```

> Since v3.4.0.

- `allow`: allows breaks within spliced code (original)
- this option will not prevent line overflow even if `${` is within bounds,
because this option doesn't allow breaking right after `${`
- `avoid`: attemps to avoid breaks within the spliced code, regardless of line overflow
- `oneline`: formats the splice on a single line, or breaks after `${` if overflows

### `optIn.annotationNewlines`

This boolean parameter controls newlines after annotations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ case class Align(
private[config] val openParenTupleSite: Option[Boolean] = None,
beforeOpenParenDefnSite: Boolean = false,
beforeOpenParenCallSite: Boolean = false,
inInterpolation: Boolean = false,
tokens: Seq[AlignToken] = Seq(AlignToken.caseArrow),
arrowEnumeratorGenerator: Boolean = false,
tokenCategory: Map[String, String] = Map(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ case class Newlines(
afterInfixMaxCountPerExprForSome: Int = 10,
afterInfixMaxCountPerFile: Int = 500,
avoidForSimpleOverflow: Seq[AvoidForSimpleOverflow] = Seq.empty,
inInterpolation: InInterpolation = InInterpolation.allow,
avoidAfterYield: Boolean = true
) {
if (
Expand Down Expand Up @@ -376,6 +377,15 @@ object Newlines {
ReaderUtil.oneOf[AvoidForSimpleOverflow](punct, tooLong, slc)
}

sealed abstract class InInterpolation
object InInterpolation {
case object allow extends InInterpolation
case object avoid extends InInterpolation
case object oneline extends InInterpolation
implicit val codec: ConfCodecEx[InInterpolation] =
ReaderUtil.oneOf[InInterpolation](allow, avoid, oneline)
}

sealed abstract class AfterCurlyLambdaParams
object AfterCurlyLambdaParams {
case object preserve extends AfterCurlyLambdaParams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,11 @@ class Router(formatOps: FormatOps) {
)
case FormatToken(start: T.Interpolation.Start, _, _) =>
val end = matching(start)
val policy =
if (isTripleQuote(formatToken.meta.left.text)) NoPolicy
else PenalizeAllNewlines(end, BreakSingleLineInterpolatedString)
val split = Split(NoSplit, 0).withPolicy(policy)
val okNewlines =
style.newlines.inInterpolation.ne(Newlines.InInterpolation.avoid) &&
isTripleQuote(formatToken.meta.left.text)
def policy = PenalizeAllNewlines(end, BreakSingleLineInterpolatedString)
val split = Split(NoSplit, 0).withPolicy(policy, okNewlines)
Seq(
if (getStripMarginChar(formatToken).isEmpty) split
else if (style.align.stripMargin)
Expand Down Expand Up @@ -186,7 +187,49 @@ class Router(formatOps: FormatOps) {
// Interpolated string left brace
case FormatToken(open @ T.LeftBrace(), _, _)
if leftOwner.is[SomeInterpolate] =>
Seq(Split(Space(style.spaces.inInterpolatedStringCurlyBraces), 0))
val close = matching(open)
val alignIndents =
if (style.align.inInterpolation) Some {
Seq(
Indent(Length.StateColumn, close, ExpiresOn.After),
Indent(Length.Num(style.indent.main), close, ExpiresOn.Before),
Indent(Length.Num(-1), close, ExpiresOn.After)
)
}
else None
def spaceSplit(implicit fileLine: FileLine) =
Split(Space(style.spaces.inInterpolatedStringCurlyBraces), 0)
.withIndents(alignIndents.getOrElse(Nil))
def newlineSplit(cost: Int)(implicit fileLine: FileLine) = {
def mainIndents = Seq(
Indent(style.indent.main, close, ExpiresOn.Before),
Indent(style.indent.main, close, ExpiresOn.After)
)
Split(Newline, cost)
.withIndents(alignIndents.getOrElse(mainIndents))
.withPolicy(decideNewlinesOnlyBeforeClose(close))
}
style.newlines.inInterpolation match {
case Newlines.InInterpolation.avoid => Seq(spaceSplit)
case _ if newlines != 0 && style.newlines.source.eq(Newlines.keep) =>
Seq(newlineSplit(0))
case Newlines.InInterpolation.oneline =>
/* sequence of tokens:
* - 0 RBrace
* - 1 Interpolation.SpliceEnd (empty)
* - 2 Interpolation.Part (next string)
* - 3 Interpolation.End (quotes) or Interpolation.SliceStart/LBrace (${)
*/
val afterClose = tokens(close, 3)
val lastPart = afterClose.left.is[T.Interpolation.End]
val slbEnd = if (lastPart) afterClose.left else afterClose.right
Seq(
spaceSplit.withSingleLine(slbEnd),
newlineSplit(1)
)
case _ => Seq(spaceSplit)
}

case FormatToken(_, close @ T.RightBrace(), _)
if rightOwner.is[SomeInterpolate] =>
Seq(Split(Space(style.spaces.inInterpolatedStringCurlyBraces), 0))
Expand Down
173 changes: 173 additions & 0 deletions scalafmt-tests/src/test/resources/default/String.stat
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,176 @@ object X {
.f(" sldkfjdslkf sdlkfj dslkfjds flkdsjf lsdkjf dslkfjds sdklfjsd sdlkjfdskj")
}
}
<<< #3079 avoid
maxColumn = 50
newlines.inInterpolation = avoid
===
object a {
s"foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz, br)} fo4
|""".stripMargin
}
>>>
object a {
s"foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz, br)} fo4
|""".stripMargin
}
<<< #3079 allow
maxColumn = 50
newlines.inInterpolation = allow
===
object a {
s"foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz, br)} fo4
|""".stripMargin
}
>>>
object a {
s"foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(
baz,
bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz,
br)} fo4
|""".stripMargin
}
<<< #3079 allow, source=keep
maxColumn = 50
newlines.source = keep
newlines.inInterpolation = allow
includeCurlyBraceInSelectChains = true
===
object a {
s"foo1 ${
bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz, br)} fo4
|""".stripMargin
}
>>>
object a {
s"foo1 ${
bar
} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(
baz,
bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz,
br)} fo4
|""".stripMargin
}
<<< #3079 allow, source=keep, align
maxColumn = 50
newlines.source = keep
newlines.inInterpolation = allow
align.inInterpolation = true
includeCurlyBraceInSelectChains = true
===
object a {
s"foo1 ${
bar} foo2 ${baz(bar)} foo3 ${
qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz, br)} fo4
|""".stripMargin
}
>>>
object a {
s"foo1 ${
bar
} foo2 ${baz(bar)} foo3 ${
qux(baz, bar)
} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz,
bar)} foo4
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't there be an additional break here? foo4 seems to reach maxColumn (51) if I calculated correctly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ah... let me check

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you were right. added another option, oneline, which is least likely to overflow, and mentioned in the docs the caveat about the others. please take another look.

|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz,
br)} fo4
|""".stripMargin
}
<<< #3079 oneline
maxColumn = 50
newlines.inInterpolation = oneline
===
object a {
s"foo1 ${
bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz, br)} fo4
|""".stripMargin
}
>>>
object a {
s"foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${
qux(baz, bar)
} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${
qux(bz, br)
} fo4
|""".stripMargin
}
<<< #3079 oneline, align
maxColumn = 50
newlines.inInterpolation = oneline
align.inInterpolation = true
===
object a {
s"foo1 ${
bar} foo2 ${baz(bar)} foo3 ${
qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${qux(bz, br)} fo4
|""".stripMargin
}
>>>
object a {
s"foo1 ${bar} foo2 ${baz(bar)} foo3 ${qux(baz, bar)} foo4"
s"""
|foo1 ${bar} foo2 ${baz(bar)} foo3 ${
qux(baz,
bar)
} foo4
|""".stripMargin
s"""
|fo1 ${br} fo2 ${bz(br)} fo3 ${
qux(bz, br)
} fo4
|""".stripMargin
}