Repository: scalameta/scalameta (scala)

Issue:
Wrong indent in Scaladoc due to `i.e.`

## Configuration (required) ##

```
runner.dialect = "scala3"
version = "3.7.3"
maxColumn = 80
```

## Command-line parameters (required) ##

Pull request created by scala-steward https://github.com/ThoughtWorksInc/Dsl.scala/pull/660

## Steps

scala-steward created a pull request to upgrade scalafmt from 3.7.1 to 3.7.3

## Problem

Scalafmt formats code like this:
```scala
  *   Note that [[Dsl.Keyword#unary_$bang !-notation]] can be used on both
  *   `trampoline3` and `trampolineSuccess0` even when they are different types,
  * i.e. `trampoline3` is a vanilla [[scalaz.Free.Trampoline Trampoline]], while
  * `trampolineSuccess0` is a
```

## Expectation

I would like the formatted output to look like this:
```scala
  *   Note that [[Dsl.Keyword#unary_$bang !-notation]] can be used on both
  *   `trampoline3` and `trampolineSuccess0` even when they are different types,
  *   i.e. `trampoline3` is a vanilla [[scalaz.Free.Trampoline Trampoline]], while
  *   `trampolineSuccess0` is a
```

## Workaround

Use scalafmt 3.7.1

## Notes

See https://github.com/ThoughtWorksInc/Dsl.scala/pull/660/files#diff-48d17d795a0c78238baf49e1f634066cb12b49a96718615460ebc2c99fb69530R84-R115

Pull request description:
Fixes #3145.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
index 3dd94aebed..4b28467bb2 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
@@ -112,30 +112,36 @@ object ScaladocParser {
     pattern.map { case (x, y) => new Link(x, y) }
   }
 
