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 158ba1d4fd8..8c9700339bb 100644 --- a/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt +++ b/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.idea.formatter import com.intellij.formatting.* import com.intellij.lang.ASTNode +import com.intellij.psi.PsiComment import com.intellij.psi.PsiElement import com.intellij.psi.TokenType import com.intellij.psi.codeStyle.CodeStyleSettings @@ -27,7 +28,6 @@ import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.leaves import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings -import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes private val QUALIFIED_OPERATION = TokenSet.create(DOT, SAFE_ACCESS) private val QUALIFIED_EXPRESSIONS = TokenSet.create(KtNodeTypes.DOT_QUALIFIED_EXPRESSION, KtNodeTypes.SAFE_ACCESS_EXPRESSION) @@ -112,14 +112,16 @@ abstract class KotlinCommonBlock( // Create fake ".something" or "?.something" block here, so child indentation will be // relative to it when it starts from new line (see Indent javadoc). - val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) - Indent.Type.CONTINUATION - else - Indent.Type.NORMAL val isNonFirstChainedCall = operationBlockIndex > 0 && isCallBlock(nodeSubBlocks[operationBlockIndex - 1]) + val enforceIndentToChildren = isNonFirstChainedCall && hasLineBreakBefore(nodeSubBlocks[operationBlockIndex]) + val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) { + if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST + } else { + Indent.Type.NORMAL + } val indent = Indent.getIndent( indentType, false, - isNonFirstChainedCall && hasLineBreakBefore(nodeSubBlocks[operationBlockIndex - 1]) + enforceIndentToChildren ) val wrap = if ((settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN || isNonFirstChainedCall) && canWrapCallChain(node)) @@ -566,18 +568,10 @@ fun needWrapArgumentList(psi: PsiElement): Boolean { } private fun hasLineBreakBefore(block: ASTBlock): Boolean { - val topLevelParent = block.node.parents().firstOrNull { - it.treeParent.elementType == KtNodeTypes.BLOCK || it.treeParent.elementType == KtStubElementTypes.FILE - } ?: return false - for (leaf in block.node.leaves(forward = false)) { - if (leaf.textContains('\n')) { - return true - } - if (leaf.textRange.startOffset == topLevelParent.startOffset) { - break - } - } - return false + val prevSibling = block.node.leaves(false) + .dropWhile { it.psi is PsiComment || it.elementType == KtTokens.RBRACE } + .firstOrNull() + return prevSibling?.elementType == TokenType.WHITE_SPACE && prevSibling?.textContains('\n') == true } fun NodeIndentStrategy.PositionStrategy.continuationIf( diff --git a/idea/testData/formatter/KT22071.after.kt b/idea/testData/formatter/KT22071.after.kt new file mode 100644 index 00000000000..6ea342a3369 --- /dev/null +++ b/idea/testData/formatter/KT22071.after.kt @@ -0,0 +1,51 @@ +package templates + +import kotlin.coroutines.experimental.buildSequence +import kotlin.reflect.KTypeProjection +import kotlin.reflect.full.createType +import kotlin.reflect.full.isSubtypeOf + + +typealias TemplateGroup = () -> Sequence + +fun templateGroupOf(vararg templates: MemberTemplate): TemplateGroup = { templates.asSequence() } + +abstract class TemplateGroupBase : TemplateGroup { + + override fun invoke(): Sequence = buildSequence { + with(this@TemplateGroupBase) { + this::class.members.filter { it.name.startsWith("f_") }.forEach { + require(it.parameters.size == 1) { "Member $it violates naming convention" } + when { + it.returnType.isSubtypeOf(typeMemberTemplate) -> + yield(it.call(this) as MemberTemplate) + it.returnType.isSubtypeOf(typeIterableOfMemberTemplates) -> + @Suppress("UNCHECKED_CAST") + yieldAll(it.call(this) as Iterable) + else -> + error("Member $it violates naming convention") + } + } + } + }.run { + if (defaultActions.isEmpty()) this else onEach { t -> defaultActions.forEach(t::builder) } + } + + private val defaultActions = mutableListOf() + + fun defaultBuilder(builderAction: MemberBuildAction) { + defaultActions += builderAction + } + + companion object { + private val typeMemberTemplate = MemberTemplate::class.createType() + private val typeIterableOfMemberTemplates = Iterable::class.createType(arguments = listOf(KTypeProjection.invariant(typeMemberTemplate))) + } + +} + +fun foo() { + cacheFile.objectInputStream().use { + // one indent here + } +} diff --git a/idea/testData/formatter/KT22071.kt b/idea/testData/formatter/KT22071.kt new file mode 100644 index 00000000000..182db36c02b --- /dev/null +++ b/idea/testData/formatter/KT22071.kt @@ -0,0 +1,51 @@ +package templates + +import kotlin.coroutines.experimental.buildSequence +import kotlin.reflect.KTypeProjection +import kotlin.reflect.full.createType +import kotlin.reflect.full.isSubtypeOf + + +typealias TemplateGroup = () -> Sequence + +fun templateGroupOf(vararg templates: MemberTemplate): TemplateGroup = { templates.asSequence() } + +abstract class TemplateGroupBase : TemplateGroup { + + override fun invoke(): Sequence = buildSequence { + with(this@TemplateGroupBase) { + this::class.members.filter { it.name.startsWith("f_") }.forEach { + require(it.parameters.size == 1) { "Member $it violates naming convention" } + when { + it.returnType.isSubtypeOf(typeMemberTemplate) -> + yield(it.call(this) as MemberTemplate) + it.returnType.isSubtypeOf(typeIterableOfMemberTemplates) -> + @Suppress("UNCHECKED_CAST") + yieldAll(it.call(this) as Iterable) + else -> + error("Member $it violates naming convention") + } + } + } + }.run { + if (defaultActions.isEmpty()) this else onEach { t -> defaultActions.forEach(t::builder) } + } + + private val defaultActions = mutableListOf() + + fun defaultBuilder(builderAction: MemberBuildAction) { + defaultActions += builderAction + } + + companion object { + private val typeMemberTemplate = MemberTemplate::class.createType() + private val typeIterableOfMemberTemplates = Iterable::class.createType(arguments = listOf(KTypeProjection.invariant(typeMemberTemplate))) + } + +} + +fun foo() { + cacheFile.objectInputStream().use { + // one indent here + } +} diff --git a/idea/testData/formatter/KT22115.after.kt b/idea/testData/formatter/KT22115.after.kt new file mode 100644 index 00000000000..cd83d37a107 --- /dev/null +++ b/idea/testData/formatter/KT22115.after.kt @@ -0,0 +1,9 @@ +private fun String.foo(): List = emptyList() + +fun test(s: String) { + s.foo().flatMap { m -> + m.foo().map { e -> + e to m + } + } +} diff --git a/idea/testData/formatter/KT22115.kt b/idea/testData/formatter/KT22115.kt new file mode 100644 index 00000000000..cd83d37a107 --- /dev/null +++ b/idea/testData/formatter/KT22115.kt @@ -0,0 +1,9 @@ +private fun String.foo(): List = emptyList() + +fun test(s: String) { + s.foo().flatMap { m -> + m.foo().map { e -> + e to m + } + } +} diff --git a/idea/testData/formatter/KT22148.after.kt b/idea/testData/formatter/KT22148.after.kt new file mode 100644 index 00000000000..d582ebcedba --- /dev/null +++ b/idea/testData/formatter/KT22148.after.kt @@ -0,0 +1,14 @@ +fun foo() { + async { + // ... + }.logFailures().invokeOnCompletion { + if (it != null) showErrorAndContinue() + finish() + } + + FirebaseAuth.getInstance().addAuthStateListener(object : FirebaseAuth.AuthStateListener { + override fun onAuthStateChanged(auth: FirebaseAuth) { + // ... + } + }) +} diff --git a/idea/testData/formatter/KT22148.kt b/idea/testData/formatter/KT22148.kt new file mode 100644 index 00000000000..d582ebcedba --- /dev/null +++ b/idea/testData/formatter/KT22148.kt @@ -0,0 +1,14 @@ +fun foo() { + async { + // ... + }.logFailures().invokeOnCompletion { + if (it != null) showErrorAndContinue() + finish() + } + + FirebaseAuth.getInstance().addAuthStateListener(object : FirebaseAuth.AuthStateListener { + override fun onAuthStateChanged(auth: FirebaseAuth) { + // ... + } + }) +} diff --git a/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java b/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java index c644e2d2f40..cb523cca77c 100644 --- a/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/formatter/FormatterTestGenerated.java @@ -1,17 +1,6 @@ /* - * Copyright 2010-2017 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. + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. */ package org.jetbrains.kotlin.formatter; @@ -542,6 +531,24 @@ public class FormatterTestGenerated extends AbstractFormatterTest { doTest(fileName); } + @TestMetadata("KT22071.after.kt") + public void testKT22071() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/KT22071.after.kt"); + doTest(fileName); + } + + @TestMetadata("KT22115.after.kt") + public void testKT22115() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/KT22115.after.kt"); + doTest(fileName); + } + + @TestMetadata("KT22148.after.kt") + public void testKT22148() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/KT22148.after.kt"); + doTest(fileName); + } + @TestMetadata("KeepLineBreak.after.kt") public void testKeepLineBreak() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/formatter/KeepLineBreak.after.kt");