[formatter tests] new way of specifying test formatting model

This commit is contained in:
Yaroslav Lepenkin
2016-11-24 17:13:28 +03:00
parent d473435ef3
commit dad861243c
6 changed files with 620 additions and 0 deletions
@@ -0,0 +1,110 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting
import com.intellij.formatting.*
import com.intellij.formatting.engine.testModel.TestBlock
import com.intellij.openapi.util.TextRange
class BlockAttributes(val alignment: Alignment? = null, val wrap: Wrap? = null, val indent: Indent? = null, val spacing: Spacing? = null)
class CompositeTestBlock(startOffset: Int,
attributes: BlockAttributes,
private val children: List<TestBlockBase>) : TestBlockBase(startOffset, attributes)
{
override fun getSubBlocks() = children
override fun isLeaf() = false
override val endOffset = children.last().endOffset
}
class LeafTestBlock(startOffset: Int, attributes: BlockAttributes, val text: String) : TestBlockBase(startOffset, attributes) {
override fun getSubBlocks() = emptyList<Block>()
override fun isLeaf() = true
override val endOffset = startOffset + text.length
}
abstract class TestBlockBase(val startOffset: Int,
private val attributes: BlockAttributes) : Block {
override fun getAlignment() = attributes.alignment
override fun getTextRange() = TextRange(startOffset, endOffset)
override fun getWrap() = attributes.wrap
override fun getIndent() = attributes.indent
override fun getSpacing(child1: Block?, child2: Block) = attributes.spacing
override fun getChildAttributes(newChildIndex: Int): ChildAttributes = ChildAttributes.DELEGATE_TO_NEXT_CHILD
override fun isIncomplete() = false
abstract val endOffset: Int
}
class AttributesProvider {
private val alignments = hashMapOf<Int, Alignment>()
fun extract(text: String): BlockAttributes {
val attributes: List<String> = text.split(",").map(String::trim)
val alignment = getAlignment(attributes)
return BlockAttributes(alignment = alignment)
}
private fun getAlignment(attributes: List<String>): Alignment? {
val index = attributes.find { it.startsWith("a") }?.substring(1)?.toInt()
if (index == null || index < 0) return null
var alignment = alignments[index]
if (alignment != null) {
return alignment
}
alignment = AlignmentImpl(true, Alignment.Anchor.LEFT)
alignments[index] = alignment
return alignment
}
}
fun TestBlock.Composite.toFormattingBlock(startOffset: Int): CompositeTestBlock {
val childBlocks = mutableListOf<TestBlockBase>()
val attributesProvider = AttributesProvider()
var previousStartOffset = startOffset
children.forEach {
val block = when (it) {
is TestBlock.Composite -> {
val composite = it.toFormattingBlock(previousStartOffset)
previousStartOffset = composite.endOffset
composite
}
is TestBlock.Leaf -> {
val attributes = attributesProvider.extract(it.attributes)
val leaf = LeafTestBlock(previousStartOffset, attributes, it.text)
previousStartOffset = leaf.endOffset
leaf
}
is TestBlock.Space -> {
previousStartOffset += it.text.length
null
}
}
block?.let { childBlocks.add(it) }
}
val attributes = attributesProvider.extract(attributes)
return CompositeTestBlock(startOffset, attributes, childBlocks)
}
@@ -0,0 +1,62 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting.engine
import com.intellij.formatting.FormatTextRanges
import com.intellij.formatting.FormatterImpl
import com.intellij.formatting.engine.testModel.TestFormattingModel
import com.intellij.formatting.engine.testModel.getRoot
import com.intellij.formatting.toFormattingBlock
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.util.TextRange
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.testFramework.LightPlatformTestCase
import junit.framework.TestCase
import org.junit.Test
class FormatterEngineTests : LightPlatformTestCase() {
@Test
fun `test simple alignment`() {
doTest(
"""
[a0]fooooo [a1]foo
[a0]go [a1]boo
""",
"""
fooooo foo
go boo
""")
}
}
fun doTest(before: String, expectedText: String) {
val root = getRoot(before.trimStart())
val rootFormattingBlock = root.toFormattingBlock(0)
val beforeText = root.text
val document = EditorFactory.getInstance().createDocument(beforeText)
val model = TestFormattingModel(rootFormattingBlock, document)
val settings = CodeStyleSettings()
val textRanges = FormatTextRanges(TextRange(0, document.textLength - 1), true)
val formatter = FormatterImpl()
formatter.format(model, settings, settings.indentOptions, textRanges)
TestCase.assertEquals(document.text, expectedText.trimStart())
}
@@ -0,0 +1,69 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting.engine.testModel
import com.intellij.formatting.Block
import com.intellij.formatting.FormattingDocumentModel
import com.intellij.formatting.FormattingModel
import com.intellij.lang.ASTNode
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.util.TextRange
import com.intellij.util.text.CharArrayUtil
class TestFormattingModel(private val rootBlock: Block,
private val document: Document) : FormattingModel, FormattingDocumentModel {
override fun getRootBlock() = rootBlock
override fun getDocumentModel() = this
override fun replaceWhiteSpace(textRange: TextRange, whiteSpace: String): TextRange {
WriteCommandAction.runWriteCommandAction(null) {
document.replaceString(textRange.startOffset, textRange.endOffset, whiteSpace)
}
return TextRange(textRange.startOffset, textRange.startOffset + whiteSpace.length)
}
override fun shiftIndentInsideRange(node: ASTNode, range: TextRange, indent: Int): TextRange {
throw UnsupportedOperationException()
}
override fun commitChanges() {
}
override fun getLineNumber(offset: Int) = document.getLineNumber(offset)
override fun getLineStartOffset(line: Int) = document.getLineStartOffset(line)
override fun getText(textRange: TextRange) = document.getText(textRange)
override fun getTextLength() = document.textLength
override fun getDocument() = document
override fun containsWhiteSpaceSymbolsOnly(startOffset: Int, endOffset: Int): Boolean {
return CharArrayUtil.containsOnlyWhiteSpaces(document.getText(TextRange(startOffset, endOffset)))
}
override fun adjustWhiteSpaceIfNecessary(whiteSpaceText: CharSequence,
startOffset: Int,
endOffset: Int,
nodeAfter: ASTNode?,
changedViaPsi: Boolean): CharSequence {
throw UnsupportedOperationException("not implemented")
}
}
@@ -0,0 +1,166 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting.engine.testModel
enum class Label {
SPACE,
LPARENTH,
RPARENTH,
ATTR_START,
ATTR_END,
TEXT
}
private class TestBlockTreeBuilder {
private val stack = mutableListOf(TestBlock.Composite(""))
private var recentSeenAttributes: String? = null
fun attributes(attributes: String) {
recentSeenAttributes = attributes
}
fun compositeBlockStart() {
val lastChild = TestBlock.Composite(recentSeenAttributes!!)
recentSeenAttributes = null
top().addChild(lastChild)
stack.add(lastChild)
}
fun space(text: String) {
if (recentSeenAttributes != null) leafBlock("")
val space = TestBlock.Space(text)
top().addChild(space)
}
fun leafBlock(text: String) {
top().addChild(TestBlock.Leaf(recentSeenAttributes!!, text))
recentSeenAttributes = null
}
private fun top() = stack.last()
fun compositeBlockEnd() {
if (recentSeenAttributes != null) leafBlock("")
stack.removeAt(stack.lastIndex)
}
fun rootBlock(): TestBlock.Composite {
if (recentSeenAttributes != null) leafBlock("")
return stack.first()
}
}
class TestModelParser(private val text: String) {
private val builder = TestBlockTreeBuilder()
private fun getLabel(c: Char): Label {
return when (c) {
' ', '\n' -> Label.SPACE
'[' -> Label.ATTR_START
']' -> Label.ATTR_END
'(' -> Label.LPARENTH
')' -> Label.RPARENTH
else -> Label.TEXT
}
}
fun parse(): TestBlock {
var currentIndex = 0
while (currentIndex < text.length) {
val char = text[currentIndex]
val label = getLabel(char)
currentIndex = when (label) {
Label.ATTR_START -> handleAttributes(currentIndex)
Label.ATTR_END -> currentIndex + 1
Label.TEXT -> handleText(currentIndex)
Label.LPARENTH -> handleLParenth(currentIndex)
Label.RPARENTH -> handleRParenth(currentIndex)
Label.SPACE -> handleSpace(currentIndex)
}
}
return builder.rootBlock()
}
private fun handleSpace(currentIndex: Int): Int {
val spaceEnd = text.firstIndexOfAnyLabel(currentIndex, Label.ATTR_START)
builder.space(text.substring(currentIndex, spaceEnd))
return spaceEnd
}
private fun handleLParenth(currentIndex: Int): Int {
builder.compositeBlockStart()
return currentIndex + 1
}
private fun handleRParenth(currentIndex: Int): Int {
builder.compositeBlockEnd()
return currentIndex + 1
}
private fun handleText(currentIndex: Int): Int {
val textEnd = text.firstIndexOfAnyLabel(currentIndex, Label.SPACE, Label.ATTR_START, Label.RPARENTH)
val blockText = text.substring(currentIndex, textEnd)
builder.leafBlock(blockText)
return textEnd
}
private fun handleAttributes(currentIndex: Int): Int {
val attrsEnd = text.firstIndexOfLabel(currentIndex, Label.ATTR_END)
builder.attributes(text.substring(currentIndex + 1, attrsEnd))
return attrsEnd
}
private fun String.firstIndexOfLabel(start: Int, label: Label): Int {
val indexOfLabel = substring(start).indexOfFirst { getLabel(it) == label }
return if (indexOfLabel > 0) start + indexOfLabel else length
}
private fun String.firstIndexOfAnyLabel(start: Int, vararg labels: Label): Int {
val allLabels = labels.toHashSet()
val indexOfLabel = substring(start).indexOfFirst { allLabels.contains(getLabel(it)) }
return if (indexOfLabel > 0) start + indexOfLabel else length
}
}
sealed class TestBlock {
abstract val text: String
class Space(override val text: String) : TestBlock()
class Leaf(val attributes: String, override val text: String) : TestBlock()
class Composite(val attributes: String) : TestBlock() {
val children = mutableListOf<TestBlock>()
fun addChild(block: TestBlock) = children.add(block)
override val text: String
get() = children.joinToString("", transform = { it.text })
}
}
fun getRoot(text: String): TestBlock.Composite {
val parser = TestModelParser(text)
return parser.parse() as TestBlock.Composite
}
@@ -0,0 +1,85 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting.engine.testModelTests
import com.intellij.formatting.Block
import com.intellij.formatting.CompositeTestBlock
import com.intellij.formatting.engine.testModel.getRoot
import com.intellij.formatting.toFormattingBlock
import com.intellij.openapi.util.TextRange
import com.intellij.testFramework.Assertions
import com.intellij.testFramework.Assertions.assertThat
import com.intellij.util.containers.TreeTraversal
import org.junit.Test
class TestFormattingModelBuilderTest {
@Test
fun `test model with correct ranges is built`() {
val root = getRoot(
"""[a0]foo [a1]goo
[]hoo
[a1]([a2]qoo [a2]woo [a3]([]roo []too))"""
)
val rootFormattingBlock = root.toFormattingBlock(0)
val subBlocks = rootFormattingBlock.subBlocks
Assertions.assertThat(subBlocks).hasSize(4)
Assertions.assertThat(subBlocks.last().subBlocks).hasSize(3)
Assertions.assertThat(subBlocks.last().subBlocks.last().subBlocks).hasSize(2)
assertLeafRanges(rootFormattingBlock, 0..3, 12..15, 30..33, 34..37, 50..53, 60..63, 64..67)
}
private fun assertLeafRanges(rootFormattingBlock: CompositeTestBlock, vararg ranges: IntRange) {
val textRanges = ranges.map { TextRange(it.start, it.endInclusive) }
val leafs = TreeTraversal.LEAVES_DFS.createIterator<Block>(rootFormattingBlock.subBlocks, { it.subBlocks }).toList()
Assertions.assertThat(textRanges).isEqualTo(leafs.map { it.textRange })
}
@Test
fun `test alignment parsing`() {
val root = getRoot("[a0]foo [a1]goo [a0]hoo")
val rootBlock = root.toFormattingBlock(0)
assertLeafRanges(rootBlock, 0..3, 4..7, 8..11)
val children = rootBlock.subBlocks
assertThat(children[0].alignment).isNotNull()
assertThat(children[1].alignment).isNotNull()
assertThat(children[0].alignment).isEqualTo(children[2].alignment)
}
@Test
fun `test empty block`() {
val root = getRoot(
"""[a0]fooooo [a1]
[a0]go [a1]boo
""").toFormattingBlock(0)
val children = root.subBlocks
assertThat(children).hasSize(4)
assertThat(children[0].alignment).isEqualTo(children[2].alignment)
assertThat(children[1].alignment).isEqualTo(children[3].alignment)
assertThat(children[1].textRange.isEmpty).isTrue()
}
}
@@ -0,0 +1,128 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.formatting.engine.testModelTests
import com.intellij.formatting.engine.testModel.TestBlock
import com.intellij.formatting.engine.testModel.getRoot
import com.intellij.testFramework.Assertions.assertThat
import org.junit.Test
class TestModelParserTest {
@Test
fun `test simple block parsing`() {
val children = getBlocks("[a1]block")
assertThat(children).hasSize(1)
val child = children[0]
assertLeaf(child, "a1", "block")
}
@Test
fun `test block with no attributes`() {
val children = getBlocks("[]block")
assertThat(children).hasSize(1)
val child = children[0]
assertLeaf(child, "", "block")
}
@Test
fun `test empty block with no attributes`() {
val children = getBlocks("[]")
assertThat(children).hasSize(1)
val child = children[0]
assertLeaf(child, "", "")
}
@Test
fun `test multiple blocks`() {
val children = getRoot("[a1]foo \n\n\n \n\n\n [a2]goo").children
assertThat(children).hasSize(3)
assertLeaf(children[0], "a1", "foo")
assertSpace(children[1], " \n\n\n \n\n\n ")
assertLeaf(children[2], "a2", "goo")
}
@Test
fun `test simple block with space`() {
val children = getBlocks("[a1]foo \n ")
assertThat(children).hasSize(2)
assertLeaf(children[0], "a1", "foo")
assertSpace(children[1], " \n ")
}
@Test
fun `test composite block`() {
var children = getBlocks("[]([]foo \n \n \n [a3]goo)")
assertThat(children).hasSize(1)
children = (children[0] as TestBlock.Composite).children
assertThat(children).hasSize(3)
assertLeaf(children[0], "", "foo")
assertSpace(children[1], " \n \n \n ")
assertLeaf(children[2], "a3", "goo")
}
@Test
fun `test multiple composite blocks`() {
var children = getBlocks("[]([]([]foo \n\n\n []goo) \n\n\n []([]woo))")
assertThat(children).hasSize(1)
children = (children[0] as TestBlock.Composite).children
assertThat(children).hasSize(3)
assertThat((children[0] as TestBlock.Composite).children).hasSize(3)
assertThat((children[2] as TestBlock.Composite).children).hasSize(1)
}
@Test
fun `test nested composite blocks`() {
var children = getBlocks("[a0]([a1]([a2]([a3]([a4]foo))))")
for (i in 0..3) {
assertThat(children).hasSize(1)
val child = children[0] as TestBlock.Composite
assertThat(child.attributes).isEqualTo("a$i")
children = child.children
}
val leaf = children[0] as TestBlock.Leaf
assertThat(leaf.attributes).isEqualTo("a4")
assertThat(leaf.text).isEqualTo("foo")
}
private fun assertLeaf(testBlock: TestBlock, attrs: String, text: String) {
val leaf = testBlock as TestBlock.Leaf
assertThat(leaf.text).isEqualTo(text)
assertThat(leaf.attributes).isEqualTo(attrs)
}
private fun assertSpace(testBlock: TestBlock, space: String) {
val leaf = testBlock as TestBlock.Space
assertThat(leaf.text).isEqualTo(space)
}
private fun getBlocks(text: String): MutableList<TestBlock> {
val root = getRoot(text)
val children = root.children
return children
}
}