-  private def nextPartParser[_: P](mdOffset: Int = 0): P[Unit] = P {
+  private def nextPartParser[_: P](indent: Int, mdOffset: Int = 0): P[Unit] = P {
     // used to terminate previous part, hence indent can be less
-    def mdCodeBlockPrefix = hspace.rep(max = getMdOffsetMax(mdOffset)) ~ mdCodeBlockFence
-    def anotherBeg = P(CharIn("@=") | (codePrefix ~ nl) | listPrefix | tableSep | tableDelim)
-    nl | mdCodeBlockPrefix | hspaces0 ~/ anotherBeg
+    nl | hspacesMinWithLen(0).flatMap { offset =>
+      def dedented = if (offset < indent) Pass else Fail
+      def mdCodeBlockPrefix = if (offset <= getMdOffsetMax(mdOffset)) mdCodeBlockFence else Fail
+      dedented |
+        CharIn("@=") |
+        (codePrefix ~ nl) | mdCodeBlockPrefix |
+        tableSep | tableDelim |
+        listPrefix ~ &(" ")
+    }
   }
 
-  private def textParser[_: P](mdOffset: Int = 0): P[Text] = P {
-    def end = P(nl ~/ nextPartParser(mdOffset))
+  private def textParser[_: P](indent: Int, mdOffset: Int = 0): P[Text] = P {
+    def end = P(nl ~/ nextPartParser(indent, mdOffset))
     def part: P[TextPart] = P(codeExprParser | linkParser | wordParser)
     def sep = P(!end ~ nl.? ~ hspaces0)
     hspaces0 ~ part.rep(1, sep = sep).map(x => Text(x))
   }
 
   // this is to be used at the start of a line
-  private def leadTextParser[_: P](mdOffset: Int = 0) =
-    !nextPartParser(mdOffset) ~ textParser(mdOffset)
+  private def leadTextParser[_: P](indent: Int, mdOffset: Int = 0) =
+    !nextPartParser(indent, mdOffset) ~ textParser(indent, mdOffset)
 
-  private def tagParser[_: P]: P[Tag] = P {
-    def label = P((nl ~ !nextPartParser()).? ~ hspaces0 ~ wordParser)
+  private def tagParser[_: P](indent: Int): P[Tag] = P {
+    def label = P((nl ~ !nextPartParser(indent)).? ~ hspaces0 ~ wordParser)
     // special case: @usecase takes a single code line, on the same line
     def labelInline = P(hspaces0 ~ (!nl ~ AnyChar).rep(1).!.map(Word))
     def desc = P {
-      (textParser().? ~ embeddedTermsParser())
+      (textParser(indent).? ~ embeddedTermsParser())
         .map { case (x, terms) => x.fold(terms)(_ +: terms) }
     }
     hspaces0 ~ ("@" ~ labelParser).!.flatMap { tag =>
@@ -162,7 +168,7 @@ object ScaladocParser {
   }
 
   private def listItemParser[_: P](indent: Int, mdOffset: Int) = P {
-    (textParser(mdOffset) ~ embeddedTermsParser(indent, mdOffset))
+    (textParser(indent, mdOffset) ~ embeddedTermsParser(indent, mdOffset))
       .map { case (x, terms) => ListItem(x, terms) }
   }
 
@@ -238,16 +244,16 @@ object ScaladocParser {
 
   private def termParser[_: P] = P {
     def leadingParser = // might not consume full line
-      tagParser
+      tagParser(0)
     def completeParser = // will consume full line
       listBlockParser() |
         mdCodeBlockParser() |
         codeBlockParser |
         headingParser |
         tableParser |
-        textParser() // keep at the end, this is the fallback
+        textParser(0) // keep at the end, this is the fallback
     (nl | Start) ~ (leadingParser | (completeParser ~ nlOrEndPeek)) |
-      textParser() // could be following an element leaving trailing text, e.g. tagParser
+      textParser(0) // could be following an element leaving trailing text, e.g. tagParser
   }
 
   private def embeddedTermsParser[_: P](indent: Int = 0, mdOffset: Int = 0): P[Seq[Term]] = P {
@@ -256,7 +262,7 @@ object ScaladocParser {
         mdCodeBlockParser(mdOffset) |
         codeBlockParser |
         tableParser |
-        leadTextParser(mdOffset) // keep at the end, this is the fallback
+        leadTextParser(indent, mdOffset) // keep at the end, this is the fallback
     (nl ~ completeParser ~ nlOrEndPeek).rep
   }
 


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
index ce225f0774..322863c8f0 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
@@ -609,6 +609,102 @@ class ScaladocParserSuite extends FunSuite {
     assertEquals(result, expected)
   }
 
+  test("lists 2: #3145") {
+    val list11 = "List11"
+    val list12 = "List12"
+    val list21 = "List21"
+    val list22 = "List22"
+    val list32 = "List32"
+
+    val result =
+      parseString(
+        s"""
+      /**
+        * Some text:
+        * 1. $list11
+        *  i.e. foo
+        *  i. $list21
+        * - $list12
+        *  i. $list22
+        *  i.e. bar
+        *   i. $list32
+        *  baz
+        */
+       """
+      )
+    val expected = Option(
+      Scaladoc(
+        Seq(
+          Paragraph(
+            Seq(
+              Text(Seq(Word("Some"), Word("text:"))),
+              ListBlock(
+                "1.",
+                Seq(
+                  ListItem(
+                    Text(
+                      Seq(
+                        Word(list11),
+                        Word("i.e."),
+                        Word("foo")
+                      )
+                    ),
+                    Seq(
+                      ListBlock(
+                        "i.",
+                        Seq(ListItem(Text(Seq(Word(list21)))))
+                      )
+                    )
+                  )
+                )
+              ),
+              ListBlock(
+                "-",
+                Seq(
+                  ListItem(
+                    Text(Seq(Word(list12))),
+                    Seq(
+                      ListBlock(
+                        "i.",
+                        Seq(
+                          ListItem(
+                            Text(
+                              Seq(
+                                Word(list22),
+                                Word("i.e."),
+                                Word("bar")
+                              )
+                            ),
+                            Seq(
+                              ListBlock(
+                                "i.",
+                                Seq(
+                                  ListItem(
+                                    Text(
+                                      Seq(
+                                        Word(list32)
+                                      )
+                                    )
+                                  )
+                                )
+                              ),
+                              Text(Seq(Word("baz")))
+                            )
+                          )
+                        )
+                      )
+                    )
+                  )
+                )
+              )
+            )
+          )
+        )
+      )
+    )
+    assertEquals(result, expected)
+  }
+
   test("lists 3") {
     val list11 = "List11"
     val list12 = "List12"