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()
+ )
+ }
+}