Browse Source

[Do not merge] Update from recent changes in AS, bump to compose 1.2.0

compose-tooling
Ilya Ryzhenkov 2 years ago
parent
commit
5d84bbaa18
  1. 2
      idea-plugin/build.gradle.kts
  2. BIN
      idea-plugin/lib/compiler-hosted-1.1.0-SNAPSHOT.jar
  3. BIN
      idea-plugin/lib/compiler-hosted-1.2.0-SNAPSHOT.jar
  4. 3
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableCallChecker.kt
  5. 6
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeCodeCompletionConfigurable.kt
  6. 2
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFqNames.kt
  7. 32
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeLibraryNamespace.kt
  8. 10
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginIrGenerationExtension.kt
  9. 2
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginUtils.kt
  10. 2
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeSettings.kt
  11. 2
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeCompletionContributor.kt
  12. 4
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeImplementationsCompletionContributor.kt
  13. 43
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeModifierCompletionContributor.kt
  14. 191
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/Constants.kt
  15. 291
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/ConstraintLayoutJsonCompletionContributor.kt
  16. 46
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormat.kt
  17. 27
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/JsonPsiUtil.kt
  18. 101
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/PatternUtils.kt
  19. 433
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/CompletionProviders.kt
  20. 101
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/BaseJsonElementModel.kt
  21. 70
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetModel.kt
  22. 66
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetsPropertyModel.kt
  23. 29
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintsModel.kt
  24. 53
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/CompletionResultSetUtils.kt
  25. 46
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithCaretInsertHandler.kt
  26. 86
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithLiveTemplateInsertHandler.kt
  27. 43
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithNewLineInsertHandler.kt
  28. 50
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/InsertionFormat.kt
  29. 1
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManagerFactory.kt
  30. 3
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreateComposableFunction.kt
  31. 5
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreatePreviewAction.kt
  32. 94
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeSurroundWithWidgetAction.kt
  33. 1
      idea-plugin/src/main/kotlin/com/android/tools/idea/flags/StudioFlags.kt

2
idea-plugin/build.gradle.kts

