diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt index b5001a4f930..2bc49676cd9 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt @@ -391,6 +391,7 @@ fun KtModifierListOwner.hasActualModifier() = hasModifier(KtTokens.IMPL_KEYWORD) fun KtModifierList.hasActualModifier() = hasModifier(KtTokens.IMPL_KEYWORD) || hasModifier(KtTokens.ACTUAL_KEYWORD) fun ASTNode.children() = generateSequence(firstChildNode) { node -> node.treeNext } +fun ASTNode.parents() = generateSequence(treeParent) { node -> node.treeParent } fun ASTNode.siblings(forward: Boolean = true): Sequence { if (forward) { 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 0f9512458aa..c681668350f 100644 --- a/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt +++ b/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt @@ -34,9 +34,11 @@ import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.* import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.children +import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings private val QUALIFIED_OPERATION = TokenSet.create(DOT, SAFE_ACCESS) +private val QUALIFIED_EXPRESSIONS = TokenSet.create(KtNodeTypes.DOT_QUALIFIED_EXPRESSION, KtNodeTypes.SAFE_ACCESS_EXPRESSION) private val KDOC_COMMENT_INDENT = 1 private val BINARY_EXPRESSIONS = TokenSet.create(KtNodeTypes.BINARY_EXPRESSION, KtNodeTypes.BINARY_WITH_TYPE, KtNodeTypes.IS_EXPRESSION) @@ -92,7 +94,7 @@ abstract class KotlinCommonBlock( var nodeSubBlocks = buildSubBlocks() - if (node.elementType === KtNodeTypes.DOT_QUALIFIED_EXPRESSION || node.elementType === KtNodeTypes.SAFE_ACCESS_EXPRESSION) { + if (node.elementType in QUALIFIED_EXPRESSIONS) { val operationBlockIndex = findNodeBlockIndex(nodeSubBlocks, QUALIFIED_OPERATION) if (operationBlockIndex != -1) { // Create fake ".something" or "?.something" block here, so child indentation will be @@ -103,10 +105,17 @@ abstract class KotlinCommonBlock( Indent.getContinuationWithoutFirstIndent() else Indent.getNormalIndent() + val isNonFirstChainedCall = operationBlockIndex > 0 && isCallBlock(nodeSubBlocks[operationBlockIndex - 1]) + val wrap = if ((settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN || isNonFirstChainedCall) && + canWrapCallChain(node)) + Wrap.createWrap(settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP, true) + else + null + val operationSyntheticBlock = SyntheticKotlinBlock( operationBlock.node, nodeSubBlocks.subList(operationBlockIndex, nodeSubBlocks.size), - null, indent, null, spacingBuilder) { createSyntheticSpacingNodeBlock(it) } + null, indent, wrap, spacingBuilder) { createSyntheticSpacingNodeBlock(it) } nodeSubBlocks = nodeSubBlocks.subList(0, operationBlockIndex) + operationSyntheticBlock } @@ -117,6 +126,20 @@ abstract class KotlinCommonBlock( return nodeSubBlocks } + private fun isCallBlock(astBlock: ASTBlock): Boolean { + val node = astBlock.node + return node.elementType in QUALIFIED_EXPRESSIONS && node.lastChildNode?.elementType == KtNodeTypes.CALL_EXPRESSION + } + + private fun canWrapCallChain(node: ASTNode): Boolean { + val callChainParent = node.parents().firstOrNull { it.elementType !in QUALIFIED_EXPRESSIONS } ?: return true + return callChainParent.elementType in CODE_BLOCKS || + callChainParent.elementType == KtNodeTypes.PROPERTY || + (callChainParent.elementType == KtNodeTypes.BINARY_EXPRESSION && + (callChainParent.psi as KtBinaryExpression).operationToken in KtTokens.ALL_ASSIGNMENTS) || + callChainParent.elementType == KtNodeTypes.RETURN + } + fun createChildIndent(child: ASTNode): Indent? { val childParent = child.treeParent val childType = child.elementType @@ -182,7 +205,7 @@ abstract class KotlinCommonBlock( KtNodeTypes.TRY -> ChildAttributes(Indent.getNoneIndent(), null) - KtNodeTypes.DOT_QUALIFIED_EXPRESSION, KtNodeTypes.SAFE_ACCESS_EXPRESSION -> ChildAttributes(Indent.getContinuationWithoutFirstIndent(), null) + in QUALIFIED_EXPRESSIONS -> ChildAttributes(Indent.getContinuationWithoutFirstIndent(), null) KtNodeTypes.VALUE_PARAMETER_LIST, KtNodeTypes.VALUE_ARGUMENT_LIST -> { val subBlocks = getSubBlocks() @@ -463,7 +486,7 @@ private val INDENT_RULES = arrayOf( .set(Indent.getContinuationWithoutFirstIndent()), strategy("Chained calls") - .within(KtNodeTypes.DOT_QUALIFIED_EXPRESSION, KtNodeTypes.SAFE_ACCESS_EXPRESSION) + .within(QUALIFIED_EXPRESSIONS) .notForType(KtTokens.DOT, KtTokens.SAFE_ACCESS) .forElement { it.treeParent.firstChildNode != it } .set { settings -> diff --git a/idea/src/org/jetbrains/kotlin/idea/formatter/KotlinLanguageCodeStyleSettingsProvider.kt b/idea/src/org/jetbrains/kotlin/idea/formatter/KotlinLanguageCodeStyleSettingsProvider.kt index 9399a36922c..ef591602e59 100644 --- a/idea/src/org/jetbrains/kotlin/idea/formatter/KotlinLanguageCodeStyleSettingsProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/formatter/KotlinLanguageCodeStyleSettingsProvider.kt @@ -51,6 +51,14 @@ class KotlinLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvide param2: String) { @Deprecated val foo = 1 } + + fun multilineMethod( + foo: String, + bar: String + ) { + foo.toUpperCase().trim() + .length + } } @Deprecated val bar = 1 @@ -247,7 +255,9 @@ class KotlinLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvide "METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE", "CALL_PARAMETERS_LPAREN_ON_NEXT_LINE", "CALL_PARAMETERS_RPAREN_ON_NEXT_LINE", - "ENUM_CONSTANTS_WRAP" + "ENUM_CONSTANTS_WRAP", + "METHOD_CALL_CHAIN_WRAP", + "WRAP_FIRST_METHOD_IN_CALL_CHAIN" ) consumer.renameStandardOption(CodeStyleSettingsCustomizable.WRAPPING_SWITCH_STATEMENT, "'when' statements") consumer.renameStandardOption("FIELD_ANNOTATION_WRAP", "Property annotations") diff --git a/idea/testData/formatter/CallChainWrapping.after.inv.kt b/idea/testData/formatter/CallChainWrapping.after.inv.kt new file mode 100644 index 00000000000..8cd2dad6e21 --- /dev/null +++ b/idea/testData/formatter/CallChainWrapping.after.inv.kt @@ -0,0 +1,31 @@ +val x = foo + .bar() + .baz() + .quux() + +val y = xyzzy(foo.bar().baz().quux()) + +fun foo() { + foo + .bar() + .baz() + .quux() + + z = foo + .bar() + .baz() + .quux() + + z += foo + .bar() + .baz() + .quux() + + return foo + .bar() + .baz() + .quux() +} + +// SET_INT: METHOD_CALL_CHAIN_WRAP = 2 +// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN \ No newline at end of file diff --git a/idea/testData/formatter/CallChainWrapping.after.kt b/idea/testData/formatter/CallChainWrapping.after.kt new file mode 100644 index 00000000000..b8390fa4764 --- /dev/null +++ b/idea/testData/formatter/CallChainWrapping.after.kt @@ -0,0 +1,26 @@ +val x = foo.bar() + .baz() + .quux() + +val y = xyzzy(foo.bar().baz().quux()) + +fun foo() { + foo.bar() + .baz() + .quux() + + z = foo.bar() + .baz() + .quux() + + z += foo.bar() + .baz() + .quux() + + return foo.bar() + .baz() + .quux() +} + +// SET_INT: METHOD_CALL_CHAIN_WRAP = 2 +// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN \ No newline at end of file diff --git a/idea/testData/formatter/CallChainWrapping.kt b/idea/testData/formatter/CallChainWrapping.kt new file mode 100644 index 00000000000..3d81b16bf5f --- /dev/null +++ b/idea/testData/formatter/CallChainWrapping.kt @@ -0,0 +1,16 @@ +val x = foo.bar().baz().quux() + +val y = xyzzy(foo.bar().baz().quux()) + +fun foo() { + foo.bar().baz().quux() + + z = foo.bar().baz().quux() + + z += foo.bar().baz().quux() + + return foo.bar().baz().quux() +} + +// SET_INT: METHOD_CALL_CHAIN_WRAP = 2 +// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN \ 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 d6d95443524..1a1e2e9b3cb 100644 --- a/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java @@ -128,6 +128,12 @@ public class FormatterTestGenerated extends AbstractFormatterTest { doTest(fileName); } + @TestMetadata("CallChainWrapping.after.kt") + public void testCallChainWrapping() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/CallChainWrapping.after.kt"); + doTest(fileName); + } + @TestMetadata("CallLParenthOnNextLine.after.kt") public void testCallLParenthOnNextLine() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/CallLParenthOnNextLine.after.kt"); @@ -1148,6 +1154,12 @@ public class FormatterTestGenerated extends AbstractFormatterTest { doTestInverted(fileName); } + @TestMetadata("CallChainWrapping.after.inv.kt") + public void testCallChainWrapping() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/CallChainWrapping.after.inv.kt"); + doTestInverted(fileName); + } + @TestMetadata("CallLParenthOnNextLine.after.inv.kt") public void testCallLParenthOnNextLine() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/CallLParenthOnNextLine.after.inv.kt");