diff --git a/ChangeLog.md b/ChangeLog.md index 207e913e659..29b37a23b10 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -477,6 +477,7 @@ These artifacts include extensions for the types available in the latter JDKs, s - [`KT-15068`](https://youtrack.jetbrains.com/issue/KT-15068) Implement intention which rename file according to the top-level class name - Implement quickfix which enables/disables coroutine support in module or project - [`KT-15056`](https://youtrack.jetbrains.com/issue/KT-15056) Implement intention which converts object literal to class +- [`KT-8855`](https://youtrack.jetbrains.com/issue/KT-8855) Implement "Create label" quick fix ## 1.0.6 diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/CreateLabelFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/CreateLabelFix.kt new file mode 100644 index 00000000000..c980e568c8f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/CreateLabelFix.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2010-2016 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. + */ + +package org.jetbrains.kotlin.idea.quickfix + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.refactoring.chooseContainerElementIfNecessary +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.parents + +sealed class CreateLabelFix( + expression: KtLabelReferenceExpression +) : KotlinQuickFixAction(expression) { + class ForLoop(expression: KtLabelReferenceExpression) : CreateLabelFix(expression) { + override val chooserTitle = "Select loop statement to label" + + override fun getCandidateExpressions(labelReferenceExpression: KtLabelReferenceExpression) = + labelReferenceExpression.getContainingLoops().toList() + } + + class ForLambda(expression: KtLabelReferenceExpression) : CreateLabelFix(expression) { + override val chooserTitle = "Select lambda to label" + + override fun getCandidateExpressions(labelReferenceExpression: KtLabelReferenceExpression) = + labelReferenceExpression.getContainingLambdas().toList() + } + + override fun getFamilyName() = "Create label" + + override fun getText() = "Create label ${element?.getReferencedName() ?: ""}@" + + abstract val chooserTitle: String + + abstract fun getCandidateExpressions(labelReferenceExpression: KtLabelReferenceExpression): List + + override fun startInWriteAction() = false + + private fun doCreateLabel(expression: KtLabelReferenceExpression, it: KtExpression, project: Project) { + project.executeWriteCommand(text) { + it.replace(KtPsiFactory(project).createExpressionByPattern("${expression.getReferencedName()}@ $0", it)) + } + } + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + val expression = element ?: return + if (editor == null) return + + val containers = getCandidateExpressions(expression) + + if (ApplicationManager.getApplication().isUnitTestMode) { + return doCreateLabel(expression, containers.last(), project) + } + + chooseContainerElementIfNecessary( + containers, + editor, + chooserTitle, + true, + { it }, + { + doCreateLabel(expression, it, project) + } + ) + } + + companion object : KotlinSingleIntentionActionFactory() { + private fun KtLabelReferenceExpression.getContainingLoops(): Sequence { + return parents + .takeWhile { !(it is KtDeclarationWithBody || it is KtClassBody || it is KtFile) } + .filterIsInstance() + } + + private fun KtLabelReferenceExpression.getContainingLambdas(): Sequence { + return parents + .takeWhile { !(it is KtDeclarationWithBody && it !is KtFunctionLiteral || it is KtClassBody || it is KtFile) } + .filterIsInstance() + } + + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val labelReferenceExpression = diagnostic.psiElement as? KtLabelReferenceExpression ?: return null + val parentExpression = (labelReferenceExpression.parent as? KtContainerNode)?.parent + return when (parentExpression) { + is KtBreakExpression, is KtContinueExpression -> { + if (labelReferenceExpression.getContainingLoops().any()) CreateLabelFix.ForLoop(labelReferenceExpression) else null + } + is KtReturnExpression -> { + if (labelReferenceExpression.getContainingLambdas().any()) CreateLabelFix.ForLambda(labelReferenceExpression) else null + } + else -> null + } + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index a03fe85221d..eef407ecc5d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -472,5 +472,7 @@ class QuickFixRegistrar : QuickFixContributor { EXPERIMENTAL_FEATURE_ERROR.registerFactory(ChangeCoroutineSupportFix) EXPERIMENTAL_FEATURE_WARNING.registerFactory(ChangeCoroutineSupportFix) + + UNRESOLVED_REFERENCE.registerFactory(CreateLabelFix) } } diff --git a/idea/testData/quickfix/createLabel/breakInLoop.kt b/idea/testData/quickfix/createLabel/breakInLoop.kt new file mode 100644 index 00000000000..c4aea5954f8 --- /dev/null +++ b/idea/testData/quickfix/createLabel/breakInLoop.kt @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +fun test() { + while (true) { + break@foo + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/breakInLoop.kt.after b/idea/testData/quickfix/createLabel/breakInLoop.kt.after new file mode 100644 index 00000000000..5277993aedb --- /dev/null +++ b/idea/testData/quickfix/createLabel/breakInLoop.kt.after @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +fun test() { + foo@ while (true) { + break@foo + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/breakInOuterLoop.kt b/idea/testData/quickfix/createLabel/breakInOuterLoop.kt new file mode 100644 index 00000000000..2c7308346f4 --- /dev/null +++ b/idea/testData/quickfix/createLabel/breakInOuterLoop.kt @@ -0,0 +1,9 @@ +// "Create label foo@" "true" + +fun test() { + while (true) { + while (true) { + break@foo + } + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/breakInOuterLoop.kt.after b/idea/testData/quickfix/createLabel/breakInOuterLoop.kt.after new file mode 100644 index 00000000000..2a459f8e4b0 --- /dev/null +++ b/idea/testData/quickfix/createLabel/breakInOuterLoop.kt.after @@ -0,0 +1,9 @@ +// "Create label foo@" "true" + +fun test() { + foo@ while (true) { + while (true) { + break@foo + } + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/breakInlambdaBeforeLoop.kt b/idea/testData/quickfix/createLabel/breakInlambdaBeforeLoop.kt new file mode 100644 index 00000000000..f9a23b860a4 --- /dev/null +++ b/idea/testData/quickfix/createLabel/breakInlambdaBeforeLoop.kt @@ -0,0 +1,11 @@ +// "Create label foo@" "false" +// ERROR: The label '@foo' does not denote a loop +// ERROR: Unresolved reference: @foo + +fun bar(f: () -> Unit) { } + +fun test() { + while (true) { + bar { break@foo } + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/breakNoLoop.kt b/idea/testData/quickfix/createLabel/breakNoLoop.kt new file mode 100644 index 00000000000..29e2106e016 --- /dev/null +++ b/idea/testData/quickfix/createLabel/breakNoLoop.kt @@ -0,0 +1,8 @@ +// "Create label foo@" "false" +// ACTION: Convert to expression body +// ERROR: The label '@foo' does not denote a loop +// ERROR: Unresolved reference: @foo + +fun test() { + break@foo +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/continueInLoop.kt b/idea/testData/quickfix/createLabel/continueInLoop.kt new file mode 100644 index 00000000000..fd29069e509 --- /dev/null +++ b/idea/testData/quickfix/createLabel/continueInLoop.kt @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +fun test() { + while (true) { + continue@foo + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/continueInLoop.kt.after b/idea/testData/quickfix/createLabel/continueInLoop.kt.after new file mode 100644 index 00000000000..5329a2d8424 --- /dev/null +++ b/idea/testData/quickfix/createLabel/continueInLoop.kt.after @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +fun test() { + foo@ while (true) { + continue@foo + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/continueNoLoop.kt b/idea/testData/quickfix/createLabel/continueNoLoop.kt new file mode 100644 index 00000000000..df19cda0e42 --- /dev/null +++ b/idea/testData/quickfix/createLabel/continueNoLoop.kt @@ -0,0 +1,8 @@ +// "Create label foo@" "false" +// ACTION: Convert to expression body +// ERROR: The label '@foo' does not denote a loop +// ERROR: Unresolved reference: @foo + +fun test() { + continue@foo +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/returnInLambda.kt b/idea/testData/quickfix/createLabel/returnInLambda.kt new file mode 100644 index 00000000000..2d9e72032e4 --- /dev/null +++ b/idea/testData/quickfix/createLabel/returnInLambda.kt @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +inline fun Int.bar(f: (Int) -> Unit) { } + +fun test() { + 1.bar { if (it == 2) return@foo } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/returnInLambda.kt.after b/idea/testData/quickfix/createLabel/returnInLambda.kt.after new file mode 100644 index 00000000000..4ad7ad9d3c3 --- /dev/null +++ b/idea/testData/quickfix/createLabel/returnInLambda.kt.after @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +inline fun Int.bar(f: (Int) -> Unit) { } + +fun test() { + 1.bar foo@ { if (it == 2) return@foo } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/returnInOuterLambda.kt b/idea/testData/quickfix/createLabel/returnInOuterLambda.kt new file mode 100644 index 00000000000..77ba0946c11 --- /dev/null +++ b/idea/testData/quickfix/createLabel/returnInOuterLambda.kt @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +inline fun Int.bar(f: (Int) -> Unit) { } + +fun test() { + 1.bar { 2.bar { if (it == 2) return@foo } } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/returnInOuterLambda.kt.after b/idea/testData/quickfix/createLabel/returnInOuterLambda.kt.after new file mode 100644 index 00000000000..65930fba552 --- /dev/null +++ b/idea/testData/quickfix/createLabel/returnInOuterLambda.kt.after @@ -0,0 +1,7 @@ +// "Create label foo@" "true" + +inline fun Int.bar(f: (Int) -> Unit) { } + +fun test() { + 1.bar foo@ { 2.bar { if (it == 2) return@foo } } +} \ No newline at end of file diff --git a/idea/testData/quickfix/createLabel/returnNoLambda.kt b/idea/testData/quickfix/createLabel/returnNoLambda.kt new file mode 100644 index 00000000000..dd4b590bb22 --- /dev/null +++ b/idea/testData/quickfix/createLabel/returnNoLambda.kt @@ -0,0 +1,7 @@ +// "Create label foo@" "false" +// ACTION: Convert to expression body +// ERROR: Unresolved reference: @foo + +fun test(): Int { + return@foo 1 +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 4ff2f854575..b3965bb3a2d 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -4151,6 +4151,69 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/createLabel") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class CreateLabel extends AbstractQuickFixTest { + public void testAllFilesPresentInCreateLabel() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/createLabel"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("breakInLoop.kt") + public void testBreakInLoop() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/breakInLoop.kt"); + doTest(fileName); + } + + @TestMetadata("breakInOuterLoop.kt") + public void testBreakInOuterLoop() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/breakInOuterLoop.kt"); + doTest(fileName); + } + + @TestMetadata("breakInlambdaBeforeLoop.kt") + public void testBreakInlambdaBeforeLoop() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/breakInlambdaBeforeLoop.kt"); + doTest(fileName); + } + + @TestMetadata("breakNoLoop.kt") + public void testBreakNoLoop() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/breakNoLoop.kt"); + doTest(fileName); + } + + @TestMetadata("continueInLoop.kt") + public void testContinueInLoop() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/continueInLoop.kt"); + doTest(fileName); + } + + @TestMetadata("continueNoLoop.kt") + public void testContinueNoLoop() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/continueNoLoop.kt"); + doTest(fileName); + } + + @TestMetadata("returnInLambda.kt") + public void testReturnInLambda() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/returnInLambda.kt"); + doTest(fileName); + } + + @TestMetadata("returnInOuterLambda.kt") + public void testReturnInOuterLambda() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/returnInOuterLambda.kt"); + doTest(fileName); + } + + @TestMetadata("returnNoLambda.kt") + public void testReturnNoLambda() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/createLabel/returnNoLambda.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/quickfix/decreaseVisibility") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)