diff --git a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index 2db53f647e8..68b6b05e9f3 100644 --- a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -82,6 +82,7 @@ import org.jetbrains.kotlin.idea.decompiler.textBuilder.AbstractCommonDecompiled import org.jetbrains.kotlin.idea.decompiler.textBuilder.AbstractCommonDecompiledTextTest import org.jetbrains.kotlin.idea.decompiler.textBuilder.AbstractJsDecompiledTextFromJsMetadataTest import org.jetbrains.kotlin.idea.decompiler.textBuilder.AbstractJvmDecompiledTextTest +import org.jetbrains.kotlin.idea.editor.AbstractEnterAfterUnmatchedBraceHandlerTest import org.jetbrains.kotlin.idea.editor.AbstractMultiLineStringIndentTest import org.jetbrains.kotlin.idea.editor.backspaceHandler.AbstractBackspaceHandlerTest import org.jetbrains.kotlin.idea.editor.quickDoc.AbstractQuickDocProviderTest @@ -547,6 +548,10 @@ fun main(args: Array) { model("editor/backspaceHandler") } + testClass { + model("editor/enterHandler/afterUnmatchedBrace") + } + testClass { model("editor/enterHandler/multilineString") } diff --git a/idea/resources/META-INF/idea.xml b/idea/resources/META-INF/idea.xml index 6ce5d82dcf0..fcd4e854ca1 100644 --- a/idea/resources/META-INF/idea.xml +++ b/idea/resources/META-INF/idea.xml @@ -79,6 +79,8 @@ id="KotlinEnterHandler" order="before EnterBetweenBracesHandler"/> + diff --git a/idea/src/org/jetbrains/kotlin/idea/editor/KotlinEnterAfterUnmatchedBraceHandler.kt b/idea/src/org/jetbrains/kotlin/idea/editor/KotlinEnterAfterUnmatchedBraceHandler.kt new file mode 100644 index 00000000000..dc14e727c5f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/editor/KotlinEnterAfterUnmatchedBraceHandler.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.idea.editor + +import com.intellij.codeInsight.editorActions.enter.EnterAfterUnmatchedBraceHandler +import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.util.Ref +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.startOffset + +class KotlinEnterAfterUnmatchedBraceHandler : EnterAfterUnmatchedBraceHandler() { + override fun preprocessEnter( + file: PsiFile, + editor: Editor, + caretOffsetRef: Ref, + caretAdvance: Ref, + dataContext: DataContext, + originalHandler: EditorActionHandler? + ): EnterHandlerDelegate.Result { + val caretOffset = caretOffsetRef.get() - 1 + val element = file.findElementAt(caretOffset) + if (element?.node?.elementType == KtTokens.LBRACE) { + return super.preprocessEnter(file, editor, caretOffsetRef, caretAdvance, dataContext, originalHandler) + } + if (element !is PsiWhiteSpace) { + return EnterHandlerDelegate.Result.Continue + } + val prevElement = CodeInsightUtils.getElementAtOffsetIgnoreWhitespaceAfter(file, caretOffset) + if (prevElement != null && prevElement.node.elementType == KtTokens.LBRACE) { + return super.preprocessEnter(file, editor, Ref(prevElement.startOffset + 1), caretAdvance, dataContext, originalHandler) + } + return EnterHandlerDelegate.Result.Continue + } + + override fun getRBraceOffset(file: PsiFile, editor: Editor, caretOffset: Int): Int { + val element = file.findElementAt(caretOffset - 1) + val endOffset = when (val parent = element?.parent) { + is KtFunctionLiteral -> { + val call = parent.getStrictParentOfType() + if (call?.isDeclarationInitializer() == true) { + (parent.parent as? KtLambdaExpression)?.bodyExpression?.statements?.firstOrNull()?.endOffset + } else { + null + } + } + is KtWhenExpression -> { + if (parent.isDeclarationInitializer()) { + (parent.entries.firstOrNull()?.conditions?.firstOrNull() as? KtWhenConditionWithExpression)?.endOffset + } else { + null + } + } + else -> null + } + return endOffset ?: super.getRBraceOffset(file, editor, caretOffset) + } + + private fun KtExpression.isDeclarationInitializer(): Boolean { + return (parent as? KtDeclarationWithInitializer)?.initializer == this + } +} \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeFunctionInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeFunctionInitializer.kt new file mode 100644 index 00000000000..f227eea42e1 --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeFunctionInitializer.kt @@ -0,0 +1,15 @@ +// WITH_RUNTIME +fun test(): Int = bar { foo() + +fun foo() = 42 + +fun bar(f: () -> Int) = f() +//----- +// WITH_RUNTIME +fun test(): Int = bar { + foo() +} + +fun foo() = 42 + +fun bar(f: () -> Int) = f() \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeLocalPropertyInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeLocalPropertyInitializer.kt new file mode 100644 index 00000000000..80859862cdc --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeLocalPropertyInitializer.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +fun test() { + val test = run { 1 +} +//----- +// WITH_RUNTIME +fun test() { + val test = run { + 1 + } +} \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeMemberPropertyInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeMemberPropertyInitializer.kt new file mode 100644 index 00000000000..2691868f7a7 --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeMemberPropertyInitializer.kt @@ -0,0 +1,19 @@ +// WITH_RUNTIME +class Test { + val test = run {foo() + + fun foo(): Int { + return 42 + } +} +//----- +// WITH_RUNTIME +class Test { + val test = run { + foo() + } + + fun foo(): Int { + return 42 + } +} \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeTopLevelPropertyInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeTopLevelPropertyInitializer.kt new file mode 100644 index 00000000000..6cd31f65f0d --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeTopLevelPropertyInitializer.kt @@ -0,0 +1,11 @@ +// WITH_RUNTIME +val test = run {foo() + +fun foo() = 42 +//----- +// WITH_RUNTIME +val test = run { + foo() +} + +fun foo() = 42 \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/notApplicableOnInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/notApplicableOnInitializer.kt new file mode 100644 index 00000000000..8c1d6351d3a --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/notApplicableOnInitializer.kt @@ -0,0 +1,14 @@ +// WITH_RUNTIME +fun test(): Int = bar { foo() + +fun foo() = 42 + +fun bar(f: () -> Int) = f() +//----- +// WITH_RUNTIME +fun test(): Int = bar { f + oo() + +fun foo() = 42 + +fun bar(f: () -> Int) = f() \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeLocalPropertyInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeLocalPropertyInitializer.kt new file mode 100644 index 00000000000..8fdf8f46b0f --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeLocalPropertyInitializer.kt @@ -0,0 +1,9 @@ +fun test() { + val test = when { 1 +} +//----- +fun test() { + val test = when { + 1 + } +} \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeMemberPropertyInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeMemberPropertyInitializer.kt new file mode 100644 index 00000000000..6f8bfc2403f --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeMemberPropertyInitializer.kt @@ -0,0 +1,17 @@ +class Test { + val test = when {foo() + + fun foo(): Int { + return 42 + } +} +//----- +class Test { + val test = when { + foo() + } + + fun foo(): Int { + return 42 + } +} \ No newline at end of file diff --git a/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeTopLevelPropertyInitializer.kt b/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeTopLevelPropertyInitializer.kt new file mode 100644 index 00000000000..1ce29d222bf --- /dev/null +++ b/idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeTopLevelPropertyInitializer.kt @@ -0,0 +1,9 @@ +val test = when {foo() + +fun foo() = 42 +//----- +val test = when { + foo() +} + +fun foo() = 42 \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/editor/AbstractEnterAfterUnmatchedBraceHandlerTest.kt b/idea/tests/org/jetbrains/kotlin/idea/editor/AbstractEnterAfterUnmatchedBraceHandlerTest.kt new file mode 100644 index 00000000000..b46780bfbdb --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/editor/AbstractEnterAfterUnmatchedBraceHandlerTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.idea.editor + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.testFramework.EditorTestUtil +import com.intellij.testFramework.LightProjectDescriptor +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase +import org.jetbrains.kotlin.idea.test.KotlinLightProjectDescriptor +import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor +import org.jetbrains.kotlin.test.InTextDirectivesUtils +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.jetbrains.kotlin.utils.rethrow +import java.io.File +import java.io.IOException + +abstract class AbstractEnterAfterUnmatchedBraceHandlerTest : KotlinLightCodeInsightFixtureTestCase() { + companion object { + private const val FILE_SEPARATOR = "//-----" + } + + protected fun doTest(path: String) { + val multiFileText = FileUtil.loadFile(File(path), true) + + val beforeFile = multiFileText.substringBefore(FILE_SEPARATOR).trim() + val afterFile = multiFileText.substringAfter(FILE_SEPARATOR).trim() + + myFixture.setCaresAboutInjection(false) + myFixture.configureByText(KotlinFileType.INSTANCE, beforeFile) + myFixture.type('\n') + + val caretModel = myFixture.editor.caretModel + val offset = caretModel.offset + val actualTextWithCaret = StringBuilder(myFixture.editor.document.text).insert(offset, EditorTestUtil.CARET_TAG).toString() + + if (afterFile != actualTextWithCaret) { + KotlinTestUtils.assertEqualsToFile(File(path), "$beforeFile\n$FILE_SEPARATOR\n$actualTextWithCaret") + } + } + + override fun getProjectDescriptor(): LightProjectDescriptor { + if (isAllFilesPresentInTest()) return KotlinLightProjectDescriptor.INSTANCE + return try { + val fileText = FileUtil.loadFile(File(testDataPath, fileName()), true) + if (InTextDirectivesUtils.isDirectiveDefined(fileText, "WITH_RUNTIME")) { + KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE + } else { + JAVA_LATEST + } + } catch (e: IOException) { + throw rethrow(e) + } + } +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/editor/EnterAfterUnmatchedBraceHandlerTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/editor/EnterAfterUnmatchedBraceHandlerTestGenerated.java new file mode 100644 index 00000000000..ba4fc84e2b7 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/editor/EnterAfterUnmatchedBraceHandlerTestGenerated.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.idea.editor; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/testData/editor/enterHandler/afterUnmatchedBrace") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class EnterAfterUnmatchedBraceHandlerTestGenerated extends AbstractEnterAfterUnmatchedBraceHandlerTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + public void testAllFilesPresentInAfterUnmatchedBrace() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/editor/enterHandler/afterUnmatchedBrace"), Pattern.compile("^(.+)\\.kt$"), null, true); + } + + @TestMetadata("lambdaArgumentBeforeFunctionInitializer.kt") + public void testLambdaArgumentBeforeFunctionInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeFunctionInitializer.kt"); + } + + @TestMetadata("lambdaArgumentBeforeLocalPropertyInitializer.kt") + public void testLambdaArgumentBeforeLocalPropertyInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeLocalPropertyInitializer.kt"); + } + + @TestMetadata("lambdaArgumentBeforeMemberPropertyInitializer.kt") + public void testLambdaArgumentBeforeMemberPropertyInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeMemberPropertyInitializer.kt"); + } + + @TestMetadata("lambdaArgumentBeforeTopLevelPropertyInitializer.kt") + public void testLambdaArgumentBeforeTopLevelPropertyInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/lambdaArgumentBeforeTopLevelPropertyInitializer.kt"); + } + + @TestMetadata("notApplicableOnInitializer.kt") + public void testNotApplicableOnInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/notApplicableOnInitializer.kt"); + } + + @TestMetadata("whenBeforeLocalPropertyInitializer.kt") + public void testWhenBeforeLocalPropertyInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeLocalPropertyInitializer.kt"); + } + + @TestMetadata("whenBeforeMemberPropertyInitializer.kt") + public void testWhenBeforeMemberPropertyInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeMemberPropertyInitializer.kt"); + } + + @TestMetadata("whenBeforeTopLevelPropertyInitializer.kt") + public void testWhenBeforeTopLevelPropertyInitializer() throws Exception { + runTest("idea/testData/editor/enterHandler/afterUnmatchedBrace/whenBeforeTopLevelPropertyInitializer.kt"); + } +}