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

State: extend indent if relativeToLhsFirstLine #3334

Merged
merged 2 commits into from
Oct 7, 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
41 changes: 41 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,47 @@ def fooDef
}
```

#### `indent.relativeToLhsLastLine`

When the left-hand side of an infix or `match` expression is itself broken over
several lines, with the last line indented relative to the first line, this flag
determines whether the indent is relative to the first or the last line.

This parameter takes a list of values including:

- `match`: applies to match expressions
- `infix`: applies to infix expressions

```scala mdoc:defaults
indent.relativeToLhsLastLine
```

```scala mdoc:scalafmt
indent.relativeToLhsLastLine = []
---
foo // c1
.bar match {
case baz => qux
}
foo // c1
.bar infix {
case baz => qux
}
```

```scala mdoc:scalafmt
indent.relativeToLhsLastLine = [match, infix]
---
foo // c1
.bar match {
case baz => qux
}
foo // c1
.bar infix {
case baz => qux
}
```

### `indentOperator`

Normally, the first eligible break _inside_ a chain of infix operators is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ case class Indents(
matchSite: Option[Int] = None,
private[config] val ctorSite: Option[Int] = None,
extraBeforeOpenParenDefnSite: Int = 0,
relativeToLhsLastLine: Seq[Indents.RelativeToLhs] = Nil,
@annotation.ExtraName("deriveSite")
extendSite: Int = 4,
withSiteRelativeToExtends: Int = 0,
Expand All @@ -50,4 +51,13 @@ object Indents {
generic.deriveSurface
implicit lazy val codec: ConfCodecEx[Indents] =
generic.deriveCodecEx(Indents()).noTypos

sealed abstract class RelativeToLhs
object RelativeToLhs {
case object `match` extends RelativeToLhs
case object `infix` extends RelativeToLhs

implicit val reader: ConfCodecEx[RelativeToLhs] = ReaderUtil
.oneOf[RelativeToLhs](`match`, `infix`)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ case class ModExt(
) {
lazy val indentation = indents.mkString("[", ", ", "]")

@inline
def isNL: Boolean = mod.isNewline

def withIndent(length: => Length, expire: => Token, when: ExpiresOn): ModExt =
length match {
case Length.Num(0, _) => this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ case class Split(
def indentation: String = modExt.indentation

@inline
def isNL: Boolean = modExt.mod.isNewline
def isNL: Boolean = modExt.isNL

@inline
def length: Int = modExt.mod.length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import java.util.regex.Pattern

import scala.annotation.tailrec
import scala.meta.tokens.Token
import scala.meta.{Term, Tree}

import org.scalafmt.config.Comments
import org.scalafmt.config.ScalafmtConfig
import org.scalafmt.config.{Comments, Indents, ScalafmtConfig}
import org.scalafmt.util.TreeOps._

/** A partial formatting solution up to splits.length number of tokens.
Expand Down Expand Up @@ -43,20 +43,26 @@ final case class State(
if (right.is[Token.EOF]) (initialNextSplit, 0, Seq.empty)
else {
val offset = column - indentation
def getUnexpired(indents: Seq[ActualIndent]): Seq[ActualIndent] =
indents.filter(_.notExpiredBy(tok))
def getPushes(modExt: ModExt): Seq[ActualIndent] =
getUnexpired(modExt.getActualIndents(offset))
def getUnexpired(modExt: ModExt, indents: Seq[ActualIndent] = Nil) = {
val extendedEnd = getRelativeToLhsLastLineEnd(modExt.isNL)
(modExt.getActualIndents(offset) ++ indents).flatMap { x =>
if (x.notExpiredBy(tok)) Some(x)
else
extendedEnd
.map(y => x.copy(expireEnd = y, expiresAt = ExpiresOn.After))
}
}

val initialModExt = initialNextSplit.modExt
val indents = initialModExt.indents
val nextPushes = getPushes(initialModExt) ++ getUnexpired(pushes)
val nextPushes = getUnexpired(initialModExt, pushes)
val nextIndent = Indent.getIndent(nextPushes)
initialNextSplit.modExt.mod match {
case m: NewlineT
if !tok.left.is[Token.Comment] && m.alt.isDefined &&
nextIndent >= m.alt.get.mod.length + column =>
val alt = m.alt.get
val altPushes = getPushes(alt)
val altPushes = getUnexpired(alt)
val altIndent = Indent.getIndent(altPushes)
val split = initialNextSplit.withMod(alt.withIndents(indents))
(split, nextIndent + altIndent, nextPushes ++ altPushes)
Expand Down Expand Up @@ -255,6 +261,59 @@ final case class State(
}
}

private def getRelativeToLhsLastLineEnd(isNL: Boolean)(implicit
style: ScalafmtConfig,
tokens: FormatTokens
): Option[Int] = {
val allowed = style.indent.relativeToLhsLastLine

def treeEnd(x: Tree) = tokens.getLast(x).left.end
def indentEnd(ft: FormatToken, isNL: Boolean)(onComment: => Option[Int]) = {
val leftOwner = ft.meta.leftOwner
ft.left match {
case _: Token.KwMatch
if leftOwner.is[Term.Match] &&
allowed.contains(Indents.RelativeToLhs.`match`) =>
Some(treeEnd(leftOwner))
case _: Token.Ident if !isNL =>
leftOwner.parent match {
case Some(p: Term.ApplyInfix)
if p.op.eq(leftOwner) &&
allowed.contains(Indents.RelativeToLhs.`infix`) =>
Some(treeEnd(p))
case _ => None
}
case _: Token.Comment if !isNL => onComment
case _ => None
}
}

val tok = tokens(depth)
val right = tok.right
if (allowed.isEmpty) None
else if (right.is[Token.Comment]) Some(right.end)
else
indentEnd(tok, isNL) {
val earlierState = prev.prevNonCommentSameLine
indentEnd(tokens(earlierState.depth), earlierState.split.isNL)(None)
}.orElse {
val delay = !isNL && (right match {
case _: Token.KwMatch =>
tok.meta.rightOwner.is[Term.Match] &&
allowed.contains(Indents.RelativeToLhs.`match`)
case _: Token.Ident =>
tok.meta.rightOwner.parent.exists(_.is[Term.ApplyInfix]) &&
allowed.contains(Indents.RelativeToLhs.`infix`)
case _ => false
})
if (delay) Some(right.end) else None
}
}

@tailrec
private def prevNonCommentSameLine(implicit tokens: FormatTokens): State =
if (split.isNL || !tokens(depth).left.is[Token.Comment]) this
else prev.prevNonCommentSameLine
}

object State {
Expand Down
82 changes: 82 additions & 0 deletions scalafmt-tests/src/test/resources/newlines/source_classic.stat
Original file line number Diff line number Diff line change
Expand Up @@ -6005,3 +6005,85 @@ class Foo() {
false
}
}
<<< #3327 match
indent.relativeToLhsLastLine = [match]
===
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ match /* c2 */ {
case Some(Row(value: Long)) => value
case _ => 0
}
}
}
>>>
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ match /* c2 */ {
case Some(Row(value: Long)) =>
value
case _ => 0
}
}
}
<<< #3327 infix
indent.relativeToLhsLastLine = [infix]
===
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(Row(value: Long)) => value
case _ => 0
}
}
}
>>>
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(Row(value: Long)) =>
value
case _ => 0
}
}
}
<<< #3327 infix 2
indent.relativeToLhsLastLine = [infix]
===
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(Row(value: Long)) => value
case _ => 0
} /* c3 */ infix /* c4 */ {
case Some(Row(value: Long)) => value
case _ => 0
}
}
}
>>>
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(Row(value: Long)) =>
value
case _ => 0
} /* c3 */ infix /* c4 */ {
case Some(Row(value: Long)) =>
value
case _ => 0
}
}
}
85 changes: 85 additions & 0 deletions scalafmt-tests/src/test/resources/newlines/source_fold.stat
Original file line number Diff line number Diff line change
Expand Up @@ -5714,3 +5714,88 @@ class Foo() {
false
}
}
<<< #3327 match
indent.relativeToLhsLastLine = [match]
===
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ match /* c2 */ {
case Some(Row(value: Long)) => value
case _ => 0
}
}
}
>>>
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ match /* c2 */ {
case Some(Row(value: Long)) =>
value
case _ => 0
}
}
}
<<< #3327 infix
indent.relativeToLhsLastLine = [infix]
===
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(Row(value: Long)) => value
case _ => 0
}
}
}
>>>
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(
Row(value: Long)
) => value
case _ => 0
}
}
}
<<< #3327 infix 2
indent.relativeToLhsLastLine = [infix]
===
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(Row(value: Long)) => value
case _ => 0
} /* c3 */ infix /* c4 */ {
case Some(Row(value: Long)) => value
case _ => 0
}
}
}
>>>
object A {
private def get(ds: Any): IO[Long] =
IO.delay {
ds // c1
.headOption /* c1 */ infix /* c2 */ {
case Some(
Row(value: Long)
) => value
case _ => 0
} /* c3 */ infix /* c4 */ {
case Some(
Row(value: Long)
) => value
case _ => 0
}
}
}
Loading