@ -24,7 +24,7 @@ repositories {
dependencies {
implementation("org.jetbrains.compose:preview-rpc")
implementation(files("lib/compiler-hosted-1.1.0-SNAPSHOT.jar"))
implementation(files("lib/compiler-hosted-1.2.0-SNAPSHOT.jar"))
}
intellij {

BIN
idea-plugin/lib/compiler-hosted-1.1.0-SNAPSHOT.jar

Binary file not shown.

BIN
idea-plugin/lib/compiler-hosted-1.2.0-SNAPSHOT.jar

Binary file not shown.

3
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableCallChecker.kt

@ -402,7 +402,8 @@ fun ResolvedCall<*>.isReadOnlyComposableInvocation(): Boolean {
if (this is VariableAsFunctionResolvedCall) {
return false
}
return when (val candidateDescriptor = candidateDescriptor) {
val candidateDescriptor = candidateDescriptor
return when (candidateDescriptor) {
is ValueParameterDescriptor -> false
is LocalVariableDescriptor -> false
is PropertyDescriptor -> {

6
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeCodeCompletionConfigurable.kt

@ -19,7 +19,8 @@ import com.intellij.application.options.editor.CheckboxDescriptor
import com.intellij.application.options.editor.checkBox
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.layout.*
import com.intellij.ui.layout.PropertyBinding
import com.intellij.ui.layout.panel
/**
* Provides additional options in Settings | Editor | Code Completion section.
@ -31,7 +32,8 @@ class ComposeCodeCompletionConfigurable : BoundConfigurable("Compose") {
private val checkboxDescriptor = CheckboxDescriptor(
ComposeBundle.message("compose.enable.insertion.handler"),
settings.state::isComposeInsertHandlerEnabled)
PropertyBinding({ settings.state.isComposeInsertHandlerEnabled }, { settings.state.isComposeInsertHandlerEnabled = it })
)
override fun createPanel(): DialogPanel {
return panel {

2
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFqNames.kt

@ -24,7 +24,9 @@ import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.annotations.argumentValue
import org.jetbrains.kotlin.resolve.constants.ConstantValue
import org.jetbrains.kotlin.resolve.descriptorUtil.annotationClass
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils.NO_EXPECTED_TYPE
import org.jetbrains.kotlin.types.TypeUtils.UNIT_EXPECTED_TYPE

32
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeLibraryNamespace.kt

@ -15,14 +15,20 @@
*/
package com.android.tools.compose
private const val COMPOSE_PACKAGE = "androidx.compose.ui"
const val COMPOSE_UI_PACKAGE = "androidx.compose.ui"
const val COMPOSE_UI_TOOLING_PACKAGE = "$COMPOSE_UI_PACKAGE.tooling"
const val COMPOSE_UI_TOOLING_PREVIEW_PACKAGE = "$COMPOSE_UI_TOOLING_PACKAGE.preview"
/** Preview element name */
const val COMPOSE_PREVIEW_ANNOTATION_NAME = "Preview"
const val COMPOSE_PREVIEW_ANNOTATION_FQN = "$COMPOSE_UI_TOOLING_PREVIEW_PACKAGE.$COMPOSE_PREVIEW_ANNOTATION_NAME"
const val COMPOSE_PREVIEW_PARAMETER_ANNOTATION_FQN = "$COMPOSE_UI_TOOLING_PREVIEW_PACKAGE.PreviewParameter"
const val COMPOSE_PREVIEW_ACTIVITY_FQN = "$COMPOSE_UI_TOOLING_PACKAGE.PreviewActivity"
const val COMPOSE_VIEW_ADAPTER_FQN = "$COMPOSE_UI_TOOLING_PACKAGE.ComposeViewAdapter"
const val COMPOSABLE_ANNOTATION_NAME = "Composable"
const val COMPOSE_ALIGNMENT = "${COMPOSE_PACKAGE}.Alignment"
const val COMPOSE_ALIGNMENT = "${COMPOSE_UI_PACKAGE}.Alignment"
const val COMPOSE_ALIGNMENT_HORIZONTAL = "${COMPOSE_ALIGNMENT}.Horizontal"
const val COMPOSE_ALIGNMENT_VERTICAL = "${COMPOSE_ALIGNMENT}.Vertical"
@ -30,26 +36,10 @@ const val COMPOSE_ARRANGEMENT = "androidx.compose.foundation.layout.Arrangement"
const val COMPOSE_ARRANGEMENT_HORIZONTAL = "${COMPOSE_ARRANGEMENT}.Horizontal"
const val COMPOSE_ARRANGEMENT_VERTICAL = "${COMPOSE_ARRANGEMENT}.Vertical"
const val COMPOSE_MODIFIER_FQN = "$COMPOSE_UI_PACKAGE.Modifier"
const val COMPOSE_STRING_RESOURCE_FQN = "$COMPOSE_UI_PACKAGE.res.stringResource"
val COMPOSABLE_FQ_NAMES = setOf(
"androidx.compose.$COMPOSABLE_ANNOTATION_NAME",
"androidx.compose.runtime.$COMPOSABLE_ANNOTATION_NAME"
)
/**
* Represents the Jetpack Compose library package name. The compose libraries will move from
* `androidx.ui` to `androidx.compose` and this enum encapsulates the naming for the uses in tools.
*/
enum class ComposeLibraryNamespace(
val packageName: String,
/** Package containing the API preview definitions. Elements here will be referenced by the user. */
val apiPreviewPackage: String = "$packageName.tooling.preview"
) {
ANDROIDX_COMPOSE(COMPOSE_PACKAGE);
val composeModifierClassName: String = "$packageName.Modifier"
/** Only composables with this annotations will be rendered to the surface. */
val previewAnnotationName = "$apiPreviewPackage.$COMPOSE_PREVIEW_ANNOTATION_NAME"
}

10
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginIrGenerationExtension.kt

@ -17,6 +17,8 @@
package com.android.tools.compose
import androidx.compose.compiler.plugins.kotlin.ComposeIrGenerationExtension
import androidx.compose.compiler.plugins.kotlin.IncompatibleComposeRuntimeVersionException
import com.intellij.openapi.progress.ProcessCanceledException
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
@ -27,7 +29,13 @@ class ComposePluginIrGenerationExtension : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
try {
ComposeIrGenerationExtension(reportsDestination = null,
metricsDestination = null).generate(moduleFragment, pluginContext);
metricsDestination = null,
generateFunctionKeyMetaClasses = true,
intrinsicRememberEnabled = false).generate(moduleFragment, pluginContext);
} catch (e : ProcessCanceledException) {
// From ProcessCanceledException javadoc: "Usually, this exception should not be caught, swallowed, logged, or handled in any way.
// Instead, it should be rethrown so that the infrastructure can handle it correctly."
throw e;
} catch (t : Throwable) {
t.printStackTrace()
}

2
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginUtils.kt

@ -31,7 +31,7 @@ import org.jetbrains.kotlin.types.typeUtil.supertypes
fun isModifierChainLongerThanTwo(element: KtElement): Boolean {
if (element.getChildrenOfType<KtDotQualifiedExpression>().isNotEmpty()) {
val fqName = element.resolveToCall(BodyResolveMode.PARTIAL)?.getReturnType()?.fqName?.asString()
if (fqName == ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName) {
if (fqName == COMPOSE_MODIFIER_FQN) {
return true
}
}

2
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeSettings.kt

@ -26,7 +26,7 @@ import com.intellij.openapi.components.Storage
@State(name = "ComposeSettings", storages = [Storage("composeSettings.xml")])
class ComposeSettings : SimplePersistentStateComponent<ComposeSettingsState>(ComposeSettingsState()) {
companion object {
fun getInstance(): ComposeSettings = ApplicationManager.getApplication().getService(ComposeSettings::class.java)
fun getInstance() = ApplicationManager.getApplication().getService(ComposeSettings::class.java)
}
}

2
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeCompletionContributor.kt

@ -302,7 +302,7 @@ class ComposeCompletionWeigher : CompletionWeigher() {
private fun InsertionContext.getNextElementIgnoringWhitespace(): PsiElement? {
val elementAtCaret = file.findElementAt(editor.caretModel.offset) ?: return null
return elementAtCaret.getNextSiblingIgnoringWhitespace(true)
return elementAtCaret.getNextSiblingIgnoringWhitespace(true) ?: return null
}
private fun InsertionContext.isNextElementOpenCurlyBrace() = getNextElementIgnoringWhitespace()?.text?.startsWith("{") == true

4
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeImplementationsCompletionContributor.kt

@ -96,8 +96,8 @@ class ComposeImplementationsCompletionContributor : CompletionContributor() {
}
private fun getKotlinClass(project: Project, classFqName: String): KtClassOrObject? {
return KotlinFullClassNameIndex
.getInstance().get(classFqName, project, project.allScope())
return KotlinFullClassNameIndex.getInstance()
.get(classFqName, project, project.allScope())
.firstOrNull()
.safeAs<KtClassOrObject>()
}

43
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeModifierCompletionContributor.kt

@ -15,7 +15,7 @@
*/
package com.android.tools.compose.code.completion
import com.android.tools.compose.ComposeLibraryNamespace
import com.android.tools.compose.COMPOSE_MODIFIER_FQN
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.modules.*
import com.intellij.codeInsight.completion.CompletionContributor
@ -23,6 +23,7 @@ import com.intellij.codeInsight.completion.CompletionInitializationContext
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.completion.PrefixMatcher
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementDecorator
@ -85,14 +86,10 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs
*
* Moves extension functions for method called on modifier [isMethodCalledOnModifier] up in the completion list.
*
* @see ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName
* @see COMPOSE_MODIFIER_FQN
*/
class ComposeModifierCompletionContributor : CompletionContributor() {
companion object {
private val modifierFqName = ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName
}
override fun fillCompletionVariants(parameters: CompletionParameters, resultSet: CompletionResultSet) {
val element = parameters.position
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || !element.inComposeModule() || parameters.originalFile !is KtFile) {
@ -109,10 +106,10 @@ class ComposeModifierCompletionContributor : CompletionContributor() {
val nameExpression = createNameExpression(element)
val extensionFunctions = getExtensionFunctionsForModifier(nameExpression, element)
val extensionFunctions = getExtensionFunctionsForModifier(nameExpression, element, resultSet.prefixMatcher)
ProgressManager.checkCanceled()
val (returnsModifier, others) = extensionFunctions.partition { it.returnType?.fqName?.asString() == modifierFqName }
val (returnsModifier, others) = extensionFunctions.partition { it.returnType?.fqName?.asString() == COMPOSE_MODIFIER_FQN }
val lookupElementFactory = createLookupElementFactory(nameExpression, parameters)
val isNewModifier = !isMethodCalledOnImportedModifier && element.parentOfType<KtDotQualifiedExpression>() == null
@ -182,17 +179,19 @@ class ComposeModifierCompletionContributor : CompletionContributor() {
private fun createNameExpression(originalElement: PsiElement): KtSimpleNameExpression {
val originalFile = originalElement.containingFile.safeAs<KtFile>()!!
val file = KtPsiFactory(originalFile.project).createAnalyzableFile("temp.kt", "val x = $modifierFqName.call", originalFile)
val file = KtPsiFactory(originalFile.project).createAnalyzableFile("temp.kt", "val x = $COMPOSE_MODIFIER_FQN.call", originalFile)
return file.getChildOfType<KtProperty>()!!.getChildOfType<KtDotQualifiedExpression>()!!.lastChild as KtSimpleNameExpression
}
private fun getExtensionFunctionsForModifier(nameExpression: KtSimpleNameExpression,
originalPosition: PsiElement): Collection<CallableDescriptor> {
private fun getExtensionFunctionsForModifier(
nameExpression: KtSimpleNameExpression,
originalPosition: PsiElement,
prefixMatcher: PrefixMatcher
): Collection<CallableDescriptor> {
val file = nameExpression.containingFile as KtFile
val searchScope = getResolveScope(file)
val resolutionFacade = file.getResolutionFacade()
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS)
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_FOR_COMPLETION)
val callTypeAndReceiver = CallTypeAndReceiver.detect(nameExpression)
fun isVisible(descriptor: DeclarationDescriptor): Boolean {
@ -204,13 +203,16 @@ class ComposeModifierCompletionContributor : CompletionContributor() {
}
val indicesHelper = KotlinIndicesHelper(resolutionFacade, searchScope, ::isVisible, file = file)
return indicesHelper.getCallableTopLevelExtensions(callTypeAndReceiver, nameExpression, bindingContext, null) { true }
val nameFilter = { name: String -> prefixMatcher.prefixMatches(name) }
return indicesHelper.getCallableTopLevelExtensions(callTypeAndReceiver, nameExpression, bindingContext, null, nameFilter)
}
private val PsiElement.isModifierProperty: Boolean
get() {
val property = contextOfType<KtProperty>() ?: return false
return property.type()?.fqName?.asString() == modifierFqName
// Case val myModifier:Modifier = <caret>
val property = parent?.parent?.safeAs<KtProperty>() ?: return false
return property.type()?.fqName?.asString() == COMPOSE_MODIFIER_FQN
}
private val PsiElement.isModifierArgument: Boolean
@ -229,7 +231,7 @@ class ComposeModifierCompletionContributor : CompletionContributor() {
callee.valueParameters.getOrNull(argumentIndex)?.type()?.fqName
}
return argumentTypeFqName?.asString() == modifierFqName
return argumentTypeFqName?.asString() == COMPOSE_MODIFIER_FQN
}
/**
@ -243,7 +245,7 @@ class ComposeModifierCompletionContributor : CompletionContributor() {
val fqName = elementOnWhichMethodCalled.resolveToCall(BodyResolveMode.PARTIAL)?.getReturnType()?.fqName ?:
// Case Modifier.%this%
elementOnWhichMethodCalled.safeAs<KtNameReferenceExpression>()?.resolve().safeAs<KtClass>()?.fqName
return fqName?.asString() == modifierFqName
return fqName?.asString() == COMPOSE_MODIFIER_FQN
}
/**
@ -280,14 +282,15 @@ class ComposeModifierCompletionContributor : CompletionContributor() {
override fun handleInsert(context: InsertionContext) {
val psiDocumentManager = PsiDocumentManager.getInstance(context.project)
if (insertModifier) {
// Compose plugin inserts Modifier if completion character is '\n', doesn't happened with '\t'. Looks like a bug.
if (insertModifier && context.completionChar != '\n') {
context.document.insertString(context.startOffset, callOnModifierObject)
context.offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, context.startOffset + callOnModifierObject.length)
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document)
}
val ktFile = context.file as KtFile
val modifierDescriptor = ktFile.resolveImportReference(FqName(modifierFqName)).singleOrNull()
val modifierDescriptor = ktFile.resolveImportReference(FqName(COMPOSE_MODIFIER_FQN)).singleOrNull()
modifierDescriptor?.let { ImportInsertHelper.getInstance(context.project).importDescriptor(ktFile, it) }
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document)

191
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/Constants.kt

@ -16,6 +16,197 @@
package com.android.tools.compose.code.completion.constraintlayout
internal object KeyWords {
/**
* Name of the property within a MotionScene that contains several ConstraintSet declarations.
*/
const val ConstraintSets = "ConstraintSets"
/**
* Name of the property within a MotionScene that contains several Transition declarations.
*/
const val Transitions = "Transitions"
/**
* Name of the property used to indicate that the containing ConstraintSet inherits its constraints from the ConstraintSet given by the
* `Extends` property value.
*/
const val Extends = "Extends"
/**
* Reserved ID for the containing layout. Typically referenced in constraint blocks.
*/
const val ParentId = "parent"
/**
* Name of the Visibility property in a constraint block.
*/
const val Visibility = "visibility"
/**
* Name of the Clear property in a constraint block.
*
* Populated by an array of options to clear inherited parameters from [Extends].
*
* @see ClearOption
*/
const val Clear = "clear"
}
/**
* Common interface to simplify handling multiple enum Classes.
*
* [keyWord] is the case-sensitive string used in the syntax.
*/
internal interface ConstraintLayoutKeyWord {
val keyWord: String
}
//region Constrain KeyWords
/**
* The classic anchors used to constrain a widget.
*/
internal enum class StandardAnchor(override val keyWord: String) : ConstraintLayoutKeyWord {
Start("start"),
Left("left"),
End("end"),
Right("right"),
Top("top"),
Bottom("bottom"),
Baseline("baseline");
companion object {
fun isVertical(keyWord: String) = verticalAnchors.any { it.keyWord == keyWord }
fun isHorizontal(keyWord: String) = horizontalAnchors.any { it.keyWord == keyWord }
val horizontalAnchors: List<StandardAnchor> = listOf(Start, End, Left, Right)
val verticalAnchors: List<StandardAnchor> = listOf(Top, Bottom, Baseline)
}
}
/**
* Non-typical anchors.
*
* These implicitly apply multiple [StandardAnchor]s.
*/
internal enum class SpecialAnchor(override val keyWord: String) : ConstraintLayoutKeyWord {
Center("center"),
CenterH("centerHorizontally"),
CenterV("centerVertically")
}
/**
* Supported keywords to define the dimension of a widget.
*/
internal enum class Dimension(override val keyWord: String) : ConstraintLayoutKeyWord {
Width("width"),
Height("height")
}
/**
* Keywords to apply rendering time transformations to a widget.
*/
internal enum class RenderTransform(override val keyWord: String) : ConstraintLayoutKeyWord {
Alpha("alpha"),
ScaleX("scaleX"),
ScaleY("scaleY"),
RotationX("rotationX"),
RotationY("rotationY"),
RotationZ("rotationZ"),
TranslationX("translationX"),
TranslationY("translationY"),
TranslationZ("translationZ"),
}
//endregion
internal enum class DimBehavior(override val keyWord: String) : ConstraintLayoutKeyWord {
Spread("spread"),
Wrap("wrap"),
PreferWrap("preferWrap"),
MatchParent("parent")
}
internal enum class VisibilityMode(override val keyWord: String): ConstraintLayoutKeyWord {
Visible("visible"),
Invisible("invisible"),
Gone("gone")
}
internal enum class ClearOption(override val keyWord: String): ConstraintLayoutKeyWord {
Constraints("constraints"),
Dimensions("dimensions"),
Transforms("transforms")
}
internal enum class TransitionField(override val keyWord: String): ConstraintLayoutKeyWord {
From("from"),
To("to"),
PathArc("pathMotionArc"),
KeyFrames("KeyFrames"),
OnSwipe("onSwipe")
}
internal enum class OnSwipeField(override val keyWord: String): ConstraintLayoutKeyWord {
AnchorId("anchor"),
Direction("direction"),
Side("side"),
Mode("mode")
}
internal enum class OnSwipeSide(override val keyWord: String): ConstraintLayoutKeyWord {
Top("top"),
Left("left"),
Right("right"),
Bottom("bottom"),
Middle("middle"),
Start("start"),
End("end")
}
internal enum class OnSwipeDirection(override val keyWord: String): ConstraintLayoutKeyWord {
Up("up"),
Down("down"),
Left("left"),
Right("right"),
Start("start"),
End("end"),
Clockwise("clockwise"),
AntiClockwise("anticlockwise")
}
internal enum class OnSwipeMode(override val keyWord: String): ConstraintLayoutKeyWord {
Velocity("velocity"),
Spring("spring")
}
internal enum class KeyFrameField(override val keyWord: String): ConstraintLayoutKeyWord {
Positions("KeyPositions"),
Attributes("KeyAttributes"),
Cycles("KeyCycles")
}
/**
* Common fields used by any of [KeyFrameField].
*/
internal enum class KeyFrameChildCommonField(override val keyWord: String): ConstraintLayoutKeyWord {
TargetId("target"),
Frames("frames"),
Easing("transitionEasing"),
Fit("curveFit"),
}
internal enum class KeyPositionField(override val keyWord: String): ConstraintLayoutKeyWord {
PercentX("percentX"),
PercentY("percentY"),
PercentWidth("percentWidth"),
PercentHeight("percentHeight"),
PathArc("pathMotionArc"),
Type("type")
}
internal enum class KeyCycleField(override val keyWord: String): ConstraintLayoutKeyWord {
Period("period"),
Offset("offset"),
Phase("phase")
}

291
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/ConstraintLayoutJsonCompletionContributor.kt

@ -15,27 +15,47 @@
*/
package com.android.tools.compose.code.completion.constraintlayout
import com.android.tools.compose.code.completion.constraintlayout.provider.AnchorablesProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ClearOptionsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintIdsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintSetFieldsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintSetNamesProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.EnumValuesCompletionProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.KeyFrameChildFieldsCompletionProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.KeyFramesFieldsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.OnSwipeFieldsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.TransitionFieldsProvider
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.modules.inComposeModule
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.json.JsonElementTypes
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonReferenceExpression
import com.intellij.openapi.util.Key
import com.intellij.patterns.PlatformPatterns
import com.intellij.patterns.PsiElementPattern
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.util.ProcessingContext
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType
import com.intellij.json.JsonLanguage
import com.intellij.json.psi.JsonStringLiteral
internal const val BASE_DEPTH_FOR_LITERAL_IN_PROPERTY = 2
internal const val BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT = BASE_DEPTH_FOR_LITERAL_IN_PROPERTY + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of the list of ConstraintSets. With respect to the ConstraintSets root element. */
private const val CONSTRAINT_SET_LIST_PROPERTY_DEPTH = BASE_DEPTH_FOR_LITERAL_IN_PROPERTY + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of a ConstraintSet. With respect to the ConstraintSets root element. */
private const val CONSTRAINT_SET_PROPERTY_DEPTH = CONSTRAINT_SET_LIST_PROPERTY_DEPTH + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of a Transition. With respect to the Transitions root element. */
private const val TRANSITION_PROPERTY_DEPTH = CONSTRAINT_SET_PROPERTY_DEPTH
/** Depth for a literal of a property of a Constraints block. With respect to the ConstraintSets root element. */
internal const val CONSTRAINT_BLOCK_PROPERTY_DEPTH = CONSTRAINT_SET_PROPERTY_DEPTH + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of an OnSwipe block. With respect to the Transitions root element. */
internal const val ONSWIPE_PROPERTY_DEPTH = TRANSITION_PROPERTY_DEPTH + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of a KeyFrames block. With respect to the Transitions root element. */
internal const val KEYFRAMES_PROPERTY_DEPTH = ONSWIPE_PROPERTY_DEPTH
/**
* [CompletionContributor] for the JSON5 format supported in ConstraintLayout-Compose (and MotionLayout).
@ -45,111 +65,154 @@ import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType
*/
class ConstraintLayoutJsonCompletionContributor : CompletionContributor() {
init {
// region ConstraintSets
extend(
CompletionType.BASIC,
// Complete field names in ConstraintSets
jsonPropertyName().withConstraintSetsParentAtLevel(CONSTRAINT_SET_PROPERTY_DEPTH),
ConstraintSetFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete constraints field names (width, height, start, end, etc.)
jsonPropertyName().withConstraintSetsParentAtLevel(CONSTRAINT_BLOCK_PROPERTY_DEPTH),
ConstraintsProvider
)
extend(
CompletionType.BASIC,
// Complete ConstraintSet names in Extends keyword
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, KeyWords.Extends),
ConstraintSetNamesProvider
)
extend(
CompletionType.BASIC,
jsonPropertyName().withConstraintSetsParentAtLevel(6),
// Complete IDs on special anchors, they take a single string value
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, SpecialAnchor.values().map { it.keyWord }),
ConstraintIdsProvider
)
}
}
/**
* [SmartPsiElementPointer] to the [JsonProperty] corresponding to the ConstraintSets property.
*/
private typealias ConstraintSetsPropertyPointer = SmartPsiElementPointer<JsonProperty>
private val constraintSetsPropertyKey =
Key.create<ConstraintSetsPropertyPointer>("compose.json.autocomplete.constraint.sets.property")
/**
* Completion provider that looks for the 'ConstraintSets' declaration and caches it, provides useful functions for inheritors that want to
* provide completions based con the contents of the 'ConstraintSets' [JsonProperty].
*/
private abstract class ConstraintSetCompletionProvider : CompletionProvider<CompletionParameters>() {
final override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val setsProperty = if (context[constraintSetsPropertyKey] != null) {
context[constraintSetsPropertyKey]!!
}
else {
parameters.position.getTopmostParentOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.firstOrNull {
it.name == KeyWords.ConstraintSets
}?.let {
val pointer = SmartPointerManager.createPointer(it)
context.put(constraintSetsPropertyKey, pointer)
return@let pointer
}
}
addCompletions(setsProperty, parameters, result)
}
/**
* Inheritors should implement this function that may pass a reference to the ConstraintSets property.
*/
abstract fun addCompletions(
constraintSetsProperty: ConstraintSetsPropertyPointer?,
parameters: CompletionParameters,
result: CompletionResultSet
extend(
CompletionType.BASIC,
// Complete IDs in the constraint array (first position)
jsonStringValue()
// First element in the array, ie: there is no PsiElement preceding the desired one at this level
.withParent(psiElement<JsonStringLiteral>().atIndexOfJsonArray(0))
.insideConstraintArray(),
ConstraintIdsProvider
)
extend(
CompletionType.BASIC,
// Complete anchors in the constraint array (second position)
jsonStringValue()
// Second element in the array, ie: there is one PsiElement preceding the desired one at this level
.withParent(psiElement<JsonStringLiteral>().atIndexOfJsonArray(1))
.insideConstraintArray(),
AnchorablesProvider
)
extend(
CompletionType.BASIC,
// Complete a clear option within the 'clear' array
jsonStringValue()
.insideClearArray(),
ClearOptionsProvider
)
extend(
CompletionType.BASIC,
// Complete non-numeric dimension values for width & height
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, Dimension.values().map { it.keyWord }),
EnumValuesCompletionProvider(DimBehavior::class)
)
extend(
CompletionType.BASIC,
// Complete Visibility mode values
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, KeyWords.Visibility),
EnumValuesCompletionProvider(VisibilityMode::class)
)
//endregion
/**
* Returns the available constraint IDs for the given [constraintSetName], this is done by reading all IDs in all ConstraintSets and
* subtracting the IDs already present in [constraintSetName].
*/
protected fun ConstraintSetsPropertyPointer.findConstraintIdsForSet(constraintSetName: String): List<String> {
val availableNames = mutableSetOf(KeyWords.Extends)
val usedNames = mutableSetOf<String>()
this.element?.getChildOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.forEach { cSetProperty ->
cSetProperty.getChildOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.forEach { constraintNameProperty ->
if (cSetProperty.name == constraintSetName) {
usedNames.add(constraintNameProperty.name)
}
else {
availableNames.add(constraintNameProperty.name)
}
}
}
availableNames.removeAll(usedNames)
return availableNames.toList()
}
}
/**
* Provides options to autocomplete constraint IDs for constraint set declarations, based on the IDs already defined by the user in other
* constraint sets.
*/
private object ConstraintIdsProvider : ConstraintSetCompletionProvider() {
override fun addCompletions(constraintSetsProperty: SmartPsiElementPointer<JsonProperty>?,
parameters: CompletionParameters,
result: CompletionResultSet) {
val parentName = parameters.position.getParentOfType<JsonProperty>(true)?.getParentOfType<JsonProperty>(true)?.name
if (constraintSetsProperty != null && parentName != null) {
constraintSetsProperty.findConstraintIdsForSet(parentName).forEach {
val template = if (it == KeyWords.Extends) JsonStringValueTemplate else JsonNewObjectTemplate
result.addLookupElement(name = it, tailText = null, template)
}
}
//region Transitions
extend(
CompletionType.BASIC,
// Complete fields of a Transition block
jsonPropertyName()
.withTransitionsParentAtLevel(TRANSITION_PROPERTY_DEPTH),
TransitionFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete existing ConstraintSet names for `from` and `to` Transition properties
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, listOf(TransitionField.From.keyWord, TransitionField.To.keyWord))
.withTransitionsParentAtLevel(TRANSITION_PROPERTY_DEPTH),
// TODO(b/207030860): Guarantee that provided names for 'from' or 'to' are distinct from each other,
// ie: both shouldn't reference the same ConstraintSet
ConstraintSetNamesProvider
)
extend(
CompletionType.BASIC,
// Complete fields of a KeyFrames block
jsonPropertyName()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT, TransitionField.KeyFrames.keyWord)
.withTransitionsParentAtLevel(KEYFRAMES_PROPERTY_DEPTH),
KeyFramesFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete fields of an OnSwipe block
jsonPropertyName()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT, TransitionField.OnSwipe.keyWord)
.withTransitionsParentAtLevel(ONSWIPE_PROPERTY_DEPTH),
OnSwipeFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete the possible IDs for the OnSwipe `anchor` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.AnchorId.keyWord),
ConstraintIdsProvider
)
extend(
CompletionType.BASIC,
// Complete the known values for the OnSwipe `side` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.Side.keyWord),
EnumValuesCompletionProvider(OnSwipeSide::class)
)
extend(
CompletionType.BASIC,
// Complete the known values for the OnSwipe `direction` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.Direction.keyWord),
EnumValuesCompletionProvider(OnSwipeDirection::class)
)
extend(
CompletionType.BASIC,
// Complete the known values for the OnSwipe `mode` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.Mode.keyWord),
EnumValuesCompletionProvider(OnSwipeMode::class)
)
extend(
CompletionType.BASIC,
// Complete the fields for any of the possible KeyFrames children
jsonPropertyName()
// A level deeper considering the array surrounding the object
.withPropertyParentAtLevel(BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT + 1, KeyFrameField.values().map { it.keyWord }),
KeyFrameChildFieldsCompletionProvider
)
//endregion
}
}
private fun jsonPropertyName() = PlatformPatterns.psiElement(JsonElementTypes.IDENTIFIER)
private inline fun <reified T : PsiElement> psiElement() = PlatformPatterns.psiElement(T::class.java)
private fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, name: String) =
this.withSuperParent(level, psiElement<JsonProperty>().withChild(psiElement<JsonReferenceExpression>().withText(name)))
private fun PsiElementPattern<*, *>.withConstraintSetsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, "ConstraintSets")
private fun CompletionResultSet.addLookupElement(name: String, tailText: String? = null, format: InsertionFormat? = null) {
var lookupBuilder = if (format == null) {
LookupElementBuilder.create(name)
}
else {
LookupElementBuilder.create(format, name).withInsertHandler(InsertionFormatHandler)
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
if (!StudioFlags.COMPOSE_CONSTRAINTLAYOUT_COMPLETION.get() ||
parameters.position.inComposeModule() != true ||
parameters.position.language != JsonLanguage.INSTANCE) {
// TODO(b/207030860): Allow in other contexts once the syntax is supported outside Compose
return
}
lookupBuilder = lookupBuilder.withCaseSensitivity(false)
if (tailText != null) {
lookupBuilder = lookupBuilder.withTailText(tailText, true)
super.fillCompletionVariants(parameters, result)
}
addElement(lookupBuilder)
}

46
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormat.kt

@ -15,29 +15,33 @@
*/
package com.android.tools.compose.code.completion.constraintlayout
/**
* Describes a string that may be automatically inserted when selecting an autocomplete option.
*/
internal sealed class InsertionFormat(
val insertableString: String
)
import com.android.tools.compose.completion.inserthandler.LiteralNewLineFormat
import com.android.tools.compose.completion.inserthandler.LiteralWithCaretFormat
import com.android.tools.compose.completion.inserthandler.LiveTemplateFormat
/**
* Inserts the string after the auto-completed value.
*
* The caret will be moved to the position marked by the '|' character.
*/
internal class LiteralWithCaretFormat(literalFormat: String) : InsertionFormat(literalFormat)
internal val JsonStringValueTemplate = LiteralWithCaretFormat(": '|',")
internal val JsonNumericValueTemplate = LiteralWithCaretFormat(": |,")
internal val JsonNewObjectTemplate = LiteralNewLineFormat(": {\n}")
internal val JsonStringArrayTemplate = LiteralWithCaretFormat(": ['|'],")
internal val JsonObjectArrayTemplate = LiteralNewLineFormat(": [{\n}],")
internal val ConstrainAnchorTemplate = LiveTemplateFormat(": ['<>', '<>', <0>],")
internal val ClearAllTemplate = LiteralWithCaretFormat(
literalFormat = ": ['${ClearOption.Constraints}', '${ClearOption.Dimensions}', '${ClearOption.Transforms}'],"
)
/**
* Inserts the string after the auto-complete value.
* Returns a [LiveTemplateFormat] that contains a template for a Json array with numeric type, where the size of the array is given by
* [count] and the user may edit each of the values in the array using Live Templates.
*
* It will insert a new line as if it was done by an ENTER keystroke, marked by the '\n' character.
*
* Note that it will only apply the new line on the first '\n' character.
* E.g.: For [count] = 3, returns the template: `": [0, 0, 0],"`, where every value may be changed by the user.
*/
internal class LiteralNewLineFormat(literalFormat: String) : InsertionFormat(literalFormat)
internal val JsonStringValueTemplate = LiteralWithCaretFormat(": '|',")
internal val JsonNewObjectTemplate = LiteralNewLineFormat(": {\n}")
internal fun buildJsonNumberArrayTemplate(count: Int): LiveTemplateFormat {
val times = count.coerceAtLeast(1)
return LiveTemplateFormat(": [" + "<0>, ".repeat(times).removeSuffix(", ") + "],")
}

27
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/JsonPsiUtil.kt

@ -0,0 +1,27 @@
/*
* Copyright (C) 2022 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.code.completion.constraintlayout
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.json.psi.JsonProperty
import com.intellij.psi.util.parentOfType
/**
* From the element being invoked, returns the [JsonProperty] parent that also includes the [JsonProperty] from which completion is
* triggered.
*/
internal fun getJsonPropertyParent(parameters: CompletionParameters): JsonProperty? =
parameters.position.parentOfType<JsonProperty>(withSelf = true)?.parentOfType<JsonProperty>(withSelf = false)

101
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/PatternUtils.kt

@ -0,0 +1,101 @@
/*
* Copyright (C) 2022 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.code.completion.constraintlayout
import com.intellij.json.JsonElementTypes
import com.intellij.json.psi.JsonArray
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonReferenceExpression
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.json.psi.JsonValue
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns
import com.intellij.patterns.PsiElementPattern
import com.intellij.patterns.StandardPatterns
import com.intellij.patterns.StringPattern
import com.intellij.psi.PsiElement
import com.intellij.util.ProcessingContext
// region ConstraintLayout Pattern Helpers
internal fun jsonPropertyName() = PlatformPatterns.psiElement(JsonElementTypes.IDENTIFIER)
internal fun jsonStringValue() =
PlatformPatterns.psiElement(JsonElementTypes.SINGLE_QUOTED_STRING).withParent<JsonStringLiteral>()
internal fun PsiElementPattern<*, *>.withConstraintSetsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, KeyWords.ConstraintSets)
internal fun PsiElementPattern<*, *>.withTransitionsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, KeyWords.Transitions)
internal fun PsiElementPattern<*, *>.insideClearArray() = inArrayWithinConstraintBlockProperty {
// For the 'clear' constraint block property
matches(KeyWords.Clear)
}
internal fun PsiElementPattern<*, *>.insideConstraintArray() = inArrayWithinConstraintBlockProperty {
// The parent property name may only be a StandardAnchor
oneOf(StandardAnchor.values().map { it.keyWord })
}
/**
* [PsiElementPattern] that matches an element in a [JsonArray] within a Constraint block. Where the property the array is assigned to, has
* a name that is matched by [matchPropertyName].
*/
internal fun PsiElementPattern<*, *>.inArrayWithinConstraintBlockProperty(matchPropertyName: StringPattern.() -> StringPattern) =
withSuperParent(2, psiElement<JsonArray>())
.withSuperParent(
BASE_DEPTH_FOR_LITERAL_IN_PROPERTY + 1, // JsonArray adds one level
psiElement<JsonProperty>().withChild(
// The first expression in a JsonProperty corresponds to the name of the property
psiElement<JsonReferenceExpression>().withText(StandardPatterns.string().matchPropertyName())
)
)
.withConstraintSetsParentAtLevel(CONSTRAINT_BLOCK_PROPERTY_DEPTH + 1) // JsonArray adds one level
// endregion
// region Kotlin Syntax Helpers
internal inline fun <reified T : PsiElement> psiElement(): PsiElementPattern<T, PsiElementPattern.Capture<T>> =
PlatformPatterns.psiElement(T::class.java)
internal inline fun <reified T : PsiElement> PsiElementPattern<*, *>.withParent() = this.withParent(T::class.java)
/**
* Pattern such that when traversing up the tree from the current element, the element at [level] is a [JsonProperty]. And its name matches
* the given [name].
*/
internal fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, name: String) =
withPropertyParentAtLevel(level, listOf(name))
/**
* Pattern such that when traversing up the tree from the current element, the element at [level] is a [JsonProperty]. Which name matches
* one of the given [names].
*/
internal fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, names: Collection<String>) =
this.withSuperParent(level, psiElement<JsonProperty>().withChild(
psiElement<JsonReferenceExpression>().withText(StandardPatterns.string().oneOf(names)))
)
/**
* Verifies that the current element is at the given [index] of the elements contained by its [JsonArray] parent.
*/
internal fun <T : JsonValue> PsiElementPattern<T, PsiElementPattern.Capture<T>>.atIndexOfJsonArray(index: Int) =
with(object : PatternCondition<T>("atIndexOfJsonArray") {
override fun accepts(element: T, context: ProcessingContext?): Boolean {
val parent = element.context as? JsonArray ?: return false
val children = parent.valueList
val indexOfSelf = children.indexOf(element)
return index == indexOfSelf
}
})
// endregion

