From 2731abd417a4a1709b584bfbcfeb37551c99d959 Mon Sep 17 00:00:00 2001 From: Ilia Bogdanovich Date: Wed, 6 Mar 2024 16:12:21 +0100 Subject: [PATCH] FL-24489: Adjusted `prev` live template logic Compose Preview template `prev` in IJ/AS now only shows up for Android source set. The hierachy of Kotlin contexts is replicated inside the "Android" group within the context selection. Compose Preview template `prev` in Fleet uses proper annotation, based on the source set where it is called: - For Android, it uses androidx Preview annotation. - For Common & JVM it uses jetbrains Preview annotation. - For iOS and other sources it is not showing up at all. Also, the template in Fleet shows up only for top-level declarations, fixing FL-23101, and `prevCol` template is only enabled for Android source set. GitOrigin-RevId: 6537741769014f659a1daa05318d2d8e541df6b7 --- .../intellij.android.templates.iml | 2 + .../intellij.android.templates.tests.iml | 2 + .../messages/TemplatesBundle.properties | 1 + .../src/META-INF/android-templates.xml | 8 ++ .../tools/idea/templates/TemplatesBundle.kt | 20 ++++ .../live/AndroidKotlinTemplateContextType.kt | 67 +++++++++++ .../AndroidKotlinTemplateContextTypeTest.kt | 105 ++++++++++++++++++ ...AndroidSourceSetTemplateContextTypeTest.kt | 82 ++++++++++++++ .../templates/AndroidComposePreview.xml | 17 ++- .../templates/AndroidComposePreviewTest.kt | 84 ++++++++++++++ 10 files changed, 379 insertions(+), 9 deletions(-) create mode 100644 android-templates/resources/messages/TemplatesBundle.properties create mode 100644 android-templates/src/com/android/tools/idea/templates/TemplatesBundle.kt create mode 100644 android-templates/src/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextType.kt create mode 100644 android-templates/testSrc/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextTypeTest.kt create mode 100644 android-templates/testSrc/com/android/tools/idea/templates/live/AndroidSourceSetTemplateContextTypeTest.kt create mode 100644 compose-ide-plugin/testSrc/com/android/tools/compose/templates/AndroidComposePreviewTest.kt diff --git a/android-templates/intellij.android.templates.iml b/android-templates/intellij.android.templates.iml index 4ef1814b014..196eb65e8fa 100644 --- a/android-templates/intellij.android.templates.iml +++ b/android-templates/intellij.android.templates.iml @@ -44,5 +44,7 @@ + + \ No newline at end of file diff --git a/android-templates/intellij.android.templates.tests.iml b/android-templates/intellij.android.templates.tests.iml index 08023f6b4b5..a1f769505af 100644 --- a/android-templates/intellij.android.templates.tests.iml +++ b/android-templates/intellij.android.templates.tests.iml @@ -51,5 +51,7 @@ + + \ No newline at end of file diff --git a/android-templates/resources/messages/TemplatesBundle.properties b/android-templates/resources/messages/TemplatesBundle.properties new file mode 100644 index 00000000000..b02b1926aae --- /dev/null +++ b/android-templates/resources/messages/TemplatesBundle.properties @@ -0,0 +1 @@ +templates.live.context.android=Android diff --git a/android-templates/src/META-INF/android-templates.xml b/android-templates/src/META-INF/android-templates.xml index ce71fb58003..d8c6fdfa819 100644 --- a/android-templates/src/META-INF/android-templates.xml +++ b/android-templates/src/META-INF/android-templates.xml @@ -24,6 +24,14 @@ + + + + + + + + diff --git a/android-templates/src/com/android/tools/idea/templates/TemplatesBundle.kt b/android-templates/src/com/android/tools/idea/templates/TemplatesBundle.kt new file mode 100644 index 00000000000..f557eaaf1e8 --- /dev/null +++ b/android-templates/src/com/android/tools/idea/templates/TemplatesBundle.kt @@ -0,0 +1,20 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.android.tools.idea.templates + +import com.intellij.DynamicBundle +import com.intellij.openapi.util.NlsContexts +import org.jetbrains.annotations.PropertyKey + +private const val BUNDLE_NAME = "messages.TemplatesBundle" + +class TemplatesBundle private constructor() { + companion object { + private val ourBundle = DynamicBundle(TemplatesBundle::class.java, BUNDLE_NAME) + + @NlsContexts.Label + @JvmStatic + fun message(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg params: Any?): String { + return ourBundle.getMessage(key, *params) + } + } +} diff --git a/android-templates/src/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextType.kt b/android-templates/src/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextType.kt new file mode 100644 index 00000000000..70b8566e79b --- /dev/null +++ b/android-templates/src/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextType.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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 com.android.tools.idea.templates.live + +import com.android.tools.idea.templates.TemplatesBundle +import com.intellij.codeInsight.template.TemplateActionContext +import com.intellij.codeInsight.template.TemplateContextType +import com.intellij.openapi.roots.ProjectFileIndex +import org.jetbrains.android.facet.AndroidFacet +import org.jetbrains.kotlin.idea.base.util.module +import org.jetbrains.kotlin.idea.liveTemplates.KotlinTemplateContextType + +/** + * This [TemplateContextType] replicates the structure of [KotlinTemplateContextType], + * intersecting it with the [AndroidSourceSetTemplateContextType]. + */ +internal sealed class AndroidKotlinTemplateContextType( + private val kotlin: KotlinTemplateContextType, +) : TemplateContextType(kotlin.presentableName) { + private val android = AndroidSourceSetTemplateContextType() + override fun isInContext(templateActionContext: TemplateActionContext): Boolean { + return android.isInContext(templateActionContext) && kotlin.isInContext(templateActionContext) + } + + class Generic : AndroidKotlinTemplateContextType(KotlinTemplateContextType.Generic()) + + class TopLevel : AndroidKotlinTemplateContextType(KotlinTemplateContextType.TopLevel()) + + class ObjectDeclaration : AndroidKotlinTemplateContextType(KotlinTemplateContextType.ObjectDeclaration()) + + class Class : AndroidKotlinTemplateContextType(KotlinTemplateContextType.Class()) + + class Statement : AndroidKotlinTemplateContextType(KotlinTemplateContextType.Statement()) + + class Expression : AndroidKotlinTemplateContextType(KotlinTemplateContextType.Expression()) + + class Comment : AndroidKotlinTemplateContextType(KotlinTemplateContextType.Comment()) +} + +/** + * Checks if the template is applied to an Android-specific source set. + * This template is used to hide the Android-related templates from unrelated to Android source sets (like common, jvm, ios, etc.) + */ +internal class AndroidSourceSetTemplateContextType : TemplateContextType( + TemplatesBundle.message("templates.live.context.android") +) { + override fun isInContext(templateActionContext: TemplateActionContext): Boolean { + val file = templateActionContext.file + val module = file.module ?: ProjectFileIndex.getInstance(file.project) + .getModuleForFile(file.virtualFile ?: file.viewProvider.virtualFile) + if (module == null || module.isDisposed) return false + return AndroidFacet.getInstance(module) != null + } +} diff --git a/android-templates/testSrc/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextTypeTest.kt b/android-templates/testSrc/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextTypeTest.kt new file mode 100644 index 00000000000..4201ddb3ae9 --- /dev/null +++ b/android-templates/testSrc/com/android/tools/idea/templates/live/AndroidKotlinTemplateContextTypeTest.kt @@ -0,0 +1,105 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.android.tools.idea.templates.live + +import com.intellij.codeInsight.template.TemplateActionContext +import org.jetbrains.kotlin.idea.liveTemplates.KotlinTemplateContextType +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Mockito.mock +import org.mockito.Mockito.mockConstruction +import org.mockito.Mockito.`when` +import kotlin.reflect.KClass + +/** + * Unit-test for [AndroidKotlinTemplateContextType] + */ +@RunWith(Parameterized::class) +class AndroidKotlinTemplateContextTypeTest( + private val kotlinTemplateClass: KClass, + private val androidInContext: Boolean, + private val kotlinInContext: Boolean, + private val expectedResult: Boolean, +) { + @Test + fun test() { + val templateActionContext = mock() + withMockedAndroidSourceSet(templateActionContext) { + withMockedKotlinTemplate(templateActionContext) { + // Prepare + val context = AndroidKotlinTemplateContextType.Generic() + + // Do + val result = context.isInContext(templateActionContext) + + // Check + assertEquals(expectedResult, result) + } + } + } + + private fun withMockedAndroidSourceSet(templateActionContext: TemplateActionContext, block: () -> Unit) { + mockConstruction(AndroidSourceSetTemplateContextType::class.java) { mock, _ -> + `when`(mock.isInContext(templateActionContext)).thenReturn(androidInContext) + `when`(mock.presentableName).thenReturn("name") + block() + }.close() + } + + private fun withMockedKotlinTemplate(templateActionContext: TemplateActionContext, block: () -> Unit) { + mockConstruction(kotlinTemplateClass.java) { mock, _ -> + `when`(mock.isInContext(templateActionContext)).thenReturn(kotlinInContext) + `when`(mock.presentableName).thenReturn("name") + block() + }.close() + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "class={0} android={1} kotlin={2}; result={3}") + fun testData(): Array> = arrayOf( + // Generic + arrayOf(KotlinTemplateContextType.Generic::class, true, true, true), + arrayOf(KotlinTemplateContextType.Generic::class, true, false, false), + arrayOf(KotlinTemplateContextType.Generic::class, false, true, false), + arrayOf(KotlinTemplateContextType.Generic::class, false, false, false), + + // TopLevel + arrayOf(KotlinTemplateContextType.TopLevel::class, true, true, true), + arrayOf(KotlinTemplateContextType.TopLevel::class, true, false, false), + arrayOf(KotlinTemplateContextType.TopLevel::class, false, true, false), + arrayOf(KotlinTemplateContextType.TopLevel::class, false, false, false), + + // ObjectDeclaration + arrayOf(KotlinTemplateContextType.ObjectDeclaration::class, true, true, true), + arrayOf(KotlinTemplateContextType.ObjectDeclaration::class, true, false, false), + arrayOf(KotlinTemplateContextType.ObjectDeclaration::class, false, true, false), + arrayOf(KotlinTemplateContextType.ObjectDeclaration::class, false, false, false), + + // Class + arrayOf(KotlinTemplateContextType.Class::class, true, true, true), + arrayOf(KotlinTemplateContextType.Class::class, true, false, false), + arrayOf(KotlinTemplateContextType.Class::class, false, true, false), + arrayOf(KotlinTemplateContextType.Class::class, false, false, false), + + // Statement + arrayOf(KotlinTemplateContextType.Statement::class, true, true, true), + arrayOf(KotlinTemplateContextType.Statement::class, true, false, false), + arrayOf(KotlinTemplateContextType.Statement::class, false, true, false), + arrayOf(KotlinTemplateContextType.Statement::class, false, false, false), + + // Expression + arrayOf(KotlinTemplateContextType.Expression::class, true, true, true), + arrayOf(KotlinTemplateContextType.Expression::class, true, false, false), + arrayOf(KotlinTemplateContextType.Expression::class, false, true, false), + arrayOf(KotlinTemplateContextType.Expression::class, false, false, false), + + // Comment + arrayOf(KotlinTemplateContextType.Comment::class, true, true, true), + arrayOf(KotlinTemplateContextType.Comment::class, true, false, false), + arrayOf(KotlinTemplateContextType.Comment::class, false, true, false), + arrayOf(KotlinTemplateContextType.Comment::class, false, false, false), + ) + } +} diff --git a/android-templates/testSrc/com/android/tools/idea/templates/live/AndroidSourceSetTemplateContextTypeTest.kt b/android-templates/testSrc/com/android/tools/idea/templates/live/AndroidSourceSetTemplateContextTypeTest.kt new file mode 100644 index 00000000000..ce82f63a6f7 --- /dev/null +++ b/android-templates/testSrc/com/android/tools/idea/templates/live/AndroidSourceSetTemplateContextTypeTest.kt @@ -0,0 +1,82 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.android.tools.idea.templates.live + +import com.intellij.codeInsight.template.TemplateActionContext +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.psi.PsiFile +import org.jetbrains.android.facet.AndroidFacet +import org.junit.AfterClass +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.BeforeClass +import org.junit.Test +import org.mockito.MockedStatic +import org.mockito.Mockito.mock +import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.`when` + +/** + * Unit-test for [AndroidSourceSetTemplateContextType] + */ +class AndroidSourceSetTemplateContextTypeTest { + private val context = AndroidSourceSetTemplateContextType() + + @Test + fun `android facet is present`() { + // Prepare + val templateActionContext = mockedTemplateActionContext(hasAndroidFacet = true) + + // Do + val result = context.isInContext(templateActionContext) + + // Check + assertTrue(result) + } + + @Test + fun `android facet is not present`() { + // Prepare + val templateActionContext = mockedTemplateActionContext(hasAndroidFacet = false) + + // Do + val result = context.isInContext(templateActionContext) + + // Check + assertFalse(result) + } + + private fun mockedTemplateActionContext(hasAndroidFacet: Boolean): TemplateActionContext { + val templateActionContext = mock() + val file = mock() + val module = mock() + `when`(templateActionContext.file).thenReturn(file) + `when`(ModuleUtilCore.findModuleForPsiElement(file)).thenReturn(module) + if (hasAndroidFacet) { + val androidFacet = mock() + `when`(AndroidFacet.getInstance(module)).thenReturn(androidFacet) + } else { + `when`(AndroidFacet.getInstance(module)).thenReturn(null) + } + return templateActionContext + } + + companion object { + private lateinit var mockedModuleUtilCore: MockedStatic + private lateinit var mockedAndroidFacet: MockedStatic + + @JvmStatic + @BeforeClass + fun setUp() { + mockedModuleUtilCore = mockStatic(ModuleUtilCore::class.java) + mockedAndroidFacet = mockStatic(AndroidFacet::class.java) + } + + @JvmStatic + @AfterClass + fun tearDown() { + mockedModuleUtilCore.close() + mockedAndroidFacet.close() + } + } +} diff --git a/compose-ide-plugin/resources/templates/AndroidComposePreview.xml b/compose-ide-plugin/resources/templates/AndroidComposePreview.xml index 35585a87cbb..7770cb13f6d 100644 --- a/compose-ide-plugin/resources/templates/AndroidComposePreview.xml +++ b/compose-ide-plugin/resources/templates/AndroidComposePreview.xml @@ -7,8 +7,8 @@ toShortenFQNames="true"> - @@ -22,14 +22,13 @@ - - diff --git a/compose-ide-plugin/testSrc/com/android/tools/compose/templates/AndroidComposePreviewTest.kt b/compose-ide-plugin/testSrc/com/android/tools/compose/templates/AndroidComposePreviewTest.kt new file mode 100644 index 00000000000..caba93dba3f --- /dev/null +++ b/compose-ide-plugin/testSrc/com/android/tools/compose/templates/AndroidComposePreviewTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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 com.android.tools.compose.templates + +import com.android.tools.idea.testing.caret +import com.android.tools.idea.testing.loadNewFile +import com.intellij.codeInsight.template.impl.InvokeTemplateAction +import com.intellij.codeInsight.template.impl.LiveTemplateCompletionContributor +import com.intellij.codeInsight.template.impl.TemplateManagerImpl +import com.intellij.codeInsight.template.impl.TemplateSettings +import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase + +class AndroidComposePreviewTest : JavaCodeInsightFixtureTestCase() { + override fun setUp() { + super.setUp() + LiveTemplateCompletionContributor.setShowTemplatesInTests(true, myFixture.testRootDisposable) + TemplateManagerImpl.setTemplateTesting(myFixture.testRootDisposable) + } + + fun testPrevTemplate() { + myFixture.loadNewFile( + "src/com/example/Test.kt", + // language=kotlin + """ + package com.example + + $caret + """.trimIndent() + ) + + val template = TemplateSettings.getInstance().getTemplate("prev", "AndroidComposePreview") + InvokeTemplateAction(template, myFixture.editor, project, HashSet()).perform() + + myFixture.checkResult( + // language=kotlin + """ + package com.example + + @androidx.compose.ui.tooling.preview.Preview + @androidx.compose.runtime.Composable + fun () { + + } + """.trimIndent() + ) + } + + fun testPrevColTemplate() { + myFixture.loadNewFile( + "src/com/example/Test.kt", + // language=kotlin + """ + package com.example + + $caret + """.trimIndent() + ) + + val template = TemplateSettings.getInstance().getTemplate("prevCol", "AndroidComposePreview") + InvokeTemplateAction(template, myFixture.editor, project, HashSet()).perform() + + myFixture.checkResult( + // language=kotlin + """ + package com.example + + class : CollectionPreviewParameterProvider<>(listOf()) + """.trimIndent() + ) + } +}