From e80fdb3742771a1e673370077b7148e7dbc1c3ea Mon Sep 17 00:00:00 2001 From: thelumiereguy Date: Wed, 26 Jan 2022 18:32:06 +0530 Subject: [PATCH] integrated plugin and all its files --- .../RemoveComposableIntention.kt | 40 ++++ .../RemoveParentComposableIntention.kt | 56 +++++ .../ComposableFunctionFinder.kt | 8 + .../DeepComposableFunctionFinder.kt | 42 ++++ .../NestedComposableFinder.kt | 34 +++ .../utils/get_root_element/GetRootElement.kt | 39 ++++ .../IsIntentionAvailable.kt | 25 ++ .../IsPsiElementComposable.kt | 21 ++ .../WrapWithComposableIntentionGroup.kt | 72 ++++++ .../BaseWrapWithComposableAction.kt | 54 +++++ .../wrap_with_actions/WrapWithActions.kt | 70 ++++++ .../src/main/resources/META-INF/plugin.xml | 26 +++ .../after.kt.template | 0 .../before.kt.template | 5 + .../description.html | 7 + .../after.kt.template | 4 + .../before.kt.template | 6 + .../description.html | 7 + .../description.html | 15 ++ .../templates/WrappedComposables.xml | 65 ++++++ .../get_root_element/GetRootElementTest.kt | 216 ++++++++++++++++++ 21 files changed, 812 insertions(+) create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_composable/RemoveComposableIntention.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_parent_composable/RemoveParentComposableIntention.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/ComposableFunctionFinder.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/DeepComposableFunctionFinder.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/NestedComposableFinder.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/get_root_element/GetRootElement.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/is_intention_available/IsIntentionAvailable.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/is_psi_element_composable/IsPsiElementComposable.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/WrapWithComposableIntentionGroup.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/wrap_with_actions/BaseWrapWithComposableAction.kt create mode 100644 idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/wrap_with_actions/WrapWithActions.kt create mode 100644 idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/after.kt.template create mode 100644 idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/before.kt.template create mode 100644 idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/description.html create mode 100644 idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/after.kt.template create mode 100644 idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/before.kt.template create mode 100644 idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/description.html create mode 100644 idea-plugin/src/main/resources/intentionDescriptions/WrapWithComposableIntentionGroup/description.html create mode 100644 idea-plugin/src/main/resources/templates/WrappedComposables.xml create mode 100644 idea-plugin/src/test/kotlin/org/jetbrains/compose/intentions/utils/get_root_element/GetRootElementTest.kt diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_composable/RemoveComposableIntention.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_composable/RemoveComposableIntention.kt new file mode 100644 index 0000000000..137613c87e --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_composable/RemoveComposableIntention.kt @@ -0,0 +1,40 @@ +package org.jetbrains.compose.intentions.remove_composable + +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.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.is_intention_available.IsIntentionAvailable +import org.jetbrains.compose.intentions.utils.composable_finder.ComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.composable_finder.DeepComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.get_root_element.GetRootElement + +class RemoveComposableIntention : PsiElementBaseIntentionAction(), Iconable, LowPriorityAction, IsIntentionAvailable { + + override fun getText(): String { + return "Remove this Composable" + } + + override fun getFamilyName(): String { + return "Compose Multiplatform intentions" + } + + private val composableFunctionFinder: ComposableFunctionFinder = DeepComposableFunctionFinder() + + private val getRootElement = GetRootElement() + + override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean { + return element.isAvailable(composableFunctionFinder) + } + + override fun invoke(project: Project, editor: Editor?, element: PsiElement) { + getRootElement(element.parent)?.delete() + } + + override fun getIcon(flags: Int): Icon = PreviewIcons.COMPOSE + +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_parent_composable/RemoveParentComposableIntention.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_parent_composable/RemoveParentComposableIntention.kt new file mode 100644 index 0000000000..eed5e7a926 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/remove_parent_composable/RemoveParentComposableIntention.kt @@ -0,0 +1,56 @@ +package org.jetbrains.compose.intentions.remove_parent_composable + +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 com.intellij.psi.util.parentOfType +import javax.swing.Icon +import org.jetbrains.compose.desktop.ide.preview.PreviewIcons +import org.jetbrains.compose.intentions.utils.is_intention_available.IsIntentionAvailable +import org.jetbrains.compose.intentions.utils.composable_finder.ComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.composable_finder.NestedComposableFinder +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtValueArgumentList + +class RemoveParentComposableIntention : PsiElementBaseIntentionAction(), + Iconable, + PriorityAction, + IsIntentionAvailable { + + override fun getText(): String { + return "Remove the parent Composable" + } + + override fun getFamilyName(): String { + return "Compose Multiplatform intentions" + } + + private val composableFunctionFinder: ComposableFunctionFinder = NestedComposableFinder() + + override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean { + return element.isAvailable(composableFunctionFinder) + } + + override fun invoke(project: Project, editor: Editor?, element: PsiElement) { + val wrapper = if (element.parent is KtValueArgumentList) { + element.parent.prevSibling as? KtNameReferenceExpression ?: return + } else { + element.parentOfType() ?: return + } + val callExpression = (wrapper.parent as? KtCallExpression) ?: return + val lambdaBlock = + callExpression.lambdaArguments.firstOrNull()?.getLambdaExpression()?.functionLiteral?.bodyExpression + ?: return + callExpression.replace(lambdaBlock) + } + + override fun getIcon(flags: Int): Icon = PreviewIcons.COMPOSE + + override fun getPriority(): PriorityAction.Priority { + return PriorityAction.Priority.NORMAL + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/ComposableFunctionFinder.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/ComposableFunctionFinder.kt new file mode 100644 index 0000000000..5874b7af1c --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/ComposableFunctionFinder.kt @@ -0,0 +1,8 @@ +package org.jetbrains.compose.intentions.utils.composable_finder + +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/composable_finder/DeepComposableFunctionFinder.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/DeepComposableFunctionFinder.kt new file mode 100644 index 0000000000..f130acd35a --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/DeepComposableFunctionFinder.kt @@ -0,0 +1,42 @@ +package org.jetbrains.compose.intentions.utils.composable_finder + +import com.intellij.psi.PsiElement +import org.jetbrains.compose.intentions.utils.is_psi_element_composable.IsPsiElementComposable +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 DeepComposableFunctionFinder : ComposableFunctionFinder, IsPsiElementComposable { + + override fun isFunctionComposable(psiElement: PsiElement): Boolean { + return when (psiElement) { + is KtNameReferenceExpression -> psiElement.isComposable() + is KtCallExpression -> 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/composable_finder/NestedComposableFinder.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/NestedComposableFinder.kt new file mode 100644 index 0000000000..7b50cd79ec --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/composable_finder/NestedComposableFinder.kt @@ -0,0 +1,34 @@ +package org.jetbrains.compose.intentions.utils.composable_finder + +import com.intellij.psi.PsiElement +import org.jetbrains.compose.intentions.utils.is_psi_element_composable.IsPsiElementComposable +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtLambdaArgument +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType + + +class NestedComposableFinder : ComposableFunctionFinder, IsPsiElementComposable { + + 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/get_root_element/GetRootElement.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/get_root_element/GetRootElement.kt new file mode 100644 index 0000000000..a4d5087c76 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/get_root_element/GetRootElement.kt @@ -0,0 +1,39 @@ +package org.jetbrains.compose.intentions.utils.get_root_element + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.* + +/** + * KtValueArgumentList -> Parent -> KtNameReferenceExpression -> Parent -> KtCallExpression -> Parent -> KtPropertyDelegate -> Parent -> Property + * KtNameReferenceExpression -> Parent -> KtCallExpression -> Parent -> KtDotQualifiedExpression -> Parent -> KtPropertyDelegate -> Property + * KtNameReferenceExpression -> Parent -> KtCallExpression -> Parent -> KtPropertyDelegate -> Parent -> Property + * KtNameReferenceExpression -> Parent -> KtCallExpression -> Parent -> Property + * KtNameReferenceExpression -> Parent -> KtCallExpression + **/ +class GetRootElement { + + /** + * element can be CallExpression (Composable Function) or Property (Composable Property like remember) + */ + tailrec operator fun invoke(element: PsiElement, iteration: Int = 0): PsiElement? { + if (iteration > 5) { // fail safe + return null + } + + return when (element) { + is KtProperty -> element + is KtNameReferenceExpression, + is KtValueArgumentList -> invoke(element.parent, iteration + 1) + is KtDotQualifiedExpression, + is KtCallExpression -> { + when (element.parent) { + is KtProperty, + is KtDotQualifiedExpression -> invoke(element.parent, iteration + 1) //composable dot expression + is KtPropertyDelegate -> invoke(element.parent.parent, iteration + 1) //composable dot expression + else -> element + } + } + else -> element + } + } +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/is_intention_available/IsIntentionAvailable.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/is_intention_available/IsIntentionAvailable.kt new file mode 100644 index 0000000000..ac3dca4005 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/is_intention_available/IsIntentionAvailable.kt @@ -0,0 +1,25 @@ +package org.jetbrains.compose.intentions.utils.is_intention_available + +import com.intellij.psi.PsiElement +import org.jetbrains.compose.intentions.utils.composable_finder.ComposableFunctionFinder +import org.jetbrains.kotlin.idea.KotlinLanguage + +interface IsIntentionAvailable { + + fun PsiElement.isAvailable( + 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/is_psi_element_composable/IsPsiElementComposable.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/is_psi_element_composable/IsPsiElementComposable.kt new file mode 100644 index 0000000000..f5334eb9ee --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/utils/is_psi_element_composable/IsPsiElementComposable.kt @@ -0,0 +1,21 @@ +package org.jetbrains.compose.intentions.utils.is_psi_element_composable + +import org.jetbrains.compose.desktop.ide.preview.isComposableFunction +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 + +interface IsPsiElementComposable { + + fun KtCallExpression.isComposable(): Boolean { + return getChildOfType()?.isComposable() ?: false + } + + fun KtNameReferenceExpression.isComposable(): Boolean { + val ktNamedFunction = resolve() as? KtNamedFunction ?: return false + return ktNamedFunction.isComposableFunction() + } + +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/WrapWithComposableIntentionGroup.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/WrapWithComposableIntentionGroup.kt new file mode 100644 index 0000000000..9f852974cb --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/WrapWithComposableIntentionGroup.kt @@ -0,0 +1,72 @@ +package org.jetbrains.compose.intentions.wrap_with_composable + +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.openapi.util.Iconable +import com.intellij.psi.PsiFile +import com.intellij.ui.popup.list.ListPopupImpl +import javax.swing.Icon +import org.jetbrains.compose.desktop.ide.preview.PreviewIcons +import org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions.BaseWrapWithComposableAction +import org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions.WrapWithBoxIntention +import org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions.WrapWithCardIntention +import org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions.WrapWithColumnIntention +import org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions.WrapWithLzyColumnIntention +import org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions.WrapWithLzyRowIntention +import org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions.WrapWithRowIntention + +class WrapWithComposableIntentionGroup : + IntentionActionGroup( + listOf( + WrapWithBoxIntention(), + WrapWithCardIntention(), + WrapWithColumnIntention(), + WrapWithRowIntention(), + WrapWithLzyColumnIntention(), + WrapWithLzyRowIntention() + ) + ), Iconable { + + 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" + } + + override fun getIcon(flags: Int): Icon = PreviewIcons.COMPOSE +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/wrap_with_actions/BaseWrapWithComposableAction.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/wrap_with_actions/BaseWrapWithComposableAction.kt new file mode 100644 index 0000000000..5f1f57e551 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/wrap_with_actions/BaseWrapWithComposableAction.kt @@ -0,0 +1,54 @@ +package org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions + +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.is_intention_available.IsIntentionAvailable +import org.jetbrains.compose.intentions.utils.composable_finder.ComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.composable_finder.DeepComposableFunctionFinder +import org.jetbrains.compose.intentions.utils.get_root_element.GetRootElement + +abstract class BaseWrapWithComposableAction : PsiElementBaseIntentionAction(), + HighPriorityAction, + IsIntentionAvailable { + + private val composableFunctionFinder: ComposableFunctionFinder by lazy { + DeepComposableFunctionFinder() + } + + private val getRootElement by lazy { + GetRootElement() + } + + override fun getFamilyName(): String { + return "Compose Multiplatform intentions" + } + + override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean { + return element.isAvailable(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/wrap_with_composable/wrap_with_actions/WrapWithActions.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/wrap_with_actions/WrapWithActions.kt new file mode 100644 index 0000000000..2947413848 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/intentions/wrap_with_composable/wrap_with_actions/WrapWithActions.kt @@ -0,0 +1,70 @@ +package org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions + +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("WwB", "ComposeMultiplatformTemplates") + } +} + +class WrapWithCardIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with Card" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("WwC", "ComposeMultiplatformTemplates") + } +} + +class WrapWithColumnIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with Column" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("WwCol", "ComposeMultiplatformTemplates") + } +} + +class WrapWithRowIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with Row" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("WwRow", "ComposeMultiplatformTemplates") + } +} + +class WrapWithLzyColumnIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with LazyColumn" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("WwLazyCol", "ComposeMultiplatformTemplates") + } +} + +class WrapWithLzyRowIntention : BaseWrapWithComposableAction() { + + override fun getText(): String { + return "Wrap with LazyRow" + } + + override fun getTemplate(): TemplateImpl? { + return TemplateSettings.getInstance().getTemplate("WwLazyRow", "ComposeMultiplatformTemplates") + } +} diff --git a/idea-plugin/src/main/resources/META-INF/plugin.xml b/idea-plugin/src/main/resources/META-INF/plugin.xml index 9c1c8fcb9f..d7766ff62a 100644 --- a/idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/idea-plugin/src/main/resources/META-INF/plugin.xml @@ -49,4 +49,30 @@ + + + + + + + + org.jetbrains.compose.intentions.wrap_with_composable.WrapWithComposableIntentionGroup + + Composable intentions + + + + org.jetbrains.compose.intentions.remove_composable.RemoveComposableIntention + + Composable intentions + + + + org.jetbrains.compose.intentions.remove_parent_composable.RemoveParentComposableIntention + + Composable intentions + + + + 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..e69de29bb2 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..1790311b19 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/before.kt.template @@ -0,0 +1,5 @@ +@Composable +fun Column() { + Text("Abc") + 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..f1701ba0ab --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveComposableIntention/description.html @@ -0,0 +1,7 @@ + + +

+ A simple intention to remove the 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..4ef8977c78 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/after.kt.template @@ -0,0 +1,4 @@ +@Composable +Button() { + 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..5e20244dd9 --- /dev/null +++ b/idea-plugin/src/main/resources/intentionDescriptions/RemoveParentComposableIntention/description.html @@ -0,0 +1,7 @@ + + +

+ A simple intention to just remove the 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/WrappedComposables.xml b/idea-plugin/src/main/resources/templates/WrappedComposables.xml new file mode 100644 index 0000000000..e98ff7c158 --- /dev/null +++ b/idea-plugin/src/main/resources/templates/WrappedComposables.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + diff --git a/idea-plugin/src/test/kotlin/org/jetbrains/compose/intentions/utils/get_root_element/GetRootElementTest.kt b/idea-plugin/src/test/kotlin/org/jetbrains/compose/intentions/utils/get_root_element/GetRootElementTest.kt new file mode 100644 index 0000000000..3ef4d9fab8 --- /dev/null +++ b/idea-plugin/src/test/kotlin/org/jetbrains/compose/intentions/utils/get_root_element/GetRootElementTest.kt @@ -0,0 +1,216 @@ +package org.jetbrains.compose.intentions.utils.get_root_element + +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 GetRootElementTest : LightJavaCodeInsightFixtureTestCase() { + + private val getRootElement = GetRootElement() + + @Test + fun `when a name reference expression is selected , but root is a property , the property should be returned as root element`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + val systemUiController = rememberSystemUiController() + """.trimIndent() + .trim() + + val file = ktPsiFactory.createFile(template) + + val property = file.lastChild as KtProperty + + val ktNameReferenceExpression = (property.lastChild as KtCallExpression).firstChild as KtNameReferenceExpression + + TestCase.assertEquals(property, getRootElement(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 = """ + @Composable + fun Box(block:()->Unit) { + + } + + fun OuterComposable() { + // Call Expression - Box + // | + // v + Box() { + + } + } + """.trimIndent().trim() + + val file = ktPsiFactory.createFile(template) + + val ktNamedFunction = file.lastChild as KtNamedFunction + + val callExpression = ktNamedFunction.lastChild.children.find { it is KtCallExpression }!! + + TestCase.assertEquals( + callExpression, + getRootElement(callExpression.firstChild as 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() { + + } // Name Reference Expression + } + """.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( + callExpression, + getRootElement(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 = """ + // Delegated property + // | + // v + 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( + property, + getRootElement(referenceExpression) + ) + } + + @Test + fun `when a name reference expression is selected, with a delegated property with dot qualified expression as root, property should be returned`() { + val ktPsiFactory = KtPsiFactory(project) + + @Language("Kotlin") + val template = """ + val repeatingAnimation = rememberInfiniteTransition() + + // Dot qualified expression + // | + // v + 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( + property, + getRootElement(referenceExpression) + ) + } + + + @Test + fun `when a name 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() + + // Dot qualified expression + // | + // v + 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( + property, + getRootElement(referenceExpression) + ) + } + +}