433
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/CompletionProviders.kt

@ -0,0 +1,433 @@
/*
* Copyright (C) 2021 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.code.completion.constraintlayout.provider
import com.android.tools.compose.code.completion.constraintlayout.ClearAllTemplate
import com.android.tools.compose.code.completion.constraintlayout.ClearOption
import com.android.tools.compose.code.completion.constraintlayout.ConstrainAnchorTemplate
import com.android.tools.compose.code.completion.constraintlayout.ConstraintLayoutKeyWord
import com.android.tools.compose.code.completion.constraintlayout.Dimension
import com.android.tools.compose.code.completion.constraintlayout.JsonNewObjectTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonNumericValueTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonObjectArrayTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonStringArrayTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonStringValueTemplate
import com.android.tools.compose.code.completion.constraintlayout.KeyCycleField
import com.android.tools.compose.code.completion.constraintlayout.KeyFrameChildCommonField
import com.android.tools.compose.code.completion.constraintlayout.KeyFrameField
import com.android.tools.compose.code.completion.constraintlayout.KeyPositionField
import com.android.tools.compose.code.completion.constraintlayout.KeyWords
import com.android.tools.compose.code.completion.constraintlayout.OnSwipeField
import com.android.tools.compose.code.completion.constraintlayout.RenderTransform
import com.android.tools.compose.code.completion.constraintlayout.SpecialAnchor
import com.android.tools.compose.code.completion.constraintlayout.StandardAnchor
import com.android.tools.compose.code.completion.constraintlayout.TransitionField
import com.android.tools.compose.code.completion.constraintlayout.buildJsonNumberArrayTemplate
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent
import com.android.tools.compose.code.completion.constraintlayout.provider.model.ConstraintSetModel
import com.android.tools.compose.code.completion.constraintlayout.provider.model.ConstraintSetsPropertyModel
import com.android.tools.compose.code.completion.constraintlayout.provider.model.JsonPropertyModel
import com.android.tools.compose.completion.addLookupElement
import com.android.tools.compose.completion.inserthandler.InsertionFormat
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.json.psi.JsonArray
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.psi.PsiElement
import com.intellij.psi.util.parentOfType
import com.intellij.util.ProcessingContext
import kotlin.reflect.KClass
/**
* Completion provider that looks for the 'ConstraintSets' declaration and passes a model that provides useful functions for inheritors that
* want to provide completions based on the contents of the 'ConstraintSets' [JsonProperty].
*/
internal abstract class BaseConstraintSetsCompletionProvider : CompletionProvider<CompletionParameters>() {
final override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val constraintSetsModel = createConstraintSetsModel(initialElement = parameters.position)
if (constraintSetsModel != null) {
ProgressManager.checkCanceled()
addCompletions(constraintSetsModel, parameters, result)
}
}
/**
* Inheritors should implement this function that may pass a reference to the ConstraintSets property.
*/
abstract fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
)
/**
* Finds the [JsonProperty] for the 'ConstraintSets' declaration and returns its model.
*
* The `ConstraintSets` property is expected to be a property of the root [JsonObject].
*/
private fun createConstraintSetsModel(initialElement: PsiElement): ConstraintSetsPropertyModel? {
// Start with the closest JsonObject towards the root
var currentJsonObject: JsonObject? = initialElement.parentOfType<JsonObject>(withSelf = true) ?: return null
lateinit var topLevelJsonObject: JsonObject
// Then find the top most JsonObject while checking for cancellation
while (currentJsonObject != null) {
topLevelJsonObject = currentJsonObject
currentJsonObject = currentJsonObject.parentOfType<JsonObject>(withSelf = false)
ProgressManager.checkCanceled()
}
// The last non-null JsonObject is the topmost, the ConstraintSets property is expected within this element
val constraintSetsProperty = topLevelJsonObject.findProperty(KeyWords.ConstraintSets) ?: return null
// TODO(b/207030860): Consider creating the model even if there's no property that is explicitly called 'ConstraintSets'
// ie: imply that the root JsonObject is the ConstraintSets object, with the downside that figuring out the correct context would
// be much more difficult
return ConstraintSetsPropertyModel(constraintSetsProperty)
}
}
/**
* Provides options to autocomplete constraint IDs for constraint set declarations, based on the IDs already defined by the user in other
* constraint sets.
*/
internal object ConstraintSetFieldsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val currentConstraintSet = ConstraintSetModel.getModelForCompletionOnConstraintSetProperty(parameters) ?: return
val currentSetName = currentConstraintSet.name ?: return
constraintSetsPropertyModel.getRemainingFieldsForConstraintSet(currentSetName).forEach { fieldName ->
val template = if (fieldName == KeyWords.Extends) JsonStringValueTemplate else JsonNewObjectTemplate
result.addLookupElement(lookupString = fieldName, tailText = null, template)
}
}
}
/**
* Autocomplete options with the names of all available ConstraintSets, except from the one the autocomplete was invoked from.
*/
internal object ConstraintSetNamesProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val currentConstraintSet = ConstraintSetModel.getModelForCompletionOnConstraintSetProperty(parameters)
val currentSetName = currentConstraintSet?.name
val names = constraintSetsPropertyModel.getConstraintSetNames().toMutableSet()
if (currentSetName != null) {
names.remove(currentSetName)
}
names.forEach(result::addLookupElement)
}
}
/**
* Autocomplete options used to define the constraints of a widget (defined by the ID) within a ConstraintSet
*/
internal object ConstraintsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
val existingFieldsSet = parentPropertyModel.declaredFieldNamesSet
StandardAnchor.values().forEach {
if (!existingFieldsSet.contains(it.keyWord)) {
result.addLookupElement(lookupString = it.keyWord, tailText = " [...]", format = ConstrainAnchorTemplate)
}
}
if (!existingFieldsSet.contains(KeyWords.Visibility)) {
result.addLookupElement(lookupString = KeyWords.Visibility, format = JsonStringValueTemplate)
}
result.addEnumKeyWordsWithStringValueTemplate<SpecialAnchor>(existingFieldsSet)
result.addEnumKeyWordsWithNumericValueTemplate<Dimension>(existingFieldsSet)
result.addEnumKeyWordsWithNumericValueTemplate<RenderTransform>(existingFieldsSet)
// Complete 'clear' if the containing ConstraintSet has `extendsFrom`
val containingConstraintSetModel = parentPropertyModel.getParentProperty()?.let {
ConstraintSetModel(it)
}
if (containingConstraintSetModel?.extendsFrom != null) {
// Add an option with an empty string array and another one with all clear options
result.addLookupElement(lookupString = KeyWords.Clear, format = JsonStringArrayTemplate)
result.addLookupElement(lookupString = KeyWords.Clear, format = ClearAllTemplate, tailText = " [<all>]")
}
}
}
/**
* Provides IDs when autocompleting a constraint array.
*
* The ID may be either 'parent' or any of the declared IDs in all ConstraintSets, except the ID of the constraints block from which this
* provider was invoked.
*/
internal object ConstraintIdsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val possibleIds = constraintSetsPropertyModel.constraintSets.flatMap { it.declaredIds }.toCollection(HashSet())
// Parent ID should always be present
possibleIds.add(KeyWords.ParentId)
// Remove the current ID
getJsonPropertyParent(parameters)?.name?.let(possibleIds::remove)
possibleIds.forEach { id ->
result.addLookupElement(lookupString = id)
}
}
}
/**
* Provides the appropriate anchors when completing a constraint array.
*
* [StandardAnchor.verticalAnchors] can only be constrained to other vertical anchors. Same logic for [StandardAnchor.horizontalAnchors].
*/
internal object AnchorablesProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val currentAnchorKeyWord = parameters.position.parentOfType<JsonProperty>(withSelf = true)?.name ?: return
val possibleAnchors = when {
StandardAnchor.isVertical(currentAnchorKeyWord) -> StandardAnchor.verticalAnchors
StandardAnchor.isHorizontal(currentAnchorKeyWord) -> StandardAnchor.horizontalAnchors
else -> emptyList()
}
possibleAnchors.forEach { result.addLookupElement(lookupString = it.keyWord) }
}
}
/**
* Provides the appropriate options when completing string literals within a `clear` array.
*
* @see ClearOption
*/
internal object ClearOptionsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val existing = parameters.position.parentOfType<JsonArray>(withSelf = false)?.valueList
?.filterIsInstance<JsonStringLiteral>()
?.map { it.value }
?.toSet() ?: emptySet()
addEnumKeywords<ClearOption>(result, existing)
}
}
/**
* Provides completion for the fields of a `Transition`.
*
* @see TransitionField
*/
internal object TransitionFieldsProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
TransitionField.values().forEach {
if (parentPropertyModel.containsPropertyOfName(it.keyWord)) {
// skip
return@forEach
}
when (it) {
TransitionField.OnSwipe,
TransitionField.KeyFrames -> {
result.addLookupElement(lookupString = it.keyWord, format = JsonNewObjectTemplate)
}
else -> {
result.addLookupElement(lookupString = it.keyWord, format = JsonStringValueTemplate)
}
}
}
}
}
/**
* Provides completion for the fields of an `OnSwipe` block.
*
* @see OnSwipeField
*/
internal object OnSwipeFieldsProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
result.addEnumKeyWordsWithStringValueTemplate<OnSwipeField>(parentPropertyModel.declaredFieldNamesSet)
}
}
/**
* Provides completion for the fields of a `KeyFrames` block.
*
* @see KeyFrameField
*/
internal object KeyFramesFieldsProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
addEnumKeywords<KeyFrameField>(
result = result,
format = JsonObjectArrayTemplate,
existing = parentPropertyModel.declaredFieldNamesSet
)
}
}
/**
* Provides completion for the fields of KeyFrame children. A KeyFrame child can be any of [KeyFrameField].
*/
internal object KeyFrameChildFieldsCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
// TODO(b/207030860): For consistency, make it so that JsonPropertyModel may be used here. It currently won't work because the model
// doesn't consider a property defined by an array of objects.
// Obtain existing list of existing properties
val parentObject = parameters.position.parentOfType<JsonObject>(withSelf = false) ?: return
val existingFieldsSet = parentObject.propertyList.map { it.name }.toSet()
// We have to know the type of KeyFrame we are autocompleting for (KeyPositions, KeyAttributes, etc)
val keyFrameTypeName = parentObject.parentOfType<JsonProperty>(withSelf = false)?.name ?: return
// Look for the `frames` property, we want to know the size of its array (if present), since all other numeric properties should have an
// array of the same size
val framesProperty = parentObject.findProperty(KeyFrameChildCommonField.Frames.keyWord)
val arrayCountInFramesProperty = (framesProperty?.value as? JsonArray)?.valueList?.size ?: 1
// Create the template that will be used by any numeric property we autocomplete
val jsonNumberArrayTemplate = buildJsonNumberArrayTemplate(count = arrayCountInFramesProperty)
// We've done some read operations, check for cancellation
ProgressManager.checkCanceled()
// Common fields for any type of KeyFrame
KeyFrameChildCommonField.values().forEach {
if (existingFieldsSet.contains(it.keyWord)) {
return@forEach
}
when (it) {
KeyFrameChildCommonField.Frames -> result.addLookupElement(lookupString = it.keyWord, format = jsonNumberArrayTemplate)
else -> result.addLookupElement(lookupString = it.keyWord, format = JsonStringValueTemplate)
}
}
// Figure out which type of KeyFrame the completion is being called on, and offer completion for their respective fields
when (keyFrameTypeName) {
KeyFrameField.Positions.keyWord -> {
addKeyPositionFields(result, existingFieldsSet) {
// Some KeyPosition fields take either a Number Array value or a String value
if (isNumberArrayType(it)) jsonNumberArrayTemplate else JsonStringValueTemplate
}
}
KeyFrameField.Attributes.keyWord -> {
// KeyAttributes properties are the same as the RenderTransform fields
addEnumKeywords<RenderTransform>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet)
}
KeyFrameField.Cycles.keyWord -> {
// KeyCycles properties are a mix of RenderTransform fields and KeyCycles specific fields
addEnumKeywords<RenderTransform>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet)
addEnumKeywords<KeyCycleField>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet)
}
else -> {
thisLogger().warn("Completion on unknown KeyFrame type: $keyFrameTypeName")
}
}
}
/**
* Add LookupElements to the [result] for each non-repeated [KeyPositionField] using the [InsertionFormat] returned by [templateProvider].
*/
private fun addKeyPositionFields(
result: CompletionResultSet,
existing: Set<String>,
templateProvider: (KeyPositionField) -> InsertionFormat
) {
KeyPositionField.values().forEach { keyPositionField ->
if (existing.contains(keyPositionField.keyWord)) {
// Skip repeated fields
return@forEach
}
result.addLookupElement(lookupString = keyPositionField.keyWord, format = templateProvider(keyPositionField))
}
}
private fun isNumberArrayType(keyPositionField: KeyPositionField) =
when (keyPositionField) {
// Only some KeyPosition fields receive a Number value
KeyPositionField.PercentX,
KeyPositionField.PercentY,
KeyPositionField.PercentWidth,
KeyPositionField.PercentHeight -> true
else -> false
}
}
/**
* Provides plaint-text completion for each of the elements in the Enum.
*
* The provided values come from [ConstraintLayoutKeyWord.keyWord].
*/
internal class EnumValuesCompletionProvider<E>(private val enumClass: KClass<E>)
: CompletionProvider<CompletionParameters>() where E : Enum<E>, E : ConstraintLayoutKeyWord {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
enumClass.java.enumConstants.forEach {
result.addLookupElement(lookupString = it.keyWord)
}
}
}
/**
* Add the [ConstraintLayoutKeyWord.keyWord] of the enum constants as a completion result that takes a string for its value.
*/
private inline fun <reified E> CompletionResultSet.addEnumKeyWordsWithStringValueTemplate(
existing: Set<String>
) where E : Enum<E>, E : ConstraintLayoutKeyWord {
addEnumKeywords<E>(result = this, existing = existing, format = JsonStringValueTemplate)
}
/**
* Add the [ConstraintLayoutKeyWord.keyWord] of the enum constants as a completion result that takes a number for its value.
*/
private inline fun <reified E> CompletionResultSet.addEnumKeyWordsWithNumericValueTemplate(
existing: Set<String>
) where E : Enum<E>, E : ConstraintLayoutKeyWord {
addEnumKeywords<E>(result = this, existing = existing, format = JsonNumericValueTemplate)
}
/**
* Helper function to simplify adding enum constant members to the completion result.
*/
private inline fun <reified E> addEnumKeywords(
result: CompletionResultSet,
existing: Set<String> = emptySet(),
format: InsertionFormat? = null
) where E : Enum<E>, E : ConstraintLayoutKeyWord {
E::class.java.enumConstants.forEach { constant ->
if (!existing.contains(constant.keyWord)) {
result.addLookupElement(lookupString = constant.keyWord, format = format)
}
}
}

