diff --git a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt index 7e838612c..d19305639 100644 --- a/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt +++ b/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessor.kt @@ -28,6 +28,8 @@ import org.commonmark.parser.IncludeSourceSpans import org.commonmark.parser.Parser import org.commonmark.renderer.text.TextContentRenderer import org.intellij.lang.annotations.Language +import org.jetbrains.annotations.TestOnly +import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.jewel.foundation.ExperimentalJewelApi import org.jetbrains.jewel.markdown.InlineMarkdown import org.jetbrains.jewel.markdown.MarkdownBlock @@ -56,12 +58,13 @@ public class MarkdownProcessor( public constructor(vararg extensions: MarkdownProcessorExtension) : this(extensions.toList()) private val commonMarkParser = Parser.builder() - .extensions(extensions.map { it.parserExtension }) - .also { + .let { builder -> + builder.extensions(extensions.map(MarkdownProcessorExtension::parserExtension)) if (optimizeEdits) { - it.includeSourceSpans(IncludeSourceSpans.BLOCKS) + builder.includeSourceSpans(IncludeSourceSpans.BLOCKS) } - }.build() + builder.build() + } private val textContentRenderer = TextContentRenderer.builder() @@ -72,6 +75,9 @@ public class MarkdownProcessor( private var currentState = State(emptyList(), emptyList(), emptyList()) + @TestOnly + internal fun getCurrentIndexesInTest() = currentState.indexes + /** * Parses a Markdown document, translating from CommonMark 0.31.2 * to a list of [MarkdownBlock]. Inline Markdown in leaf nodes @@ -101,11 +107,17 @@ public class MarkdownProcessor( * @see DefaultInlineMarkdownRenderer */ public fun processMarkdownDocument(@Language("Markdown") rawMarkdown: String): List { - if (!optimizeEdits) { - return textToBlocks(rawMarkdown).mapNotNull { child -> - child.tryProcessMarkdownBlock() - } + return if (!optimizeEdits) { + textToBlocks(rawMarkdown) + } else { + processWithQuickEdits(rawMarkdown) + }.mapNotNull { child -> + child.tryProcessMarkdownBlock() } + } + + @VisibleForTesting + internal fun processWithQuickEdits(@Language("Markdown") rawMarkdown: String): List { val (previousLines, previousBlocks, previousIndexes) = currentState val newLines = rawMarkdown.lines() val nLinesDelta = newLines.size - previousLines.size @@ -143,6 +155,10 @@ public class MarkdownProcessor( currLastBlock = i currLastLine = begin } + if (firstLine > lastLine + nLinesDelta) { + // no change + return previousBlocks + } val updatedText = newLines.subList(firstLine, lastLine + nLinesDelta).joinToString("\n", postfix = "\n") val updatedBlocks: List = textToBlocks(updatedText) val updatedIndexes = @@ -163,12 +179,9 @@ public class MarkdownProcessor( updatedBlocks + previousBlocks.subList(lastBlock, previousBlocks.size) ) - val result = newBlocks.mapNotNull { child -> - child.tryProcessMarkdownBlock() - } val newIndexes = previousIndexes.subList(0, firstBlock) + updatedIndexes + suffixIndexes currentState = State(newLines, newBlocks, newIndexes) - return result + return newBlocks } private fun textToBlocks(strings: String): List { diff --git a/markdown/core/src/test/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessorTest.kt b/markdown/core/src/test/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessorTest.kt index c3e80fe15..214c6b168 100644 --- a/markdown/core/src/test/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessorTest.kt +++ b/markdown/core/src/test/kotlin/org/jetbrains/jewel/markdown/processing/MarkdownProcessorTest.kt @@ -1,55 +1,641 @@ package org.jetbrains.jewel.markdown.processing -import org.jetbrains.jewel.markdown.BlockWithInlineMarkdown -import org.jetbrains.jewel.markdown.MarkdownBlock +import org.commonmark.node.Block +import org.commonmark.node.Document +import org.commonmark.node.Node +import org.commonmark.parser.IncludeSourceSpans +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.intellij.lang.annotations.Language import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertSame import org.junit.Test +private val rawMarkdown = """ + Paragraph 0 + # Header 1 + Paragraph 2 + * list item 3-1 + * list item 3-2 + * list item 3-3 + ## Header 4 + Paragraph 5 + continue paragraph 5 + + + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + + Paragraph 8 +""".trimIndent() + class MarkdownProcessorTest { + private val htmlRenderer = HtmlRenderer.builder().build() + + @Test + fun `first blocks stay the same`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits( + """ + Paragraph 0 + # Header 1 + Paragraph 2 + + * list item 3-1 + * list item 3-2 + """.trimIndent(), + ) + assertSame(firstRun[0], secondRun[0]) + assertSame(firstRun[1], secondRun[1]) + assertNotSame(firstRun[2], secondRun[2]) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
+ + """.trimIndent(), + secondRun, + ) + } + @Test - fun `test my processor`() { - val pp = MarkdownProcessor() - pp.processMarkdownDocument( - """ - |Paragraph 1 - | - |Paragraph 2 - | - |Second paragraph - |not very important - | - |* m1 - |* m2 - """.trimMargin(), - ) - val secondProcess = pp.processMarkdownDocument( - """ - |Paragraph 1 - | - |Paragraph 2 - | - |Not a paragraph - |not very important - | - |* m1 - |* m2 - """.trimMargin(), - ) - // TODO: update after changing the underlying model, to check the first elements are the same - assertEquals("Paragraph 1", (secondProcess[0] as BlockWithInlineMarkdown).inlineContent.content) - assertEquals("Paragraph 2", (secondProcess[1] as BlockWithInlineMarkdown).inlineContent.content) - assertEquals( - "Not a paragraph not very important", - (secondProcess[2] as BlockWithInlineMarkdown).inlineContent.content, - ) - assertArrayEquals( - arrayOf( - "Paragraph(inlineContent=InlineMarkdown(content=m1))", - "Paragraph(inlineContent=InlineMarkdown(content=m2))", - ), - (secondProcess[3] as MarkdownBlock.ListBlock).items.flatMap { it.content.map(MarkdownBlock::toString) }.toTypedArray(), + fun `first block edited`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits( + """ + Paragraph *CHANGE* + # Header 1 + Paragraph 2 + * list item 3-1 + * list item 3-2 + * list item 3-3 + ## Header 4 + Paragraph 5 + continue paragraph 5 + + + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + + Paragraph 8 + """.trimIndent(), + ) + assertHtmlEquals( + """ +

Paragraph CHANGE

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+

Header 4

+

Paragraph 5 + continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph 8

+ + """.trimIndent(), + secondRun, ) + assertNotSame(firstRun[0], secondRun[0]) + assertNotSame(firstRun[1], secondRun[1]) + assertSame(firstRun[2], secondRun[2]) } + + @Test + fun `last block edited`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits( + """ + Paragraph 0 + # Header 1 + Paragraph 2 + * list item 3-1 + * list item 3-2 + * list item 3-3 + ## Header 4 + Paragraph 5 + continue paragraph 5 + + + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + + Paragraph *CHANGE* + """.trimIndent(), + ) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+

Header 4

+

Paragraph 5 + continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph CHANGE

+ + """.trimIndent(), + secondRun, + ) + assertSame(firstRun[5], secondRun[5]) + assertSame(firstRun[6], secondRun[6]) + assertNotSame(firstRun[7], secondRun[7]) + assertNotSame(firstRun[8], secondRun[8]) + } + + @Test + fun `middle block edited`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits( + """ + Paragraph 0 + # Header 1 + Paragraph 2 + * list item 3-1 + * list item *CHANGE* + * list item 3-3 + ## Header 4 + Paragraph 5 + continue paragraph 5 + + + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + + Paragraph 8 + """.trimIndent(), + ) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item CHANGE
  • +
  • list item 3-3
  • +
+

Header 4

+

Paragraph 5 + continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph 8

+ + """.trimIndent(), + secondRun, + ) + assertSame(firstRun[0], secondRun[0]) + assertSame(firstRun[1], secondRun[1]) + assertNotSame(firstRun[2], secondRun[2]) + assertNotSame(firstRun[3], secondRun[3]) + assertNotSame(firstRun[4], secondRun[4]) + assertSame(firstRun[5], secondRun[5]) + } + + @Test + fun `blocks merged`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits( + """ + Paragraph 0 + # Header 1 + Paragraph 2 + * list item 3-1 + * list item 3-2 + * list item 3-3 + ## Header 4 + Paragraph 5 + continue paragraph 5 + + + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + Paragraph 8 + """.trimIndent(), + ) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+

Header 4

+

Paragraph 5 + continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7 + Paragraph 8

+ + """.trimIndent(), + secondRun, + ) + assertSame(firstRun[5], secondRun[5]) + assertSame(firstRun[6], secondRun[6]) + assertNotSame(firstRun[7], secondRun[7]) + } + + @Test + fun `blocks split`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits( + """ + Paragraph 0 + # Header 1 + Paragraph 2 + * list item 3-1 + * list item 3-2 + * list item 3-3 + ## Header 4 + Paragraph 5 + + continue paragraph 5 + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + + Paragraph 8 + """.trimIndent(), + ) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+

Header 4

+

Paragraph 5

+

continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph 8

+ + """.trimIndent(), + secondRun, + ) + assertSame(firstRun[3], secondRun[3]) + assertNotSame(firstRun[4], secondRun[4]) + assertNotSame(firstRun[5], secondRun[5]) + assertSame(firstRun[7], secondRun[8]) + } + + @Test + fun `blocks deleted`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits( + """ + Paragraph 0 + # Header 1 + Paragraph 2 + * list item 3-1 + * list item 3-2 + * list item 3-3 + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + + Paragraph 8 + """.trimIndent(), + ) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph 8

+ + """.trimIndent(), + secondRun, + ) + assertSame(firstRun[2], secondRun[2]) + assertNotSame(firstRun[3], secondRun[3]) + assertNotSame(firstRun[4], secondRun[4]) + assertNotSame(firstRun[6], secondRun[4]) + assertSame(firstRun[7], secondRun[5]) + } + + @Test + fun `blocks added`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondDocument = """ + Paragraph 0 + # Header 1 + Paragraph 2 + * list item 3-1 + * list item 3-2 + * list item 3-3 + ## Header 4 + + + *CHANGE* + + Paragraph 5 + continue paragraph 5 + + + ``` + line 6-1 + line 6-2 + ``` + Paragraph 7 + + Paragraph 8 + """.trimIndent() + val secondRun = processor.processWithQuickEdits( + secondDocument, + ) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+

Header 4

+

CHANGE

+

Paragraph 5 + continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph 8

+ + """.trimIndent(), + secondRun, + ) + assertSame(firstRun[2], secondRun[2]) + assertSame(firstRun[3], secondRun[3]) + assertNotSame(firstRun[4], secondRun[4]) + assertNotSame(firstRun[5], secondRun[6]) + assertSame(firstRun[6], secondRun[7]) + assertIndexesEqual(secondDocument, processor.getCurrentIndexesInTest()) + } + + @Test + fun `no changes`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits(rawMarkdown) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+

Header 4

+

Paragraph 5 + continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph 8

+ + """.trimIndent(), + secondRun, + ) + assertSame(firstRun[0], secondRun[0]) + } + + @Test + fun `empty line added`() { + val processor = MarkdownProcessor() + val firstRun = processor.processWithQuickEdits(rawMarkdown) + val secondRun = processor.processWithQuickEdits("\n" + rawMarkdown) + assertHtmlEquals( + """ +

Paragraph 0

+

Header 1

+

Paragraph 2

+
    +
  • list item 3-1
  • +
  • list item 3-2
  • +
  • list item 3-3
  • +
+

Header 4

+

Paragraph 5 + continue paragraph 5

+
line 6-1
+            line 6-2
+            
+

Paragraph 7

+

Paragraph 8

+ + """.trimIndent(), + secondRun, + ) + assertNotSame(firstRun[0], secondRun[0]) + assertSame(firstRun[1], secondRun[1]) + } + + @Test + fun `chained changes`() { + val processor = MarkdownProcessor() + processor.processWithQuickEdits( + """ + # Header 0 + # Header 1 + # Header 2 + + + + + # Header 3 + # Header 4 + # Header 5 + # Header 6 + # Header 7 + # Header 8 + # Header 9 + + """.trimIndent(), + ) + processor.processWithQuickEdits( + """ + # Header 0 + # Header 1 + some paragraph + + + + + # Header 2 + # Header 3 + # Header 7 + # Header 8 + # Header 9 + + """.trimIndent(), + ) + val forthRun = processor.processWithQuickEdits( + """ + # Header 0 + # Header 1 + + + some paragraph + # Header 2 + # Header 7 + # Header 8 + # Header 9 + + """.trimIndent(), + ) + val fifthDocument = """ + # Header 0 + # Header 1 + + + some paragraph + # Header 2 + # Header 7 + + + - list item 1 + - list item 2 + # Header 8 + # Header 9 + + """.trimIndent() + val fifthRun = processor.processWithQuickEdits( + fifthDocument, + ) + + assertIndexesEqual(fifthDocument, processor.getCurrentIndexesInTest()) + + assertSame(forthRun[0], fifthRun[0]) + assertSame(forthRun[1], fifthRun[1]) + assertSame(forthRun[2], fifthRun[2]) + assertSame(forthRun[3], fifthRun[3]) + assertNotSame(forthRun[4], fifthRun[4]) + assertSame(forthRun[6], fifthRun[7]) + assertHtmlEquals( + """ +

Header 0

+

Header 1

+

some paragraph

+

Header 2

+

Header 7

+
    +
  • list item 1
  • +
  • list item 2
  • +
+

Header 8

+

Header 9

+ + """.trimIndent(), + fifthRun, + ) + } + + private fun assertHtmlEquals(@Language("html") text: String, actual: List) { + assertEquals(text, actual.joinToString("") { htmlRenderer.render(it) }) + } +} + +private fun Node.children(): List { + var child = firstChild + return buildList { + while (child != null) { + add(child) + child = child.next + } + } +} + +private fun assertIndexesEqual( + lastProcessedDocument: String, + currentIndexes: List>, +) { + val commonmarkDocument = + Parser.builder().includeSourceSpans(IncludeSourceSpans.BLOCKS).build() + .parse(lastProcessedDocument) as Document + val expected = + (commonmarkDocument).children().map { + it.sourceSpans.first().lineIndex to it.sourceSpans.last().lineIndex + }.toTypedArray() + assertArrayEquals( + expected, + currentIndexes.toTypedArray(), + ) }