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 bf96504d85c..443f376c57d 100644 --- a/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt +++ b/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt @@ -127,7 +127,11 @@ abstract class KotlinCommonBlock( // relative to it when it starts from new line (see Indent javadoc). val isNonFirstChainedCall = operationBlockIndex > 0 && isCallBlock(nodeSubBlocks[operationBlockIndex - 1]) - val enforceIndentToChildren = isNonFirstChainedCall && hasLineBreakBefore(nodeSubBlocks[operationBlockIndex]) + + // 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(nodeSubBlocks[operationBlockIndex - 1]) + val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) { if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST } else { @@ -159,6 +163,18 @@ abstract class KotlinCommonBlock( return subList(0, index) + operationSyntheticBlock } + private fun anyCallInCallChainIsWrapped(astBlock: ASTBlock): Boolean { + var result: ASTBlock? = astBlock + while (true) { + if (result == null || !isCallBlock(result)) return false + val dot = result.node?.findChildByType(QUALIFIED_OPERATION) + if (dot != null && hasLineBreakBefore(dot)) { + return true + } + result = result.subBlocks.firstOrNull() as? ASTBlock? + } + } + private fun isCallBlock(astBlock: ASTBlock): Boolean { val node = astBlock.node return node.elementType in QUALIFIED_EXPRESSIONS && node.lastChildNode?.elementType == KtNodeTypes.CALL_EXPRESSION @@ -602,9 +618,9 @@ fun needWrapArgumentList(psi: PsiElement): Boolean { return args?.singleOrNull()?.getArgumentExpression() !is KtObjectLiteralExpression } -private fun hasLineBreakBefore(block: ASTBlock): Boolean { - val prevSibling = block.node.leaves(false) - .dropWhile { it.psi is PsiComment || it.elementType == KtTokens.RBRACE } +private fun hasLineBreakBefore(node: ASTNode): Boolean { + val prevSibling = node.leaves(false) + .dropWhile { it.psi is PsiComment } .firstOrNull() return prevSibling?.elementType == TokenType.WHITE_SPACE && prevSibling?.textContains('\n') == true } diff --git a/idea/testData/formatter/callChain/KT22346.after.kt b/idea/testData/formatter/callChain/KT22346.after.kt new file mode 100644 index 00000000000..0372bec8c1d --- /dev/null +++ b/idea/testData/formatter/callChain/KT22346.after.kt @@ -0,0 +1,7 @@ +val x = foo.bar { + it + 2 +}.let { + println(it) +} + +// SET_FALSE: CONTINUATION_INDENT_FOR_CHAINED_CALLS diff --git a/idea/testData/formatter/callChain/KT22346.kt b/idea/testData/formatter/callChain/KT22346.kt new file mode 100644 index 00000000000..0372bec8c1d --- /dev/null +++ b/idea/testData/formatter/callChain/KT22346.kt @@ -0,0 +1,7 @@ +val x = foo.bar { + it + 2 +}.let { + println(it) +} + +// SET_FALSE: CONTINUATION_INDENT_FOR_CHAINED_CALLS diff --git a/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java b/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java index 3b127fbe55b..0b6e1cb75f9 100644 --- a/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java @@ -988,6 +988,12 @@ public class FormatterTestGenerated extends AbstractFormatterTest { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/callChain/KT22148.after.kt"); doTest(fileName); } + + @TestMetadata("KT22346.after.kt") + public void testKT22346() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/callChain/KT22346.after.kt"); + doTest(fileName); + } } @TestMetadata("idea/testData/formatter/fileAnnotations")