101
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/BaseJsonElementModel.kt

@ -0,0 +1,101 @@
/*
* Copyright (C) 2021 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.code.completion.constraintlayout.provider.model
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.json.psi.JsonElement
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonProperty
import com.intellij.openapi.progress.ProgressManager
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
/**
* Base model for [JsonElement], sets a pointer to avoid holding to the element itself.
*/
internal abstract class BaseJsonElementModel<E: JsonElement>(element: E) {
protected val elementPointer = SmartPointerManager.createPointer(element)
}
/**
* Base model for a [JsonProperty].
*
* Populates some common fields and provides useful function while avoiding holding to PsiElement instances.
*/
internal open class JsonPropertyModel(element: JsonProperty): BaseJsonElementModel<JsonProperty>(element) {
/**
* The [JsonObject] that describes this [JsonProperty].
*/
private val innerJsonObject: JsonObject? = elementPointer.element?.getChildOfType<JsonObject>()
/**
* A mapping of the containing [JsonProperty]s by their declare name.
*/
private val propertiesByName: Map<String, JsonProperty> =
innerJsonObject?.propertyList?.associateBy { it.name } ?: emptyMap()
/**
* [List] of all the children of this element that are [JsonProperty].
*/
protected val innerProperties: Collection<JsonProperty> = propertiesByName.values
/**
* Name of the [JsonProperty].
*/
val name: String?
get() = elementPointer.element?.name
/**
* A set of names for all declared properties in this [JsonProperty].
*/
val declaredFieldNamesSet: Set<String> = propertiesByName.keys
/**
* For the children of the current element, returns the [JsonProperty] which name matches the given [name]. Null if none of them does.
*/
protected fun findProperty(name: String): JsonProperty? = propertiesByName[name]
/**
* Returns true if this [JsonProperty] contains another [JsonProperty] declared by the given [name].
*/
fun containsPropertyOfName(name: String): Boolean = propertiesByName.containsKey(name)
/**
* Returns the containing [JsonProperty].
*
* May return null if this model is for a top level [JsonProperty].
*/
fun getParentProperty(): JsonProperty? = elementPointer.element?.parentOfType<JsonProperty>(withSelf = false)
companion object {
/**
* Returns the [JsonPropertyModel] where the completion is performed on an inner [JsonProperty], including if the completion is on the
* value side of the inner [JsonProperty].
*
* In other words, the model of the second [JsonProperty] parent if the element on [CompletionParameters.getPosition] is NOT a
* [JsonProperty].
*
* Or the model of the first [JsonProperty] parent if the element on [CompletionParameters.getPosition] is a [JsonProperty].
*/
fun getModelForCompletionOnInnerJsonProperty(parameters: CompletionParameters): JsonPropertyModel? {
val parentJsonProperty = getJsonPropertyParent(parameters) ?: return null
ProgressManager.checkCanceled()
return JsonPropertyModel(parentJsonProperty)
}
}
}

