diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/TestModelBuilder.kt b/platform/platform-tests/testSrc/com/intellij/formatting/TestModelBuilder.kt new file mode 100644 index 000000000000..5ad23f223877 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/formatting/TestModelBuilder.kt @@ -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(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() + 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() + + fun extract(text: String): BlockAttributes { + val attributes: List = text.split(",").map(String::trim) + val alignment = getAlignment(attributes) + return BlockAttributes(alignment = alignment) + } + + private fun getAlignment(attributes: List): 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() + 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) +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/engine/FormatterEngineTests.kt b/platform/platform-tests/testSrc/com/intellij/formatting/engine/FormatterEngineTests.kt new file mode 100644 index 000000000000..aa085ec6efc8 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/formatting/engine/FormatterEngineTests.kt @@ -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()) +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModel/TestFormattingModel.kt b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModel/TestFormattingModel.kt new file mode 100644 index 000000000000..dc7e5838b60c --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModel/TestFormattingModel.kt @@ -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") + } +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModel/TestModelParser.kt b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModel/TestModelParser.kt new file mode 100644 index 000000000000..3d2d7fec0d17 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModel/TestModelParser.kt @@ -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() + 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 +} diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModelTests/TestFormattingModelBuilderTest.kt b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModelTests/TestFormattingModelBuilderTest.kt new file mode 100644 index 000000000000..8ace5f838b98 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModelTests/TestFormattingModelBuilderTest.kt @@ -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(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() + } + +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModelTests/TestModelParserTest.kt b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModelTests/TestModelParserTest.kt new file mode 100644 index 000000000000..117500698737 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/formatting/engine/testModelTests/TestModelParserTest.kt @@ -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 { + val root = getRoot(text) + val children = root.children + return children + } + +} \ No newline at end of file