diff --git a/docs/configuration.md b/docs/configuration.md index d962a3b2cd..f220f5d8a7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2576,6 +2576,44 @@ newlines.inInterpolation - `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 +### `newlines.ignoreInSyntax` + +The formatter frequently chooses between adding a newline and continuing the +same line but either prohibiting or heavily discouraging subsequent newlines +_between_ tokens, to fit the rest of the expression on the same line. + +However, in many cases and, for historical reasons, intentionally, newlines +_within_ tokens have been frequently ignored, leading to "single-line" blocks +which actually span multiple lines. + +This boolean parameter now allows controlling whether to ignore newlines found +in syntax of strings or other possibly multi-line tokens when newlines are +otherwise prohibited or undesirable (such as for single-line formatting). + +```scala mdoc:defaults +newlines.ignoreInSyntax +``` + +> Since v3.7.13. Prior to that, this behaviour was always enabled. + +```scala mdoc:scalafmt +newlines.ignoreInSyntax = true +--- +// ignores newline in string, pretends everything fits on one line +println(s"""${1} + """.stripMargin +) +``` + +```scala mdoc:scalafmt +newlines.ignoreInSyntax = false +--- +// detects newline in string, forces proper multi-line formatting +println(s"""${1} + """.stripMargin +) +``` + ### `optIn.annotationNewlines` This boolean parameter controls newlines after annotations. diff --git a/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Newlines.scala b/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Newlines.scala index 0d4defd91a..ff222ea4fb 100644 --- a/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Newlines.scala +++ b/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Newlines.scala @@ -202,6 +202,7 @@ case class Newlines( afterInfixMaxCountPerFile: Int = 500, avoidForSimpleOverflow: Seq[AvoidForSimpleOverflow] = Seq.empty, inInterpolation: InInterpolation = InInterpolation.allow, + ignoreInSyntax: Boolean = true, avoidAfterYield: Boolean = true ) { if ( diff --git a/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatOps.scala b/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatOps.scala index fcb1f728d5..5d053c8e6c 100644 --- a/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatOps.scala +++ b/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatOps.scala @@ -1687,7 +1687,7 @@ class FormatOps( nlSplitFunc: Int => Split, isKeep: Boolean, spaceIndents: Seq[Indent] = Seq.empty - ): Seq[Split] = { + )(implicit style: ScalafmtConfig): Seq[Split] = { def bheadFT = tokens.getHead(body) val blastFT = tokens.getLastNonTrivial(body) val blast = blastFT.left @@ -1788,14 +1788,14 @@ class FormatOps( nlSplitFunc: Int => Split, isKeep: Boolean, spaceIndents: Seq[Indent] - ): Seq[Split] = + )(implicit style: ScalafmtConfig): Seq[Split] = if (body.tokens.isEmpty) Seq(Split(Space, 0)) else foldedNonEmptyNonComment(body, nlSplitFunc, isKeep, spaceIndents) private def unfoldedSpaceNonEmptyNonComment( body: Tree, slbOnly: Boolean - ): Split = { + )(implicit style: ScalafmtConfig): Split = { val expire = nextNonCommentSameLine(tokens.getLastNonTrivial(body)).left def slbSplit(end: T)(implicit fileLine: FileLine) = Split(Space, 0).withSingleLine(end, noSyntaxNL = true) @@ -1816,7 +1816,7 @@ class FormatOps( nlSplitFunc: Int => Split, spaceIndents: Seq[Indent], slbOnly: Boolean - ): Seq[Split] = + )(implicit style: ScalafmtConfig): Seq[Split] = if (body.tokens.isEmpty) Seq(Split(Space, 0).withIndents(spaceIndents)) else { val spaceSplit = unfoldedSpaceNonEmptyNonComment(body, slbOnly) @@ -1848,7 +1848,7 @@ class FormatOps( body: Tree, isKeep: Boolean, spaceIndents: Seq[Indent] = Seq.empty - )(nlSplitFunc: Int => Split): Seq[Split] = + )(nlSplitFunc: Int => Split)(implicit style: ScalafmtConfig): Seq[Split] = checkComment(ft, nlSplitFunc) { _ => foldedNonComment(body, nlSplitFunc, isKeep, spaceIndents) } @@ -1857,7 +1857,7 @@ class FormatOps( ft: FormatToken, body: Tree, spaceIndents: Seq[Indent] = Seq.empty - )(nlSplitFunc: Int => Split): Seq[Split] = + )(nlSplitFunc: Int => Split)(implicit style: ScalafmtConfig): Seq[Split] = checkComment(ft, nlSplitFunc) { _ => unfoldedNonComment(body, nlSplitFunc, spaceIndents, true) } diff --git a/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/Split.scala b/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/Split.scala index 34d58c4471..d81af529ae 100644 --- a/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/Split.scala +++ b/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/Split.scala @@ -2,6 +2,7 @@ package org.scalafmt.internal import scala.meta.tokens.Token +import org.scalafmt.config.ScalafmtConfig import org.scalafmt.internal.Policy.NoPolicy import org.scalafmt.util.PolicyOps import org.scalameta.FileLine @@ -142,7 +143,7 @@ case class Split( noSyntaxNL: Boolean = false, killOnFail: Boolean = false, rank: Int = 0 - )(implicit fileLine: FileLine): Split = + )(implicit fileLine: FileLine, style: ScalafmtConfig): Split = withSingleLineAndOptimal( expire, expire, @@ -158,7 +159,7 @@ case class Split( noSyntaxNL: Boolean = false, killOnFail: Boolean = false, rank: Int = 0 - )(implicit fileLine: FileLine): Split = + )(implicit fileLine: FileLine, style: ScalafmtConfig): Split = expire.fold(this)( withSingleLine(_, exclude, noSyntaxNL, killOnFail, rank) ) @@ -170,7 +171,7 @@ case class Split( noSyntaxNL: Boolean = false, killOnFail: Boolean = false, rank: Int = 0 - )(implicit fileLine: FileLine): Split = + )(implicit fileLine: FileLine, style: ScalafmtConfig): Split = withOptimalToken(optimal, killOnFail) .withSingleLineNoOptimal(expire, exclude, noSyntaxNL, rank) @@ -179,7 +180,7 @@ case class Split( exclude: => TokenRanges = TokenRanges.empty, noSyntaxNL: Boolean = false, rank: Int = 0 - )(implicit fileLine: FileLine): Split = + )(implicit fileLine: FileLine, style: ScalafmtConfig): Split = withPolicy( SingleLineBlock(expire, exclude, noSyntaxNL = noSyntaxNL, rank = rank) ) diff --git a/scalafmt-core/shared/src/main/scala/org/scalafmt/util/PolicyOps.scala b/scalafmt-core/shared/src/main/scala/org/scalafmt/util/PolicyOps.scala index e88cd26593..1d915477b3 100644 --- a/scalafmt-core/shared/src/main/scala/org/scalafmt/util/PolicyOps.scala +++ b/scalafmt-core/shared/src/main/scala/org/scalafmt/util/PolicyOps.scala @@ -2,6 +2,7 @@ package org.scalafmt.util import scala.meta.tokens.{Token => T} +import org.scalafmt.config.ScalafmtConfig import org.scalafmt.internal._ import org.scalameta.FileLine @@ -16,12 +17,13 @@ object PolicyOps { penalizeLambdas: Boolean = true, noSyntaxNL: Boolean = false, val rank: Int = 0 - )(implicit fileLine: FileLine) + )(implicit fileLine: FileLine, style: ScalafmtConfig) extends Policy.Clause { override val noDequeue: Boolean = false + private val checkSyntax = noSyntaxNL || !style.newlines.ignoreInSyntax override val f: Policy.Pf = { case Decision(ft, s) if penalizeLambdas || !ft.left.is[T.RightArrow] => - if (noSyntaxNL && ft.leftHasNewline) s.map(_.withPenalty(penalty)) + if (checkSyntax && ft.leftHasNewline) s.map(_.withPenalty(penalty)) else s.map(x => if (x.isNL) x.withPenalty(penalty) else x) } override def toString: String = s"PNL:${super.toString}+$penalty" @@ -33,7 +35,7 @@ object PolicyOps { penalty: Int, penalizeLambdas: Boolean = true, noSyntaxNL: Boolean = false - )(implicit fileLine: FileLine): Policy = { + )(implicit fileLine: FileLine, style: ScalafmtConfig): Policy = { new PenalizeAllNewlines( Policy.End.Before(expire), penalty, @@ -54,15 +56,16 @@ object PolicyOps { okSLC: Boolean = false, noSyntaxNL: Boolean = false, val rank: Int = 0 - )(implicit fileLine: FileLine) + )(implicit fileLine: FileLine, style: ScalafmtConfig) extends Policy.Clause { import TokenOps.isLeftCommentThenBreak override val noDequeue: Boolean = true override def toString: String = "SLB:" + super.toString + private val checkSyntax = noSyntaxNL || !style.newlines.ignoreInSyntax override val f: Policy.Pf = { case Decision(ft, s) if !(ft.right.is[T.EOF] || okSLC && isLeftCommentThenBreak(ft)) => - if (noSyntaxNL && ft.leftHasNewline) Seq.empty else s.filterNot(_.isNL) + if (checkSyntax && ft.leftHasNewline) Seq.empty else s.filterNot(_.isNL) } } @@ -74,7 +77,7 @@ object PolicyOps { okSLC: Boolean = false, noSyntaxNL: Boolean = false, rank: Int = 0 - )(implicit fileLine: FileLine): Policy = + )(implicit fileLine: FileLine, style: ScalafmtConfig): Policy = policyWithExclude(exclude, Policy.End.On, Policy.End.After)( Policy.End.On(expire), new SingleLineBlock( diff --git a/scalafmt-tests/src/test/resources/default/String.stat b/scalafmt-tests/src/test/resources/default/String.stat index 021b84093b..622928a1fe 100644 --- a/scalafmt-tests/src/test/resources/default/String.stat +++ b/scalafmt-tests/src/test/resources/default/String.stat @@ -599,3 +599,34 @@ val s = raw"""${ else "else" end if }""" +<<< #3608 ignoreInSyntax +maxColumn = 30 +assumeStandardLibraryStripMargin = true +newlines.ignoreInSyntax = true +=== +println("""| + |A very long string + |with multiple lines + |""".stripMargin +) +>>> +println("""| + |A very long string + |with multiple lines + |""".stripMargin) +<<< #3608 !ignoreInSyntax +maxColumn = 30 +assumeStandardLibraryStripMargin = true +newlines.ignoreInSyntax = false +=== +println("""| + |A very long string + |with multiple lines + |""".stripMargin +) +>>> +println( + """| + |A very long string + |with multiple lines + |""".stripMargin) diff --git a/scalafmt-tests/src/test/resources/test/StripMargin.stat b/scalafmt-tests/src/test/resources/test/StripMargin.stat index b505d88f40..fdaedfba1d 100644 --- a/scalafmt-tests/src/test/resources/test/StripMargin.stat +++ b/scalafmt-tests/src/test/resources/test/StripMargin.stat @@ -314,6 +314,20 @@ final class MyClass { println(s"""${1} """.stripMargin) } +<<< #2025 3 !ignoreInSyntax +newlines.ignoreInSyntax = false +=== +final class MyClass { + println(s"""${1} + """.stripMargin) +} +>>> +final class MyClass { + println( + s"""${1} + """.stripMargin + ) +} <<< #3090 string without margin maxColumn = 100 optIn.breaksInsideChains = true