70
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetModel.kt

@ -0,0 +1,70 @@
/*
* Copyright (C) 2021 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.code.completion.constraintlayout.provider.model
import com.android.tools.compose.code.completion.constraintlayout.KeyWords
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.openapi.progress.ProgressManager
/**
* Model for the JSON block corresponding to a single ConstraintSet.
*
* A ConstraintSet is a state that defines a specific layout of the contents in a ConstraintLayout.
*/
internal class ConstraintSetModel(jsonProperty: JsonProperty) : JsonPropertyModel(jsonProperty) {
/**
* List of properties that have a constraint block assigned to it.
*/
private val propertiesWithConstraints = innerProperties.filter { it.name != KeyWords.Extends }
/**
* Name of the ConstraintSet this is extending constraints from.
*/
val extendsFrom: String? = (findProperty(KeyWords.Extends)?.value as? JsonStringLiteral)?.value
/**
* List of IDs declared in this ConstraintSet.
*/
val declaredIds = propertiesWithConstraints.map { it.name }
/**
* The constraints (by widget ID) explicitly declared in this ConstraintSet.
*
* Note that it does not resolve constraints inherited from [extendsFrom].
*/
val constraintsById: Map<String, ConstraintsModel> =
propertiesWithConstraints.associate { property ->
property.name to ConstraintsModel(property)
}
// TODO(b/207030860): Add a method that can pull all resolved constraints for each widget ID, it could be useful to make sure we are not
// offering options that are implicitly present from the 'Extends' ConstraintSet
companion object {
/**
* Returns a [ConstraintSetModel], for when the completion is performed on a property or the value of a property within a ConstraintSet
* declaration.
*/
fun getModelForCompletionOnConstraintSetProperty(parameters: CompletionParameters): ConstraintSetModel? {
val parentJsonProperty = getJsonPropertyParent(parameters) ?: return null
ProgressManager.checkCanceled()
return ConstraintSetModel(parentJsonProperty)
}
}
}

