Handle implicit and malformed code blocks by adding explicit markers

Summary:
A bunch of issues throw off the KDoc formatter when code blocks happen with three-backticks in the middle of the line, or also code blocks with no three-backticks.

This normalizes our handling of it, and adds ``` into code blocks when the lexer decided a token is a code block, but we did not see an explicit marker.

Reviewed By: cgrushko

Differential Revision: D20515472

fbshipit-source-id: a7dceeaccc249f7bceda488bb32991d1757e98ab
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt
index 3bb13a8..a4382cb 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt
@@ -86,8 +86,6 @@
         KDocTokens.TEXT -> {
           if (tokenText.isBlank()) {
             tokens.add(Token(WHITESPACE, " "))
-          } else if (tokenText == "```") {
-            tokens.add(Token(CODE_BLOCK_MARKER, tokenText))
           } else {
             val words = tokenText.trim().split(" +".toRegex())
             var first = true
@@ -102,6 +100,8 @@
               // END_KDOC properly. We want to recover in such cases
               if (word == "*/") {
                 tokens.add(Token(END_KDOC, word))
+              } else if (word == "```") {
+                tokens.add(Token(CODE_BLOCK_MARKER, word))
               } else {
                 tokens.add(Token(LITERAL, word))
                 tokens.add(Token(WHITESPACE, " "))
@@ -145,7 +145,7 @@
         TABLE_CLOSE_TAG -> output.writeTableClose(token)
         TAG -> output.writeTag(token)
         CODE -> output.writeCodeLine(token)
-        CODE_BLOCK_MARKER -> output.writeCodeBlockMarker(token)
+        CODE_BLOCK_MARKER -> output.writeExplicitCodeBlockMarker(token)
         BLANK_LINE -> output.requestBlankLine()
         WHITESPACE -> output.requestWhitespace()
         LITERAL -> output.writeLiteral(token)
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocWriter.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocWriter.kt
index d991dbf..8f51519 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocWriter.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocWriter.kt
@@ -26,6 +26,7 @@
 import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.NEWLINE
 import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.NONE
 import com.facebook.ktfmt.kdoc.KDocWriter.RequestedWhitespace.WHITESPACE
+import com.facebook.ktfmt.kdoc.Token.Type.CODE_BLOCK_MARKER
 import com.facebook.ktfmt.kdoc.Token.Type.HEADER_OPEN_TAG
 import com.facebook.ktfmt.kdoc.Token.Type.LIST_ITEM_OPEN_TAG
 import com.facebook.ktfmt.kdoc.Token.Type.PARAGRAPH_OPEN_TAG
@@ -86,12 +87,14 @@
   }
 
   fun writeEndJavadoc() {
+    requestCloseCodeBlockMarker()
     output.append("\n")
     appendSpaces(blockIndent + 1)
     output.append("*/")
   }
 
   fun writeListItemOpen(token: Token) {
+    requestCloseCodeBlockMarker()
     requestNewline()
 
     if (continuingListItemOfInnermostList) {
@@ -153,25 +156,41 @@
   }
 
   fun writeCodeLine(token: Token) {
-    if (!inCodeBlock) {
-      requestNewline()
-    }
+    requestOpenCodeBlockMarker()
     requestNewline()
     if (token.value.isNotEmpty()) {
       writeToken(token)
     }
   }
 
-  fun writeCodeBlockMarker(token: Token) {
-    if (!inCodeBlock) {
-      requestNewline()
+  /** Adds a code block marker if we are not in a code block currently */
+  private fun requestCloseCodeBlockMarker() {
+    if (inCodeBlock) {
+      this.requestedWhitespace = NEWLINE
+      writeExplicitCodeBlockMarker(Token(CODE_BLOCK_MARKER, "```"))
+      inCodeBlock = false
     }
+  }
+
+  /** Adds a code block marker if we are in a code block currently */
+  private fun requestOpenCodeBlockMarker() {
+    if (!inCodeBlock) {
+      this.requestedWhitespace = NEWLINE
+      writeExplicitCodeBlockMarker(Token(CODE_BLOCK_MARKER, "```"))
+      inCodeBlock = true
+    }
+  }
+
+  fun writeExplicitCodeBlockMarker(token: Token) {
+    requestNewline()
     writeToken(token)
     requestNewline()
     inCodeBlock = !inCodeBlock
   }
 
   fun writeLiteral(token: Token) {
+    requestCloseCodeBlockMarker()
+
     writeToken(token)
   }
 
@@ -301,7 +320,7 @@
   }
 
   private fun innerIndent(): Int {
-    return continuingListItemCount.value() * 4 + continuingListCount.value() * 2
+    return 0
   }
 
   // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end).
diff --git a/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt b/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt
index 9fecdcf..5181946 100644
--- a/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt
@@ -847,6 +847,81 @@
       |""".trimMargin())
 
   @Test
+  fun `formatting kdoc lists with line wraps breaks and merges correctly`() {
+    val code =
+        """
+      |/**
+      | * Here are some fruit I like:
+      | * - Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana
+      | * - Apple Apple Apple Apple
+      | *   Apple Apple
+      | *
+      | * This is another paragraph
+      | */
+      |""".trimMargin()
+    val expected =
+        """
+      |/**
+      | * Here are some fruit I like:
+      | * - Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana
+      | * Banana Banana Banana Banana Banana
+      | * - Apple Apple Apple Apple Apple Apple
+      | *
+      | * This is another paragraph
+      | */
+      |""".trimMargin()
+    assertThatFormatting(code).isEqualTo(expected)
+  }
+
+  @Test
+  fun `too many spaces on list continuation mean it's a code block, so mark it accordingly`() {
+    val code =
+        """
+      |/**
+      | * Here are some fruit I like:
+      | * - Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana
+      | *     Banana Banana Banana Banana Banana
+      | */
+      |""".trimMargin()
+    val expected =
+        """
+      |/**
+      | * Here are some fruit I like:
+      | * - Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana
+      | * ```
+      | *     Banana Banana Banana Banana Banana
+      | * ```
+      | */
+      |""".trimMargin()
+    assertThatFormatting(code).isEqualTo(expected)
+  }
+
+  @Test
+  fun `add explicit code markers around indented code`() {
+    val code =
+        """
+      |/**
+      | * This is a code example:
+      | *
+      | *     this_is_code()
+      | *
+      | * This is not code again
+      | */
+      |""".trimMargin()
+    val expected =
+        """
+      |/**
+      | * This is a code example:
+      | * ```
+      | *     this_is_code()
+      | * ```
+      | * This is not code again
+      | */
+      |""".trimMargin()
+    assertThatFormatting(code).isEqualTo(expected)
+  }
+
+  @Test
   fun `formatting kdoc preserves lists of asterisks`() =
       assertFormatted(
           """
@@ -2315,29 +2390,58 @@
       |""".trimMargin())
 
   @Test
-  fun `deal with code blocks starting mid-line`() =
-      assertFormatted(
-          """
+  fun `handle stray code markers in lines and produce stable output`() {
+    val code =
+        """
       |/**
-      | * Look code: ``` aaa
+      | * Look! code: ``` aaa
       | * fun f() = Unit
       | * foo
       | * ```
       | */
       |class MyClass {}
-      |""".trimMargin())
+      |""".trimMargin()
+    assertFormatted(format(code))
+  }
 
   @Test
-  fun `deal with code blocks starting and ending mid-line`() =
-      assertFormatted(
-          """
+  fun `code block with triple backtick`() {
+    val code =
+        """
       |/**
-      | * Look code: ``` aaa
+      | * Look! code:
+      | * ```
+      | * aaa ``` wow
+      | * ```
+      | */
+      |class MyClass {}
+      |""".trimMargin()
+    val expected =
+        """
+      |/**
+      | * Look! code:
+      | * ```
+      | * aaa ``` wow
+      | * ```
+      | */
+      |class MyClass {}
+      |""".trimMargin()
+    assertThatFormatting(code).isEqualTo(expected)
+  }
+
+  @Test
+  fun `when code closer in mid of line, produce stable output`() {
+    val code =
+        """
+      |/**
+      | * Look! code: ``` aaa
       | * fun f() = Unit
       | * foo ``` wow
       | */
       |class MyClass {}
-      |""".trimMargin())
+      |""".trimMargin()
+    assertFormatted(format(code))
+  }
 
   @Test
   fun `handle KDoc with link reference`() =
@@ -2419,12 +2523,13 @@
   }
 
   @Test
-  fun `do not crash because of malformed KDocs`() =
-      assertFormatted(
-          """
+  fun `do not crash because of malformed KDocs and produce stable output`() {
+    val code = """
       |/** Surprise ``` */
       |class MyClass {}
-      |""".trimMargin())
+      |""".trimMargin()
+    assertFormatted(format(code))
+  }
 
   @Test
   fun `Respect spacing of text after link`() =