From 7a3f6f53122b5faabddcbef93683561cb13b0a69 Mon Sep 17 00:00:00 2001 From: Dmitry Gridin Date: Wed, 18 Mar 2020 12:37:28 +0000 Subject: [PATCH] Formatter: fix continuation indent in call chain #KT-37545 Fixed --- .../idea/formatter/KotlinCommonBlock.kt | 39 ++-- .../formatter/callChain/KT37545.after.kt | 175 ++++++++++++++++++ idea/testData/formatter/callChain/KT37545.kt | 175 ++++++++++++++++++ .../formatter/FormatterTestGenerated.java | 5 + 4 files changed, 380 insertions(+), 14 deletions(-) create mode 100644 idea/testData/formatter/callChain/KT37545.after.kt create mode 100644 idea/testData/formatter/callChain/KT37545.kt diff --git a/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt b/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt index 069a6c823d7..438ba9c78c5 100644 --- a/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt +++ b/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt @@ -131,13 +131,16 @@ abstract class KotlinCommonBlock( val block = nodeSubBlocks.first() val wrap = createWrapForQualifierExpression(node) - val indent = createIndentForQualifierExpression(node) - val newBlock = block.processBlock(indent, wrap) + val enforceIndentToChildren = anyCallInCallChainIsWrapped(node) + val indent = createIndentForQualifierExpression(enforceIndentToChildren) + val newBlock = block.processBlock(wrap, enforceIndentToChildren) return nodeSubBlocks.replaceBlock(newBlock, 0).splitAtIndex(operationBlockIndex, indent, wrap) } - private fun ASTBlock.processBlock(indent: Indent, wrap: Wrap?): ASTBlock { + private fun ASTBlock.processBlock(wrap: Wrap?, enforceIndentToChildren: Boolean): ASTBlock { val currentNode = requireNode() + val enforceIndent = enforceIndentToChildren && anyCallInCallChainIsWrapped(currentNode) + val indent = createIndentForQualifierExpression(enforceIndent) @Suppress("UNCHECKED_CAST") val subBlocks = subBlocks as List @@ -150,8 +153,7 @@ abstract class KotlinCommonBlock( else null - val newBlock = subBlocks.elementAt(index).processBlock(indent, resultWrap) - + val newBlock = subBlocks.elementAt(index).processBlock(resultWrap, enforceIndent) return subBlocks.replaceBlock(newBlock, index).let { val operationIndex = subBlocks.indexOfBlockWithType(QUALIFIED_OPERATION) if (operationIndex != -1) @@ -178,11 +180,9 @@ abstract class KotlinCommonBlock( else null - private fun createIndentForQualifierExpression(node: ASTNode): Indent { - // enforce indent to children when there's a line break before the dot in any call in the chain (meaning that - // the call chain following that call is indented) - val enforceIndentToChildren = anyCallInCallChainIsWrapped(node) - + // enforce indent to children when there's a line break before the dot in any call in the chain (meaning that + // the call chain following that call is indented) + private fun createIndentForQualifierExpression(enforceIndentToChildren: Boolean): Indent { val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) { if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST } else { @@ -825,11 +825,22 @@ private val ASTNode.isCall: Boolean get() = unwrapQualifier()?.lastChildNode?.elementType == CALL_EXPRESSION private fun anyCallInCallChainIsWrapped(node: ASTNode): Boolean { - val sequentialCalls = generateSequence(node) { it.firstChildNode }.takeWhile { it.isCall } + val sequentialNodes = generateSequence(node) { + when (it.elementType) { + POSTFIX_EXPRESSION, in QUALIFIED_EXPRESSIONS -> it.firstChildNode + PARENTHESIZED -> getSiblingWithoutWhitespaceAndComments(it.firstChildNode, true) + else -> null + } + } - return sequentialCalls.any { call -> - val qualifier = call.findChildByType(QUALIFIED_OPERATION) - qualifier != null && hasLineBreakBefore(qualifier) + return sequentialNodes.any { + val checkedElement = when (it.elementType) { + in QUALIFIED_EXPRESSIONS -> it.findChildByType(QUALIFIED_OPERATION) + PARENTHESIZED -> it.lastChildNode + else -> null + } + + checkedElement != null && hasLineBreakBefore(checkedElement) } } diff --git a/idea/testData/formatter/callChain/KT37545.after.kt b/idea/testData/formatter/callChain/KT37545.after.kt new file mode 100644 index 00000000000..eda3ac86478 --- /dev/null +++ b/idea/testData/formatter/callChain/KT37545.after.kt @@ -0,0 +1,175 @@ +import X.combine + +class IndentationIssue { + fun method() { + X.Y.combine( + 1, + 1 + ) + .let { + "zzz" + } + + X.Y.combine( + 1, + 1 + ).let { + "zzz" + } + + X.Y./* + comment + */combine( + 1, + 1 + ) + .let { + "zzz" + } + + X.Y./* + comment + */combine( + 1, + 1 + ).let { + "zzz" + } + + (X.Y.combine( + 1, + 1 + ) + ).let { + "zzz" + } + + X.Y + .combine( + 1, + 1 + ) + .let { "zzz" } + + X.Y + .combine( + 1, + 1 + ).let { + "zzz" + } + + X + .Y + .combine( + 1, + 1 + ) + .let { "zzz" } + + X + .Y.combine( + 1, + 1 + ) + .let { "zzz" } + + combine( + 1, + 1 + ) + .let { "zzz" } + + combine( + 1, + 2 + ).let { + "zzz" + }.let { + "zzz" + } + + (combine( + 1, + 2 + ).let { + "zzz" + }).let { + "zzz" + } + + (combine( + 1, + 2 + ) + .let { + "zzz" + }).let { + "zzz" + } + + combine( + 1, + 2 + ).let { + "zzz" + }!!.let { + "zzz" + } + + combine( + 1, + 2 + ) + .let { + "zzz" + }!!.let { + "zzz" + } + + (combine( + 1, + 2 + ) + .let { + "zzz" + })!!.let { + "zzz" + } + + ((combine( + 1, + 2 + ) + ).let { + "zzz" + }!!)!!.let { + "zzz" + } + + ((combine( + 1, + 2 + )).let { + "zzz" + }!!)!!.let { + "zzz" + } + + ((combine( + 1, + 2 + )).let { + "zzz" + }!! + )!!.let { + "zzz" + } + } +} + +object X { + fun combine(x: L, y: R): T = TODO() + + val Y get() = this +} \ No newline at end of file diff --git a/idea/testData/formatter/callChain/KT37545.kt b/idea/testData/formatter/callChain/KT37545.kt new file mode 100644 index 00000000000..bcfa2ed967d --- /dev/null +++ b/idea/testData/formatter/callChain/KT37545.kt @@ -0,0 +1,175 @@ +import X.combine + +class IndentationIssue { + fun method() { + X.Y.combine( + 1, + 1 + ) + .let { + "zzz" + } + + X.Y.combine( + 1, + 1 + ).let { + "zzz" + } + + X.Y./* + comment + */combine( + 1, + 1 + ) + .let { + "zzz" + } + + X.Y./* + comment + */combine( + 1, + 1 + ).let { + "zzz" + } + + (X.Y.combine( + 1, + 1 + ) + ).let { + "zzz" + } + + X.Y + .combine( + 1, + 1 + ) + .let { "zzz" } + + X.Y + .combine( + 1, + 1 + ).let { + "zzz" + } + + X + .Y + .combine( + 1, + 1 + ) + .let { "zzz" } + + X + .Y.combine( + 1, + 1 + ) + .let { "zzz" } + + combine( + 1, + 1 + ) + .let { "zzz" } + + combine( + 1, + 2 + ).let { + "zzz" + }.let { + "zzz" + } + + (combine( + 1, + 2 + ).let { + "zzz" + }).let { + "zzz" + } + + (combine( + 1, + 2 + ) + .let { + "zzz" + }).let { + "zzz" + } + + combine( + 1, + 2 + ).let { + "zzz" + }!!.let { + "zzz" + } + + combine( + 1, + 2 + ) + .let { + "zzz" + }!!.let { + "zzz" + } + + (combine( + 1, + 2 + ) + .let { + "zzz" + })!!.let { + "zzz" + } + + ((combine( + 1, + 2 + ) + ).let { + "zzz" + }!!)!!.let { + "zzz" + } + + ((combine( + 1, + 2 + )).let { + "zzz" + }!!)!!.let { + "zzz" + } + + ((combine( + 1, + 2 + )).let { + "zzz" + }!! + )!!.let { + "zzz" + } + } +} + +object X { + fun combine(x: L, y: R): T = TODO() + + val Y get() = this +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java b/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java index 6f06ce741a2..895ca977c5a 100644 --- a/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java @@ -937,6 +937,11 @@ public class FormatterTestGenerated extends AbstractFormatterTest { runTest("idea/testData/formatter/callChain/KT36466.after.kt"); } + @TestMetadata("KT37545.after.kt") + public void testKT37545() throws Exception { + runTest("idea/testData/formatter/callChain/KT37545.after.kt"); + } + @TestMetadata("NotCallChain.after.kt") public void testNotCallChain() throws Exception { runTest("idea/testData/formatter/callChain/NotCallChain.after.kt");