66
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetsPropertyModel.kt

@ -0,0 +1,66 @@
/*
* Copyright (C) 2021 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.code.completion.constraintlayout.provider.model
import com.android.tools.compose.code.completion.constraintlayout.KeyWords
import com.intellij.json.psi.JsonProperty
/**
* Model for the `ConstraintSets` Json block.
*
* The `ConstraintSets` Json block, is a collection of different ConstraintSets, each of which describes a state of the layout by defining
* properties of each of its widgets such as width, height or their layout constraints.
*
* @param constraintSetsElement The PSI element of the `ConstraintSets` Json property
*/
internal class ConstraintSetsPropertyModel(
constraintSetsElement: JsonProperty
) : JsonPropertyModel(constraintSetsElement) {
// TODO(b/209839226): Explore how we could use these models to validate the syntax or structure of the JSON as well as to check logic
// correctness through Inspections/Lint
/**
* List of all ConstraintSet elements in the Json block.
*/
val constraintSets: List<ConstraintSetModel> = innerProperties.map { ConstraintSetModel(it) }
/**
* The names of all ConstraintSets in this block.
*/
fun getConstraintSetNames(): Collection<String> {
return declaredFieldNamesSet
}
/**
* Returns the remaining possible fields for the given [constraintSetName], this is done by reading all fields in all ConstraintSets and
* subtracting the fields already present in [constraintSetName]. Most of these should be the IDs that represent constrained widgets.
*/
fun getRemainingFieldsForConstraintSet(constraintSetName: String): List<String> {
val availableNames = mutableSetOf(KeyWords.Extends)
val usedNames = mutableSetOf<String>()
constraintSets.forEach { constraintSet ->
constraintSet.declaredFieldNamesSet.forEach { propertyName ->
if (constraintSet.name == constraintSetName) {
usedNames.add(propertyName)
}
else {
availableNames.add(propertyName)
}
}
}
availableNames.removeAll(usedNames)
return availableNames.toList()
}
}

