diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/RemoveComposableIntention.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/RemoveComposableIntention.kt new file mode 100644 index 0000000000..c04b5b491d --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/RemoveComposableIntention.kt @@ -0,0 +1,36 @@ +package org.jetbrains.compose.intentions + +import com.intellij.codeInsight.intention.LowPriorityAction +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import org.jetbrains.compose.intentions.utils.composableFinder.ComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.composableFinder.ComposableFunctionFinderImpl +import org.jetbrains.compose.intentions.utils.getRootPsiElement.GetRootPsiElement +import org.jetbrains.compose.intentions.utils.isIntentionAvailable + +class RemoveComposableIntention : + PsiElementBaseIntentionAction(), + LowPriorityAction { + + override fun getText(): String { + return "Remove this Composable" + } + + override fun getFamilyName(): String { + return "Compose Multiplatform intentions" + } + + private val composableFunctionFinder: ComposableFunctionFinder = ComposableFunctionFinderImpl() + + private val getRootElement = GetRootPsiElement() + + override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean { + return element.isIntentionAvailable(composableFunctionFinder) + } + + override fun invoke(project: Project, editor: Editor?, element: PsiElement) { + getRootElement(element.parent)?.delete() + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/RemoveParentComposableIntention.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/RemoveParentComposableIntention.kt new file mode 100644 index 0000000000..54c2718f0b --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/RemoveParentComposableIntention.kt @@ -0,0 +1,48 @@ +package org.jetbrains.compose.intentions + +import com.intellij.codeInsight.intention.PriorityAction +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Iconable +import com.intellij.psi.PsiElement +import javax.swing.Icon +import org.jetbrains.compose.desktop.ide.preview.PreviewIcons +import org.jetbrains.compose.intentions.utils.composableFinder.ChildComposableFinder +import org.jetbrains.compose.intentions.utils.composableFinder.ComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.getRootPsiElement.GetRootPsiElement +import org.jetbrains.compose.intentions.utils.isIntentionAvailable +import org.jetbrains.kotlin.psi.KtCallExpression + +class RemoveParentComposableIntention : + PsiElementBaseIntentionAction(), + PriorityAction { + + override fun getText(): String { + return "Remove the parent Composable" + } + + override fun getFamilyName(): String { + return "Compose Multiplatform intentions" + } + + private val getRootElement = GetRootPsiElement() + + private val composableFunctionFinder: ComposableFunctionFinder = ChildComposableFinder() + + override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean { + return element.isIntentionAvailable(composableFunctionFinder) + } + + override fun invoke(project: Project, editor: Editor?, element: PsiElement) { + val callExpression = getRootElement(element.parent) as? KtCallExpression ?: return + val lambdaBlock = + callExpression.lambdaArguments.firstOrNull()?.getLambdaExpression()?.functionLiteral?.bodyExpression + ?: return + callExpression.replace(lambdaBlock) + } + + override fun getPriority(): PriorityAction.Priority { + return PriorityAction.Priority.NORMAL + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/WrapWithComposableIntentionGroup.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/WrapWithComposableIntentionGroup.kt new file mode 100644 index 0000000000..9cb6a74819 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/WrapWithComposableIntentionGroup.kt @@ -0,0 +1,67 @@ +package org.jetbrains.compose.intentions + +import com.intellij.codeInsight.intention.impl.IntentionActionGroup +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.ListPopup +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep +import com.intellij.psi.PsiFile +import com.intellij.ui.popup.list.ListPopupImpl +import org.jetbrains.compose.intentions.wrapActions.BaseWrapWithComposableAction +import org.jetbrains.compose.intentions.wrapActions.WrapWithBoxIntention +import org.jetbrains.compose.intentions.wrapActions.WrapWithCardIntention +import org.jetbrains.compose.intentions.wrapActions.WrapWithColumnIntention +import org.jetbrains.compose.intentions.wrapActions.WrapWithLzyColumnIntention +import org.jetbrains.compose.intentions.wrapActions.WrapWithLzyRowIntention +import org.jetbrains.compose.intentions.wrapActions.WrapWithRowIntention + +class WrapWithComposableIntentionGroup : + IntentionActionGroup( + listOf( + WrapWithBoxIntention(), + WrapWithCardIntention(), + WrapWithColumnIntention(), + WrapWithRowIntention(), + WrapWithLzyColumnIntention(), + WrapWithLzyRowIntention() + ) + ) { + + private fun createPopup( + project: Project, + actions: List, + invokeAction: (BaseWrapWithComposableAction) -> Unit + ): ListPopup { + + val step = object : BaseListPopupStep(null, actions) { + + override fun getTextFor(action: BaseWrapWithComposableAction) = action.text + + override fun onChosen(selectedValue: BaseWrapWithComposableAction, finalChoice: Boolean): PopupStep<*>? { + invokeAction(selectedValue) + return FINAL_CHOICE + } + } + + return ListPopupImpl(project, step) + } + + override fun getFamilyName(): String { + return "Compose Multiplatform intentions" + } + + override fun chooseAction( + project: Project, + editor: Editor, + file: PsiFile, + actions: List, + invokeAction: (BaseWrapWithComposableAction) -> Unit + ) { + createPopup(project, actions, invokeAction).showInBestPositionFor(editor) + } + + override fun getGroupText(actions: List): String { + return "Wrap with Composable" + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/PsiUtils.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/PsiUtils.kt new file mode 100644 index 0000000000..8591d73af6 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/PsiUtils.kt @@ -0,0 +1,36 @@ +package org.jetbrains.compose.intentions.utils + +import com.intellij.psi.PsiElement +import org.jetbrains.compose.desktop.ide.preview.isComposableFunction +import org.jetbrains.compose.intentions.utils.composableFinder.ComposableFunctionFinder +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.nj2k.postProcessing.resolve +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType + +internal fun KtCallExpression.isComposable(): Boolean { + return getChildOfType()?.isComposable() ?: false +} + +internal fun KtNameReferenceExpression.isComposable(): Boolean { + val ktNamedFunction = resolve() as? KtNamedFunction ?: return false + return ktNamedFunction.isComposableFunction() +} + +internal fun PsiElement.isIntentionAvailable( + composableFunctionFinder: ComposableFunctionFinder +): Boolean { + if (language != KotlinLanguage.INSTANCE) { + return false + } + + if (!isWritable) { + return false + } + + return parent?.let { parentPsiElement -> + composableFunctionFinder.isFunctionComposable(parentPsiElement) + } ?: false +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ChildComposableFinder.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ChildComposableFinder.kt new file mode 100644 index 0000000000..d4300fd2d8 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ChildComposableFinder.kt @@ -0,0 +1,33 @@ +package org.jetbrains.compose.intentions.utils.composableFinder + +import com.intellij.psi.PsiElement +import org.jetbrains.compose.intentions.utils.isComposable +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtLambdaArgument +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType + +class ChildComposableFinder : ComposableFunctionFinder { + + override fun isFunctionComposable(psiElement: PsiElement): Boolean { + + if (psiElement is KtCallExpression) { + psiElement.getChildOfType()?.let { lambdaChild -> + return getComposableFromChildLambda(lambdaChild) + } + } + + if (psiElement.parent is KtCallExpression) { + psiElement.parent.getChildOfType()?.let { lambdaChild -> + return getComposableFromChildLambda(lambdaChild) + } + } + + return false + } + + private fun getComposableFromChildLambda(lambdaArgument: KtLambdaArgument): Boolean { + val bodyExpression = lambdaArgument.getLambdaExpression()?.functionLiteral?.bodyExpression + val ktCallExpression = bodyExpression?.getChildOfType() ?: return false + return ktCallExpression.isComposable() + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ComposableFunctionFinder.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ComposableFunctionFinder.kt new file mode 100644 index 0000000000..e56aca5cd7 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ComposableFunctionFinder.kt @@ -0,0 +1,7 @@ +package org.jetbrains.compose.intentions.utils.composableFinder + +import com.intellij.psi.PsiElement + +interface ComposableFunctionFinder { + fun isFunctionComposable(psiElement: PsiElement): Boolean +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ComposableFunctionFinderImpl.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ComposableFunctionFinderImpl.kt new file mode 100644 index 0000000000..2837149706 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composableFinder/ComposableFunctionFinderImpl.kt @@ -0,0 +1,40 @@ +package org.jetbrains.compose.intentions.utils.composableFinder + +import com.intellij.psi.PsiElement +import org.jetbrains.compose.intentions.utils.isComposable +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtPropertyDelegate +import org.jetbrains.kotlin.psi.KtValueArgumentList +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType + +class ComposableFunctionFinderImpl : ComposableFunctionFinder { + + override fun isFunctionComposable(psiElement: PsiElement): Boolean { + return when (psiElement) { + is KtNameReferenceExpression -> psiElement.isComposable() + is KtProperty -> detectComposableFromKtProperty(psiElement) + is KtValueArgumentList -> { + val parent = psiElement.parent as? KtCallExpression ?: return false + parent.isComposable() + } + else -> false + } + } + + /** + * To handle both property and property delegates + */ + private fun detectComposableFromKtProperty(psiElement: KtProperty): Boolean { + psiElement.getChildOfType().let { propertyChildExpression -> + return if (propertyChildExpression == null) { + val propertyDelegate = psiElement.getChildOfType() ?: return false + val ktCallExpression = propertyDelegate.getChildOfType() ?: return false + ktCallExpression.isComposable() + } else { + propertyChildExpression.isComposable() + } + } + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/getRootPsiElement/GetRootPsiElement.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/getRootPsiElement/GetRootPsiElement.kt new file mode 100644 index 0000000000..21985cc4a7 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/getRootPsiElement/GetRootPsiElement.kt @@ -0,0 +1,40 @@ +package org.jetbrains.compose.intentions.utils.getRootPsiElement + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtPropertyDelegate +import org.jetbrains.kotlin.psi.KtValueArgumentList + +/** + * To get the root element of a selected Psi element + */ +class GetRootPsiElement { + + /** + * @param element can be + * 1. KtCallExpression, KtNameReferenceExpression - Box() + * 2. KtDotQualifiedExpression - repeatingAnimation.animateFloat + * 3. KtProperty - val systemUiController = rememberSystemUiController() + * 4. KtValueArgumentList - () + */ + tailrec operator fun invoke(element: PsiElement): PsiElement? { + return when (element) { + is KtProperty -> element + is KtNameReferenceExpression, + is KtValueArgumentList -> invoke(element.parent) + is KtDotQualifiedExpression, + is KtCallExpression -> { + when (element.parent) { + is KtProperty, + is KtDotQualifiedExpression -> invoke(element.parent) // composable dot expression + is KtPropertyDelegate -> invoke(element.parent.parent) // composable dot expression + else -> element + } + } + else -> null + } + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrapActions/BaseWrapWithComposableAction.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrapActions/BaseWrapWithComposableAction.kt new file mode 100644 index 0000000000..abb145b5df --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrapActions/BaseWrapWithComposableAction.kt @@ -0,0 +1,53 @@ +package org.jetbrains.compose.intentions.wrapActions + +import com.intellij.codeInsight.intention.HighPriorityAction +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.codeInsight.template.impl.InvokeTemplateAction +import com.intellij.codeInsight.template.impl.TemplateImpl +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import org.jetbrains.compose.intentions.utils.composableFinder.ComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.composableFinder.ComposableFunctionFinderImpl +import org.jetbrains.compose.intentions.utils.getRootPsiElement.GetRootPsiElement +import org.jetbrains.compose.intentions.utils.isIntentionAvailable + +abstract class BaseWrapWithComposableAction : + PsiElementBaseIntentionAction(), + HighPriorityAction { + + private val composableFunctionFinder: ComposableFunctionFinder by lazy { + ComposableFunctionFinderImpl() + } + + private val getRootElement by lazy { + GetRootPsiElement() + } + + override fun getFamilyName(): String { + return "Compose Multiplatform intentions" + } + + override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean { + return element.isIntentionAvailable(composableFunctionFinder) + } + + override fun startInWriteAction(): Boolean = true + + override fun invoke(project: Project, editor: Editor?, element: PsiElement) { + getRootElement(element.parent)?.let { rootElement -> + val selectionModel = editor!!.selectionModel + val textRange = rootElement.textRange + selectionModel.setSelection(textRange.startOffset, textRange.endOffset) + + InvokeTemplateAction( + getTemplate(), + editor, + project, + HashSet() + ).perform() + } + } + + protected abstract fun getTemplate(): TemplateImpl? +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrapActions/WrapWithActions.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrapActions/WrapWithActions.kt new file mode 100644 index 0000000000..0070cd49fe --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrapActions/WrapWithActions.kt @@ -0,0 +1,70 @@ +package org.jetbrains.compose.intentions.wrapActions + +import com.intellij.codeInsight.template.impl.TemplateImpl +import com.intellij.codeInsight.template.impl.TemplateSettings + +class WrapWithBoxIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with Box" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("boxcomp", "ComposeMultiplatformTemplates") + } +} + +class WrapWithCardIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with Card" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("cardcomp", "ComposeMultiplatformTemplates") + } +} + +class WrapWithColumnIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with Column" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("columncomp", "ComposeMultiplatformTemplates") + } +} + +class WrapWithRowIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with Row" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("rowcomp", "ComposeMultiplatformTemplates") + } +} + +class WrapWithLzyColumnIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with LazyColumn" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("lazycolumncomp", "ComposeMultiplatformTemplates") + } +} + +class WrapWithLzyRowIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with LazyRow" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("lazyrowcomp", "ComposeMultiplatformTemplates") + } +} diff --git a/idea-plugin/src/main/resources/META-INF/plugin.xml b/idea-plugin/src/main/resources/META-INF/plugin.xml index 947d927ea3..01390d511f 100644 --- a/idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/idea-plugin/src/main/resources/META-INF/plugin.xml @@ -52,4 +52,30 @@ + + + + + + + + org.jetbrains.compose.intentions.WrapWithComposableIntentionGroup + + Compose Multiplatform + + + + org.jetbrains.compose.intentions.RemoveComposableIntention + + Compose Multiplatform + + + + org.jetbrains.compose.intentions.RemoveParentComposableIntention + + Compose Multiplatform + + + + diff --git a/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/after.kt.template b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/after.kt.template new file mode 100644 index 0000000000..0d9a8e27e6 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/after.kt.template @@ -0,0 +1,4 @@ +@Composable +fun Column() { + +} \ No newline at end of file diff --git a/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/before.kt.template b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/before.kt.template new file mode 100644 index 0000000000..645cdc2567 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/before.kt.template @@ -0,0 +1,4 @@ +@Composable +fun Column() { + Text("Abc") +} diff --git a/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/description.html b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/description.html new file mode 100644 index 0000000000..d20e311522 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/description.html @@ -0,0 +1,7 @@ + + +

+ A simple intention to remove a Composable altogether. +

+ + diff --git a/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/after.kt.template b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/after.kt.template new file mode 100644 index 0000000000..1e591257a4 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/after.kt.template @@ -0,0 +1,4 @@ +@Composable +fun Column() { + Text("Abc") +} diff --git a/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/before.kt.template b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/before.kt.template new file mode 100644 index 0000000000..0f9a85fbe7 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/before.kt.template @@ -0,0 +1,6 @@ +@Composable +fun Column() { + Button(){ + Text("Abc") + } +} diff --git a/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/description.html b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/description.html new file mode 100644 index 0000000000..8103575e27 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/description.html @@ -0,0 +1,7 @@ + + +

+ An intention to remove a parent Composable, and unwrap its children. +

+ + diff --git a/idea-plugin/src/main/resources/intentionDescriptions/WrapWithComposableIntentionGroup/description.html b/idea-plugin/src/main/resources/intentionDescriptions/WrapWithComposableIntentionGroup/description.html new file mode 100644 index 0000000000..9f4e5916d8 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/WrapWithComposableIntentionGroup/description.html @@ -0,0 +1,15 @@ + + +

+ A simple intention to wrap your Composables with another + Composable. Just keep your caret in the Editor on the composable, and press on the yellow bulb on the left, or press + Alt+Enter to show hints/intentions. You can choose to - + 1. Wrap with Box + 2. Wrap with Card + 3. Wrap with Column + 4. Wrap with Row + 5. Wrap with LazyColumn + 6. Wrap with LazyRow +

+ + diff --git a/idea-plugin/src/main/resources/templates/ComposeMultiplatformTemplates.xml b/idea-plugin/src/main/resources/templates/ComposeMultiplatformTemplates.xml new file mode 100644 index 0000000000..f313b6fc39 --- /dev/null +++ b/idea-plugin/src/main/resources/templates/ComposeMultiplatformTemplates.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + diff --git a/idea-plugin/src/test/kotlin/org/jetbrains/compose/intentions/utils/getRootPsiElement/GetRootPsiElementTest.kt b/idea-plugin/src/test/kotlin/org/jetbrains/compose/intentions/utils/getRootPsiElement/GetRootPsiElementTest.kt new file mode 100644 index 0000000000..b5c11e0e3c --- /dev/null +++ b/idea-plugin/src/test/kotlin/org/jetbrains/compose/intentions/utils/getRootPsiElement/GetRootPsiElementTest.kt @@ -0,0 +1,307 @@ +package org.jetbrains.compose.intentions.utils.getRootPsiElement + +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase +import junit.framework.TestCase +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtValueArgumentList +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class GetRootPsiElementTest : LightJavaCodeInsightFixtureTestCase() { + + private val getRootElement = GetRootPsiElement() + + @Test + fun `when a name reference expression is selected , but root is a property , the property should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + val systemUiController = rememberSystemUiController() + """.trimIndent() + + val file = ktPsiFactory.createFile(template) + + val property = file.lastChild as KtProperty + + val ktNameReferenceExpression = (property.lastChild as KtCallExpression).firstChild as KtNameReferenceExpression + + TestCase.assertEquals("rememberSystemUiController", ktNameReferenceExpression.text) + + TestCase.assertEquals(property, getRootElement.invoke(ktNameReferenceExpression)) + } + + @Test + fun `when a name reference expression is selected, with a call expression as root, call expression should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + fun Box(block:()->Unit) { + + } + + fun OuterComposable() { + Box() { + + } + } + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val ktNamedFunction = file.lastChild as KtNamedFunction + + val callExpression = ktNamedFunction.lastChild.children.find { it is KtCallExpression }!! + val ktNameReferenceExpression = callExpression.firstChild as KtNameReferenceExpression + + TestCase.assertEquals("Box", ktNameReferenceExpression.text) + + TestCase.assertEquals( + callExpression, + getRootElement.invoke(ktNameReferenceExpression) + ) + } + + @Test + fun `when an argument list element is selected, with a call expression as root, call expression should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + @Composable + fun Box(block:()->Unit) { + + } + + fun OuterComposable() { + // Argument List Element + // | + // v + Box() { + + } + } + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val ktNamedFunction = file.lastChild as KtNamedFunction + + val callExpression = ktNamedFunction.lastChild.children.find { it is KtCallExpression }!! + + val argumentListElement = callExpression.firstChild.nextSibling as KtValueArgumentList + + TestCase.assertEquals("()", argumentListElement.text) + + TestCase.assertEquals( + callExpression, + getRootElement.invoke(argumentListElement) + ) + } + + @Test + fun `when a name reference expression is selected, with a delegated property as root, property should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + var isComposable by remember { + true + } + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val property = file.lastChild as KtProperty + + val referenceExpression = property.lastChild.lastChild.firstChild as KtNameReferenceExpression + + TestCase.assertEquals("remember", referenceExpression.text) + + TestCase.assertEquals( + property, + getRootElement.invoke(referenceExpression) + ) + } + + @Test + fun `when a name reference expression with dot reference expression is selected, with a delegated property as root, property should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + val repeatingAnimation = rememberInfiniteTransition() + + val offset by repeatingAnimation.animateFloat( + 0f, + -20f, + infiniteRepeatable( + repeatMode = RepeatMode.Reverse, + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ) + ) + ) + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val property = file.lastChild as KtProperty + + val dotQualifiedExpression = property.lastChild.lastChild as KtDotQualifiedExpression + + val referenceExpression = dotQualifiedExpression.lastChild.firstChild as KtNameReferenceExpression + + TestCase.assertEquals("animateFloat", referenceExpression.text) + + TestCase.assertEquals( + property, + getRootElement.invoke(referenceExpression) + ) + } + + @Test + fun `when a name reference expression with dot reference expression is selected, with a property and dot qualified expression as root, property should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + val repeatingAnimation = rememberInfiniteTransition() + + val offset = repeatingAnimation.animateFloat( + 0f, + -20f, + infiniteRepeatable( + repeatMode = RepeatMode.Reverse, + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ) + ) + ) + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val property = file.lastChild as KtProperty + + val dotQualifiedExpression = property.lastChild as KtDotQualifiedExpression + + val referenceExpression = dotQualifiedExpression.lastChild.firstChild as KtNameReferenceExpression + + TestCase.assertEquals("animateFloat", referenceExpression.text) + + TestCase.assertEquals( + property, + getRootElement.invoke(referenceExpression) + ) + } + + @Test + fun `when a dot qualified expression is selected, with a delegated property as root, property should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + val repeatingAnimation = rememberInfiniteTransition() + + val offset by repeatingAnimation.animateFloat( + 0f, + -20f, + infiniteRepeatable( + repeatMode = RepeatMode.Reverse, + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ) + ) + ) + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val property = file.lastChild as KtProperty + + val dotQualifiedExpression = property.lastChild.lastChild as KtDotQualifiedExpression + + TestCase.assertEquals( + """ + repeatingAnimation.animateFloat( + 0f, + -20f, + infiniteRepeatable( + repeatMode = RepeatMode.Reverse, + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ) + ) + ) + """.trimIndent().trim(), dotQualifiedExpression.text + ) + + TestCase.assertEquals( + property, + getRootElement.invoke(dotQualifiedExpression) + ) + } + + @Test + fun `when a dot qualified expression is selected, with a property as root, property should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + val repeatingAnimation = rememberInfiniteTransition() + + val offset = repeatingAnimation.animateFloat( + 0f, + -20f, + infiniteRepeatable( + repeatMode = RepeatMode.Reverse, + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ) + ) + ) + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val property = file.lastChild as KtProperty + + val dotQualifiedExpression = property.lastChild as KtDotQualifiedExpression + + TestCase.assertEquals( + """ + repeatingAnimation.animateFloat( + 0f, + -20f, + infiniteRepeatable( + repeatMode = RepeatMode.Reverse, + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ) + ) + ) + """.trimIndent().trim(), dotQualifiedExpression.text + ) + + TestCase.assertEquals( + property, + getRootElement.invoke(dotQualifiedExpression) + ) + } +}