Skip to content

Commit

Permalink
FL-24489: Adjusted prev live template logic
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Ilia Bogdanovich authored and intellij-monorepo-bot committed Mar 25, 2024
1 parent 411f72a commit 2731abd
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 9 deletions.
2 changes: 2 additions & 0 deletions android-templates/intellij.android.templates.iml
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,7 @@
<orderEntry type="module" module-name="kotlin.base.plugin" />
<orderEntry type="module" module-name="kotlin.base.project-structure" />
<orderEntry type="module" module-name="kotlin.base.facet" />
<orderEntry type="module" module-name="kotlin.code-insight.live-templates.shared" />
<orderEntry type="module" module-name="kotlin.base.util" />
</component>
</module>
2 changes: 2 additions & 0 deletions android-templates/intellij.android.templates.tests.iml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,7 @@
<orderEntry type="module" module-name="intellij.platform.ide.core.impl" scope="TEST" />
<orderEntry type="module" module-name="intellij.java.testFramework" scope="TEST" />
<orderEntry type="library" scope="TEST" name="Guava" level="project" />
<orderEntry type="module" module-name="kotlin.code-insight.live-templates.shared" scope="TEST" />
</component>
<component name="TestModuleProperties" production-module="intellij.android.templates" />
</module>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
templates.live.context.android=Android
8 changes: 8 additions & 0 deletions android-templates/src/META-INF/android-templates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
<defaultLiveTemplates file="liveTemplates/AndroidParcelable"/>
<defaultLiveTemplates file="liveTemplates/AndroidXML"/>
<liveTemplateContext contextId="XML_ATTRIBUTE" implementation="com.android.tools.idea.templates.live.XmlAttributeContextType"/>
<liveTemplateContext contextId="ANDROID" implementation="com.android.tools.idea.templates.live.AndroidSourceSetTemplateContextType" />
<liveTemplateContext contextId="ANDROID_KOTLIN" baseContextId="ANDROID" implementation="com.android.tools.idea.templates.live.AndroidKotlinTemplateContextType$Generic"/>
<liveTemplateContext contextId="ANDROID_KOTLIN_TOP_LEVEL" baseContextId="ANDROID_KOTLIN" implementation="com.android.tools.idea.templates.live.AndroidKotlinTemplateContextType$TopLevel"/>
<liveTemplateContext contextId="ANDROID_KOTLIN_STATEMENT" baseContextId="ANDROID_KOTLIN" implementation="com.android.tools.idea.templates.live.AndroidKotlinTemplateContextType$Statement"/>
<liveTemplateContext contextId="ANDROID_KOTLIN_CLASS" baseContextId="ANDROID_KOTLIN" implementation="com.android.tools.idea.templates.live.AndroidKotlinTemplateContextType$Class"/>
<liveTemplateContext contextId="ANDROID_KOTLIN_EXPRESSION" baseContextId="ANDROID_KOTLIN" implementation="com.android.tools.idea.templates.live.AndroidKotlinTemplateContextType$Expression"/>
<liveTemplateContext contextId="ANDROID_KOTLIN_COMMENT" baseContextId="ANDROID_KOTLIN" implementation="com.android.tools.idea.templates.live.AndroidKotlinTemplateContextType$Comment"/>
<liveTemplateContext contextId="ANDROID_KOTLIN_OBJECT_DECLARATION" baseContextId="ANDROID_KOTLIN" implementation="com.android.tools.idea.templates.live.AndroidKotlinTemplateContextType$ObjectDeclaration"/>
</extensions>

<extensionPoints>
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<out KotlinTemplateContextType>,
private val androidInContext: Boolean,
private val kotlinInContext: Boolean,
private val expectedResult: Boolean,
) {
@Test
fun test() {
val templateActionContext = mock<TemplateActionContext>()
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<Array<Any>> = 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),
)
}
}
Original file line number Diff line number Diff line change
@@ -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<TemplateActionContext>()
val file = mock<PsiFile>()
val module = mock<Module>()
`when`(templateActionContext.file).thenReturn(file)
`when`(ModuleUtilCore.findModuleForPsiElement(file)).thenReturn(module)
if (hasAndroidFacet) {
val androidFacet = mock<AndroidFacet>()
`when`(AndroidFacet.getInstance(module)).thenReturn(androidFacet)
} else {
`when`(AndroidFacet.getInstance(module)).thenReturn(null)
}
return templateActionContext
}

companion object {
private lateinit var mockedModuleUtilCore: MockedStatic<ModuleUtilCore>
private lateinit var mockedAndroidFacet: MockedStatic<AndroidFacet>

@JvmStatic
@BeforeClass
fun setUp() {
mockedModuleUtilCore = mockStatic(ModuleUtilCore::class.java)
mockedAndroidFacet = mockStatic(AndroidFacet::class.java)
}

@JvmStatic
@AfterClass
fun tearDown() {
mockedModuleUtilCore.close()
mockedAndroidFacet.close()
}
}
}
17 changes: 8 additions & 9 deletions compose-ide-plugin/resources/templates/AndroidComposePreview.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
toShortenFQNames="true">
<variable name="NAME" expression="" defaultValue="" alwaysStopAt="true"/>
<context>
<option name="KOTLIN" value="true"/>
<option name="KOTLIN_COMMENT" value="false"/>
<option name="ANDROID_KOTLIN" value="true" />
<option name="ANDROID_KOTLIN_COMMENT" value="false"/>
</context>
</template>

Expand All @@ -22,14 +22,13 @@
<variable name="TYPE" expression="" defaultValue="" alwaysStopAt="true"/>
<variable name="DATA" expression="" defaultValue="" alwaysStopAt="true"/>
<context>
<option name="KOTLIN" value="true"/>
<option name="KOTLIN_CLASS" value="false"/>
<option name="KOTLIN_COMMENT" value="false"/>
<option name="KOTLIN_EXPRESSION" value="false"/>
<option name="KOTLIN_OBJECT_DECLARATION" value="false"/>
<option name="KOTLIN_STATEMENT" value="false"/>
<option name="ANDROID_KOTLIN" value="true" />
<option name="ANDROID_KOTLIN_CLASS" value="false"/>
<option name="ANDROID_KOTLIN_COMMENT" value="false"/>
<option name="ANDROID_KOTLIN_EXPRESSION" value="false"/>
<option name="ANDROID_KOTLIN_OBJECT_DECLARATION" value="false"/>
<option name="ANDROID_KOTLIN_STATEMENT" value="false"/>
</context>
</template>
<!-- endregion -->
</templateSet>

Loading

0 comments on commit 2731abd

Please sign in to comment.