29
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintsModel.kt

@ -0,0 +1,29 @@
/*
* Copyright (C) 2021 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.code.completion.constraintlayout.provider.model
import com.intellij.json.psi.JsonProperty
/**
* Model for the JSON block that corresponds to the constraints applied on a widget (defined by an ID).
*
* Constraints are a set of instructions that define the widget's dimensions, position with respect to other widgets and render-time
* transforms.
*/
internal class ConstraintsModel(jsonProperty: JsonProperty): JsonPropertyModel(jsonProperty) {
// TODO(b/207030860): Fill the contents of this model as is necessary, keeping in mind that it would be useful to have fields like
// 'verticalConstraints', 'hasBaseline', 'dimensionBehavior', etc...
}

53
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/CompletionResultSetUtils.kt

@ -0,0 +1,53 @@
/*
* Copyright (C) 2022 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.completion
import com.android.tools.compose.completion.inserthandler.FormatWithCaretInsertHandler
import com.android.tools.compose.completion.inserthandler.FormatWithLiveTemplateInsertHandler
import com.android.tools.compose.completion.inserthandler.FormatWithNewLineInsertHandler
import com.android.tools.compose.completion.inserthandler.InsertionFormat
import com.android.tools.compose.completion.inserthandler.LiteralNewLineFormat
import com.android.tools.compose.completion.inserthandler.LiteralWithCaretFormat
import com.android.tools.compose.completion.inserthandler.LiveTemplateFormat
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
/**
* Utility function to simplify adding [com.intellij.codeInsight.lookup.LookupElement]s with [InsertionFormat] support.
*
* Note that the added lookup element is case-sensitive.
*
* @param lookupString The base text to autocomplete, also used to match the user input with a completion result.
* @param tailText Grayed out text shown after the LookupElement name, not part of the actual completion.
* @param format InsertionFormat to handle the rest of the completion. See different implementations of [InsertionFormat] for more.
*/
fun CompletionResultSet.addLookupElement(lookupString: String, tailText: String? = null, format: InsertionFormat? = null) {
// Populate the lookupObject param to allow multiple LookupElements with the same lookupString, differentiated by the tailText.
var lookupBuilder = LookupElementBuilder.create(tailText ?: lookupString, lookupString)
if (format != null) {
val insertionHandler = when (format) {
is LiteralWithCaretFormat -> FormatWithCaretInsertHandler(format)
is LiteralNewLineFormat -> FormatWithNewLineInsertHandler(format)
is LiveTemplateFormat -> FormatWithLiveTemplateInsertHandler(format)
}
lookupBuilder = lookupBuilder.withInsertHandler(insertionHandler)
}
lookupBuilder = lookupBuilder.withCaseSensitivity(true)
if (tailText != null) {
lookupBuilder = lookupBuilder.withTailText(tailText, true)
}
addElement(lookupBuilder)
}

46
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithCaretInsertHandler.kt

@ -0,0 +1,46 @@
/*
* Copyright (C) 2022 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.completion.inserthandler
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.editor.EditorModificationUtil
import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.psi.PsiDocumentManager
/**
* Handles insertions of an [InsertionFormat], moving the caret at the position specified by the '|' character.
*/
class FormatWithCaretInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
with(context) {
val isMoveCaret = format.insertableString.contains('|')
val stringToInsert = format.insertableString.replace("|", "")
// Insert the string without the reserved character: |
EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true)
PsiDocumentManager.getInstance(project).commitDocument(document)
// Move caret to the position indicated by '|'
EditorActionUtil.moveCaretToLineEnd(editor, false, true)
if (isMoveCaret && stringToInsert.isNotEmpty()) {
val caretPosition = format.insertableString.indexOf('|').coerceAtLeast(0)
EditorModificationUtil.moveCaretRelatively(editor, caretPosition - stringToInsert.length)
}
}
}
}

86
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithLiveTemplateInsertHandler.kt

@ -0,0 +1,86 @@
/*
* Copyright (C) 2022 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.completion.inserthandler
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.daemon.impl.quickfix.EmptyExpression
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.ConstantNode
/**
* Handles insertions of an [InsertionFormat] using the [TemplateManager], stopping at every '<>' for user input.
*/
class FormatWithLiveTemplateInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val templateManager = TemplateManager.getInstance(context.project)
val template = templateManager.createTemplate("", "")
// Create template from the given format
getTemplateSegments(format).forEach { segment ->
val text = segment.textSegment
if (segment.takesUserInput) {
if (text.isNotEmpty()) {
template.addVariable(ConstantNode(text), true)
}
else {
template.addVariable(EmptyExpression(), true)
}
}
else {
template.addTextSegment(text)
}
}
templateManager.startTemplate(context.editor, template)
}
}
/**
* Extracts the insertable text segments from the [InsertionFormat] indicating whether each segment is simple text or if it expects user
* input.
*/
private fun getTemplateSegments(format: InsertionFormat): List<LiveTemplateSegment> {
val segments = mutableListOf<LiveTemplateSegment>()
val templateText = format.insertableString
var start = 0
var end = 0
// Normal text does not take user input
var isNormalText = true
while (end < templateText.length) {
val currentChar = templateText.elementAtOrNull(end)
if (currentChar == '<' || currentChar == '>') {
// Stop at the marker characters and add any pending segment
segments.add(LiveTemplateSegment(takesUserInput = !isNormalText, templateText.substring(start, end)))
isNormalText = currentChar == '>'
start = end + 1 // update start but skip this char
}
end++
}
if (end - start > 1) {
// Add the last segment if not empty (end index is exclusive)
segments.add(LiveTemplateSegment(takesUserInput = !isNormalText, templateText.substring(start, end)))
}
return segments
}
private data class LiveTemplateSegment(
val takesUserInput: Boolean,
val textSegment: String
)

43
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormatHandler.kt → idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithNewLineInsertHandler.kt

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 The Android Open Source Project
* Copyright (C) 2022 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.
@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.compose.code.completion.constraintlayout
package com.android.tools.compose.completion.inserthandler
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
@ -27,45 +27,12 @@ import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.psi.PsiDocumentManager
/**
* An [InsertHandler] to handle [InsertionFormat].
* Handles insertions of an [InsertionFormat], applying new a line at the `\n` character.
*
* The [InsertionFormat] object needs to be present in [LookupElement.getObject] to be handled here.
* Applies the new line with [IdeActions.ACTION_EDITOR_ENTER] and moves the caret at the end of the new line.
*/
internal object InsertionFormatHandler : InsertHandler<LookupElement> {
class FormatWithNewLineInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val format = item.`object` as? InsertionFormat ?: return
when (format) {
is LiteralWithCaretFormat -> handleCaretInsertion(context, format)
is LiteralNewLineFormat -> handleNewLineInsertion(context, format)
}
}
/**
* Handles insertions of [LiteralWithCaretFormat], moving the caret at the position specified by the '|' character.
*/
private fun handleCaretInsertion(context: InsertionContext, format: LiteralWithCaretFormat) {
with(context) {
val isMoveCaret = format.insertableString.contains('|')
val stringToInsert = format.insertableString.replace("|", "")
// Insert the string without the reserved character: |
EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true)
PsiDocumentManager.getInstance(project).commitDocument(document)
// Move caret to the position indicated by '|'
EditorActionUtil.moveCaretToLineEnd(editor, false, true)
if (isMoveCaret && stringToInsert.isNotEmpty()) {
val caretPosition = format.insertableString.indexOf('|').coerceAtLeast(0)
EditorModificationUtil.moveCaretRelatively(editor, caretPosition - stringToInsert.length)
}
}
}
/**
* Handles insertions of [LiteralNewLineFormat], applying the new line with the [IdeActions.ACTION_EDITOR_ENTER] and moving the caret at
* the end of the new line.
*/
private fun handleNewLineInsertion(context: InsertionContext, format: LiteralNewLineFormat) {
val literal = format.insertableString
with(context) {
val newLineOffset = literal.indexOf('\n')

50
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/InsertionFormat.kt

@ -0,0 +1,50 @@
/*
* Copyright (C) 2022 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.completion.inserthandler
/**
* Describes a string that may be automatically inserted when selecting an autocomplete option.
*/
sealed class InsertionFormat(
val insertableString: String
)
/**
* Inserts the string after the auto-completed value.
*
* The caret will be moved to the position marked by the '|' character.
*/
class LiteralWithCaretFormat(literalFormat: String) : InsertionFormat(literalFormat)
/**
* Inserts the string after the auto-complete value.
*
* It will insert a new line as if it was done by an ENTER keystroke, marked by the '\n' character.
*
* Note that it will only apply the new line on the first '\n' character.
*/
class LiteralNewLineFormat(literalFormat: String) : InsertionFormat(literalFormat)
/**
* Inserts a string driven by Live templates. The string is inserted after the auto-completed value.
*
* Use '<' and '>' to delimit a range of text the user is expected to edit, may contain multiple instances of these delimiters.
*
* Eg: For the string `"<0123>, <text>"`. The '0123' will be selected in the editor for the user to modify, once they press Enter, it
* will select 'text' for the user to modify until all marked snippets of the strings are handled or the user presses ESC to keep the text
* as is.
*/
class LiveTemplateFormat(templateFormat: String) : InsertionFormat(templateFormat)

1
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManagerFactory.kt

@ -19,6 +19,7 @@ import com.intellij.debugger.PositionManager
import com.intellij.debugger.PositionManagerFactory
import com.intellij.debugger.engine.DebugProcess
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManager
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManagerFactory
class ComposePositionManagerFactory : PositionManagerFactory() {
override fun createPositionManager(process: DebugProcess): PositionManager {

3
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreateComposableFunction.kt

@ -42,6 +42,7 @@ import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelectorOrThis
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
class ComposeUnresolvedFunctionFixContributor : QuickFixContributor {
@ -83,7 +84,7 @@ private class ComposeUnresolvedFunctionFixFactory : KotlinSingleIntentionActionF
// Composable function usually starts with uppercase first letter.
if (name.isBlank() || !name[0].isUpperCase()) return null
val ktCreateCallableFromUsageFix = CreateCallableFromUsageFix(unresolvedCall) { listOfNotNull(createNewComposeFunctionInfo(name, unresolvedCall)) }
val ktCreateCallableFromUsageFix = CreateCallableFromUsageFix(unresolvedCall) { listOfNotNull(createNewComposeFunctionInfo(name, it)) }
// Since CreateCallableFromUsageFix is no longer an 'open' class, we instead use delegation to customize the text.
return object : IntentionAction by ktCreateCallableFromUsageFix {

5
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreatePreviewAction.kt

@ -16,8 +16,8 @@
package com.android.tools.compose.intentions
import com.android.tools.compose.COMPOSABLE_FQ_NAMES
import com.android.tools.compose.COMPOSE_PREVIEW_ANNOTATION_FQN
import com.android.tools.compose.ComposeBundle
import com.android.tools.compose.ComposeLibraryNamespace
import com.android.tools.compose.isComposableAnnotation
import com.android.tools.compose.fqNameMatches
import com.android.tools.idea.flags.StudioFlags
@ -74,8 +74,7 @@ class ComposeCreatePreviewAction : IntentionAction {
if (editor == null || file == null) return
val composableAnnotationEntry = getComposableAnnotationEntry(editor, file) ?: return
val composableFunction = composableAnnotationEntry.parentOfType<KtFunction>() ?: return
val previewAnnotationEntry = KtPsiFactory(project).createAnnotationEntry(
"@" + ComposeLibraryNamespace.ANDROIDX_COMPOSE.previewAnnotationName)
val previewAnnotationEntry = KtPsiFactory(project).createAnnotationEntry("@${COMPOSE_PREVIEW_ANNOTATION_FQN}")
ShortenReferences.DEFAULT.process(composableFunction.addAnnotationEntry(previewAnnotationEntry))
}

94
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeSurroundWithWidgetAction.kt

@ -29,10 +29,19 @@ 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.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.prevLeaf
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import com.intellij.ui.popup.list.ListPopupImpl
import org.jetbrains.kotlin.idea.codeInsight.surroundWith.statement.KotlinStatementSurroundDescriptor
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.util.isLineBreak
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
/**
* Intention action that includes [ComposeSurroundWithBoxAction], [ComposeSurroundWithRowAction], [ComposeSurroundWithColumnAction].
@ -76,6 +85,58 @@ class ComposeSurroundWithWidgetActionGroup :
override fun getFamilyName() = ComposeBundle.message("surround.with.widget.intention.text")
}
/**
* Finds the first [KtCallExpression] at the given offset stopping if it finds any [KtNamedFunction] so it does not
* exit the `Composable`.
*/
private fun PsiFile.findParentCallExpression(offset: Int): PsiElement? =
PsiTreeUtil.findElementOfClassAtOffsetWithStopSet(this, offset, KtCallExpression::class.java,
false, KtNamedFunction::class.java)
/**
* Finds the nearest surroundable [PsiElement] starting at the given offset and looking at the parents. If the offset is at
* the end of a line, this method might look in the immediately previous offset.
*/
private fun findNearestSurroundableElement(file: PsiFile, offset: Int): PsiElement? {
val nearestElement = file.findElementAt(offset)?.let {
if (it.isLineBreak()) {
file.findParentCallExpression(it.prevLeaf(true)?.startOffset ?: (offset - 1))
}
else it
} ?: return null
return file.findParentCallExpression(nearestElement.startOffset)
}
/**
* Finds the [TextRange] to surround based on the current [editor] selection. It returns null if there is no block that
* can be selected.
*/
fun findSurroundingSelectionRange(file: PsiFile, editor: Editor): TextRange? {
if (!editor.selectionModel.hasSelection()) return null
// We try to select full call elements to avoid the selection falling in the middle of, for example, a string.
// This way, selecting the middle of two strings would still wrap the parent calls like for the following example:
//
// Text("Hello <selection>world!")
// Button(...)
// Text("By</selection>e")
//
// Would wrap the three elements instead of just the Button.
val startSelectionOffset = findNearestSurroundableElement(file, editor.selectionModel.selectionStart)?.startOffset ?: Int.MAX_VALUE
val endSelectionOffset = findNearestSurroundableElement(file, editor.selectionModel.selectionEnd)?.endOffset ?: -1
val statements = CodeInsightUtils.findElements(file,
minOf(editor.selectionModel.selectionStart, startSelectionOffset),
maxOf(editor.selectionModel.selectionEnd, endSelectionOffset),
CodeInsightUtils.ElementKind.EXPRESSION)
.filter { it.isInsideComposableCode() }
if (statements.isNotEmpty()) {
return TextRange.create(statements.minOf { it.startOffset }, statements.maxOf { it.endOffset })
}
return null
}
/**
* Surrounds selected statements inside a @Composable function with a widget.
*
@ -87,26 +148,31 @@ abstract class ComposeSurroundWithWidgetAction : IntentionAction, HighPriorityAc
override fun startInWriteAction(): Boolean = true
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> return false
file == null || editor == null -> return false
!file.isWritable || file !is KtFile || !editor.selectionModel.hasSelection() -> return false
else -> {
val element = file.findElementAt(editor.caretModel.offset) ?: return false
if (!element.isInsideComposableCode()) return false
val statements = KotlinStatementSurroundDescriptor()
.getElementsToSurround(file, editor.selectionModel.selectionStart, editor.selectionModel.selectionEnd)
return statements.isNotEmpty()
private fun findSurroundableRange(file: PsiFile, editor: Editor): TextRange? = if (editor.selectionModel.hasSelection()) {
findSurroundingSelectionRange(file, editor)
}
else {
findNearestSurroundableElement(file, editor.caretModel.offset)?.textRange
}
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false
file == null || editor == null -> false
!file.isWritable || file !is KtFile -> false
else -> findSurroundableRange(file, editor) != null
}
protected abstract fun getTemplate(): TemplateImpl?
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (editor == null || file == null) return
val surroundRange = findSurroundableRange(file, editor) ?: return
// Extend the selection if it does not match the inferred range
if (editor.selectionModel.selectionStart != surroundRange.startOffset ||
editor.selectionModel.selectionEnd != surroundRange.endOffset) {
editor.selectionModel.setSelection(surroundRange.startOffset, surroundRange.endOffset)
}
InvokeTemplateAction(getTemplate(), editor, project, HashSet()).perform()
}

1
idea-plugin/src/main/kotlin/com/android/tools/idea/flags/StudioFlags.kt

@ -14,6 +14,7 @@ object StudioFlags {
val COMPOSE_COMPLETION_PRESENTATION = Flag(true)
val COMPOSE_COMPLETION_WEIGHER = Flag(true)
val COMPOSE_STATE_OBJECT_CUSTOM_RENDERER = Flag(true)
val COMPOSE_CONSTRAINTLAYOUT_COMPLETION = Flag(true)
}
class Flag<T>(val value: T) {

Loading…
Cancel
Save