Ilya Ryzhenkov
2 years ago
82 changed files with 6483 additions and 31 deletions
@ -1,3 +1,10 @@
|
||||
pluginManagement { |
||||
repositories { |
||||
maven("https://oss.sonatype.org/content/repositories/snapshots/") |
||||
gradlePluginPortal() |
||||
} |
||||
} |
||||
|
||||
includeBuild("../gradle-plugins") { |
||||
name = "compose-gradle-components" |
||||
} |
@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.tools.compose |
||||
|
||||
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors |
||||
import com.intellij.openapi.editor.colors.TextAttributesKey |
||||
import com.intellij.psi.PsiElement |
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor |
||||
import org.jetbrains.kotlin.idea.highlighter.KotlinHighlightingVisitorExtension |
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall |
||||
|
||||
// Used to apply styles for calls to @Composable functions. |
||||
class ComposableHighlighter : KotlinHighlightingVisitorExtension() { |
||||
override fun highlightDeclaration(elementToHighlight: PsiElement, descriptor: DeclarationDescriptor): TextAttributesKey? { |
||||
return null |
||||
} |
||||
|
||||
override fun highlightCall(elementToHighlight: PsiElement, resolvedCall: ResolvedCall<*>): TextAttributesKey? { |
||||
return if (resolvedCall.isComposableInvocation()) COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY else null |
||||
} |
||||
|
||||
companion object TextAttributeRegistry { |
||||
val COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY: TextAttributesKey |
||||
const val COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME = "ComposableCallTextAttributes" |
||||
|
||||
init { |
||||
COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY = TextAttributesKey.createTextAttributesKey( |
||||
COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME, |
||||
DefaultLanguageHighlighterColors.FUNCTION_CALL) |
||||
} |
||||
} |
||||
} |
||||
|
@ -0,0 +1,520 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.tools.compose |
||||
|
||||
import com.intellij.psi.PsiElement |
||||
import org.jetbrains.kotlin.container.StorageComponentContainer |
||||
import org.jetbrains.kotlin.container.useInstance |
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor |
||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor |
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor |
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor |
||||
import org.jetbrains.kotlin.descriptors.PropertyDescriptor |
||||
import org.jetbrains.kotlin.descriptors.PropertyGetterDescriptor |
||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor |
||||
import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor |
||||
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor |
||||
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi |
||||
import org.jetbrains.kotlin.platform.TargetPlatform |
||||
import org.jetbrains.kotlin.psi.KtAnnotatedExpression |
||||
import org.jetbrains.kotlin.psi.KtAnnotationEntry |
||||
import org.jetbrains.kotlin.psi.KtCallableReferenceExpression |
||||
import org.jetbrains.kotlin.psi.KtClass |
||||
import org.jetbrains.kotlin.psi.KtElement |
||||
import org.jetbrains.kotlin.psi.KtExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtFunction |
||||
import org.jetbrains.kotlin.psi.KtFunctionLiteral |
||||
import org.jetbrains.kotlin.psi.KtLambdaExpression |
||||
import org.jetbrains.kotlin.psi.KtProperty |
||||
import org.jetbrains.kotlin.psi.KtPropertyAccessor |
||||
import org.jetbrains.kotlin.psi.KtPsiUtil |
||||
import org.jetbrains.kotlin.psi.KtTryExpression |
||||
import org.jetbrains.kotlin.resolve.BindingContext |
||||
import org.jetbrains.kotlin.resolve.calls.checkers.AdditionalTypeChecker |
||||
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker |
||||
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext |
||||
import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext |
||||
import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch |
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall |
||||
import org.jetbrains.kotlin.resolve.calls.model.VariableAsFunctionResolvedCall |
||||
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall |
||||
import org.jetbrains.kotlin.resolve.calls.util.getValueArgumentForExpression |
||||
import org.jetbrains.kotlin.resolve.inline.InlineUtil.canBeInlineArgument |
||||
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInline |
||||
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlineParameter |
||||
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlinedArgument |
||||
import org.jetbrains.kotlin.resolve.sam.getSingleAbstractMethodOrNull |
||||
import org.jetbrains.kotlin.types.KotlinType |
||||
import org.jetbrains.kotlin.types.TypeUtils |
||||
import org.jetbrains.kotlin.types.lowerIfFlexible |
||||
import org.jetbrains.kotlin.types.typeUtil.builtIns |
||||
import org.jetbrains.kotlin.types.upperIfFlexible |
||||
import org.jetbrains.kotlin.util.OperatorNameConventions |
||||
|
||||
|
||||
open class ComposableCallChecker : CallChecker, AdditionalTypeChecker, |
||||
StorageComponentContainerContributor { |
||||
override fun registerModuleComponents( |
||||
container: StorageComponentContainer, |
||||
platform: TargetPlatform, |
||||
moduleDescriptor: ModuleDescriptor |
||||
) { |
||||
//if (!platform.isJvm()) return |
||||
container.useInstance(this) |
||||
} |
||||
|
||||
private fun checkInlineLambdaCall( |
||||
resolvedCall: ResolvedCall<*>, |
||||
reportOn: PsiElement, |
||||
context: CallCheckerContext |
||||
) { |
||||
if (resolvedCall !is VariableAsFunctionResolvedCall) return |
||||
val descriptor = resolvedCall.variableCall.resultingDescriptor |
||||
if (descriptor !is ValueParameterDescriptor) return |
||||
if (descriptor.type.hasDisallowComposableCallsAnnotation()) return |
||||
val function = descriptor.containingDeclaration |
||||
if ( |
||||
function is FunctionDescriptor && |
||||
function.isInline && |
||||
function.isMarkedAsComposable() |
||||
) { |
||||
val bindingContext = context.trace.bindingContext |
||||
var node: PsiElement? = reportOn |
||||
loop@while (node != null) { |
||||
when (node) { |
||||
is KtLambdaExpression -> { |
||||
val arg = getArgumentDescriptor(node.functionLiteral, bindingContext) |
||||
if (arg?.type?.hasDisallowComposableCallsAnnotation() == true) { |
||||
val parameterSrc = descriptor.findPsi() |
||||
if (parameterSrc != null) { |
||||
missingDisallowedComposableCallPropagation( |
||||
context, |
||||
parameterSrc, |
||||
descriptor, |
||||
arg |
||||
) |
||||
} |
||||
} |
||||
} |
||||
is KtFunction -> { |
||||
val fn = bindingContext[BindingContext.FUNCTION, node] |
||||
if (fn == function) { |
||||
return |
||||
} |
||||
} |
||||
} |
||||
node = node.parent as? KtElement |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun check( |
||||
resolvedCall: ResolvedCall<*>, |
||||
reportOn: PsiElement, |
||||
context: CallCheckerContext |
||||
) { |
||||
if (!resolvedCall.isComposableInvocation()) { |
||||
checkInlineLambdaCall(resolvedCall, reportOn, context) |
||||
return |
||||
} |
||||
val bindingContext = context.trace.bindingContext |
||||
var node: PsiElement? = reportOn |
||||
loop@while (node != null) { |
||||
when (node) { |
||||
is KtFunctionLiteral -> { |
||||
// keep going, as this is a "KtFunction", but we actually want the |
||||
// KtLambdaExpression |
||||
} |
||||
is KtLambdaExpression -> { |
||||
val descriptor = bindingContext[BindingContext.FUNCTION, node.functionLiteral] |
||||
if (descriptor == null) { |
||||
illegalCall(context, reportOn) |
||||
return |
||||
} |
||||
val composable = descriptor.isComposableCallable(bindingContext) |
||||
if (composable) return |
||||
val arg = getArgumentDescriptor(node.functionLiteral, bindingContext) |
||||
if (arg?.type?.hasDisallowComposableCallsAnnotation() == true) { |
||||
context.trace.record( |
||||
ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE, |
||||
descriptor, |
||||
false |
||||
) |
||||
context.trace.report( |
||||
ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION.on( |
||||
reportOn, |
||||
arg, |
||||
arg.containingDeclaration |
||||
) |
||||
) |
||||
return |
||||
} |
||||
val argTypeDescriptor = arg |
||||
?.type |
||||
?.constructor |
||||
?.declarationDescriptor as? ClassDescriptor |
||||
if (argTypeDescriptor != null) { |
||||
val sam = getSingleAbstractMethodOrNull(argTypeDescriptor) |
||||
if (sam != null && sam.hasComposableAnnotation()) { |
||||
return |
||||
} |
||||
} |
||||
|
||||
// TODO(lmr): in future, we should check for CALLS_IN_PLACE contract |
||||
val inlined = arg != null && |
||||
canBeInlineArgument(node.functionLiteral) && |
||||
isInline(arg.containingDeclaration) && |
||||
isInlineParameter(arg) |
||||
if (!inlined) { |
||||
illegalCall(context, reportOn) |
||||
return |
||||
} else { |
||||
// since the function is inlined, we continue going up the PSI tree |
||||
// until we find a composable context. We also mark this lambda |
||||
context.trace.record( |
||||
ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE, |
||||
descriptor, |
||||
true |
||||
) |
||||
} |
||||
} |
||||
is KtTryExpression -> { |
||||
val tryKeyword = node.tryKeyword |
||||
if ( |
||||
node.tryBlock.textRange.contains(reportOn.textRange) && |
||||
tryKeyword != null |
||||
) { |
||||
context.trace.report( |
||||
ComposeErrors.ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE.on(tryKeyword) |
||||
) |
||||
} |
||||
} |
||||
is KtFunction -> { |
||||
val descriptor = bindingContext[BindingContext.FUNCTION, node] |
||||
if (descriptor == null) { |
||||
illegalCall(context, reportOn) |
||||
return |
||||
} |
||||
val composable = descriptor.isComposableCallable(bindingContext) |
||||
if (!composable) { |
||||
illegalCall(context, reportOn, node.nameIdentifier ?: node) |
||||
} |
||||
if (descriptor.hasReadonlyComposableAnnotation()) { |
||||
// enforce that the original call was readonly |
||||
if (!resolvedCall.isReadOnlyComposableInvocation()) { |
||||
illegalCallMustBeReadonly( |
||||
context, |
||||
reportOn |
||||
) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
is KtProperty -> { |
||||
// NOTE: since we're explicitly going down a different branch for |
||||
// KtPropertyAccessor, the ONLY time we make it into this branch is when the |
||||
// call was done in the initializer of the property/variable. |
||||
val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, node] |
||||
if ( |
||||
descriptor !is LocalVariableDescriptor && |
||||
node.annotationEntries.hasComposableAnnotation(bindingContext) |
||||
) { |
||||
// composables shouldn't have initializers in the first place |
||||
illegalCall(context, reportOn) |
||||
return |
||||
} |
||||
} |
||||
is KtPropertyAccessor -> { |
||||
val property = node.property |
||||
val isComposable = node.annotationEntries.hasComposableAnnotation(bindingContext) |
||||
if (!isComposable) { |
||||
illegalCall(context, reportOn, property.nameIdentifier ?: property) |
||||
} |
||||
val descriptor = bindingContext[BindingContext.PROPERTY_ACCESSOR, node] |
||||
?: return |
||||
if (descriptor.hasReadonlyComposableAnnotation()) { |
||||
// enforce that the original call was readonly |
||||
if (!resolvedCall.isReadOnlyComposableInvocation()) { |
||||
illegalCallMustBeReadonly( |
||||
context, |
||||
reportOn |
||||
) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
is KtCallableReferenceExpression -> { |
||||
illegalComposableFunctionReference(context, node) |
||||
return |
||||
} |
||||
is KtFile -> { |
||||
// if we've made it this far, the call was made in a non-composable context. |
||||
illegalCall(context, reportOn) |
||||
return |
||||
} |
||||
is KtClass -> { |
||||
// composable calls are never allowed in the initializers of a class |
||||
illegalCall(context, reportOn) |
||||
return |
||||
} |
||||
} |
||||
node = node.parent as? KtElement |
||||
} |
||||
} |
||||
|
||||
private fun missingDisallowedComposableCallPropagation( |
||||
context: CallCheckerContext, |
||||
unmarkedParamEl: PsiElement, |
||||
unmarkedParamDescriptor: ValueParameterDescriptor, |
||||
markedParamDescriptor: ValueParameterDescriptor |
||||
) { |
||||
context.trace.report( |
||||
ComposeErrors.MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION.on( |
||||
unmarkedParamEl, |
||||
unmarkedParamDescriptor, |
||||
markedParamDescriptor, |
||||
markedParamDescriptor.containingDeclaration |
||||
) |
||||
) |
||||
} |
||||
|
||||
private fun illegalCall( |
||||
context: CallCheckerContext, |
||||
callEl: PsiElement, |
||||
functionEl: PsiElement? = null |
||||
) { |
||||
context.trace.report(ComposeErrors.COMPOSABLE_INVOCATION.on(callEl)) |
||||
if (functionEl != null) { |
||||
context.trace.report(ComposeErrors.COMPOSABLE_EXPECTED.on(functionEl)) |
||||
} |
||||
} |
||||
|
||||
private fun illegalCallMustBeReadonly( |
||||
context: CallCheckerContext, |
||||
callEl: PsiElement |
||||
) { |
||||
context.trace.report(ComposeErrors.NONREADONLY_CALL_IN_READONLY_COMPOSABLE.on(callEl)) |
||||
} |
||||
|
||||
private fun illegalComposableFunctionReference( |
||||
context: CallCheckerContext, |
||||
refExpr: KtCallableReferenceExpression |
||||
) { |
||||
context.trace.report(ComposeErrors.COMPOSABLE_FUNCTION_REFERENCE.on(refExpr)) |
||||
} |
||||
|
||||
override fun checkType( |
||||
expression: KtExpression, |
||||
expressionType: KotlinType, |
||||
expressionTypeWithSmartCast: KotlinType, |
||||
c: ResolutionContext<*> |
||||
) { |
||||
val bindingContext = c.trace.bindingContext |
||||
val expectedType = c.expectedType |
||||
if (expectedType === TypeUtils.NO_EXPECTED_TYPE) return |
||||
if (expectedType === TypeUtils.UNIT_EXPECTED_TYPE) return |
||||
val expectedComposable = expectedType.hasComposableAnnotation() |
||||
if (expression is KtLambdaExpression) { |
||||
val descriptor = bindingContext[BindingContext.FUNCTION, expression.functionLiteral] |
||||
?: return |
||||
val isComposable = descriptor.isComposableCallable(bindingContext) |
||||
if (expectedComposable != isComposable) { |
||||
val isInlineable = isInlinedArgument( |
||||
expression.functionLiteral, |
||||
c.trace.bindingContext, |
||||
true |
||||
) |
||||
if (isInlineable) return |
||||
|
||||
if (!expectedComposable && isComposable) { |
||||
val inferred = c.trace.bindingContext[ |
||||
ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR, |
||||
descriptor |
||||
] == true |
||||
if (inferred) { |
||||
return |
||||
} |
||||
} |
||||
|
||||
val reportOn = |
||||
if (expression.parent is KtAnnotatedExpression) |
||||
expression.parent as KtExpression |
||||
else expression |
||||
c.trace.report( |
||||
ComposeErrors.TYPE_MISMATCH.on( |
||||
reportOn, |
||||
expectedType, |
||||
expressionTypeWithSmartCast |
||||
) |
||||
) |
||||
} |
||||
return |
||||
} else { |
||||
val nullableAnyType = expectedType.builtIns.nullableAnyType |
||||
val anyType = expectedType.builtIns.anyType |
||||
|
||||
if (anyType == expectedType.lowerIfFlexible() && |
||||
nullableAnyType == expectedType.upperIfFlexible() |
||||
) return |
||||
|
||||
val nullableNothingType = expectedType.builtIns.nullableNothingType |
||||
|
||||
// Handle assigning null to a nullable composable type |
||||
if (expectedType.isMarkedNullable && |
||||
expressionTypeWithSmartCast == nullableNothingType |
||||
) return |
||||
val isComposable = expressionType.hasComposableAnnotation() |
||||
|
||||
if (expectedComposable != isComposable) { |
||||
val reportOn = |
||||
if (expression.parent is KtAnnotatedExpression) |
||||
expression.parent as KtExpression |
||||
else expression |
||||
c.trace.report( |
||||
ComposeErrors.TYPE_MISMATCH.on( |
||||
reportOn, |
||||
expectedType, |
||||
expressionTypeWithSmartCast |
||||
) |
||||
) |
||||
} |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun ResolvedCall<*>.isReadOnlyComposableInvocation(): Boolean { |
||||
if (this is VariableAsFunctionResolvedCall) { |
||||
return false |
||||
} |
||||
return when (val candidateDescriptor = candidateDescriptor) { |
||||
is ValueParameterDescriptor -> false |
||||
is LocalVariableDescriptor -> false |
||||
is PropertyDescriptor -> { |
||||
val isGetter = valueArguments.isEmpty() |
||||
val getter = candidateDescriptor.getter |
||||
if (isGetter && getter != null) { |
||||
getter.hasReadonlyComposableAnnotation() |
||||
} else { |
||||
false |
||||
} |
||||
} |
||||
is PropertyGetterDescriptor -> candidateDescriptor.hasReadonlyComposableAnnotation() |
||||
else -> candidateDescriptor.hasReadonlyComposableAnnotation() |
||||
} |
||||
} |
||||
|
||||
internal fun ResolvedCall<*>.isComposableInvocation(): Boolean { |
||||
if (this is VariableAsFunctionResolvedCall) { |
||||
if (variableCall.candidateDescriptor.type.hasComposableAnnotation()) |
||||
return true |
||||
if (functionCall.resultingDescriptor.hasComposableAnnotation()) return true |
||||
return false |
||||
} |
||||
val candidateDescriptor = candidateDescriptor |
||||
if (candidateDescriptor is FunctionDescriptor) { |
||||
if (candidateDescriptor.isOperator && |
||||
candidateDescriptor.name == OperatorNameConventions.INVOKE |
||||
) { |
||||
if (dispatchReceiver?.type?.hasComposableAnnotation() == true) { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
return when (candidateDescriptor) { |
||||
is ValueParameterDescriptor -> false |
||||
is LocalVariableDescriptor -> false |
||||
is PropertyDescriptor -> { |
||||
val isGetter = valueArguments.isEmpty() |
||||
val getter = candidateDescriptor.getter |
||||
if (isGetter && getter != null) { |
||||
getter.hasComposableAnnotation() |
||||
} else { |
||||
false |
||||
} |
||||
} |
||||
is PropertyGetterDescriptor -> |
||||
candidateDescriptor.correspondingProperty.hasComposableAnnotation() |
||||
else -> candidateDescriptor.hasComposableAnnotation() |
||||
} |
||||
} |
||||
|
||||
internal fun CallableDescriptor.isMarkedAsComposable(): Boolean { |
||||
return when (this) { |
||||
is PropertyGetterDescriptor -> hasComposableAnnotation() |
||||
is ValueParameterDescriptor -> type.hasComposableAnnotation() |
||||
is LocalVariableDescriptor -> type.hasComposableAnnotation() |
||||
is PropertyDescriptor -> false |
||||
else -> hasComposableAnnotation() |
||||
} |
||||
} |
||||
|
||||
// if you called this, it would need to be a composable call (composer, changed, etc.) |
||||
internal fun CallableDescriptor.isComposableCallable(bindingContext: BindingContext): Boolean { |
||||
// if it's marked as composable then we're done |
||||
if (isMarkedAsComposable()) return true |
||||
if ( |
||||
this is FunctionDescriptor && |
||||
bindingContext[ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR, this] == true |
||||
) { |
||||
// even though it's not marked, it is inferred as so by the type system (by being passed |
||||
// into a parameter marked as composable or a variable typed as one. This isn't much |
||||
// different than being marked explicitly. |
||||
return true |
||||
} |
||||
val functionLiteral = findPsi() as? KtFunctionLiteral |
||||
// if it isn't a function literal then we are out of things to try. |
||||
?: return false |
||||
|
||||
if (functionLiteral.annotationEntries.hasComposableAnnotation(bindingContext)) { |
||||
// in this case the function literal itself is being annotated as composable but the |
||||
// annotation isn't in the descriptor itself |
||||
return true |
||||
} |
||||
val lambdaExpr = functionLiteral.parent as? KtLambdaExpression |
||||
if ( |
||||
lambdaExpr != null && |
||||
bindingContext[ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL, lambdaExpr] == true |
||||
) { |
||||
// this lambda was marked as inferred to be composable |
||||
return true |
||||
} |
||||
// TODO(lmr): i'm not sure that this is actually needed at this point, since this should have |
||||
// been covered by the TypeResolutionInterceptorExtension |
||||
val arg = getArgumentDescriptor(functionLiteral, bindingContext) ?: return false |
||||
return arg.type.hasComposableAnnotation() |
||||
} |
||||
|
||||
internal fun getArgumentDescriptor( |
||||
argument: KtFunction, |
||||
bindingContext: BindingContext |
||||
): ValueParameterDescriptor? { |
||||
val call = KtPsiUtil.getParentCallIfPresent(argument) ?: return null |
||||
val resolvedCall = call.getResolvedCall(bindingContext) ?: return null |
||||
val valueArgument = resolvedCall.call.getValueArgumentForExpression(argument) ?: return null |
||||
val mapping = resolvedCall.getArgumentMapping(valueArgument) as? ArgumentMatch ?: return null |
||||
return mapping.valueParameter |
||||
} |
||||
|
||||
internal fun List<KtAnnotationEntry>.hasComposableAnnotation(bindingContext: BindingContext): Boolean { |
||||
for (entry in this) { |
||||
val descriptor = bindingContext.get(BindingContext.ANNOTATION, entry) ?: continue |
||||
if (descriptor.isComposableAnnotation) return true |
||||
} |
||||
return false |
||||
} |
@ -0,0 +1,194 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
|
||||
import com.android.tools.compose.ComposeErrors.COMPOSABLE_FUN_MAIN |
||||
import com.android.tools.compose.ComposeErrors.COMPOSABLE_PROPERTY_BACKING_FIELD |
||||
import com.android.tools.compose.ComposeErrors.COMPOSABLE_SUSPEND_FUN |
||||
import com.android.tools.compose.ComposeErrors.COMPOSABLE_VAR |
||||
import com.intellij.psi.PsiElement |
||||
import org.jetbrains.kotlin.builtins.isSuspendFunctionType |
||||
import org.jetbrains.kotlin.container.StorageComponentContainer |
||||
import org.jetbrains.kotlin.container.useInstance |
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor |
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor |
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor |
||||
import org.jetbrains.kotlin.descriptors.PropertyAccessorDescriptor |
||||
import org.jetbrains.kotlin.descriptors.PropertyDescriptor |
||||
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor |
||||
import org.jetbrains.kotlin.idea.MainFunctionDetector |
||||
import org.jetbrains.kotlin.platform.TargetPlatform |
||||
import org.jetbrains.kotlin.platform.jvm.isJvm |
||||
import org.jetbrains.kotlin.psi.KtDeclaration |
||||
import org.jetbrains.kotlin.psi.KtFunction |
||||
import org.jetbrains.kotlin.psi.KtProperty |
||||
import org.jetbrains.kotlin.psi.KtPropertyAccessor |
||||
import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker |
||||
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext |
||||
import org.jetbrains.kotlin.types.KotlinType |
||||
|
||||
class ComposableDeclarationChecker : DeclarationChecker, StorageComponentContainerContributor { |
||||
override fun registerModuleComponents( |
||||
container: StorageComponentContainer, |
||||
platform: TargetPlatform, |
||||
moduleDescriptor: ModuleDescriptor |
||||
) { |
||||
if (!platform.isJvm()) return |
||||
container.useInstance(this) |
||||
} |
||||
|
||||
override fun check( |
||||
declaration: KtDeclaration, |
||||
descriptor: DeclarationDescriptor, |
||||
context: DeclarationCheckerContext |
||||
) { |
||||
when { |
||||
declaration is KtProperty && |
||||
descriptor is PropertyDescriptor -> checkProperty(declaration, descriptor, context) |
||||
declaration is KtPropertyAccessor && |
||||
descriptor is PropertyAccessorDescriptor -> checkPropertyAccessor( |
||||
declaration, |
||||
descriptor, |
||||
context |
||||
) |
||||
declaration is KtFunction && |
||||
descriptor is FunctionDescriptor -> checkFunction(declaration, descriptor, context) |
||||
} |
||||
} |
||||
|
||||
private fun checkFunction( |
||||
declaration: KtFunction, |
||||
descriptor: FunctionDescriptor, |
||||
context: DeclarationCheckerContext |
||||
) { |
||||
val hasComposableAnnotation = descriptor.hasComposableAnnotation() |
||||
if (descriptor.overriddenDescriptors.isNotEmpty()) { |
||||
val override = descriptor.overriddenDescriptors.first() |
||||
if (override.hasComposableAnnotation() != hasComposableAnnotation) { |
||||
context.trace.report( |
||||
ComposeErrors.CONFLICTING_OVERLOADS.on( |
||||
declaration, |
||||
listOf(descriptor, override) |
||||
) |
||||
) |
||||
} |
||||
} |
||||
if (descriptor.isSuspend && hasComposableAnnotation) { |
||||
context.trace.report( |
||||
COMPOSABLE_SUSPEND_FUN.on(declaration.nameIdentifier ?: declaration) |
||||
) |
||||
} |
||||
val params = descriptor.valueParameters |
||||
val ktparams = declaration.valueParameters |
||||
if (params.size == ktparams.size) { |
||||
for ((param, ktparam) in params.zip(ktparams)) { |
||||
val typeRef = ktparam.typeReference |
||||
if (typeRef != null) { |
||||
checkType(param.type, typeRef, context) |
||||
} |
||||
} |
||||
} |
||||
// NOTE: only use the MainFunctionDetector if the descriptor name is main, to avoid |
||||
// unnecessarily allocating this class |
||||
if (hasComposableAnnotation && |
||||
descriptor.name.asString() == "main" && |
||||
MainFunctionDetector( |
||||
context.trace.bindingContext, |
||||
context.languageVersionSettings |
||||
).isMain(descriptor) |
||||
) { |
||||
context.trace.report( |
||||
COMPOSABLE_FUN_MAIN.on(declaration.nameIdentifier ?: declaration) |
||||
) |
||||
} |
||||
} |
||||
|
||||
private fun checkType( |
||||
type: KotlinType, |
||||
element: PsiElement, |
||||
context: DeclarationCheckerContext |
||||
) { |
||||
if (type.hasComposableAnnotation() && type.isSuspendFunctionType) { |
||||
context.trace.report( |
||||
COMPOSABLE_SUSPEND_FUN.on(element) |
||||
) |
||||
} |
||||
} |
||||
|
||||
private fun checkProperty( |
||||
declaration: KtProperty, |
||||
descriptor: PropertyDescriptor, |
||||
context: DeclarationCheckerContext |
||||
) { |
||||
val hasComposableAnnotation = descriptor |
||||
.getter |
||||
?.hasComposableAnnotation() == true |
||||
if (descriptor.overriddenDescriptors.isNotEmpty()) { |
||||
val override = descriptor.overriddenDescriptors.first() |
||||
val overrideIsComposable = override.hasComposableAnnotation() || |
||||
override.getter?.hasComposableAnnotation() == true |
||||
if (overrideIsComposable != hasComposableAnnotation) { |
||||
context.trace.report( |
||||
ComposeErrors.CONFLICTING_OVERLOADS.on( |
||||
declaration, |
||||
listOf(descriptor, override) |
||||
) |
||||
) |
||||
} |
||||
} |
||||
if (!hasComposableAnnotation) return |
||||
val initializer = declaration.initializer |
||||
val name = declaration.nameIdentifier |
||||
if (initializer != null && name != null) { |
||||
context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name)) |
||||
} |
||||
if (descriptor.isVar && name != null) { |
||||
context.trace.report(COMPOSABLE_VAR.on(name)) |
||||
} |
||||
} |
||||
|
||||
private fun checkPropertyAccessor( |
||||
declaration: KtPropertyAccessor, |
||||
descriptor: PropertyAccessorDescriptor, |
||||
context: DeclarationCheckerContext |
||||
) { |
||||
val propertyDescriptor = descriptor.correspondingProperty |
||||
val propertyPsi = declaration.parent as? KtProperty ?: return |
||||
val name = propertyPsi.nameIdentifier |
||||
val initializer = propertyPsi.initializer |
||||
val hasComposableAnnotation = descriptor.hasComposableAnnotation() |
||||
if (descriptor.overriddenDescriptors.isNotEmpty()) { |
||||
val override = descriptor.overriddenDescriptors.first() |
||||
val overrideComposable = override.hasComposableAnnotation() |
||||
if (overrideComposable != hasComposableAnnotation) { |
||||
context.trace.report( |
||||
ComposeErrors.CONFLICTING_OVERLOADS.on( |
||||
declaration, |
||||
listOf(descriptor, override) |
||||
) |
||||
) |
||||
} |
||||
} |
||||
if (!hasComposableAnnotation) return |
||||
if (initializer != null && name != null) { |
||||
context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name)) |
||||
} |
||||
if (propertyDescriptor.isVar && name != null) { |
||||
context.trace.report(COMPOSABLE_VAR.on(name)) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,64 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
import com.android.tools.modules.* |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.refactoring.rename.naming.AutomaticRenamer |
||||
import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory |
||||
import com.intellij.usageView.UsageInfo |
||||
import org.jetbrains.kotlin.idea.KotlinFileType |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtNamedFunction |
||||
|
||||
/** |
||||
* Renames KtFile if a @Composable function with the same name was renamed. |
||||
*/ |
||||
class ComposableElementAutomaticRenamerFactory : AutomaticRenamerFactory { |
||||
|
||||
override fun isApplicable(element: PsiElement): Boolean { |
||||
if (element.inComposeModule() != true || |
||||
element !is KtNamedFunction || |
||||
element.parent !is KtFile || |
||||
!element.isComposableFunction() |
||||
) return false |
||||
|
||||
val virtualFile = element.containingKtFile.virtualFile |
||||
return virtualFile?.nameWithoutExtension == element.name |
||||
} |
||||
|
||||
override fun getOptionName() = null |
||||
|
||||
override fun isEnabled() = true |
||||
|
||||
override fun setEnabled(enabled: Boolean) {} |
||||
|
||||
override fun createRenamer(element: PsiElement, newName: String?, usages: MutableCollection<UsageInfo>?): AutomaticRenamer { |
||||
return object : AutomaticRenamer() { |
||||
init { |
||||
val file = element.containingFile |
||||
myElements.add(file) |
||||
suggestAllNames(file.name, newName + "." + KotlinFileType.EXTENSION) |
||||
} |
||||
|
||||
override fun getDialogTitle() = ComposeBundle.message("rename.file") |
||||
|
||||
override fun getDialogDescription() = ComposeBundle.message("rename.files.with.following.names") |
||||
|
||||
override fun entityName() = ComposeBundle.message("file.name") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,69 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.android.tools.modules.* |
||||
import com.intellij.psi.util.parentOfType |
||||
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor |
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny |
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.AdditionalExtractableAnalyser |
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.ExtractableCodeDescriptor |
||||
import org.jetbrains.kotlin.idea.util.findAnnotation |
||||
import org.jetbrains.kotlin.psi.KtAnnotated |
||||
import org.jetbrains.kotlin.psi.KtCallExpression |
||||
import org.jetbrains.kotlin.psi.KtLambdaArgument |
||||
import org.jetbrains.kotlin.resolve.BindingContext |
||||
import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch |
||||
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall |
||||
|
||||
/** |
||||
* Adds [COMPOSABLE_FQ_NAME] annotation to a function when it's extracted from a function annotated with [COMPOSABLE_FQ_NAME] |
||||
* or Composable context. |
||||
*/ |
||||
class ComposableFunctionExtractableAnalyser : AdditionalExtractableAnalyser { |
||||
/** |
||||
* Returns @Composable annotation of type of given KtLambdaArgument if there is any otherwise returns null. |
||||
* |
||||
* Example: fun myFunction(context: @Composable () -> Unit) |
||||
* If given [KtLambdaArgument] corresponds to context parameter function returns [AnnotationDescriptor] for @Composable. |
||||
*/ |
||||
private fun KtLambdaArgument.getComposableAnnotation(bindingContext: BindingContext): AnnotationDescriptor? { |
||||
val callExpression = parent as KtCallExpression |
||||
val resolvedCall = callExpression.getResolvedCall(bindingContext) |
||||
val argument = (resolvedCall?.getArgumentMapping(this) as? ArgumentMatch)?.valueParameter ?: return null |
||||
return argument.type.annotations.findAnnotation(ComposeFqNames.Composable) |
||||
} |
||||
|
||||
override fun amendDescriptor(descriptor: ExtractableCodeDescriptor): ExtractableCodeDescriptor { |
||||
if (!StudioFlags.COMPOSE_FUNCTION_EXTRACTION.get() || |
||||
descriptor.extractionData.commonParent.inComposeModule() != true) { |
||||
return descriptor |
||||
} |
||||
|
||||
val bindingContext = descriptor.extractionData.bindingContext ?: return descriptor |
||||
val sourceFunction = descriptor.extractionData.targetSibling |
||||
if (sourceFunction is KtAnnotated) { |
||||
val composableAnnotation = sourceFunction.findAnnotation(ComposeFqNames.Composable)?.resolveToDescriptorIfAny() |
||||
if (composableAnnotation != null) { |
||||
return descriptor.copy(annotations = descriptor.annotations + composableAnnotation) |
||||
} |
||||
} |
||||
val outsideLambda = descriptor.extractionData.commonParent.parentOfType<KtLambdaArgument>(true) |
||||
val composableAnnotation = outsideLambda?.getComposableAnnotation(bindingContext) ?: return descriptor |
||||
return descriptor.copy(annotations = descriptor.annotations + composableAnnotation) |
||||
} |
||||
} |
@ -0,0 +1,44 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import com.intellij.AbstractBundle |
||||
import org.jetbrains.annotations.PropertyKey |
||||
import java.lang.ref.Reference |
||||
import java.lang.ref.SoftReference |
||||
import java.util.ResourceBundle |
||||
|
||||
private const val BUNDLE_NAME = "messages.ComposeBundle" |
||||
|
||||
class ComposeBundle private constructor() { |
||||
companion object { |
||||
private var ourBundle: Reference<ResourceBundle?>? = null |
||||
|
||||
private fun getBundle(): ResourceBundle { |
||||
var bundle: ResourceBundle? = com.intellij.reference.SoftReference.dereference(ourBundle) |
||||
if (bundle == null) { |
||||
bundle = ResourceBundle.getBundle(BUNDLE_NAME) |
||||
ourBundle = SoftReference(bundle) |
||||
} |
||||
return bundle!! |
||||
} |
||||
|
||||
@JvmStatic |
||||
fun message(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg params: Any?): String { |
||||
return AbstractBundle.message(getBundle(), key, *params) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
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.PropertyBinding |
||||
import com.intellij.ui.layout.panel |
||||
|
||||
/** |
||||
* Provides additional options in Settings | Editor | Code Completion section. |
||||
* |
||||
* Contains a checkbox that allows enable/disable [ComposeInsertHandler]. |
||||
*/ |
||||
class ComposeCodeCompletionConfigurable : BoundConfigurable("Compose") { |
||||
private val settings = ComposeSettings.getInstance() |
||||
|
||||
private val checkboxDescriptor = CheckboxDescriptor( |
||||
ComposeBundle.message("compose.enable.insertion.handler"), |
||||
PropertyBinding({ settings.state.isComposeInsertHandlerEnabled }, { settings.state.isComposeInsertHandlerEnabled = it }) |
||||
) |
||||
|
||||
override fun createPanel(): DialogPanel { |
||||
return panel { |
||||
row { |
||||
titledRow("Compose") { |
||||
row { checkBox(checkboxDescriptor) } |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,78 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.tools.compose |
||||
|
||||
import com.intellij.openapi.editor.colors.TextAttributesKey |
||||
import com.intellij.openapi.fileTypes.SyntaxHighlighter |
||||
import com.intellij.openapi.options.colors.AttributesDescriptor |
||||
import com.intellij.openapi.options.colors.ColorDescriptor |
||||
import com.intellij.openapi.options.colors.ColorSettingsPage |
||||
import org.jetbrains.kotlin.idea.highlighter.KotlinColorSettingsPage |
||||
import org.jetbrains.kotlin.idea.highlighter.KotlinHighlightingColors |
||||
import javax.swing.Icon |
||||
|
||||
// This class is used by AndroidStudio to allow the user to change the style of Compose attributes. |
||||
class ComposeColorSettingsPage : ColorSettingsPage { |
||||
override fun getHighlighter(): SyntaxHighlighter { |
||||
return KotlinColorSettingsPage().highlighter |
||||
} |
||||
|
||||
override fun getAdditionalHighlightingTagToDescriptorMap(): MutableMap<String, |
||||
TextAttributesKey> { |
||||
val attributes = HashMap<String, TextAttributesKey>() |
||||
attributes[ComposableHighlighter.COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME] = |
||||
ComposableHighlighter.COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY |
||||
attributes["ANNOTATION"] = KotlinHighlightingColors.ANNOTATION |
||||
attributes["KEYWORD"] = KotlinHighlightingColors.KEYWORD |
||||
attributes["FUNCTION_DECLARATION"] = KotlinHighlightingColors.FUNCTION_DECLARATION |
||||
attributes["FUNCTION_PARAMETER"] = KotlinHighlightingColors.PARAMETER |
||||
return attributes |
||||
} |
||||
|
||||
override fun getIcon(): Icon? { |
||||
return null |
||||
} |
||||
|
||||
override fun getAttributeDescriptors(): Array<AttributesDescriptor> { |
||||
// TODO: this needs to be localized. |
||||
return arrayOf(AttributesDescriptor("Calls to @Compose functions", |
||||
ComposableHighlighter.COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY)) |
||||
} |
||||
|
||||
override fun getColorDescriptors(): Array<ColorDescriptor> { |
||||
return emptyArray() |
||||
} |
||||
|
||||
override fun getDisplayName(): String { |
||||
// TODO: this needs to be localized. |
||||
return "Compose" |
||||
} |
||||
|
||||
override fun getDemoText(): String { |
||||
return "<ANNOTATION>@Composable</ANNOTATION>\n" + |
||||
"<KEYWORD>fun</KEYWORD> <FUNCTION_DECLARATION>Text</FUNCTION_DECLARATION>(" + |
||||
"<FUNCTION_PARAMETER>text</FUNCTION_PARAMETER>: <FUNCTION_PARAMETER>String" + |
||||
"</FUNCTION_PARAMETER>)\n" + |
||||
"}\n" + |
||||
"\n" + |
||||
"<ANNOTATION>@Composable</ANNOTATION>\n" + |
||||
"<KEYWORD>fun</KEYWORD> <FUNCTION_DECLARATION>Greeting</FUNCTION_DECLARATION>() {\n" + |
||||
" <ComposableCallTextAttributes>Text</ComposableCallTextAttributes>(" + |
||||
"<FUNCTION_PARAMETER>\"Hello\"</FUNCTION_PARAMETER>)\n" + |
||||
"}" |
||||
} |
||||
} |
@ -0,0 +1,74 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.tools.compose |
||||
|
||||
import com.android.tools.modules.inComposeModule |
||||
import com.intellij.openapi.extensions.Extensions |
||||
import com.intellij.openapi.project.Project |
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic |
||||
import org.jetbrains.kotlin.diagnostics.Errors |
||||
import org.jetbrains.kotlin.psi.KtAnnotatedExpression |
||||
import org.jetbrains.kotlin.psi.KtCallExpression |
||||
import org.jetbrains.kotlin.resolve.BindingContext |
||||
import org.jetbrains.kotlin.resolve.calls.util.getCall |
||||
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall |
||||
import org.jetbrains.kotlin.resolve.diagnostics.DiagnosticSuppressor |
||||
|
||||
class ComposeDiagnosticSuppressor : DiagnosticSuppressor { |
||||
|
||||
companion object { |
||||
fun registerExtension( |
||||
@Suppress("UNUSED_PARAMETER") project: Project, |
||||
extension: DiagnosticSuppressor |
||||
) { |
||||
@Suppress("DEPRECATION") |
||||
Extensions.getRootArea().getExtensionPoint(DiagnosticSuppressor.EP_NAME) |
||||
.registerExtension(extension) |
||||
} |
||||
} |
||||
|
||||
override fun isSuppressed(diagnostic: Diagnostic): Boolean { |
||||
|
||||
return isSuppressed(diagnostic, null) |
||||
} |
||||
|
||||
override fun isSuppressed(diagnostic: Diagnostic, bindingContext: BindingContext?): Boolean { |
||||
if (!diagnostic.psiElement.inComposeModule()) return false |
||||
if (diagnostic.factory == Errors.NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION) { |
||||
for (entry in ( |
||||
diagnostic.psiElement.parent as KtAnnotatedExpression |
||||
).annotationEntries) { |
||||
if (bindingContext != null) { |
||||
val annotation = bindingContext.get(BindingContext.ANNOTATION, entry) |
||||
if (annotation != null && annotation.isComposableAnnotation) return true |
||||
} |
||||
// Best effort, maybe jetbrains can get rid of nullability. |
||||
else if (entry.shortName?.identifier == "Composable") return true |
||||
} |
||||
} |
||||
if (diagnostic.factory == Errors.NAMED_ARGUMENTS_NOT_ALLOWED) { |
||||
if (bindingContext != null) { |
||||
val call = (diagnostic.psiElement.parent.parent.parent.parent as KtCallExpression) |
||||
.getCall(bindingContext).getResolvedCall(bindingContext) |
||||
if (call != null) { |
||||
return call.isComposableInvocation() |
||||
} |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
} |
@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import org.jetbrains.kotlin.diagnostics.rendering.CommonRenderers |
||||
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages |
||||
import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap |
||||
import org.jetbrains.kotlin.diagnostics.rendering.Renderers |
||||
import org.jetbrains.kotlin.diagnostics.rendering.Renderers.RENDER_TYPE_WITH_ANNOTATIONS |
||||
|
||||
object ComposeErrorMessages : DefaultErrorMessages.Extension { |
||||
private val MAP = DiagnosticFactoryToRendererMap("Compose") |
||||
override fun getMap() = MAP |
||||
|
||||
init { |
||||
|
||||
MAP.put( |
||||
ComposeErrors.COMPOSABLE_INVOCATION, |
||||
ComposeBundle.message("errors.composable_invocation") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.COMPOSABLE_EXPECTED, |
||||
ComposeBundle.message("errors.composable_expected") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION, |
||||
@Suppress("InvalidBundleOrProperty") |
||||
ComposeBundle.message("errors.captured_composable_invocation"), |
||||
Renderers.NAME, |
||||
Renderers.COMPACT |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.COMPOSABLE_PROPERTY_BACKING_FIELD, |
||||
ComposeBundle.message("errors.composable_property_backing_field") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.COMPOSABLE_VAR, |
||||
ComposeBundle.message("errors.composable_var") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.COMPOSABLE_SUSPEND_FUN, |
||||
ComposeBundle.message("errors.composable_suspend_fun") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE, |
||||
ComposeBundle.message("errors.illegal_try_catch_around_composable") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.COMPOSABLE_FUNCTION_REFERENCE, |
||||
ComposeBundle.message("errors.composable_function_reference") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.CONFLICTING_OVERLOADS, |
||||
@Suppress("InvalidBundleOrProperty") |
||||
ComposeBundle.message("errors.conflicting_overloads"), |
||||
CommonRenderers.commaSeparated( |
||||
Renderers.FQ_NAMES_IN_TYPES_WITH_ANNOTATIONS |
||||
) |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.TYPE_MISMATCH, |
||||
@Suppress("InvalidBundleOrProperty") |
||||
ComposeBundle.message("errors.type_mismatch"), |
||||
RENDER_TYPE_WITH_ANNOTATIONS, |
||||
RENDER_TYPE_WITH_ANNOTATIONS |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION, |
||||
@Suppress("InvalidBundleOrProperty") |
||||
ComposeBundle.message("errors.missing_disallow_composable_calls_annotation"), |
||||
Renderers.NAME, |
||||
Renderers.NAME, |
||||
Renderers.NAME |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.NONREADONLY_CALL_IN_READONLY_COMPOSABLE, |
||||
ComposeBundle.message("errors.nonreadonly_call_in_readonly_composable") |
||||
) |
||||
|
||||
MAP.put( |
||||
ComposeErrors.COMPOSABLE_FUN_MAIN, |
||||
ComposeBundle.message("errors.composable_fun_main") |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,117 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import com.intellij.psi.PsiElement |
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor |
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor |
||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor |
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0 |
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1 |
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory2 |
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory3 |
||||
import org.jetbrains.kotlin.diagnostics.Errors |
||||
import org.jetbrains.kotlin.diagnostics.PositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT |
||||
import org.jetbrains.kotlin.diagnostics.Severity |
||||
import org.jetbrains.kotlin.psi.KtCallableReferenceExpression |
||||
import org.jetbrains.kotlin.psi.KtExpression |
||||
import org.jetbrains.kotlin.types.KotlinType |
||||
|
||||
object ComposeErrors { |
||||
// error goes on the composable call in a non-composable function |
||||
@JvmField |
||||
val COMPOSABLE_INVOCATION = |
||||
DiagnosticFactory0.create<PsiElement>(Severity.ERROR) |
||||
|
||||
// error goes on the non-composable function with composable calls |
||||
@JvmField |
||||
val COMPOSABLE_EXPECTED = |
||||
DiagnosticFactory0.create<PsiElement>(Severity.ERROR) |
||||
|
||||
@JvmField |
||||
val COMPOSABLE_PROPERTY_BACKING_FIELD = |
||||
DiagnosticFactory0.create<PsiElement>( |
||||
Severity.ERROR |
||||
) |
||||
|
||||
@JvmField |
||||
val COMPOSABLE_VAR = |
||||
DiagnosticFactory0.create<PsiElement>( |
||||
Severity.ERROR |
||||
) |
||||
|
||||
@JvmField |
||||
val COMPOSABLE_SUSPEND_FUN = |
||||
DiagnosticFactory0.create<PsiElement>( |
||||
Severity.ERROR |
||||
) |
||||
|
||||
@JvmField |
||||
val CAPTURED_COMPOSABLE_INVOCATION = |
||||
DiagnosticFactory2.create<PsiElement, DeclarationDescriptor, DeclarationDescriptor>( |
||||
Severity.ERROR |
||||
) |
||||
@JvmField |
||||
var ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE = |
||||
DiagnosticFactory0.create<PsiElement>(Severity.ERROR) |
||||
|
||||
@JvmField |
||||
val COMPOSABLE_FUNCTION_REFERENCE = |
||||
DiagnosticFactory0.create<KtCallableReferenceExpression>( |
||||
Severity.ERROR |
||||
) |
||||
|
||||
@JvmField |
||||
val COMPOSABLE_FUN_MAIN = |
||||
DiagnosticFactory0.create<PsiElement>(Severity.ERROR) |
||||
|
||||
@JvmField |
||||
val MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION = |
||||
DiagnosticFactory3.create< |
||||
PsiElement, |
||||
ValueParameterDescriptor, // unmarked |
||||
ValueParameterDescriptor, // marked |
||||
CallableDescriptor |
||||
>( |
||||
Severity.ERROR |
||||
) |
||||
|
||||
@JvmField |
||||
val NONREADONLY_CALL_IN_READONLY_COMPOSABLE = DiagnosticFactory0.create<PsiElement>( |
||||
Severity.ERROR |
||||
) |
||||
|
||||
// This error matches Kotlin's CONFLICTING_OVERLOADS error, except that it renders the |
||||
// annotations with the descriptor. This is important to use for errors where the |
||||
// only difference is whether or not it is annotated with @Composable or not. |
||||
@JvmField |
||||
var CONFLICTING_OVERLOADS: DiagnosticFactory1<PsiElement, Collection<DeclarationDescriptor>> = |
||||
DiagnosticFactory1.create( |
||||
Severity.ERROR, |
||||
DECLARATION_SIGNATURE_OR_DEFAULT |
||||
) |
||||
|
||||
// This error matches Kotlin's TYPE_MISMATCH error, except that it renders the annotations |
||||
// with the types. This is important to use for type mismatch errors where the only |
||||
// difference is whether or not it is annotated with @Composable or not. |
||||
@JvmField |
||||
val TYPE_MISMATCH = |
||||
DiagnosticFactory2.create<KtExpression, KotlinType, KotlinType>(Severity.ERROR) |
||||
|
||||
init { |
||||
Errors.Initializer.initializeFactoryNamesAndDefaultErrorMessages(ComposeErrors::class.java, ComposeErrorMessages) |
||||
} |
||||
} |
@ -0,0 +1,64 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import com.android.tools.modules.* |
||||
import com.intellij.lang.ASTNode |
||||
import com.intellij.lang.folding.CustomFoldingBuilder |
||||
import com.intellij.lang.folding.FoldingDescriptor |
||||
import com.intellij.openapi.editor.Document |
||||
import com.intellij.openapi.project.DumbService |
||||
import com.intellij.openapi.util.TextRange |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.util.PsiTreeUtil |
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtNamedFunction |
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType |
||||
|
||||
/** |
||||
* Adds a folding region for a Modifier chain longer than two. |
||||
*/ |
||||
class ComposeFoldingBuilder : CustomFoldingBuilder() { |
||||
override fun buildLanguageFoldRegions(descriptors: MutableList<FoldingDescriptor>, root: PsiElement, document: Document, quick: Boolean) { |
||||
if (root !is KtFile || DumbService.isDumb(root.project) || !root.inComposeModule()) { |
||||
return |
||||
} |
||||
|
||||
val composableFunctions = root.getChildrenOfType<KtNamedFunction>().filter { it.isComposableFunction() } |
||||
|
||||
for (function in composableFunctions) { |
||||
val modifiersChains = PsiTreeUtil.findChildrenOfType(function, KtDotQualifiedExpression::class.java).filter { |
||||
it.parent !is KtDotQualifiedExpression && |
||||
isModifierChainLongerThanTwo(it) |
||||
} |
||||
|
||||
for (modifierChain in modifiersChains) { |
||||
descriptors.add(FoldingDescriptor(modifierChain.node, modifierChain.node.textRange)) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* For Modifier.adjust().adjust() -> Modifier.(...) |
||||
*/ |
||||
override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String { |
||||
return node.text.substringBefore(".").trim() + ".(...)" |
||||
} |
||||
|
||||
override fun isRegionCollapsedByDefault(node: ASTNode): Boolean = false |
||||
} |
@ -0,0 +1,86 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor |
||||
import org.jetbrains.kotlin.descriptors.SourceElement |
||||
import org.jetbrains.kotlin.descriptors.annotations.Annotated |
||||
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor |
||||
import org.jetbrains.kotlin.descriptors.annotations.Annotations |
||||
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.constants.ConstantValue |
||||
import org.jetbrains.kotlin.types.KotlinType |
||||
import org.jetbrains.kotlin.types.TypeUtils.NO_EXPECTED_TYPE |
||||
import org.jetbrains.kotlin.types.TypeUtils.UNIT_EXPECTED_TYPE |
||||
import org.jetbrains.kotlin.types.typeUtil.replaceAnnotations |
||||
|
||||
|
||||
object ComposeFqNames { |
||||
const val root = "androidx.compose.runtime" |
||||
|
||||
object old { |
||||
private const val root = "androidx.compose" |
||||
private fun fqNameFor(cname: String) = FqName("$root.$cname") |
||||
val Composable = fqNameFor("Composable") |
||||
} |
||||
|
||||
val Composable = fqNameFor("Composable") |
||||
val DisallowComposableCalls = fqNameFor("DisallowComposableCalls") |
||||
val ReadOnlyComposable = fqNameFor("ReadOnlyComposable") |
||||
private fun fqNameFor(cname: String) = FqName("$root.$cname") |
||||
|
||||
fun makeComposableAnnotation(module: ModuleDescriptor): AnnotationDescriptor = |
||||
object : AnnotationDescriptor { |
||||
override val type: KotlinType |
||||
get() { |
||||
val clazz = module.findClassAcrossModuleDependencies(ClassId.topLevel(Composable)) ?: |
||||
module.findClassAcrossModuleDependencies(ClassId.topLevel(old.Composable)) |
||||
return clazz!!.defaultType |
||||
} |
||||
override val allValueArguments: Map<Name, ConstantValue<*>> get() = emptyMap() |
||||
override val source: SourceElement get() = SourceElement.NO_SOURCE |
||||
override fun toString() = "[@Composable]" |
||||
} |
||||
} |
||||
|
||||
fun KotlinType.makeComposable(module: ModuleDescriptor): KotlinType { |
||||
if (hasComposableAnnotation()) return this |
||||
val annotation = ComposeFqNames.makeComposableAnnotation(module) |
||||
return replaceAnnotations(Annotations.create(annotations + annotation)) |
||||
} |
||||
|
||||
fun KotlinType.hasComposableAnnotation(): Boolean = |
||||
!isSpecialType && ( |
||||
annotations.findAnnotation(ComposeFqNames.Composable) != null || |
||||
annotations.findAnnotation(ComposeFqNames.old.Composable) != null |
||||
) |
||||
fun Annotated.hasComposableAnnotation(): Boolean = |
||||
annotations.findAnnotation(ComposeFqNames.Composable) != null || |
||||
annotations.findAnnotation(ComposeFqNames.old.Composable) != null |
||||
fun Annotated.hasReadonlyComposableAnnotation(): Boolean = |
||||
annotations.findAnnotation(ComposeFqNames.ReadOnlyComposable) != null |
||||
fun Annotated.hasDisallowComposableCallsAnnotation(): Boolean = |
||||
annotations.findAnnotation(ComposeFqNames.DisallowComposableCalls) != null |
||||
|
||||
internal val KotlinType.isSpecialType: Boolean get() = |
||||
this === NO_EXPECTED_TYPE || this === UNIT_EXPECTED_TYPE |
||||
|
||||
val AnnotationDescriptor.isComposableAnnotation: Boolean |
||||
get() = fqName == ComposeFqNames.Composable || |
||||
fqName == ComposeFqNames.old.Composable |
@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.psi.search.GlobalSearchScope |
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor |
||||
import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor |
||||
import org.jetbrains.kotlin.idea.kdoc.IdeKDocLinkResolutionService |
||||
import org.jetbrains.kotlin.idea.kdoc.KDocLinkResolutionService |
||||
import org.jetbrains.kotlin.idea.resolve.ResolutionFacade |
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinClassShortNameIndex |
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinFunctionShortNameIndex |
||||
import org.jetbrains.kotlin.idea.base.projectStructure.scope.KotlinSourceFilterScope |
||||
import org.jetbrains.kotlin.name.FqName |
||||
import org.jetbrains.kotlin.resolve.BindingContext |
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode |
||||
|
||||
/** |
||||
* Resolves links to functions and classes inside KDoc that are not included to the project (as byte code). |
||||
* |
||||
* It's a copy of [org.jetbrains.kotlin.idea.kdoc.IdeKDocLinkResolutionService], but with a larger search scope: |
||||
* GlobalSearchScope.everythingScope(project) instead of GlobalSearchScope.projectScope(project). |
||||
* Source code is already in the index, it attached in [AndroidModuleDependenciesSetup#setUpLibraryDependency] |
||||
*/ |
||||
class ComposeKDocLinkResolutionService : KDocLinkResolutionService { |
||||
override fun resolveKDocLink( |
||||
context: BindingContext, |
||||
fromDescriptor: DeclarationDescriptor, |
||||
resolutionFacade: ResolutionFacade, |
||||
qualifiedName: List<String> |
||||
): Collection<DeclarationDescriptor> { |
||||
val project = resolutionFacade.project |
||||
val descriptors = IdeKDocLinkResolutionService(project).resolveKDocLink(context, fromDescriptor, resolutionFacade, qualifiedName) |
||||
|
||||
if (!StudioFlags.SAMPLES_SUPPORT_ENABLED.get()) return descriptors |
||||
|
||||
val scope = KotlinSourceFilterScope.librarySources(GlobalSearchScope.everythingScope(project), project) |
||||
|
||||
val shortName = qualifiedName.lastOrNull() ?: return emptyList() |
||||
val targetFqName = FqName.fromSegments(qualifiedName) |
||||
|
||||
val functions = KotlinFunctionShortNameIndex.get(shortName, project, scope).asSequence() |
||||
val classes = KotlinClassShortNameIndex.get(shortName, project, scope).asSequence() |
||||
|
||||
val additionalDescriptors = (functions + classes) |
||||
.filter { it.fqName == targetFqName } |
||||
.map { it.unsafeResolveToDescriptor(BodyResolveMode.PARTIAL) } // TODO Filter out not visible due dependencies config descriptors |
||||
.toList() |
||||
if (additionalDescriptors.isNotEmpty()) |
||||
return additionalDescriptors + descriptors |
||||
|
||||
return descriptors |
||||
} |
||||
} |
@ -0,0 +1,45 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
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_UI_PACKAGE}.Alignment" |
||||
const val COMPOSE_ALIGNMENT_HORIZONTAL = "${COMPOSE_ALIGNMENT}.Horizontal" |
||||
const val COMPOSE_ALIGNMENT_VERTICAL = "${COMPOSE_ALIGNMENT}.Vertical" |
||||
|
||||
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" |
||||
) |
@ -0,0 +1,44 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.tools.compose |
||||
|
||||
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.config.CompilerConfiguration |
||||
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment |
||||
|
||||
@Suppress("INVISIBLE_REFERENCE", "EXPERIMENTAL_IS_NOT_ENABLED") |
||||
@OptIn(org.jetbrains.kotlin.extensions.internal.InternalNonStableExtensionPoints::class) |
||||
class ComposePluginIrGenerationExtension : IrGenerationExtension { |
||||
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { |
||||
try { |
||||
ComposeIrGenerationExtension(reportsDestination = null, |
||||
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() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,105 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package com.android.tools.compose |
||||
|
||||
import androidx.compose.compiler.plugins.kotlin.ComposeTypeResolutionInterceptorExtension |
||||
import com.android.tools.compose.ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor |
||||
import org.jetbrains.kotlin.descriptors.impl.AnonymousFunctionDescriptor |
||||
import org.jetbrains.kotlin.psi.KtElement |
||||
import org.jetbrains.kotlin.psi.KtLambdaExpression |
||||
import org.jetbrains.kotlin.psi.psiUtil.getAnnotationEntries |
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.module |
||||
import org.jetbrains.kotlin.resolve.sam.getSingleAbstractMethodOrNull |
||||
import org.jetbrains.kotlin.types.KotlinType |
||||
import org.jetbrains.kotlin.types.TypeUtils |
||||
import org.jetbrains.kotlin.types.expressions.ExpressionTypingContext |
||||
|
||||
@Suppress("INVISIBLE_REFERENCE", "EXPERIMENTAL_IS_NOT_ENABLED") |
||||
@OptIn(org.jetbrains.kotlin.extensions.internal.InternalNonStableExtensionPoints::class) |
||||
class ComposePluginTypeResolutionInterceptorExtension : ComposeTypeResolutionInterceptorExtension() { |
||||
override fun interceptFunctionLiteralDescriptor( |
||||
expression: KtLambdaExpression, |
||||
context: ExpressionTypingContext, |
||||
descriptor: AnonymousFunctionDescriptor |
||||
): AnonymousFunctionDescriptor { |
||||
|
||||
if (StudioFlags.COMPOSE_DEPLOY_LIVE_EDIT_USE_EMBEDDED_COMPILER.get()) { |
||||
return super.interceptFunctionLiteralDescriptor(expression, context, descriptor) |
||||
} |
||||
|
||||
if (descriptor.isSuspend) return descriptor |
||||
if ( |
||||
context.expectedType.hasComposableAnnotation() && |
||||
!descriptor.hasComposableAnnotation() |
||||
) { |
||||
// If the expected type has an @Composable annotation then the literal function |
||||
// expression should infer a an @Composable annotation |
||||
context.trace.record(INFERRED_COMPOSABLE_DESCRIPTOR, descriptor, true) |
||||
} |
||||
val arg = getArgumentDescriptor(expression.functionLiteral, context.trace.bindingContext) |
||||
|
||||
val argTypeDescriptor = arg |
||||
?.type |
||||
?.constructor |
||||
?.declarationDescriptor as? ClassDescriptor |
||||
if (argTypeDescriptor != null) { |
||||
val sam = getSingleAbstractMethodOrNull(argTypeDescriptor) |
||||
if (sam != null && sam.hasComposableAnnotation()) { |
||||
context.trace.record(INFERRED_COMPOSABLE_DESCRIPTOR, descriptor, true) |
||||
} |
||||
} |
||||
return descriptor |
||||
} |
||||
|
||||
override fun interceptType( |
||||
element: KtElement, |
||||
context: ExpressionTypingContext, |
||||
resultType: KotlinType |
||||
): KotlinType { |
||||
if (resultType === TypeUtils.NO_EXPECTED_TYPE) return resultType |
||||
if (element !is KtLambdaExpression) return resultType |
||||
|
||||
val arg = getArgumentDescriptor(element.functionLiteral, context.trace.bindingContext) |
||||
|
||||
val argTypeDescriptor = arg |
||||
?.type |
||||
?.constructor |
||||
?.declarationDescriptor as? ClassDescriptor |
||||
if (argTypeDescriptor != null) { |
||||
val sam = getSingleAbstractMethodOrNull(argTypeDescriptor) |
||||
if (sam != null && sam.hasComposableAnnotation()) { |
||||
context.trace.record( |
||||
ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL, |
||||
element, |
||||
true |
||||
) |
||||
return resultType.makeComposable(context.scope.ownerDescriptor.module) |
||||
} |
||||
} |
||||
|
||||
if ( |
||||
element.getAnnotationEntries().hasComposableAnnotation(context.trace.bindingContext) || |
||||
context.expectedType.hasComposableAnnotation() |
||||
) { |
||||
context.trace.record(ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL, element, true) |
||||
return resultType.makeComposable(context.scope.ownerDescriptor.module) |
||||
} |
||||
return resultType |
||||
} |
||||
} |
@ -0,0 +1,43 @@
|
||||
/* |
||||
* Copyright 2019 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 |
||||
|
||||
import com.android.tools.modules.* |
||||
import com.intellij.psi.PsiElement |
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall |
||||
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName |
||||
import org.jetbrains.kotlin.js.translate.callTranslator.getReturnType |
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression |
||||
import org.jetbrains.kotlin.psi.KtElement |
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType |
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode |
||||
import org.jetbrains.kotlin.types.KotlinType |
||||
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 == COMPOSE_MODIFIER_FQN) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
internal fun KotlinType.isClassOrExtendsClass(classFqName:String): Boolean { |
||||
return fqName?.asString() == classFqName || supertypes().any { it.fqName?.asString() == classFqName } |
||||
} |
@ -0,0 +1,35 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import com.intellij.openapi.application.ApplicationManager |
||||
import com.intellij.openapi.components.BaseState |
||||
import com.intellij.openapi.components.Service |
||||
import com.intellij.openapi.components.SimplePersistentStateComponent |
||||
import com.intellij.openapi.components.State |
||||
import com.intellij.openapi.components.Storage |
||||
|
||||
@Service |
||||
@State(name = "ComposeSettings", storages = [Storage("composeSettings.xml")]) |
||||
class ComposeSettings : SimplePersistentStateComponent<ComposeSettingsState>(ComposeSettingsState()) { |
||||
companion object { |
||||
fun getInstance() = ApplicationManager.getApplication().getService(ComposeSettings::class.java) |
||||
} |
||||
} |
||||
|
||||
class ComposeSettingsState : BaseState() { |
||||
var isComposeInsertHandlerEnabled by property(true) |
||||
} |
@ -0,0 +1,31 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor |
||||
import org.jetbrains.kotlin.psi.KtLambdaExpression |
||||
import org.jetbrains.kotlin.util.slicedMap.BasicWritableSlice |
||||
import org.jetbrains.kotlin.util.slicedMap.RewritePolicy |
||||
import org.jetbrains.kotlin.util.slicedMap.WritableSlice |
||||
|
||||
object ComposeWritableSlices { |
||||
val INFERRED_COMPOSABLE_DESCRIPTOR: WritableSlice<FunctionDescriptor, Boolean> = |
||||
BasicWritableSlice(RewritePolicy.DO_NOTHING) |
||||
val LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE: WritableSlice<FunctionDescriptor, Boolean> = |
||||
BasicWritableSlice(RewritePolicy.DO_NOTHING) |
||||
val INFERRED_COMPOSABLE_LITERAL: WritableSlice<KtLambdaExpression, Boolean> = |
||||
BasicWritableSlice(RewritePolicy.DO_NOTHING) |
||||
} |
@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright (C) 2019 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. |
||||
*/ |
||||
@file:JvmName("AndroidComposablePsiUtils") |
||||
|
||||
package com.android.tools.compose |
||||
|
||||
import com.intellij.openapi.roots.* |
||||
import com.intellij.psi.* |
||||
import com.intellij.psi.util.* |
||||
import org.jetbrains.kotlin.idea.* |
||||
import org.jetbrains.kotlin.idea.caches.resolve.* |
||||
import org.jetbrains.kotlin.psi.* |
||||
import org.jetbrains.kotlin.resolve.* |
||||
import org.jetbrains.kotlin.resolve.lazy.* |
||||
|
||||
fun KtAnnotationEntry.getQualifiedName(): String? { |
||||
return analyze(BodyResolveMode.PARTIAL).get(BindingContext.ANNOTATION, this)?.fqName?.asString() |
||||
} |
||||
|
||||
fun KtAnnotationEntry.fqNameMatches(fqName: Set<String>): Boolean { |
||||
val qualifiedName by lazy { getQualifiedName() } |
||||
val shortName = shortName?.asString() ?: return false |
||||
return fqName.filter { it.endsWith(shortName) }.any { it == qualifiedName } |
||||
} |
||||
|
||||
fun PsiElement.isComposableFunction(): Boolean { |
||||
if (this !is KtNamedFunction) return false |
||||
|
||||
return CachedValuesManager.getCachedValue(this) { |
||||
val hasComposableAnnotation = annotationEntries.any { |
||||
// fqNameMatches is expensive, so we first verify that the short name of the annotation matches. |
||||
it.shortName?.identifier == COMPOSABLE_ANNOTATION_NAME && |
||||
it.fqNameMatches(COMPOSABLE_FQ_NAMES) |
||||
} |
||||
val containingKtFile = this.containingKtFile |
||||
|
||||
CachedValueProvider.Result.create( |
||||
// TODO: see if we can handle alias imports without ruining performance. |
||||
hasComposableAnnotation, |
||||
containingKtFile, |
||||
ProjectRootModificationTracker.getInstance(project) |
||||
) |
||||
} |
||||
} |
||||
|
||||
fun PsiElement.isComposableAnnotation():Boolean = |
||||
when (this) { |
||||
is KtAnnotationEntry -> this.fqNameMatches(COMPOSABLE_FQ_NAMES) |
||||
else -> false |
||||
} |
||||
|
||||
fun PsiElement.isInsideComposableCode(): Boolean { |
||||
// TODO: also handle composable lambdas. |
||||
return language == KotlinLanguage.INSTANCE && parentOfType<KtNamedFunction>()?.isComposableFunction() == true |
||||
} |
@ -0,0 +1,381 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion |
||||
|
||||
import com.android.tools.compose.COMPOSABLE_FQ_NAMES |
||||
import com.android.tools.compose.ComposeSettings |
||||
import com.android.tools.compose.isComposableFunction |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.android.tools.idea.flags.StudioFlags.COMPOSE_COMPLETION_INSERT_HANDLER |
||||
import com.android.tools.idea.flags.StudioFlags.COMPOSE_COMPLETION_PRESENTATION |
||||
import com.android.tools.modules.* |
||||
import com.intellij.codeInsight.completion.CompletionContributor |
||||
import com.intellij.codeInsight.completion.CompletionLocation |
||||
import com.intellij.codeInsight.completion.CompletionParameters |
||||
import com.intellij.codeInsight.completion.CompletionResultSet |
||||
import com.intellij.codeInsight.completion.CompletionWeigher |
||||
import com.intellij.codeInsight.completion.InsertionContext |
||||
import com.intellij.codeInsight.daemon.impl.quickfix.EmptyExpression |
||||
import com.intellij.codeInsight.lookup.LookupElement |
||||
import com.intellij.codeInsight.lookup.LookupElementDecorator |
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation |
||||
import com.intellij.codeInsight.template.Template |
||||
import com.intellij.codeInsight.template.TemplateEditingAdapter |
||||
import com.intellij.codeInsight.template.TemplateManager |
||||
import com.intellij.codeInsight.template.impl.ConstantNode |
||||
import com.intellij.openapi.application.runWriteAction |
||||
import com.intellij.psi.PsiDocumentManager |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.impl.source.tree.LeafPsiElement |
||||
import com.intellij.psi.util.parentOfType |
||||
import com.intellij.util.asSafely |
||||
import icons.StudioIcons |
||||
import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType |
||||
import org.jetbrains.kotlin.builtins.isFunctionType |
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor |
||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor |
||||
import org.jetbrains.kotlin.idea.KotlinLanguage |
||||
import org.jetbrains.kotlin.idea.completion.BasicLookupElementFactory |
||||
import org.jetbrains.kotlin.idea.completion.LambdaSignatureTemplates |
||||
import org.jetbrains.kotlin.idea.completion.LookupElementFactory |
||||
import org.jetbrains.kotlin.idea.completion.handlers.KotlinCallableInsertHandler |
||||
import org.jetbrains.kotlin.idea.core.completion.DeclarationLookupObject |
||||
import org.jetbrains.kotlin.idea.core.completion.DescriptorBasedDeclarationLookupObject |
||||
import org.jetbrains.kotlin.idea.references.mainReference |
||||
import org.jetbrains.kotlin.idea.util.CallType |
||||
import org.jetbrains.kotlin.idea.util.CallTypeAndReceiver |
||||
import org.jetbrains.kotlin.lexer.KtTokens |
||||
import org.jetbrains.kotlin.name.FqName |
||||
import org.jetbrains.kotlin.psi.KtBlockExpression |
||||
import org.jetbrains.kotlin.psi.KtCallExpression |
||||
import org.jetbrains.kotlin.psi.KtNamedDeclaration |
||||
import org.jetbrains.kotlin.psi.KtNamedFunction |
||||
import org.jetbrains.kotlin.psi.KtSimpleNameExpression |
||||
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace |
||||
import org.jetbrains.kotlin.renderer.DescriptorRenderer |
||||
import org.jetbrains.kotlin.resolve.calls.components.hasDefaultValue |
||||
import org.jetbrains.kotlin.resolve.calls.results.argumentValueType |
||||
import org.jetbrains.kotlin.types.typeUtil.isUnit |
||||
|
||||
private val COMPOSABLE_FUNCTION_ICON = StudioIcons.Compose.Editor.COMPOSABLE_FUNCTION |
||||
|
||||
/** |
||||
* Checks if this completion is for a statement (where Compose views usually called) and not part of another expression. |
||||
*/ |
||||
private fun CompletionParameters.isForStatement(): Boolean { |
||||
return position is LeafPsiElement && |
||||
position.node.elementType == KtTokens.IDENTIFIER && |
||||
position.parent?.parent is KtBlockExpression |
||||
} |
||||
|
||||
private fun LookupElement.getFunctionDescriptor(): FunctionDescriptor? { |
||||
return this.`object` |
||||
.asSafely<DescriptorBasedDeclarationLookupObject>() |
||||
?.descriptor |
||||
?.asSafely<FunctionDescriptor>() |
||||
} |
||||
|
||||
private val List<ValueParameterDescriptor>.hasComposableChildren: Boolean |
||||
get() { |
||||
val lastArgType = lastOrNull()?.type ?: return false |
||||
return lastArgType.isBuiltinFunctionalType |
||||
&& COMPOSABLE_FQ_NAMES.any { lastArgType.annotations.hasAnnotation(FqName(it)) } |
||||
} |
||||
|
||||
private val ValueParameterDescriptor.isLambdaWithNoParameters: Boolean |
||||
get() = type.isFunctionType |
||||
// The only type in the list is the return type (can be Unit). |
||||
&& argumentValueType.arguments.size == 1 |
||||
|
||||
/** |
||||
* true if the last parameter is required, and a lambda type with no parameters. |
||||
*/ |
||||
private val List<ValueParameterDescriptor>.isLastRequiredLambdaWithNoParameters: Boolean |
||||
get() { |
||||
val lastParameter = lastOrNull() ?: return false |
||||
return !lastParameter.hasDefaultValue() && lastParameter.isLambdaWithNoParameters |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Find the [CallType] from the [InsertionContext]. The [CallType] can be used to detect if the completion is being done in a regular |
||||
* statement, an import or some other expression and decide if we want to apply the [ComposeInsertHandler]. |
||||
*/ |
||||
private fun InsertionContext.inferCallType(): CallType<*> { |
||||
// Look for an existing KtSimpleNameExpression to pass to CallTypeAndReceiver.detect so we can infer the call type. |
||||
val namedExpression = (file.findElementAt(startOffset)?.parent as? KtSimpleNameExpression)?.mainReference?.expression |
||||
?: return CallType.DEFAULT |
||||
|
||||
return CallTypeAndReceiver.detect(namedExpression).callType |
||||
} |
||||
|
||||
/** |
||||
* Modifies [LookupElement]s for composable functions, to improve Compose editing UX. |
||||
*/ |
||||
class ComposeCompletionContributor : CompletionContributor() { |
||||
override fun fillCompletionVariants(parameters: CompletionParameters, resultSet: CompletionResultSet) { |
||||
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || |
||||
parameters.position.inComposeModule() != true || |
||||
parameters.position.language != KotlinLanguage.INSTANCE) { |
||||
return |
||||
} |
||||
|
||||
resultSet.runRemainingContributors(parameters) { completionResult -> |
||||
val lookupElement = completionResult.lookupElement |
||||
val psi = lookupElement.psiElement |
||||
val newResult = when { |
||||
psi == null || !psi.isComposableFunction() -> completionResult |
||||
lookupElement.isForSpecialLambdaLookupElement() -> null |
||||
else -> completionResult.withLookupElement(ComposeLookupElement(lookupElement)) |
||||
} |
||||
|
||||
newResult?.let(resultSet::passResult) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Checks if the [LookupElement] is an additional, "special" lookup element created for functions that can be invoked using the lambda |
||||
* syntax. These are created by [LookupElementFactory.addSpecialFunctionCallElements] and can be confusing for Compose APIs that often |
||||
* use overloaded function names. |
||||
*/ |
||||
private fun LookupElement.isForSpecialLambdaLookupElement(): Boolean { |
||||
val presentation = LookupElementPresentation() |
||||
renderElement(presentation) |
||||
return presentation.tailText?.startsWith(" {...} (..., ") ?: false |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Wraps original Kotlin [LookupElement]s for composable functions to make them stand out more. |
||||
*/ |
||||
private class ComposeLookupElement(original: LookupElement) : LookupElementDecorator<LookupElement>(original) { |
||||
/** |
||||
* Set of [CallType]s that should be handled by the [ComposeInsertHandler]. |
||||
*/ |
||||
private val validCallTypes = setOf(CallType.DEFAULT, CallType.DOT) |
||||
|
||||
init { |
||||
require(original.psiElement?.isComposableFunction() == true) |
||||
} |
||||
|
||||
override fun getPsiElement(): KtNamedFunction = super.getPsiElement() as KtNamedFunction |
||||
|
||||
override fun renderElement(presentation: LookupElementPresentation) { |
||||
super.renderElement(presentation) |
||||
|
||||
if (COMPOSE_COMPLETION_PRESENTATION.get()) { |
||||
val descriptor = getFunctionDescriptor() ?: return |
||||
presentation.icon = COMPOSABLE_FUNCTION_ICON |
||||
presentation.setTypeText(if (descriptor.returnType?.isUnit() == true) null else presentation.typeText, null) |
||||
rewriteSignature(descriptor, presentation) |
||||
} |
||||
} |
||||
|
||||
override fun handleInsert(context: InsertionContext) { |
||||
val descriptor = getFunctionDescriptor() |
||||
val callType by lazy { context.inferCallType() } |
||||
return when { |
||||
!COMPOSE_COMPLETION_INSERT_HANDLER.get() -> super.handleInsert(context) |
||||
!ComposeSettings.getInstance().state.isComposeInsertHandlerEnabled -> super.handleInsert(context) |
||||
descriptor == null -> super.handleInsert(context) |
||||
!validCallTypes.contains(callType) -> super.handleInsert(context) |
||||
else -> ComposeInsertHandler(descriptor, callType).handleInsert(context, this) |
||||
} |
||||
} |
||||
|
||||
private fun rewriteSignature(descriptor: FunctionDescriptor, presentation: LookupElementPresentation) { |
||||
val allParameters = descriptor.valueParameters |
||||
val requiredParameters = allParameters.filter { !it.declaresDefaultValue() } |
||||
val inParens = if (requiredParameters.hasComposableChildren) requiredParameters.dropLast(1) else requiredParameters |
||||
val renderer = when { |
||||
requiredParameters.size < allParameters.size -> SHORT_NAMES_WITH_DOTS |
||||
inParens.isEmpty() && requiredParameters.hasComposableChildren -> { |
||||
// Don't render an empty pair of parenthesis if we're rendering a lambda afterwards. |
||||
null |
||||
} |
||||
else -> BasicLookupElementFactory.SHORT_NAMES_RENDERER |
||||
} |
||||
|
||||
presentation.clearTail() |
||||
renderer |
||||
?.renderValueParameters(inParens, false) |
||||
?.let { presentation.appendTailTextItalic(it, false) } |
||||
|
||||
if (requiredParameters.hasComposableChildren) { |
||||
presentation.appendTailText(" " + LambdaSignatureTemplates.DEFAULT_LAMBDA_PRESENTATION, true) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A version of [BasicLookupElementFactory.SHORT_NAMES_RENDERER] that adds `, ...)` at the end of the parameters list. |
||||
*/ |
||||
private val SHORT_NAMES_WITH_DOTS = BasicLookupElementFactory.SHORT_NAMES_RENDERER.withOptions { |
||||
val delegate = DescriptorRenderer.ValueParametersHandler.DEFAULT |
||||
valueParametersHandler = object : DescriptorRenderer.ValueParametersHandler { |
||||
override fun appendAfterValueParameter( |
||||
parameter: ValueParameterDescriptor, |
||||
parameterIndex: Int, |
||||
parameterCount: Int, |
||||
builder: StringBuilder |
||||
) { |
||||
delegate.appendAfterValueParameter(parameter, parameterIndex, parameterCount, builder) |
||||
} |
||||
|
||||
override fun appendBeforeValueParameter( |
||||
parameter: ValueParameterDescriptor, |
||||
parameterIndex: Int, |
||||
parameterCount: Int, |
||||
builder: StringBuilder |
||||
) { |
||||
delegate.appendBeforeValueParameter(parameter, parameterIndex, parameterCount, builder) |
||||
} |
||||
|
||||
override fun appendBeforeValueParameters(parameterCount: Int, builder: StringBuilder) { |
||||
delegate.appendBeforeValueParameters(parameterCount, builder) |
||||
} |
||||
|
||||
override fun appendAfterValueParameters(parameterCount: Int, builder: StringBuilder) { |
||||
builder.append(if (parameterCount == 0) "...)" else ", ...)") |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Set of Composable FQNs that have a conflicting name with a non-composable and where we want to promote the |
||||
* non-composable instead. |
||||
*/ |
||||
private val COMPOSABLE_CONFLICTING_NAMES = setOf( |
||||
"androidx.compose.material.MaterialTheme" |
||||
) |
||||
|
||||
/** |
||||
* Custom [CompletionWeigher] which moves composable functions up the completion list. |
||||
* |
||||
* It doesn't give composable functions "absolute" priority, some weighers are hardcoded to run first: specifically one that puts prefix |
||||
* matches above [LookupElement]s where the match is in the middle of the name. Overriding this behavior would require an extension point in |
||||
* [org.jetbrains.kotlin.idea.completion.CompletionSession.createSorter]. |
||||
* |
||||
* See [com.intellij.codeInsight.completion.PrioritizedLookupElement] for more information on how ordering of lookup elements works and how |
||||
* to debug it. |
||||
*/ |
||||
class ComposeCompletionWeigher : CompletionWeigher() { |
||||
override fun weigh(element: LookupElement, location: CompletionLocation): Int = when { |
||||
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> 0 |
||||
!StudioFlags.COMPOSE_COMPLETION_WEIGHER.get() -> 0 |
||||
location.completionParameters.position.language != KotlinLanguage.INSTANCE -> 0 |
||||
location.completionParameters.position.inComposeModule() != true -> 0 |
||||
element.isForNamedArgument() -> 3 |
||||
location.completionParameters.isForStatement() -> { |
||||
val isConflictingName = COMPOSABLE_CONFLICTING_NAMES.contains((element.psiElement as? KtNamedDeclaration)?.fqName?.asString() ?: "") |
||||
val isComposableFunction = element.psiElement?.isComposableFunction() ?: false |
||||
// This method ensures that the order of completion ends up as: |
||||
// |
||||
// Composables with non-conflicting names (like Button {}) +2 |
||||
// Non Composables with conflicting names (like the MaterialTheme object) +2 |
||||
// Composable with conflicting names (like MaterialTheme {}) +1 |
||||
// Anything else 0 |
||||
when { |
||||
isComposableFunction && !isConflictingName -> 2 |
||||
!isComposableFunction && isConflictingName -> 2 |
||||
isComposableFunction && isConflictingName -> 1 |
||||
else -> 0 |
||||
} |
||||
} |
||||
else -> 0 |
||||
} |
||||
|
||||
private fun LookupElement.isForNamedArgument() = lookupString.endsWith(" =") |
||||
} |
||||
|
||||
private fun InsertionContext.getNextElementIgnoringWhitespace(): PsiElement? { |
||||
val elementAtCaret = file.findElementAt(editor.caretModel.offset) ?: return null |
||||
return elementAtCaret.getNextSiblingIgnoringWhitespace(true) ?: return null |
||||
} |
||||
|
||||
private fun InsertionContext.isNextElementOpenCurlyBrace() = getNextElementIgnoringWhitespace()?.text?.startsWith("{") == true |
||||
|
||||
private fun InsertionContext.isNextElementOpenParenthesis() = getNextElementIgnoringWhitespace()?.text?.startsWith("(") == true |
||||
|
||||
private class ComposeInsertHandler( |
||||
private val descriptor: FunctionDescriptor, |
||||
callType: CallType<*>) : KotlinCallableInsertHandler(callType) { |
||||
override fun handleInsert(context: InsertionContext, item: LookupElement) = with(context) { |
||||
super.handleInsert(context, item) |
||||
|
||||
if (isNextElementOpenParenthesis()) return |
||||
|
||||
// All Kotlin insertion handlers do this, possibly to post-process adding a new import in the call to super above. |
||||
val psiDocumentManager = PsiDocumentManager.getInstance(project) |
||||
psiDocumentManager.commitAllDocuments() |
||||
psiDocumentManager.doPostponedOperationsAndUnblockDocument(document) |
||||
|
||||
val templateManager = TemplateManager.getInstance(project) |
||||
val allParameters = descriptor.valueParameters |
||||
val requiredParameters = allParameters.filter { !it.declaresDefaultValue() } |
||||
val insertLambda = requiredParameters.hasComposableChildren |
||||
|| allParameters.isLastRequiredLambdaWithNoParameters |
||||
val inParens = if (insertLambda) requiredParameters.dropLast(1) else requiredParameters |
||||
|
||||
val template = templateManager.createTemplate("", "").apply { |
||||
isToReformat = true |
||||
setToIndent(true) |
||||
|
||||
when { |
||||
inParens.isNotEmpty() -> { |
||||
addTextSegment("(") |
||||
inParens.forEachIndexed { index, parameter -> |
||||
if (index > 0) { |
||||
addTextSegment(", ") |
||||
} |
||||
addTextSegment(parameter.name.asString() + " = ") |
||||
if (parameter.isLambdaWithNoParameters) { |
||||
addVariable(ConstantNode("{ /*TODO*/ }"), true) |
||||
} |
||||
else { |
||||
addVariable(EmptyExpression(), true) |
||||
} |
||||
} |
||||
addTextSegment(")") |
||||
} |
||||
!insertLambda -> addTextSegment("()") |
||||
requiredParameters.size < allParameters.size -> { |
||||
addTextSegment("(") |
||||
addVariable(EmptyExpression(), true) |
||||
addTextSegment(")") |
||||
} |
||||
} |
||||
|
||||
if (insertLambda && !isNextElementOpenCurlyBrace()) { |
||||
addTextSegment(" {\n") |
||||
addEndVariable() |
||||
addTextSegment("\n}") |
||||
} |
||||
} |
||||
|
||||
templateManager.startTemplate(editor, template, object : TemplateEditingAdapter() { |
||||
override fun templateFinished(template: Template, brokenOff: Boolean) { |
||||
if (!brokenOff) { |
||||
val callExpression = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtCallExpression>() ?: return |
||||
val valueArgumentList = callExpression.valueArgumentList ?: return |
||||
if (valueArgumentList.arguments.isEmpty() && callExpression.lambdaArguments.isNotEmpty()) { |
||||
runWriteAction { valueArgumentList.delete() } |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,179 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
import com.android.tools.compose.COMPOSE_ALIGNMENT |
||||
import com.android.tools.compose.COMPOSE_ALIGNMENT_HORIZONTAL |
||||
import com.android.tools.compose.COMPOSE_ALIGNMENT_VERTICAL |
||||
import com.android.tools.compose.COMPOSE_ARRANGEMENT |
||||
import com.android.tools.compose.COMPOSE_ARRANGEMENT_HORIZONTAL |
||||
import com.android.tools.compose.COMPOSE_ARRANGEMENT_VERTICAL |
||||
import com.android.tools.compose.isClassOrExtendsClass |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.android.tools.modules.* |
||||
import com.intellij.codeInsight.completion.CompletionContributor |
||||
import com.intellij.codeInsight.completion.CompletionParameters |
||||
import com.intellij.codeInsight.completion.CompletionResultSet |
||||
import com.intellij.codeInsight.lookup.DefaultLookupItemRenderer |
||||
import com.intellij.codeInsight.lookup.LookupElement |
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder |
||||
import com.intellij.codeInsight.lookup.LookupElementDecorator |
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.openapi.roots.ProjectRootModificationTracker |
||||
import com.intellij.psi.PsiDocumentManager |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.util.CachedValueProvider |
||||
import com.intellij.psi.util.CachedValuesManager |
||||
import com.intellij.psi.util.contextOfType |
||||
import com.intellij.psi.util.parentOfType |
||||
import org.jetbrains.kotlin.idea.base.util.allScope |
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference |
||||
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName |
||||
import org.jetbrains.kotlin.idea.references.mainReference |
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex |
||||
import org.jetbrains.kotlin.idea.util.ImportInsertHelper |
||||
import org.jetbrains.kotlin.name.FqName |
||||
import org.jetbrains.kotlin.nj2k.postProcessing.type |
||||
import org.jetbrains.kotlin.psi.KtCallElement |
||||
import org.jetbrains.kotlin.psi.KtClassOrObject |
||||
import org.jetbrains.kotlin.psi.KtDeclaration |
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtLambdaArgument |
||||
import org.jetbrains.kotlin.psi.KtNamedFunction |
||||
import org.jetbrains.kotlin.psi.KtProperty |
||||
import org.jetbrains.kotlin.psi.KtValueArgument |
||||
import org.jetbrains.kotlin.psi.KtValueArgumentList |
||||
import org.jetbrains.kotlin.utils.addToStdlib.safeAs |
||||
|
||||
/** |
||||
* Suggests specific implementations of frequently used Compose interfaces in a parameter or a property position. |
||||
*/ |
||||
class ComposeImplementationsCompletionContributor : CompletionContributor() { |
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { |
||||
val elementToComplete = parameters.position |
||||
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || !elementToComplete.inComposeModule() || parameters.originalFile !is KtFile) { |
||||
return |
||||
} |
||||
val elementToCompleteTypeFqName = elementToComplete.argumentTypeFqName ?: elementToComplete.propertyTypeFqName |
||||
val project = elementToComplete.project |
||||
val (elementsToSuggest, classForImport) = when (elementToCompleteTypeFqName) { |
||||
COMPOSE_ALIGNMENT_HORIZONTAL -> Pair(getAlignments(project, COMPOSE_ALIGNMENT_HORIZONTAL), COMPOSE_ALIGNMENT) |
||||
COMPOSE_ALIGNMENT_VERTICAL -> Pair(getAlignments(project, COMPOSE_ALIGNMENT_VERTICAL), COMPOSE_ALIGNMENT) |
||||
COMPOSE_ARRANGEMENT_HORIZONTAL -> Pair(getArrangements(project, COMPOSE_ARRANGEMENT_HORIZONTAL), COMPOSE_ARRANGEMENT) |
||||
COMPOSE_ARRANGEMENT_VERTICAL -> Pair(getArrangements(project, COMPOSE_ARRANGEMENT_VERTICAL), COMPOSE_ARRANGEMENT) |
||||
else -> return |
||||
} |
||||
|
||||
val isNewElement = elementToComplete.parentOfType<KtDotQualifiedExpression>() == null |
||||
val lookupElements = elementsToSuggest.map { getStaticPropertyLookupElement(it, classForImport, isNewElement) } |
||||
result.addAllElements(lookupElements) |
||||
|
||||
if (!isNewElement) { |
||||
val addedElementsNames = elementsToSuggest.mapNotNull { it.name } |
||||
result.runRemainingContributors(parameters) { completionResult -> |
||||
val skipResult = completionResult.lookupElement.psiElement.safeAs<KtProperty>()?.name?.let { addedElementsNames.contains(it) } |
||||
if (skipResult != true) { |
||||
result.passResult(completionResult) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun getKotlinClass(project: Project, classFqName: String): KtClassOrObject? { |
||||
return KotlinFullClassNameIndex |
||||
.get(classFqName, project, project.allScope()) |
||||
.firstOrNull() |
||||
.safeAs<KtClassOrObject>() |
||||
} |
||||
|
||||
private fun getAlignments(project: Project, alignmentFqName: String): Collection<KtDeclaration> { |
||||
val alignmentClass = getKotlinClass(project, alignmentFqName) ?: return emptyList() |
||||
return CachedValuesManager.getManager(project).getCachedValue(alignmentClass) { |
||||
val alignmentTopLevelClass = getKotlinClass(project, COMPOSE_ALIGNMENT)!! |
||||
val companionObject = alignmentTopLevelClass.companionObjects.firstOrNull() |
||||
val alignments = companionObject?.declarations?.filter { |
||||
it is KtProperty && it.type()?.isClassOrExtendsClass(alignmentFqName) == true |
||||
} |
||||
CachedValueProvider.Result.create(alignments, ProjectRootModificationTracker.getInstance(project)) |
||||
} |
||||
} |
||||
|
||||
private fun getArrangements(project: Project, arrangementFqName: String): Collection<KtDeclaration> { |
||||
val arrangementClass = getKotlinClass(project, arrangementFqName) ?: return emptyList() |
||||
return CachedValuesManager.getManager(project).getCachedValue(arrangementClass) { |
||||
val arrangementTopLevelClass = getKotlinClass(project, COMPOSE_ARRANGEMENT)!! |
||||
val arrangements = arrangementTopLevelClass.declarations.filter { |
||||
it is KtProperty && it.type()?.isClassOrExtendsClass(arrangementFqName) == true |
||||
} |
||||
CachedValueProvider.Result.create(arrangements, ProjectRootModificationTracker.getInstance(project)) |
||||
} |
||||
} |
||||
|
||||
private fun getStaticPropertyLookupElement(psiElement: KtDeclaration, ktClassName: String, isNewElement: Boolean): LookupElement { |
||||
val fqName = FqName(ktClassName) |
||||
val mainLookupString = if (isNewElement) "${fqName.shortName()}.${psiElement.name}" else psiElement.name!! |
||||
val builder = LookupElementBuilder |
||||
.create(psiElement, mainLookupString) |
||||
.withLookupString(psiElement.name!!) |
||||
.bold() |
||||
.withTailText(" (${ktClassName.substringBeforeLast('.')})", true) |
||||
.withInsertHandler lambda@{ context, item -> |
||||
//Add import. |
||||
val psiDocumentManager = PsiDocumentManager.getInstance(context.project) |
||||
val ktFile = context.file as KtFile |
||||
val modifierDescriptor = ktFile.resolveImportReference(fqName).singleOrNull() ?: return@lambda |
||||
ImportInsertHelper.getInstance(context.project).importDescriptor(ktFile, modifierDescriptor) |
||||
psiDocumentManager.commitAllDocuments() |
||||
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document) |
||||
} |
||||
|
||||
return object : LookupElementDecorator<LookupElement>(builder) { |
||||
override fun renderElement(presentation: LookupElementPresentation) { |
||||
super.renderElement(presentation) |
||||
presentation.icon = DefaultLookupItemRenderer.getRawIcon(builder) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private val PsiElement.propertyTypeFqName: String? |
||||
get() { |
||||
val property = contextOfType<KtProperty>() ?: return null |
||||
return property.type()?.fqName?.asString() |
||||
} |
||||
|
||||
private val PsiElement.argumentTypeFqName: String? |
||||
get() { |
||||
val argument = contextOfType<KtValueArgument>().takeIf { it !is KtLambdaArgument } ?: return null |
||||
|
||||
val callExpression = argument.parentOfType<KtCallElement>() ?: return null |
||||
val callee = callExpression.calleeExpression?.mainReference?.resolve().safeAs<KtNamedFunction>() ?: return null |
||||
|
||||
val argumentTypeFqName = if (argument.isNamed()) { |
||||
val argumentName = argument.getArgumentName()!!.asName.asString() |
||||
callee.valueParameters.find { it.name == argumentName }?.type()?.fqName |
||||
} |
||||
else { |
||||
val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) |
||||
callee.valueParameters.getOrNull(argumentIndex)?.type()?.fqName |
||||
} |
||||
|
||||
return argumentTypeFqName?.asString() |
||||
} |
||||
|
||||
} |
@ -0,0 +1,300 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion |
||||
|
||||
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 |
||||
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 |
||||
import com.intellij.codeInsight.lookup.LookupElementPresentation |
||||
import com.intellij.openapi.progress.ProgressManager |
||||
import com.intellij.psi.PsiDocumentManager |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.util.contextOfType |
||||
import com.intellij.psi.util.parentOfType |
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor |
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor |
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility |
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze |
||||
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade |
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference |
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall |
||||
import org.jetbrains.kotlin.idea.caches.resolve.util.getResolveScope |
||||
import org.jetbrains.kotlin.idea.completion.BasicLookupElementFactory |
||||
import org.jetbrains.kotlin.idea.completion.CollectRequiredTypesContextVariablesProvider |
||||
import org.jetbrains.kotlin.idea.completion.CompletionSession |
||||
import org.jetbrains.kotlin.idea.completion.InsertHandlerProvider |
||||
import org.jetbrains.kotlin.idea.completion.LookupElementFactory |
||||
import org.jetbrains.kotlin.idea.core.KotlinIndicesHelper |
||||
import org.jetbrains.kotlin.idea.core.isVisible |
||||
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName |
||||
import org.jetbrains.kotlin.idea.references.mainReference |
||||
import org.jetbrains.kotlin.idea.util.CallType |
||||
import org.jetbrains.kotlin.idea.util.CallTypeAndReceiver |
||||
import org.jetbrains.kotlin.idea.util.ImportInsertHelper |
||||
import org.jetbrains.kotlin.idea.util.getResolutionScope |
||||
import org.jetbrains.kotlin.idea.util.receiverTypesWithIndex |
||||
import org.jetbrains.kotlin.js.translate.callTranslator.getReturnType |
||||
import org.jetbrains.kotlin.name.FqName |
||||
import org.jetbrains.kotlin.nj2k.postProcessing.resolve |
||||
import org.jetbrains.kotlin.nj2k.postProcessing.type |
||||
import org.jetbrains.kotlin.psi.KtCallElement |
||||
import org.jetbrains.kotlin.psi.KtClass |
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression |
||||
import org.jetbrains.kotlin.psi.KtExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtFunction |
||||
import org.jetbrains.kotlin.psi.KtLambdaArgument |
||||
import org.jetbrains.kotlin.psi.KtNameReferenceExpression |
||||
import org.jetbrains.kotlin.psi.KtNamedFunction |
||||
import org.jetbrains.kotlin.psi.KtProperty |
||||
import org.jetbrains.kotlin.psi.KtPsiFactory |
||||
import org.jetbrains.kotlin.psi.KtSimpleNameExpression |
||||
import org.jetbrains.kotlin.psi.KtValueArgument |
||||
import org.jetbrains.kotlin.psi.KtValueArgumentList |
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType |
||||
import org.jetbrains.kotlin.psi.psiUtil.getReceiverExpression |
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode |
||||
import org.jetbrains.kotlin.utils.addToStdlib.safeAs |
||||
|
||||
/** |
||||
* Enhances code completion for Modifier (androidx.compose.ui.Modifier) |
||||
* |
||||
* Adds Modifier extension functions to code completion in places where modifier is expected |
||||
* e.g. parameter of type Modifier, variable of type Modifier as it was called on Modifier.<caret> |
||||
* |
||||
* Moves extension functions for method called on modifier [isMethodCalledOnModifier] up in the completion list. |
||||
* |
||||
* @see COMPOSE_MODIFIER_FQN |
||||
*/ |
||||
class ComposeModifierCompletionContributor : CompletionContributor() { |
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, resultSet: CompletionResultSet) { |
||||
val element = parameters.position |
||||
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || !element.inComposeModule() || parameters.originalFile !is KtFile) { |
||||
return |
||||
} |
||||
|
||||
// It says "on imported" because only in that case we are able resolve that it called on Modifier. |
||||
val isMethodCalledOnImportedModifier = element.isMethodCalledOnModifier() |
||||
ProgressManager.checkCanceled() |
||||
val isModifierType = isMethodCalledOnImportedModifier || element.isModifierArgument || element.isModifierProperty |
||||
if (!isModifierType) return |
||||
|
||||
ProgressManager.checkCanceled() |
||||
|
||||
val nameExpression = createNameExpression(element) |
||||
|
||||
val extensionFunctions = getExtensionFunctionsForModifier(nameExpression, element, resultSet.prefixMatcher) |
||||
|
||||
ProgressManager.checkCanceled() |
||||
val (returnsModifier, others) = extensionFunctions.partition { it.returnType?.fqName?.asString() == COMPOSE_MODIFIER_FQN } |
||||
val lookupElementFactory = createLookupElementFactory(nameExpression, parameters) |
||||
|
||||
val isNewModifier = !isMethodCalledOnImportedModifier && element.parentOfType<KtDotQualifiedExpression>() == null |
||||
//Prioritise functions that return Modifier over other extension function. |
||||
resultSet.addAllElements(returnsModifier.toLookupElements(lookupElementFactory, 2.0, insertModifier = isNewModifier)) |
||||
//If user didn't type Modifier don't suggest extensions that doesn't return Modifier. |
||||
if (isMethodCalledOnImportedModifier) { |
||||
resultSet.addAllElements(others.toLookupElements(lookupElementFactory, 0.0, insertModifier = isNewModifier)) |
||||
} |
||||
|
||||
ProgressManager.checkCanceled() |
||||
|
||||
//If method is called on modifier [KotlinCompletionContributor] will add extensions function one more time, we need to filter them out. |
||||
if (isMethodCalledOnImportedModifier) { |
||||
val extensionFunctionsNames = extensionFunctions.map { it.name.asString() }.toSet() |
||||
resultSet.runRemainingContributors(parameters) { completionResult -> |
||||
val skipResult = completionResult.lookupElement.psiElement.safeAs<KtFunction>()?.name?.let { extensionFunctionsNames.contains(it) } |
||||
if (skipResult != true) { |
||||
resultSet.passResult(completionResult) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun List<CallableDescriptor>.toLookupElements( |
||||
lookupElementFactory: LookupElementFactory, |
||||
weight: Double, |
||||
insertModifier: Boolean |
||||
) = flatMap { descriptor -> |
||||
lookupElementFactory.createStandardLookupElementsForDescriptor(descriptor, useReceiverTypes = true).map { |
||||
PrioritizedLookupElement.withPriority(ModifierLookupElement(it, insertModifier), weight) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates LookupElementFactory that is similar to the one kotlin-plugin uses during completion session. |
||||
* Code partially copied from [CompletionSession]. |
||||
*/ |
||||
private fun createLookupElementFactory(nameExpression: KtSimpleNameExpression, parameters: CompletionParameters): LookupElementFactory { |
||||
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS) |
||||
val file = parameters.originalFile.safeAs<KtFile>()!! |
||||
val resolutionFacade = file.getResolutionFacade() |
||||
|
||||
val moduleDescriptor = resolutionFacade.moduleDescriptor |
||||
|
||||
val callTypeAndReceiver = CallTypeAndReceiver.detect(nameExpression) |
||||
val receiverTypes = callTypeAndReceiver.receiverTypesWithIndex( |
||||
bindingContext, nameExpression, moduleDescriptor, resolutionFacade, |
||||
stableSmartCastsOnly = true, /* we don't include smart cast receiver types for "unstable" receiver value to mark members grayed */ |
||||
withImplicitReceiversWhenExplicitPresent = true |
||||
) |
||||
|
||||
val inDescriptor = nameExpression.getResolutionScope(bindingContext, resolutionFacade).ownerDescriptor |
||||
|
||||
val insertHandler = InsertHandlerProvider(CallType.DOT, parameters.editor, ::emptyList) |
||||
val basicLookupElementFactory = BasicLookupElementFactory(nameExpression.project, insertHandler) |
||||
|
||||
return LookupElementFactory( |
||||
basicLookupElementFactory, parameters.editor, receiverTypes, |
||||
callTypeAndReceiver.callType, inDescriptor, CollectRequiredTypesContextVariablesProvider() |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* Creates "Modifier.call" expression as it would be if user typed "Modifier.<caret>" themselves. |
||||
*/ |
||||
private fun createNameExpression(originalElement: PsiElement): KtSimpleNameExpression { |
||||
val originalFile = originalElement.containingFile.safeAs<KtFile>()!! |
||||
|
||||
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, |
||||
prefixMatcher: PrefixMatcher |
||||
): Collection<CallableDescriptor> { |
||||
val file = nameExpression.containingFile as KtFile |
||||
val searchScope = getResolveScope(file) |
||||
val resolutionFacade = file.getResolutionFacade() |
||||
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_FOR_COMPLETION) |
||||
|
||||
val callTypeAndReceiver = CallTypeAndReceiver.detect(nameExpression) |
||||
fun isVisible(descriptor: DeclarationDescriptor): Boolean { |
||||
if (descriptor is DeclarationDescriptorWithVisibility) { |
||||
return descriptor.isVisible(originalPosition, callTypeAndReceiver.receiver as? KtExpression, bindingContext, resolutionFacade) |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
val indicesHelper = KotlinIndicesHelper(resolutionFacade, searchScope, ::isVisible, file = file) |
||||
|
||||
val nameFilter = { name: String -> prefixMatcher.prefixMatches(name) } |
||||
return indicesHelper.getCallableTopLevelExtensions(callTypeAndReceiver, nameExpression, bindingContext, null, nameFilter) |
||||
} |
||||
|
||||
private val PsiElement.isModifierProperty: Boolean |
||||
get() { |
||||
// 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 |
||||
get() { |
||||
val argument = contextOfType<KtValueArgument>().takeIf { it !is KtLambdaArgument } ?: return false |
||||
|
||||
val callExpression = argument.parentOfType<KtCallElement>() ?: return false |
||||
val callee = callExpression.calleeExpression?.mainReference?.resolve().safeAs<KtNamedFunction>() ?: return false |
||||
|
||||
val argumentTypeFqName = if (argument.isNamed()) { |
||||
val argumentName = argument.getArgumentName()!!.asName.asString() |
||||
callee.valueParameters.find { it.name == argumentName }?.type()?.fqName |
||||
} |
||||
else { |
||||
val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument) |
||||
callee.valueParameters.getOrNull(argumentIndex)?.type()?.fqName |
||||
} |
||||
|
||||
return argumentTypeFqName?.asString() == COMPOSE_MODIFIER_FQN |
||||
} |
||||
|
||||
/** |
||||
* Returns true if psiElement is method called on object that has Modifier type. |
||||
* |
||||
* Returns true for Modifier.align().%this%, myModifier.%this%, Modifier.%this%. |
||||
*/ |
||||
private fun PsiElement.isMethodCalledOnModifier(): Boolean { |
||||
val elementOnWhichMethodCalled: KtExpression = parent.safeAs<KtNameReferenceExpression>()?.getReceiverExpression() ?: return false |
||||
// Case Modifier.align().%this%, modifier.%this% |
||||
val fqName = elementOnWhichMethodCalled.resolveToCall(BodyResolveMode.PARTIAL)?.getReturnType()?.fqName ?: |
||||
// Case Modifier.%this% |
||||
elementOnWhichMethodCalled.safeAs<KtNameReferenceExpression>()?.resolve().safeAs<KtClass>()?.fqName |
||||
return fqName?.asString() == COMPOSE_MODIFIER_FQN |
||||
} |
||||
|
||||
/** |
||||
* Inserts "Modifier." before [delegate] and imports [ComposeModifierCompletionContributor.modifierFqName] if it's not imported. |
||||
*/ |
||||
private class ModifierLookupElement( |
||||
delegate: LookupElement, |
||||
val insertModifier: Boolean |
||||
) : LookupElementDecorator<LookupElement>(delegate) { |
||||
companion object { |
||||
private const val callOnModifierObject = "Modifier." |
||||
} |
||||
|
||||
override fun renderElement(presentation: LookupElementPresentation) { |
||||
super.renderElement(presentation) |
||||
presentation.itemText = lookupString |
||||
} |
||||
|
||||
override fun getAllLookupStrings(): MutableSet<String> { |
||||
if (insertModifier) { |
||||
val lookupStrings = super.getAllLookupStrings().toMutableSet() |
||||
lookupStrings.add(callOnModifierObject + super.getLookupString()) |
||||
return lookupStrings |
||||
} |
||||
return super.getAllLookupStrings() |
||||
} |
||||
|
||||
override fun getLookupString(): String { |
||||
if (insertModifier) { |
||||
return callOnModifierObject + super.getLookupString() |
||||
} |
||||
return super.getLookupString() |
||||
} |
||||
|
||||
override fun handleInsert(context: InsertionContext) { |
||||
val psiDocumentManager = PsiDocumentManager.getInstance(context.project) |
||||
// 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(COMPOSE_MODIFIER_FQN)).singleOrNull() |
||||
modifierDescriptor?.let { ImportInsertHelper.getInstance(context.project).importDescriptor(ktFile, it) } |
||||
psiDocumentManager.commitAllDocuments() |
||||
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document) |
||||
super.handleInsert(context) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,212 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
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") |
||||
} |
@ -0,0 +1,218 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
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.CompletionResultSet |
||||
import com.intellij.codeInsight.completion.CompletionType |
||||
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). |
||||
* |
||||
* See the official wiki in [GitHub](https://github.com/androidx/constraintlayout/wiki/ConstraintSet-JSON5-syntax) to learn more about the |
||||
* supported JSON5 syntax. |
||||
*/ |
||||
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, |
||||
// 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 |
||||
) |
||||
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 |
||||
|
||||
//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 |
||||
} |
||||
|
||||
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 |
||||
} |
||||
super.fillCompletionVariants(parameters, result) |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
import com.android.tools.compose.completion.inserthandler.LiteralNewLineFormat |
||||
import com.android.tools.compose.completion.inserthandler.LiteralWithCaretFormat |
||||
import com.android.tools.compose.completion.inserthandler.LiveTemplateFormat |
||||
|
||||
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}']," |
||||
) |
||||
|
||||
/** |
||||
* 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. |
||||
* |
||||
* E.g.: For [count] = 3, returns the template: `": [0, 0, 0],"`, where every value may be changed by the user. |
||||
*/ |
||||
internal fun buildJsonNumberArrayTemplate(count: Int): LiveTemplateFormat { |
||||
val times = count.coerceAtLeast(1) |
||||
return LiveTemplateFormat(": [" + "<0>, ".repeat(times).removeSuffix(", ") + "],") |
||||
} |
@ -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) |
@ -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 |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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... |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
) |
@ -0,0 +1,58 @@
|
||||
/* |
||||
* 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.ide.DataManager |
||||
import com.intellij.openapi.actionSystem.IdeActions |
||||
import com.intellij.openapi.editor.EditorModificationUtil |
||||
import com.intellij.openapi.editor.actionSystem.CaretSpecificDataContext |
||||
import com.intellij.openapi.editor.actionSystem.EditorActionManager |
||||
import com.intellij.openapi.editor.actions.EditorActionUtil |
||||
import com.intellij.psi.PsiDocumentManager |
||||
|
||||
/** |
||||
* Handles insertions of an [InsertionFormat], applying new a line at the `\n` character. |
||||
* |
||||
* Applies the new line with [IdeActions.ACTION_EDITOR_ENTER] and moves the caret at the end of the new line. |
||||
*/ |
||||
class FormatWithNewLineInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> { |
||||
override fun handleInsert(context: InsertionContext, item: LookupElement) { |
||||
val literal = format.insertableString |
||||
with(context) { |
||||
val newLineOffset = literal.indexOf('\n') |
||||
val stringToInsert = if (newLineOffset >= 0) { |
||||
StringBuilder(literal).deleteCharAt(newLineOffset).toString() |
||||
} |
||||
else { |
||||
literal |
||||
} |
||||
val moveBy = newLineOffset - stringToInsert.length |
||||
EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true) |
||||
PsiDocumentManager.getInstance(project).commitDocument(document) |
||||
EditorActionUtil.moveCaretToLineEnd(editor, false, true) |
||||
EditorModificationUtil.moveCaretRelatively(editor, moveBy) |
||||
val caret = editor.caretModel.currentCaret |
||||
EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER).execute( |
||||
editor, |
||||
caret, |
||||
CaretSpecificDataContext(DataManager.getInstance().getDataContext(editor.contentComponent), caret) |
||||
) |
||||
} |
||||
} |
||||
} |
@ -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) |
@ -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.debug |
||||
|
||||
import com.intellij.ui.classFilter.ClassFilter |
||||
import com.intellij.ui.classFilter.DebuggerClassFilterProvider |
||||
|
||||
class ComposeDebuggerClassesFilterProvider : DebuggerClassFilterProvider { |
||||
private companion object { |
||||
private val FILTERS = listOf(ClassFilter("androidx.compose.runtime*")) |
||||
} |
||||
|
||||
override fun getFilters(): List<ClassFilter> { |
||||
return if (ComposeDebuggerSettings.getInstance().filterComposeRuntimeClasses) FILTERS else listOf() |
||||
} |
||||
} |
@ -0,0 +1,60 @@
|
||||
/* |
||||
* 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.debug |
||||
|
||||
import com.android.tools.compose.ComposeBundle |
||||
import com.intellij.openapi.components.State |
||||
import com.intellij.openapi.components.Storage |
||||
import com.intellij.openapi.options.Configurable |
||||
import com.intellij.openapi.options.SimpleConfigurable |
||||
import com.intellij.openapi.util.Getter |
||||
import com.intellij.util.xmlb.XmlSerializerUtil |
||||
import com.intellij.xdebugger.XDebuggerUtil |
||||
import com.intellij.xdebugger.settings.DebuggerSettingsCategory |
||||
import com.intellij.xdebugger.settings.XDebuggerSettings |
||||
|
||||
@State( |
||||
name = "ComposeDebuggerSettings", |
||||
storages = [Storage("compose.debug.xml")] |
||||
) |
||||
class ComposeDebuggerSettings : XDebuggerSettings<ComposeDebuggerSettings>("compose_debugger"), Getter<ComposeDebuggerSettings> { |
||||
var filterComposeRuntimeClasses: Boolean = true |
||||
|
||||
companion object { |
||||
fun getInstance(): ComposeDebuggerSettings { |
||||
return XDebuggerUtil.getInstance()?.getDebuggerSettings(ComposeDebuggerSettings::class.java)!! |
||||
} |
||||
} |
||||
|
||||
override fun createConfigurables(category: DebuggerSettingsCategory): Collection<Configurable> = |
||||
if (category == DebuggerSettingsCategory.STEPPING) { |
||||
listOf( |
||||
SimpleConfigurable.create( |
||||
"reference.idesettings.debugger.compose", |
||||
ComposeBundle.message("compose"), |
||||
ComposeDebuggerSettingsUi::class.java, |
||||
this |
||||
) |
||||
) |
||||
} else listOf() |
||||
|
||||
override fun get() = this |
||||
override fun getState() = this |
||||
|
||||
override fun loadState(state: ComposeDebuggerSettings) { |
||||
XmlSerializerUtil.copyBean(state, this) |
||||
} |
||||
} |
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.compose.debug.ComposeDebuggerSettingsUi"> |
||||
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> |
||||
<margin top="0" left="0" bottom="0" right="0"/> |
||||
<constraints> |
||||
<xy x="20" y="20" width="500" height="400"/> |
||||
</constraints> |
||||
<properties/> |
||||
<border type="none"/> |
||||
<children> |
||||
<component id="3cc22" class="javax.swing.JCheckBox" binding="filterComposeInternalClasses"> |
||||
<constraints> |
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/> |
||||
</constraints> |
||||
<properties> |
||||
<text resource-bundle="messages/ComposeBundle" key="filter.ignore.compose.runtime.classes"/> |
||||
</properties> |
||||
</component> |
||||
<vspacer id="821e6"> |
||||
<constraints> |
||||
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/> |
||||
</constraints> |
||||
</vspacer> |
||||
</children> |
||||
</grid> |
||||
</form> |
@ -0,0 +1,48 @@
|
||||
/* |
||||
* 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.debug; |
||||
|
||||
import com.intellij.openapi.options.ConfigurableUi; |
||||
import javax.swing.JCheckBox; |
||||
import javax.swing.JComponent; |
||||
import javax.swing.JPanel; |
||||
import org.jetbrains.annotations.NotNull; |
||||
|
||||
public class ComposeDebuggerSettingsUi implements ConfigurableUi<ComposeDebuggerSettings> { |
||||
private JPanel myPanel; |
||||
private JCheckBox filterComposeInternalClasses; |
||||
|
||||
|
||||
@Override |
||||
public void reset(@NotNull ComposeDebuggerSettings settings) { |
||||
filterComposeInternalClasses.setSelected(settings.getFilterComposeRuntimeClasses()); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isModified(@NotNull ComposeDebuggerSettings settings) { |
||||
return filterComposeInternalClasses.isSelected() != settings.getFilterComposeRuntimeClasses(); |
||||
} |
||||
|
||||
@Override |
||||
public void apply(@NotNull ComposeDebuggerSettings settings) { |
||||
settings.setFilterComposeRuntimeClasses(filterComposeInternalClasses.isSelected()); |
||||
} |
||||
|
||||
@Override |
||||
public @NotNull JComponent getComponent() { |
||||
return myPanel; |
||||
} |
||||
} |
@ -0,0 +1,150 @@
|
||||
/* |
||||
* 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.debug |
||||
|
||||
import com.intellij.debugger.MultiRequestPositionManager |
||||
import com.intellij.debugger.NoDataException |
||||
import com.intellij.debugger.SourcePosition |
||||
import com.intellij.debugger.engine.DebugProcess |
||||
import com.intellij.debugger.engine.DebugProcessImpl |
||||
import com.intellij.debugger.engine.PositionManagerWithMultipleStackFrames |
||||
import com.intellij.debugger.engine.evaluation.EvaluationContext |
||||
import com.intellij.debugger.jdi.StackFrameProxyImpl |
||||
import com.intellij.debugger.requests.ClassPrepareRequestor |
||||
import com.intellij.openapi.application.runReadAction |
||||
import com.intellij.openapi.fileTypes.FileType |
||||
import com.intellij.util.ThreeState |
||||
import com.intellij.xdebugger.frame.XStackFrame |
||||
import com.sun.jdi.Location |
||||
import com.sun.jdi.ReferenceType |
||||
import com.sun.jdi.request.ClassPrepareRequest |
||||
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil |
||||
import org.jetbrains.kotlin.idea.KotlinFileType |
||||
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManager |
||||
import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
|
||||
/** |
||||
* A PositionManager capable of setting breakpoints inside of ComposableSingleton lambdas. |
||||
* |
||||
* This class essentially resolves breakpoints for lambdas generated by the compose compiler |
||||
* optimization that was introduced in I8c967b14c5d9bf67e5646e60f630f2e29e006366 |
||||
* The default [KotlinPositionManager] only locates source positions in enclosing and nested |
||||
* classes, while composable singleton lambdas are cached in a separate top-level class. |
||||
* |
||||
* See https://issuetracker.google.com/190373291 for more information. |
||||
*/ |
||||
class ComposePositionManager( |
||||
private val debugProcess: DebugProcess, |
||||
private val kotlinPositionManager: KotlinPositionManager |
||||
) : MultiRequestPositionManager by kotlinPositionManager, PositionManagerWithMultipleStackFrames { |
||||
override fun getAcceptedFileTypes(): Set<FileType> = setOf(KotlinFileType.INSTANCE) |
||||
|
||||
override fun createStackFrames(frame: StackFrameProxyImpl, debugProcess: DebugProcessImpl, location: Location): List<XStackFrame> = |
||||
kotlinPositionManager.createStackFrames(frame, debugProcess, location) |
||||
|
||||
override fun evaluateCondition(context: EvaluationContext, |
||||
frame: StackFrameProxyImpl, |
||||
location: Location, |
||||
expression: String): ThreeState = |
||||
kotlinPositionManager.evaluateCondition(context, frame, location, expression) |
||||
|
||||
/** |
||||
* Returns all prepared classes which could contain the given classPosition. |
||||
* |
||||
* This handles the case where a user sets a breakpoint in a ComposableSingleton |
||||
* lambda after the debug process has already initialized the corresponding |
||||
* `lambda-n` class. |
||||
*/ |
||||
override fun getAllClasses(classPosition: SourcePosition): List<ReferenceType> { |
||||
val file = classPosition.file |
||||
|
||||
// Unlike [KotlinPositionManager] we don't handle compiled code, since the |
||||
// Kotlin decompiler does not undo any Compose specific optimizations. |
||||
if (file !is KtFile) { |
||||
throw NoDataException.INSTANCE |
||||
} |
||||
|
||||
val vm = debugProcess.virtualMachineProxy |
||||
val singletonClasses = vm.classesByName(computeComposableSingletonsClassName(file)).flatMap { referenceType -> |
||||
if (referenceType.isPrepared) vm.nestedTypes(referenceType) else listOf() |
||||
} |
||||
|
||||
if (singletonClasses.isEmpty()) { |
||||
throw NoDataException.INSTANCE |
||||
} |
||||
|
||||
// Since [CompoundPositionManager] returns the first successful result from [getAllClasses], |
||||
// we need to query [KotlinPositionManager] here in order to locate breakpoints |
||||
// in ordinary Kotlin code. |
||||
val kotlinReferences = kotlinPositionManager.getAllClasses(classPosition) |
||||
return kotlinReferences + singletonClasses |
||||
} |
||||
|
||||
/** |
||||
* Registers search patterns in the form of [ClassPrepareRequest]s for classes which may contain |
||||
* the given `position`, but may not be loaded yet. The `requestor` will be called for any newly |
||||
* prepared class which matches any of the created search patterns. |
||||
*/ |
||||
override fun createPrepareRequests(requestor: ClassPrepareRequestor, position: SourcePosition): List<ClassPrepareRequest> { |
||||
val file = position.file |
||||
if (file !is KtFile) { |
||||
throw NoDataException.INSTANCE |
||||
} |
||||
|
||||
// Similar to getAllClasses above, [CompoundPositionManager] uses the first successful |
||||
// position manager, so we need to include the prepare requests from [KotlinPositionManager] |
||||
// in order to locate breakpoints in ordinary Kotlin code. |
||||
val kotlinRequests = kotlinPositionManager.createPrepareRequests(requestor, position) |
||||
|
||||
val singletonRequest = debugProcess.requestsManager.createClassPrepareRequest( |
||||
requestor, |
||||
"${computeComposableSingletonsClassName(file)}\$*" |
||||
) |
||||
|
||||
return if (singletonRequest == null) kotlinRequests else kotlinRequests + singletonRequest |
||||
} |
||||
|
||||
/** |
||||
* A method from [PositionManager] which was superseded by [createPrepareRequests] in [MultiRequestPositionManager]. |
||||
* Intellij code should never call this method for subclasses of [MultiRequestPositionManager]. |
||||
*/ |
||||
override fun createPrepareRequest(requestor: ClassPrepareRequestor, position: SourcePosition): ClassPrepareRequest? { |
||||
return createPrepareRequests(requestor, position).firstOrNull() |
||||
} |
||||
|
||||
/** |
||||
* Compute the name of the ComposableSingletons class for the given file. |
||||
* |
||||
* The Compose compiler plugin creates per-file ComposableSingletons classes to cache |
||||
* composable lambdas without captured variables. We need to locate these classes in order |
||||
* to search them for breakpoint locations. |
||||
* |
||||
* NOTE: The pattern for ComposableSingletons classes needs to be kept in sync with the |
||||
* code in `ComposerLambdaMemoization.getOrCreateComposableSingletonsClass`. |
||||
* The optimization was introduced in I8c967b14c5d9bf67e5646e60f630f2e29e006366 |
||||
*/ |
||||
private fun computeComposableSingletonsClassName(file: KtFile): String { |
||||
// The code in `ComposerLambdaMemoization` always uses the file short name and |
||||
// ignores `JvmName` annotations, but (implicitly) respects `JvmPackageName` |
||||
// annotations. |
||||
val filePath = file.virtualFile?.path ?: file.name |
||||
val fileName = filePath.split('/').last() |
||||
val shortName = PackagePartClassUtils.getFilePartShortName(fileName) |
||||
val fileClassFqName = runReadAction { JvmFileClassUtil.getFileClassInfoNoResolve(file) }.facadeClassFqName |
||||
return "${fileClassFqName.parent().asString()}.ComposableSingletons\$$shortName" |
||||
} |
||||
} |
@ -0,0 +1,27 @@
|
||||
/* |
||||
* 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.debug |
||||
|
||||
import com.intellij.debugger.PositionManager |
||||
import com.intellij.debugger.PositionManagerFactory |
||||
import com.intellij.debugger.engine.DebugProcess |
||||
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManager |
||||
|
||||
class ComposePositionManagerFactory : PositionManagerFactory() { |
||||
override fun createPositionManager(process: DebugProcess): PositionManager { |
||||
return ComposePositionManager(process, KotlinPositionManager(process)) |
||||
} |
||||
} |
@ -0,0 +1,258 @@
|
||||
/* |
||||
* 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.debug.render |
||||
|
||||
import com.android.tools.compose.debug.render.ComposeStateObjectClassRenderer.Companion.DEBUGGER_DISPLAY_VALUE_METHOD_NAME |
||||
import com.intellij.debugger.DebuggerContext |
||||
import com.intellij.debugger.engine.DebugProcess |
||||
import com.intellij.debugger.engine.DebugProcessImpl |
||||
import com.intellij.debugger.engine.evaluation.CodeFragmentKind |
||||
import com.intellij.debugger.engine.evaluation.EvaluateException |
||||
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil |
||||
import com.intellij.debugger.engine.evaluation.EvaluationContext |
||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl |
||||
import com.intellij.debugger.engine.evaluation.TextWithImportsImpl |
||||
import com.intellij.debugger.impl.DebuggerUtilsAsync |
||||
import com.intellij.debugger.impl.DebuggerUtilsImpl |
||||
import com.intellij.debugger.settings.NodeRendererSettings |
||||
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl |
||||
import com.intellij.debugger.ui.tree.DebuggerTreeNode |
||||
import com.intellij.debugger.ui.tree.NodeDescriptor |
||||
import com.intellij.debugger.ui.tree.ValueDescriptor |
||||
import com.intellij.debugger.ui.tree.render.CachedEvaluator |
||||
import com.intellij.debugger.ui.tree.render.ChildrenBuilder |
||||
import com.intellij.debugger.ui.tree.render.ClassRenderer |
||||
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener |
||||
import com.intellij.debugger.ui.tree.render.NodeRenderer |
||||
import com.intellij.ide.highlighter.JavaFileType |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.openapi.util.Key |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.PsiExpression |
||||
import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants |
||||
import com.sun.jdi.ClassType |
||||
import com.sun.jdi.Type |
||||
import com.sun.jdi.Value |
||||
import org.jetbrains.kotlin.idea.debugger.KotlinClassRenderer |
||||
import org.jetbrains.kotlin.idea.debugger.core.isInKotlinSources |
||||
import java.util.concurrent.CompletableFuture |
||||
|
||||
/** |
||||
* Renderer for a given compose `StateObject` type object. |
||||
* |
||||
* Basically, for a given compose state object, its underlying value (by invoking [DEBUGGER_DISPLAY_VALUE_METHOD_NAME]) |
||||
* determines how it's rendered in the "Variables" pane. This is to provide an auto-unboxing experience while debugging, |
||||
* that users can identify the data by a glance at this more readable data view. |
||||
* |
||||
* E.g. |
||||
* 1) if the underlying value is an integer `1`, the label is rendered `1`. |
||||
* 2) if the underlying value is a list, then the given object is rendered by a `List` renderer instead of the |
||||
* original `Kotlin class` renderer. That is, "size = xx" is the label, and the `ArrayRenderer` is the children renderer |
||||
* in this case. |
||||
* 3) if the underlying value is a map, then the given object is rendered by a `Map` renderer instead of the original |
||||
* `Kotlin class` renderer. That is, "size = xx" is the label, and the `ArrayRenderer` is the children renderer in |
||||
* this case. When expanding, each of the entry is rendered by the `Map.Entry` renderer. |
||||
* |
||||
* @param fqcn the fully qualified class name of the Compose State Object to apply this custom renderer to. |
||||
*/ |
||||
class ComposeStateObjectClassRenderer(private val fqcn: String) : ClassRenderer() { |
||||
// We fallback to [KotlinClassRenderer] when the following exception is thrown: |
||||
// Unable to evaluate the expression No such instance method: 'getDebuggerDisplayValue', |
||||
private val fallbackRenderer by lazy { |
||||
KotlinClassRenderer() |
||||
} |
||||
|
||||
private val prioritizedCollectionRenderers by lazy { |
||||
NodeRendererSettings.getInstance() |
||||
.alternateCollectionRenderers |
||||
.filter { it.name == "Map" || it.name == "List" } |
||||
.filter { it.isEnabled } |
||||
.toList() |
||||
} |
||||
|
||||
private val debuggerDisplayValueEvaluator = DebuggerDisplayValueEvaluator(fqcn) |
||||
|
||||
init { |
||||
setIsApplicableChecker { type: Type? -> |
||||
if (type !is ClassType || !type.isInKotlinSources()) return@setIsApplicableChecker CompletableFuture.completedFuture(false) |
||||
|
||||
DebuggerUtilsAsync.instanceOf(type, fqcn) |
||||
} |
||||
} |
||||
|
||||
companion object { |
||||
private val NODE_RENDERER_KEY = Key.create<NodeRenderer>(this::class.java.simpleName) |
||||
|
||||
// The name of the method we expect the Compose State Object to implement. We invoke it to retrieve the underlying |
||||
// Compose State Object value. |
||||
private const val DEBUGGER_DISPLAY_VALUE_METHOD_NAME = "getDebuggerDisplayValue" |
||||
} |
||||
|
||||
override fun buildChildren(value: Value, builder: ChildrenBuilder, evaluationContext: EvaluationContext) { |
||||
val debuggerDisplayValueDescriptor = try { |
||||
getDebuggerDisplayValueDescriptor(value, evaluationContext, null) |
||||
} |
||||
catch (evaluateException: EvaluateException) { |
||||
if (evaluateException.localizedMessage.startsWith("No such instance method:")) { |
||||
return fallbackRenderer.buildChildren(value, builder, evaluationContext) |
||||
} |
||||
|
||||
throw evaluateException |
||||
} |
||||
|
||||
getDelegatedRendererAsync(evaluationContext.debugProcess, debuggerDisplayValueDescriptor) |
||||
.thenAccept { renderer -> |
||||
builder.parentDescriptor.putUserData(NODE_RENDERER_KEY, renderer) |
||||
renderer.buildChildren(debuggerDisplayValueDescriptor.value, builder, evaluationContext) |
||||
} |
||||
} |
||||
|
||||
override fun getChildValueExpression(node: DebuggerTreeNode, context: DebuggerContext): PsiElement? { |
||||
return node.parent.descriptor.getUserData(NODE_RENDERER_KEY)?.getChildValueExpression(node, context) |
||||
} |
||||
|
||||
override fun isExpandableAsync( |
||||
value: Value, |
||||
evaluationContext: EvaluationContext, |
||||
parentDescriptor: NodeDescriptor |
||||
): CompletableFuture<Boolean> { |
||||
val debuggerDisplayValueDescriptor = try { |
||||
getDebuggerDisplayValueDescriptor(value, evaluationContext, null) |
||||
} |
||||
catch (evaluateException: EvaluateException) { |
||||
if (evaluateException.localizedMessage.startsWith("No such instance method:")) { |
||||
return fallbackRenderer.isExpandableAsync(value, evaluationContext, parentDescriptor) |
||||
} |
||||
|
||||
return CompletableFuture.failedFuture(evaluateException) |
||||
} |
||||
|
||||
return getDelegatedRendererAsync(evaluationContext.debugProcess, debuggerDisplayValueDescriptor) |
||||
.thenCompose { renderer -> |
||||
renderer.isExpandableAsync(debuggerDisplayValueDescriptor.value, evaluationContext, debuggerDisplayValueDescriptor) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns a [ValueDescriptor] for the underlying "debugger display value", which is evaluated by invoking the |
||||
* [DEBUGGER_DISPLAY_VALUE_METHOD_NAME] method of the Compose `StateObject` type object: [value]. |
||||
*/ |
||||
private fun getDebuggerDisplayValueDescriptor( |
||||
value: Value, |
||||
evaluationContext: EvaluationContext, |
||||
originalDescriptor: ValueDescriptor? |
||||
): ValueDescriptor { |
||||
val debugProcess = evaluationContext.debugProcess |
||||
|
||||
if (!debugProcess.isAttached) throw EvaluateExceptionUtil.PROCESS_EXITED |
||||
|
||||
val thisEvaluationContext = evaluationContext.createEvaluationContext(value) |
||||
val debuggerDisplayValue = debuggerDisplayValueEvaluator.evaluate(debugProcess.project, thisEvaluationContext) |
||||
|
||||
return object : ValueDescriptorImpl(evaluationContext.project, debuggerDisplayValue) { |
||||
override fun getDescriptorEvaluation(context: DebuggerContext): PsiExpression? = null |
||||
override fun calcValue(evaluationContext: EvaluationContextImpl?): Value = debuggerDisplayValue |
||||
override fun calcValueName(): String = "value" |
||||
|
||||
override fun setValueLabel(label: String) { |
||||
originalDescriptor?.setValueLabel(label) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return an ID of this renderer class, used by the IntelliJ platform to identify our renderer among all active |
||||
* renderers in the system. |
||||
*/ |
||||
override fun getUniqueId(): String { |
||||
return fqcn |
||||
} |
||||
|
||||
override fun calcLabel(descriptor: ValueDescriptor, evaluationContext: EvaluationContext, listener: DescriptorLabelListener): String { |
||||
val debuggerDisplayValueDescriptor: ValueDescriptor = try { |
||||
getDebuggerDisplayValueDescriptor(descriptor.value, evaluationContext, descriptor) |
||||
} |
||||
catch (evaluateException: EvaluateException) { |
||||
if (evaluateException.localizedMessage.startsWith("No such instance method:")) { |
||||
return fallbackRenderer.calcLabel(descriptor, evaluationContext, listener) |
||||
} |
||||
|
||||
throw evaluateException |
||||
} |
||||
|
||||
val renderer = getDelegatedRendererAsync(evaluationContext.debugProcess, debuggerDisplayValueDescriptor) |
||||
return calcLabelAsync(renderer, debuggerDisplayValueDescriptor, evaluationContext, listener) |
||||
.getNow(XDebuggerUIConstants.getCollectingDataMessage()) |
||||
} |
||||
|
||||
private fun calcLabelAsync( |
||||
renderer: CompletableFuture<NodeRenderer>, |
||||
descriptor: ValueDescriptor, |
||||
evaluationContext: EvaluationContext?, |
||||
listener: DescriptorLabelListener |
||||
): CompletableFuture<String> { |
||||
return renderer.thenApply { r: NodeRenderer -> |
||||
try { |
||||
val label = r.calcLabel(descriptor, evaluationContext, listener) |
||||
descriptor.setValueLabel(label) |
||||
listener.labelChanged() |
||||
return@thenApply label |
||||
} |
||||
catch (evaluateException: EvaluateException) { |
||||
descriptor.setValueLabelFailed(evaluateException) |
||||
listener.labelChanged() |
||||
return@thenApply "" |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns a [CompletableFuture] of the first applicable renderer for the given [valueDescriptor]. |
||||
*/ |
||||
private fun getDelegatedRendererAsync(debugProcess: DebugProcess, valueDescriptor: ValueDescriptor): CompletableFuture<NodeRenderer> { |
||||
val type = valueDescriptor.type |
||||
return DebuggerUtilsImpl.getApplicableRenderers(prioritizedCollectionRenderers, type) |
||||
.thenCompose { renderers -> |
||||
// Return any applicable renderer of [prioritizedCollectionRenderers]. This is to de-prioritize `Kotlin class` renderer. |
||||
// Or fallback to the default renderer. |
||||
val found = renderers.firstOrNull() ?: return@thenCompose (debugProcess as DebugProcessImpl).getAutoRendererAsync(type) |
||||
|
||||
CompletableFuture.completedFuture(found) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* [CachedEvaluator] used to invoke the [DEBUGGER_DISPLAY_VALUE_METHOD_NAME] method. |
||||
*/ |
||||
private class DebuggerDisplayValueEvaluator(private val fqcn: String) : CachedEvaluator() { |
||||
init { |
||||
referenceExpression = TextWithImportsImpl( |
||||
CodeFragmentKind.EXPRESSION, |
||||
"this.$DEBUGGER_DISPLAY_VALUE_METHOD_NAME()", |
||||
"", |
||||
JavaFileType.INSTANCE |
||||
) |
||||
} |
||||
|
||||
override fun getClassName(): String { |
||||
return fqcn |
||||
} |
||||
|
||||
fun evaluate(project: Project, context: EvaluationContext): Value { |
||||
return getEvaluator(project).evaluate(context) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,77 @@
|
||||
/* |
||||
* 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.debug.render |
||||
|
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.debugger.ui.tree.render.ChildrenRenderer |
||||
import com.intellij.debugger.ui.tree.render.CompoundRendererProvider |
||||
import com.intellij.debugger.ui.tree.render.ValueLabelRenderer |
||||
import com.sun.jdi.Type |
||||
import java.util.concurrent.CompletableFuture |
||||
import java.util.function.Function |
||||
|
||||
/** |
||||
* Base custom renderer provider for rendering a given compose `StateObject` type object. |
||||
* |
||||
* [stateObjectClassRenderer] is the actual underlying renderer for the label and the children nodes. Users can select |
||||
* the provided renderer by [rendererName] if applicable. |
||||
* |
||||
* @param fqcn the fully qualified class name of the Compose State Object to apply the underlying custom renderer to. |
||||
*/ |
||||
sealed class ComposeStateObjectRendererProviderBase(private val fqcn: String) : CompoundRendererProvider() { |
||||
private val rendererName = "Compose State Object" |
||||
private val stateObjectClassRenderer by lazy { |
||||
ComposeStateObjectClassRenderer(fqcn) |
||||
} |
||||
|
||||
override fun getName(): String { |
||||
return rendererName |
||||
} |
||||
|
||||
override fun isEnabled(): Boolean { |
||||
return StudioFlags.COMPOSE_STATE_OBJECT_CUSTOM_RENDERER.get() |
||||
} |
||||
|
||||
override fun getIsApplicableChecker(): Function<Type?, CompletableFuture<Boolean>> { |
||||
return Function { type: Type? -> |
||||
stateObjectClassRenderer.isApplicableAsync(type) |
||||
} |
||||
} |
||||
|
||||
override fun getValueLabelRenderer(): ValueLabelRenderer { |
||||
return stateObjectClassRenderer |
||||
} |
||||
|
||||
override fun getChildrenRenderer(): ChildrenRenderer { |
||||
return stateObjectClassRenderer |
||||
} |
||||
} |
||||
|
||||
class SnapshotMutableStateImplRendererProvider : ComposeStateObjectRendererProviderBase( |
||||
"androidx.compose.runtime.SnapshotMutableStateImpl" |
||||
) |
||||
|
||||
class DerivedSnapshotStateRendererProvider : ComposeStateObjectRendererProviderBase( |
||||
"androidx.compose.runtime.DerivedSnapshotState" |
||||
) |
||||
|
||||
class ComposeStateObjectListRendererProvider : ComposeStateObjectRendererProviderBase( |
||||
"androidx.compose.runtime.snapshots.SnapshotStateList" |
||||
) |
||||
|
||||
class ComposeStateObjectMapRendererProvider : ComposeStateObjectRendererProviderBase( |
||||
"androidx.compose.runtime.snapshots.SnapshotStateMap" |
||||
) |
@ -0,0 +1,69 @@
|
||||
/* |
||||
* 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.debug.render |
||||
|
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.debugger.impl.DebuggerUtilsAsync |
||||
import com.intellij.debugger.settings.NodeRendererSettings |
||||
import com.intellij.debugger.ui.tree.render.ChildrenRenderer |
||||
import com.intellij.debugger.ui.tree.render.CompoundReferenceRenderer |
||||
import com.intellij.debugger.ui.tree.render.CompoundRendererProvider |
||||
import com.intellij.debugger.ui.tree.render.ValueLabelRenderer |
||||
import com.sun.jdi.ClassType |
||||
import com.sun.jdi.Type |
||||
import org.jetbrains.kotlin.idea.debugger.core.isInKotlinSources |
||||
import java.util.concurrent.CompletableFuture |
||||
import java.util.function.Function |
||||
|
||||
/** |
||||
* Custom renderer for "MapEntry" type objects. |
||||
* |
||||
* This is to precede the `Kotlin class` renderer, as [KotlinMapEntryRenderer] provides a more readable data view, |
||||
* that the underlying `Map.Entry` renderer does the real work. |
||||
*/ |
||||
class KotlinMapEntryRenderer : CompoundRendererProvider() { |
||||
private val MAP_ENTRY_FQCN = "java.util.Map\$Entry" |
||||
|
||||
private val mapEntryLabelRender = NodeRendererSettings.getInstance().alternateCollectionRenderers.find { |
||||
it.name == "Map.Entry" |
||||
} |
||||
|
||||
override fun isEnabled(): Boolean { |
||||
if (StudioFlags.COMPOSE_STATE_OBJECT_CUSTOM_RENDERER.get()) return true |
||||
|
||||
return false |
||||
} |
||||
|
||||
override fun getName(): String { |
||||
return "Kotlin MapEntry" |
||||
} |
||||
|
||||
override fun getIsApplicableChecker(): Function<Type?, CompletableFuture<Boolean>> { |
||||
return Function { type: Type? -> |
||||
if (type !is ClassType || !type.isInKotlinSources()) return@Function CompletableFuture.completedFuture(false) |
||||
|
||||
DebuggerUtilsAsync.instanceOf(type, MAP_ENTRY_FQCN) |
||||
} |
||||
} |
||||
|
||||
override fun getValueLabelRenderer(): ValueLabelRenderer { |
||||
return (mapEntryLabelRender as CompoundReferenceRenderer).labelRenderer |
||||
} |
||||
|
||||
override fun getChildrenRenderer(): ChildrenRenderer { |
||||
return NodeRendererSettings.createEnumerationChildrenRenderer(arrayOf(arrayOf("key", "getKey()"), arrayOf("value", "getValue()"))) |
||||
} |
||||
} |
@ -0,0 +1,112 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.formatting |
||||
|
||||
import com.android.tools.compose.isModifierChainLongerThanTwo |
||||
import com.android.tools.compose.settings.ComposeCustomCodeStyleSettings |
||||
import com.android.tools.modules.* |
||||
import com.intellij.application.options.CodeStyle |
||||
import com.intellij.openapi.project.DumbService |
||||
import com.intellij.openapi.util.TextRange |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.PsiFile |
||||
import com.intellij.psi.codeStyle.CodeStyleSettings |
||||
import com.intellij.psi.codeStyle.CommonCodeStyleSettings |
||||
import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade |
||||
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor |
||||
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessorHelper |
||||
import org.jetbrains.kotlin.idea.formatter.kotlinCommonSettings |
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression |
||||
import org.jetbrains.kotlin.psi.KtElement |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid |
||||
|
||||
/** |
||||
* Runs after explicit code formatting invocation and for Modifier(androidx.compose.ui.Modifier) chain that is two modifiers or longer, |
||||
* splits it in one modifier per line. |
||||
*/ |
||||
class ComposePostFormatProcessor : PostFormatProcessor { |
||||
|
||||
private fun isAvailable(psiElement: PsiElement, settings: CodeStyleSettings): Boolean { |
||||
return psiElement.containingFile is KtFile && |
||||
psiElement.inComposeModule() && |
||||
!DumbService.isDumb(psiElement.project) && |
||||
settings.getCustomSettings(ComposeCustomCodeStyleSettings::class.java).USE_CUSTOM_FORMATTING_FOR_MODIFIERS |
||||
} |
||||
|
||||
override fun processElement(source: PsiElement, settings: CodeStyleSettings): PsiElement { |
||||
return if (isAvailable(source, settings)) ComposeModifierProcessor(settings).process(source) else source |
||||
} |
||||
|
||||
override fun processText(source: PsiFile, rangeToReformat: TextRange, settings: CodeStyleSettings): TextRange { |
||||
return if (isAvailable(source, settings)) ComposeModifierProcessor(settings).processText(source, rangeToReformat) else rangeToReformat |
||||
} |
||||
} |
||||
|
||||
class ComposeModifierProcessor(private val settings: CodeStyleSettings) : KtTreeVisitorVoid() { |
||||
private val myPostProcessor = PostFormatProcessorHelper(settings.kotlinCommonSettings) |
||||
|
||||
private fun updateResultRange(oldTextLength: Int, newTextLength: Int) { |
||||
myPostProcessor.updateResultRange(oldTextLength, newTextLength) |
||||
} |
||||
|
||||
override fun visitKtElement(element: KtElement) { |
||||
super.visitElement(element) |
||||
if (element.isPhysical && isModifierChainThatNeedToBeWrapped(element)) { |
||||
val oldTextLength: Int = element.textLength |
||||
wrapModifierChain(element as KtDotQualifiedExpression, settings) |
||||
updateResultRange(oldTextLength, element.textLength) |
||||
} |
||||
} |
||||
|
||||
fun process(formatted: PsiElement): PsiElement { |
||||
formatted.accept(this) |
||||
return formatted |
||||
} |
||||
|
||||
fun processText( |
||||
source: PsiFile, |
||||
rangeToReformat: TextRange |
||||
): TextRange { |
||||
myPostProcessor.resultTextRange = rangeToReformat |
||||
source.accept(this) |
||||
return myPostProcessor.resultTextRange |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns true if it's Modifier(androidx.compose.ui.Modifier) chain that is two modifiers or longer. |
||||
*/ |
||||
private fun isModifierChainThatNeedToBeWrapped(element: KtElement): Boolean { |
||||
// Take very top KtDotQualifiedExpression, e.g for `Modifier.adjust1().adjust2()` take whole expression, not only `Modifier.adjust1()`. |
||||
return element is KtDotQualifiedExpression && |
||||
element.parent !is KtDotQualifiedExpression && |
||||
isModifierChainLongerThanTwo(element) |
||||
} |
||||
|
||||
/** |
||||
* Splits KtDotQualifiedExpression it one call per line. |
||||
*/ |
||||
internal fun wrapModifierChain(element: KtDotQualifiedExpression, settings: CodeStyleSettings) { |
||||
CodeStyle.doWithTemporarySettings( |
||||
element.project, |
||||
settings |
||||
) { tempSettings: CodeStyleSettings -> |
||||
tempSettings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP = CommonCodeStyleSettings.WRAP_ALWAYS |
||||
tempSettings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN = true |
||||
CodeFormatterFacade(tempSettings, element.language).processElement(element.node) |
||||
} |
||||
} |
@ -0,0 +1,113 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.intentions |
||||
|
||||
import com.android.tools.compose.COMPOSABLE_ANNOTATION_NAME |
||||
import com.android.tools.compose.ComposeBundle |
||||
import com.android.tools.compose.isComposableFunction |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.codeInsight.intention.IntentionAction |
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns |
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic |
||||
import org.jetbrains.kotlin.diagnostics.Errors |
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyzeAndGetResult |
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory |
||||
import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor |
||||
import org.jetbrains.kotlin.idea.quickfix.QuickFixes |
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.CallableInfo |
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.FunctionInfo |
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.TypeInfo |
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getParameterInfos |
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getTypeInfoForTypeArguments |
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.guessTypes |
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createCallable.CreateCallableFromUsageFix |
||||
import org.jetbrains.kotlin.idea.refactoring.getExtractionContainers |
||||
import org.jetbrains.kotlin.psi.KtCallExpression |
||||
import org.jetbrains.kotlin.psi.KtNamedFunction |
||||
import org.jetbrains.kotlin.psi.KtPsiFactory |
||||
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 { |
||||
override fun registerQuickFixes(quickFixes: QuickFixes) { |
||||
if (StudioFlags.COMPOSE_EDITOR_SUPPORT.get()) { |
||||
quickFixes.register(Errors.UNRESOLVED_REFERENCE, ComposeUnresolvedFunctionFixFactory()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates quick fix(IntentionAction) for an unresolved reference inside a Composable function. |
||||
* |
||||
* Created action creates new function with @Composable annotation. |
||||
* |
||||
* Example: |
||||
* For |
||||
* |
||||
* @Composable |
||||
* fun myComposable() { |
||||
* <caret>newFunction() |
||||
* } |
||||
* |
||||
* creates |
||||
* |
||||
* @Composable |
||||
* fun newFunction() { |
||||
* TODO("Not yet implemented") |
||||
* } |
||||
* |
||||
*/ |
||||
private class ComposeUnresolvedFunctionFixFactory : KotlinSingleIntentionActionFactory() { |
||||
override fun createAction(diagnostic: Diagnostic): IntentionAction? { |
||||
val unresolvedCall = diagnostic.psiElement.parent as? KtCallExpression ?: return null |
||||
val parentFunction = unresolvedCall.getStrictParentOfType<KtNamedFunction>() ?: return null |
||||
if (!parentFunction.isComposableFunction()) return null |
||||
|
||||
val name = (unresolvedCall.calleeExpression as? KtSimpleNameExpression)?.getReferencedName() ?: return null |
||||
// Composable function usually starts with uppercase first letter. |
||||
if (name.isBlank() || !name[0].isUpperCase()) return null |
||||
|
||||
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 { |
||||
override fun getText(): String = ComposeBundle.message("create.composable.function") + " '$name'" |
||||
} |
||||
} |
||||
|
||||
private val composableAnnotation = "@$COMPOSABLE_ANNOTATION_NAME" |
||||
|
||||
// n.b. Do not cache this CallableInfo anywhere, otherwise it is easy to leak Kotlin descriptors. |
||||
// (see https://github.com/JetBrains/intellij-community/commit/608589428c). |
||||
private fun createNewComposeFunctionInfo(name: String, element: KtCallExpression): CallableInfo? { |
||||
val analysisResult = element.analyzeAndGetResult() |
||||
val fullCallExpression = element.getQualifiedExpressionForSelectorOrThis() |
||||
val expectedType = fullCallExpression.guessTypes(analysisResult.bindingContext, analysisResult.moduleDescriptor).singleOrNull() |
||||
if (expectedType != null && KotlinBuiltIns.isUnit(expectedType)) { |
||||
val parameters = element.getParameterInfos() |
||||
val typeParameters = element.getTypeInfoForTypeArguments() |
||||
val returnType = TypeInfo(expectedType, Variance.OUT_VARIANCE) |
||||
val modifierList = KtPsiFactory(element).createModifierList(composableAnnotation) |
||||
val containers = element.getQualifiedExpressionForSelectorOrThis().getExtractionContainers() |
||||
return FunctionInfo(name, TypeInfo.Empty, returnType, containers, parameters, typeParameters, modifierList = modifierList) |
||||
} |
||||
return null |
||||
} |
||||
} |
@ -0,0 +1,81 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.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.isComposableAnnotation |
||||
import com.android.tools.compose.fqNameMatches |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.codeInsight.intention.IntentionAction |
||||
import com.intellij.openapi.editor.Editor |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.psi.PsiFile |
||||
import com.intellij.psi.util.parentOfType |
||||
import org.jetbrains.kotlin.idea.core.ShortenReferences |
||||
import org.jetbrains.kotlin.psi.KtAnnotationEntry |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtFunction |
||||
import org.jetbrains.kotlin.psi.KtPsiFactory |
||||
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace |
||||
import org.jetbrains.kotlin.utils.addToStdlib.safeAs |
||||
|
||||
/** |
||||
* Adds a @Preview annotation when a full @Composable is selected or cursor at @Composable annotation. |
||||
*/ |
||||
class ComposeCreatePreviewAction : IntentionAction { |
||||
override fun startInWriteAction() = true |
||||
|
||||
override fun getText() = ComposeBundle.message("create.preview") |
||||
|
||||
override fun getFamilyName() = ComposeBundle.message("create.preview") |
||||
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { |
||||
return when { |
||||
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false |
||||
file == null || editor == null -> false |
||||
!file.isWritable || file !is KtFile -> false |
||||
else -> getComposableAnnotationEntry(editor, file) != null |
||||
} |
||||
} |
||||
|
||||
private fun getComposableAnnotationEntry(editor: Editor, file: PsiFile): KtAnnotationEntry? { |
||||
if (editor.selectionModel.hasSelection()) { |
||||
val elementAtCaret = file.findElementAt(editor.selectionModel.selectionStart)?.parentOfType<KtAnnotationEntry>() |
||||
if (elementAtCaret?.isComposableAnnotation() == true) { |
||||
return elementAtCaret |
||||
} |
||||
else { |
||||
// Case when user selected few extra blank lines before @Composable annotation. |
||||
val elementAtCaretAfterSpace = file.findElementAt(editor.selectionModel.selectionStart)?.getNextSiblingIgnoringWhitespace() |
||||
return elementAtCaretAfterSpace.safeAs<KtFunction>()?.annotationEntries?.find { it.fqNameMatches(COMPOSABLE_FQ_NAMES) } |
||||
} |
||||
} |
||||
else { |
||||
return file.findElementAt(editor.caretModel.offset)?.parentOfType<KtAnnotationEntry>()?.takeIf { it.isComposableAnnotation() } |
||||
} |
||||
} |
||||
|
||||
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { |
||||
if (editor == null || file == null) return |
||||
val composableAnnotationEntry = getComposableAnnotationEntry(editor, file) ?: return |
||||
val composableFunction = composableAnnotationEntry.parentOfType<KtFunction>() ?: return |
||||
val previewAnnotationEntry = KtPsiFactory(project).createAnnotationEntry("@${COMPOSE_PREVIEW_ANNOTATION_FQN}") |
||||
|
||||
ShortenReferences.DEFAULT.process(composableFunction.addAnnotationEntry(previewAnnotationEntry)) |
||||
} |
||||
} |
@ -0,0 +1,123 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.intentions |
||||
|
||||
import com.android.tools.compose.ComposeBundle |
||||
import com.android.tools.compose.isClassOrExtendsClass |
||||
import com.android.tools.idea.AndroidTextUtils |
||||
import com.intellij.codeInsight.daemon.QuickFixBundle |
||||
import com.intellij.codeInsight.intention.IntentionAction |
||||
import com.intellij.openapi.editor.Editor |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.psi.PsiDocumentManager |
||||
import com.intellij.psi.PsiFile |
||||
import com.intellij.psi.util.PsiTreeUtil |
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic |
||||
import org.jetbrains.kotlin.diagnostics.Errors |
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze |
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference |
||||
import org.jetbrains.kotlin.idea.debugger.sequence.psi.callName |
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory |
||||
import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor |
||||
import org.jetbrains.kotlin.idea.quickfix.QuickFixes |
||||
import org.jetbrains.kotlin.idea.util.ImportInsertHelper |
||||
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand |
||||
import org.jetbrains.kotlin.js.translate.callTranslator.getReturnType |
||||
import org.jetbrains.kotlin.name.FqName |
||||
import org.jetbrains.kotlin.psi.KtCallExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType |
||||
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall |
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode |
||||
import org.jetbrains.kotlin.utils.addToStdlib.safeAs |
||||
|
||||
/** |
||||
* Registers ComposeDelegateStateImportFixFactory for DELEGATE_SPECIAL_FUNCTION_MISSING error. |
||||
* |
||||
* DELEGATE_SPECIAL_FUNCTION_MISSING is an error when there is no getValue or setValue function for an object after "by" keyword. |
||||
* |
||||
* TODO(b/157543181):Delete code after JB bug is fixed. |
||||
*/ |
||||
class ComposeDelegateStateImportFixContributor : QuickFixContributor { |
||||
override fun registerQuickFixes(quickFixes: QuickFixes) { |
||||
quickFixes.register(Errors.DELEGATE_SPECIAL_FUNCTION_MISSING, ComposeDelegateStateImportFixFactory()) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates an IntentionAction that allow to add [androidx.compose.runtime.getValue] import for [androidx.compose.runtime.MutableState] in delegate position. |
||||
*/ |
||||
private class ComposeDelegateStateImportFixFactory : KotlinSingleIntentionActionFactory() { |
||||
private val stateMethodNames = mapOf( |
||||
"mutableStateOf" to "androidx.compose.runtime.mutableStateOf", |
||||
"state" to "androidx.compose.runtime.state") |
||||
|
||||
/** |
||||
* Returns true, if the given [callExpression] contains a call to `mutableStateOf` or `state`. This allows suggesting the automatic |
||||
* import of `getValue` even when `mutableStateOf` or `state` have not been imported yet. |
||||
*/ |
||||
private fun resolveUndefinedStateCall(callExpression: KtCallExpression): String? = |
||||
stateMethodNames[PsiTreeUtil.findChildOfType(callExpression, KtCallExpression::class.java, true)?.callName()] |
||||
|
||||
override fun createAction(diagnostic: Diagnostic): IntentionAction? { |
||||
val callExpression = diagnostic.psiElement.safeAs<KtCallExpression>() |
||||
?: diagnostic.psiElement.getChildOfType() |
||||
?: return null |
||||
val delegateType = callExpression.getResolvedCall(callExpression.analyze(BodyResolveMode.FULL))?.getReturnType() ?: return null |
||||
return when { |
||||
delegateType.isClassOrExtendsClass("androidx.compose.runtime.State") -> ComposeDelegateStateImportFixAction() |
||||
// Handle the case where the state is embedded within a remember {} call but we can not infer the type. |
||||
delegateType.isClassOrExtendsClass("androidx.compose.runtime.remember.T") && resolveUndefinedStateCall(callExpression) != null -> |
||||
ComposeDelegateStateImportFixAction(listOfNotNull(resolveUndefinedStateCall(callExpression))) |
||||
else -> null |
||||
} |
||||
} |
||||
} |
||||
|
||||
private class ComposeDelegateStateImportFixAction(additionalImports: List<String> = emptyList()) : IntentionAction { |
||||
/** |
||||
* List of all the imports that this fix will add when invoked. |
||||
*/ |
||||
private val importList = additionalImports + "androidx.compose.runtime.getValue" |
||||
|
||||
/** |
||||
* List of the short names for the [importList]. |
||||
*/ |
||||
private val importShortNames = importList.map { it.substringAfterLast(".") }.sorted() |
||||
|
||||
override fun startInWriteAction() = false |
||||
|
||||
override fun getFamilyName() = text |
||||
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?) = true |
||||
|
||||
override fun getText() = ComposeBundle.message("import.compose.state", |
||||
AndroidTextUtils.generateCommaSeparatedList(importShortNames, "and")) |
||||
|
||||
// Inspired by KotlinAddImportAction#addImport |
||||
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { |
||||
if (editor == null || file !is KtFile) return |
||||
val psiDocumentManager = PsiDocumentManager.getInstance(project) |
||||
psiDocumentManager.commitAllDocuments() |
||||
|
||||
project.executeWriteCommand(QuickFixBundle.message("add.import")) { |
||||
importList.forEach { importFqName -> |
||||
val descriptor = file.resolveImportReference(FqName(importFqName)).firstOrNull() ?: return@executeWriteCommand |
||||
ImportInsertHelper.getInstance(project).importDescriptor(file, descriptor) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,215 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.intentions |
||||
|
||||
import com.android.tools.compose.ComposeBundle |
||||
import com.android.tools.compose.isInsideComposableCode |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.codeInsight.intention.HighPriorityAction |
||||
import com.intellij.codeInsight.intention.IntentionAction |
||||
import com.intellij.codeInsight.intention.impl.IntentionActionGroup |
||||
import com.intellij.codeInsight.template.impl.InvokeTemplateAction |
||||
import com.intellij.codeInsight.template.impl.TemplateImpl |
||||
import com.intellij.codeInsight.template.impl.TemplateSettings |
||||
import com.intellij.openapi.editor.Editor |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.openapi.ui.popup.ListPopup |
||||
import com.intellij.openapi.ui.popup.PopupStep |
||||
import com.intellij.openapi.ui.popup.util.BaseListPopupStep |
||||
import com.intellij.openapi.util.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.core.util.CodeInsightUtils |
||||
import org.jetbrains.kotlin.idea.util.ElementKind |
||||
import org.jetbrains.kotlin.idea.util.findElements |
||||
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]. |
||||
* |
||||
* After this action is selected, a new pop-up appears, in which user can choose between actions listed above. |
||||
* |
||||
* @see intentionDescriptions/ComposeSurroundWithWidgetActionGroup/before.kt.template |
||||
* intentionDescriptions/ComposeSurroundWithWidgetActionGroup/after.kt.template |
||||
*/ |
||||
class ComposeSurroundWithWidgetActionGroup : |
||||
IntentionActionGroup<ComposeSurroundWithWidgetAction>( |
||||
listOf(ComposeSurroundWithBoxAction(), ComposeSurroundWithRowAction(), ComposeSurroundWithColumnAction()) |
||||
) { |
||||
override fun getGroupText(actions: List<ComposeSurroundWithWidgetAction>) = |
||||
ComposeBundle.message("surround.with.widget.intention.text") |
||||
|
||||
override fun chooseAction(project: Project, |
||||
editor: Editor, |
||||
file: PsiFile, |
||||
actions: List<ComposeSurroundWithWidgetAction>, |
||||
invokeAction: (ComposeSurroundWithWidgetAction) -> Unit) { |
||||
createPopup(project, actions, invokeAction).showInBestPositionFor(editor) |
||||
} |
||||
|
||||
private fun createPopup(project: Project, |
||||
actions: List<ComposeSurroundWithWidgetAction>, |
||||
invokeAction: (ComposeSurroundWithWidgetAction) -> Unit): ListPopup { |
||||
|
||||
val step = object : BaseListPopupStep<ComposeSurroundWithWidgetAction>(null, actions) { |
||||
override fun getTextFor(action: ComposeSurroundWithWidgetAction) = action.text |
||||
|
||||
override fun onChosen(selectedValue: ComposeSurroundWithWidgetAction, finalChoice: Boolean): PopupStep<*>? { |
||||
invokeAction(selectedValue) |
||||
return FINAL_CHOICE |
||||
} |
||||
} |
||||
|
||||
return ListPopupImpl(project, step) |
||||
} |
||||
|
||||
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 = findElements(file, |
||||
minOf(editor.selectionModel.selectionStart, startSelectionOffset), |
||||
maxOf(editor.selectionModel.selectionEnd, endSelectionOffset), |
||||
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. |
||||
* |
||||
* @see intentionDescriptions/ComposeSurroundWithWidgetActionGroup/before.kt.template |
||||
* intentionDescriptions/ComposeSurroundWithWidgetActionGroup/after.kt.template |
||||
*/ |
||||
abstract class ComposeSurroundWithWidgetAction : IntentionAction, HighPriorityAction { |
||||
override fun getFamilyName() = "Compose Surround With Action" |
||||
|
||||
override fun startInWriteAction(): Boolean = true |
||||
|
||||
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() |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Surrounds selected statements inside a @Composable function with Box widget. |
||||
*/ |
||||
class ComposeSurroundWithBoxAction : ComposeSurroundWithWidgetAction() { |
||||
override fun getText(): String = ComposeBundle.message("surround.with.box.intention.text") |
||||
|
||||
override fun getTemplate(): TemplateImpl? { |
||||
return TemplateSettings.getInstance().getTemplate("W", "AndroidCompose") |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Surrounds selected statements inside a @Composable function with Row widget. |
||||
*/ |
||||
class ComposeSurroundWithRowAction : ComposeSurroundWithWidgetAction() { |
||||
override fun getText(): String = ComposeBundle.message("surround.with.row.intention.text") |
||||
|
||||
override fun getTemplate(): TemplateImpl? { |
||||
return TemplateSettings.getInstance().getTemplate("WR", "AndroidCompose") |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Surrounds selected statements inside a @Composable function with Column widget. |
||||
*/ |
||||
class ComposeSurroundWithColumnAction : ComposeSurroundWithWidgetAction() { |
||||
override fun getText(): String = ComposeBundle.message("surround.with.column.intention.text") |
||||
|
||||
override fun getTemplate(): TemplateImpl? { |
||||
return TemplateSettings.getInstance().getTemplate("WC", "AndroidCompose") |
||||
} |
||||
} |
||||
|
@ -0,0 +1,74 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.intentions |
||||
|
||||
import com.android.tools.compose.ComposeBundle |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.codeInsight.intention.IntentionAction |
||||
import com.intellij.openapi.editor.Editor |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.psi.PsiFile |
||||
import com.intellij.psi.util.PsiTreeUtil |
||||
import com.intellij.psi.util.parentOfType |
||||
import org.jetbrains.kotlin.nj2k.postProcessing.resolve |
||||
import org.jetbrains.kotlin.psi.KtBlockExpression |
||||
import org.jetbrains.kotlin.psi.KtCallExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.KtNameReferenceExpression |
||||
import org.jetbrains.kotlin.psi.KtNamedFunction |
||||
import org.jetbrains.kotlin.utils.addToStdlib.safeAs |
||||
|
||||
|
||||
/** |
||||
* Removes wrappers like Row, Column and Box around widgets. |
||||
*/ |
||||
class ComposeUnwrapAction : IntentionAction { |
||||
private val WRAPPERS_FQ_NAMES = setOf( |
||||
"androidx.compose.foundation.layout.Box", |
||||
"androidx.compose.foundation.layout.Row", |
||||
"androidx.compose.foundation.layout.Column" |
||||
) |
||||
|
||||
override fun startInWriteAction() = true |
||||
|
||||
override fun getText() = ComposeBundle.message("remove.wrapper") |
||||
|
||||
override fun getFamilyName() = ComposeBundle.message("remove.wrapper") |
||||
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { |
||||
return when { |
||||
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false |
||||
file == null || editor == null -> false |
||||
!file.isWritable || file !is KtFile -> false |
||||
else -> isCaretAtWrapper(editor, file) |
||||
} |
||||
} |
||||
|
||||
private fun isCaretAtWrapper(editor: Editor, file: PsiFile): Boolean { |
||||
val elementAtCaret = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtNameReferenceExpression>() ?: return false |
||||
val name = elementAtCaret.resolve().safeAs<KtNamedFunction>()?.fqName?.asString() ?: return false |
||||
return WRAPPERS_FQ_NAMES.contains(name) |
||||
} |
||||
|
||||
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { |
||||
if (file == null || editor == null) return |
||||
val wrapper = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtNameReferenceExpression>() ?: return |
||||
val outerBlock = wrapper.parent.safeAs<KtCallExpression>() ?: return |
||||
val lambdaBlock = PsiTreeUtil.findChildOfType(outerBlock, KtBlockExpression::class.java, true) ?: return |
||||
outerBlock.replace(lambdaBlock) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,67 @@
|
||||
/* |
||||
* Copyright (C) 2020 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.intentions |
||||
|
||||
import com.android.tools.compose.ComposeBundle |
||||
import com.android.tools.compose.formatting.wrapModifierChain |
||||
import com.android.tools.compose.isModifierChainLongerThanTwo |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
import com.intellij.application.options.CodeStyle |
||||
import com.intellij.codeInsight.intention.IntentionAction |
||||
import com.intellij.openapi.editor.Editor |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.psi.PsiFile |
||||
import com.intellij.psi.util.parentOfType |
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.psi.psiUtil.getLastParentOfTypeInRowWithSelf |
||||
|
||||
/** |
||||
* Wraps Modifier(androidx.compose.ui.Modifier) chain that is two modifiers or longer, in one modifier per line. |
||||
*/ |
||||
class ComposeWrapModifiersAction : IntentionAction { |
||||
private companion object { |
||||
val NO_NEW_LINE_BEFORE_DOT = Regex("[^.\\n\\s]\\.") |
||||
} |
||||
|
||||
override fun startInWriteAction() = true |
||||
|
||||
override fun getText() = ComposeBundle.message("wrap.modifiers") |
||||
|
||||
override fun getFamilyName() = ComposeBundle.message("wrap.modifiers") |
||||
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { |
||||
return when { |
||||
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false |
||||
file == null || editor == null -> false |
||||
!file.isWritable || file !is KtFile -> false |
||||
else -> { |
||||
val elementAtCaret = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtDotQualifiedExpression>() |
||||
val topLevelExpression = elementAtCaret?.getLastParentOfTypeInRowWithSelf<KtDotQualifiedExpression>() ?: return false |
||||
isModifierChainLongerThanTwo(topLevelExpression) && |
||||
NO_NEW_LINE_BEFORE_DOT.containsMatchIn(topLevelExpression.text) |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { |
||||
if (file == null || editor == null) return |
||||
val elementAtCaret = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtDotQualifiedExpression>() |
||||
val topLevelExpression = elementAtCaret?.getLastParentOfTypeInRowWithSelf<KtDotQualifiedExpression>() ?: return |
||||
wrapModifierChain(topLevelExpression, CodeStyle.getSettings(file)) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,32 @@
|
||||
/* |
||||
* 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.settings; |
||||
|
||||
import com.intellij.psi.codeStyle.CodeStyleSettings; |
||||
import com.intellij.psi.codeStyle.CustomCodeStyleSettings; |
||||
|
||||
// Don't convert to Kotlin due to the serialization implementation for Settings.
|
||||
public class ComposeCustomCodeStyleSettings extends CustomCodeStyleSettings { |
||||
public boolean USE_CUSTOM_FORMATTING_FOR_MODIFIERS = true; |
||||
|
||||
protected ComposeCustomCodeStyleSettings(CodeStyleSettings container) { |
||||
super("ComposeCustomCodeStyleSettings", container); |
||||
} |
||||
|
||||
public static ComposeCustomCodeStyleSettings getInstance(CodeStyleSettings settings) { |
||||
return settings.getCustomSettings(ComposeCustomCodeStyleSettings.class); |
||||
} |
||||
} |
@ -0,0 +1,79 @@
|
||||
/* |
||||
* 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.settings |
||||
|
||||
import com.android.tools.compose.ComposeBundle |
||||
import com.intellij.openapi.ui.DialogPanel |
||||
import com.intellij.psi.codeStyle.CodeStyleConfigurable |
||||
import com.intellij.psi.codeStyle.CodeStyleSettings |
||||
import com.intellij.psi.codeStyle.CodeStyleSettingsProvider |
||||
import com.intellij.psi.codeStyle.CustomCodeStyleSettings |
||||
import com.intellij.ui.layout.panel |
||||
import org.jetbrains.kotlin.idea.KotlinLanguage |
||||
import javax.swing.JCheckBox |
||||
|
||||
|
||||
/** |
||||
* Allows to turn on and off [ComposePostFormatProcessor] in Code Style settings. |
||||
*/ |
||||
class ComposeFormattingCodeStyleSettingsProvider : CodeStyleSettingsProvider() { |
||||
|
||||
override fun hasSettingsPage() = false |
||||
|
||||
override fun createCustomSettings(settings: CodeStyleSettings): CustomCodeStyleSettings { |
||||
return ComposeCustomCodeStyleSettings(settings) |
||||
} |
||||
|
||||
override fun getConfigurableDisplayName(): String = ComposeBundle.message("compose") |
||||
override fun getLanguage() = KotlinLanguage.INSTANCE |
||||
|
||||
override fun createConfigurable(originalSettings: CodeStyleSettings, modelSettings: CodeStyleSettings): CodeStyleConfigurable { |
||||
return object : CodeStyleConfigurable { |
||||
|
||||
private lateinit var checkBox: JCheckBox |
||||
|
||||
override fun createComponent(): DialogPanel { |
||||
return panel { |
||||
row { |
||||
titledRow("Compose formatting") { |
||||
row { |
||||
checkBox = checkBox( |
||||
ComposeBundle.message("compose.enable.formatting.for.modifiers"), |
||||
ComposeCustomCodeStyleSettings.getInstance(originalSettings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS).component |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun isModified() = ComposeCustomCodeStyleSettings.getInstance( |
||||
originalSettings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS != checkBox.isSelected |
||||
|
||||
override fun apply(settings: CodeStyleSettings) { |
||||
ComposeCustomCodeStyleSettings.getInstance(settings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS = checkBox.isSelected |
||||
} |
||||
|
||||
override fun apply() = apply(originalSettings) |
||||
|
||||
override fun reset(settings: CodeStyleSettings) { |
||||
checkBox.isSelected = ComposeCustomCodeStyleSettings.getInstance(settings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS |
||||
} |
||||
|
||||
override fun getDisplayName() = ComposeBundle.message("compose") |
||||
} |
||||
|
||||
} |
||||
} |
@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
package com.android.tools.idea |
||||
|
||||
object AndroidTextUtils { |
||||
fun generateCommaSeparatedList(items: Collection<String?>, lastSeparator: String): String { |
||||
val n = items.size |
||||
if (n == 0) { |
||||
return "" |
||||
} |
||||
var i = 0 |
||||
val result = StringBuilder() |
||||
for (word in items) { |
||||
result.append(word) |
||||
if (i < n - 2) { |
||||
result.append(", ") |
||||
} else if (i == n - 2) { |
||||
result.append(" ").append(lastSeparator).append(" ") |
||||
} |
||||
i++ |
||||
} |
||||
return result.toString() |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
package com.android.tools.idea.flags |
||||
|
||||
object StudioFlags { |
||||
val COMPOSE_EDITOR_SUPPORT = Flag(true) |
||||
val COMPOSE_AUTO_DOCUMENTATION = Flag(true) |
||||
val COMPOSE_FUNCTION_EXTRACTION = Flag(true) |
||||
val COMPOSE_DEPLOY_LIVE_EDIT_USE_EMBEDDED_COMPILER = Flag(true) |
||||
val COMPOSE_COMPLETION_INSERT_HANDLER = Flag(true) |
||||
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) |
||||
val SAMPLES_SUPPORT_ENABLED = Flag(true) |
||||
} |
||||
|
||||
class Flag<T>(val value: T) { |
||||
fun get(): T = value |
||||
} |
@ -0,0 +1,28 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
package com.android.tools.modules |
||||
|
||||
import com.android.tools.compose.* |
||||
import com.intellij.openapi.module.* |
||||
import com.intellij.openapi.roots.* |
||||
import com.intellij.psi.* |
||||
import com.intellij.psi.search.* |
||||
import com.intellij.psi.util.* |
||||
import org.jetbrains.kotlin.idea.base.util.module |
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex |
||||
|
||||
fun PsiElement.inComposeModule() = module?.isComposeModule() ?: false |
||||
fun Module.isComposeModule(): Boolean { |
||||
return CachedValuesManager.getManager(project).getCachedValue(this) { |
||||
val scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(this) |
||||
val hasComposable = COMPOSABLE_FQ_NAMES.any { |
||||
KotlinFullClassNameIndex.get(it, project, scope).any() |
||||
} |
||||
val rootModificationTracker = ProjectRootModificationTracker.getInstance(project) |
||||
CachedValueProvider.Result.create(hasComposable, rootModificationTracker) |
||||
} |
||||
} |
||||
|
@ -0,0 +1,25 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
package icons |
||||
|
||||
import com.intellij.ui.* |
||||
import javax.swing.* |
||||
|
||||
object StudioIcons { |
||||
private fun load(path: String, cacheKey: Int, flags: Int): Icon { |
||||
return IconManager.getInstance().loadRasterizedIcon( |
||||
path, |
||||
StudioIcons::class.java.classLoader, cacheKey, flags |
||||
) |
||||
} |
||||
|
||||
class Compose { |
||||
object Editor { |
||||
/** 16x16 */ |
||||
val COMPOSABLE_FUNCTION = load("icons/compose/composable-function.svg", -238070477, 2) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
<!-- |
||||
Copyright 2019 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. |
||||
--> |
||||
|
||||
<list> |
||||
<option name="ComposableCallTextAttributes"> |
||||
<value> |
||||
<option name="FOREGROUND" value="009900"/> |
||||
</value> |
||||
</option> |
||||
</list> |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,7 @@
|
||||
@Preview |
||||
@Composable |
||||
fun NewsStory() { |
||||
Text("A day in Shark Fin Cove") |
||||
Text("Davenport, California") |
||||
Text("December 2018") |
||||
} |
@ -0,0 +1,6 @@
|
||||
@Composable |
||||
fun NewsStory() { |
||||
Text("A day in Shark Fin Cove") |
||||
Text("Davenport, California") |
||||
Text("December 2018") |
||||
} |
@ -0,0 +1,22 @@
|
||||
<!-- |
||||
~ Copyright (C) 2020 The Android Open Source Project |
||||
~ |
||||
~ Licensed under the Apache License, Version 2.0 (the "License"); |
||||
~ you may not use this file except in compliance with the License. |
||||
~ You may obtain a copy of the License at |
||||
~ |
||||
~ http://www.apache.org/licenses/LICENSE-2.0 |
||||
~ |
||||
~ Unless required by applicable law or agreed to in writing, software |
||||
~ distributed under the License is distributed on an "AS IS" BASIS, |
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
~ See the License for the specific language governing permissions and |
||||
~ limitations under the License. |
||||
--> |
||||
<html> |
||||
<body> |
||||
<p> |
||||
Add @Preview annotation to @Composable function. |
||||
</p> |
||||
</body> |
||||
</html> |
@ -0,0 +1,8 @@
|
||||
@Composable |
||||
fun NewsStory() { |
||||
Container { |
||||
Text("A day in Shark Fin Cove") |
||||
Text("Davenport, California") |
||||
Text("December 2018") |
||||
} |
||||
} |
@ -0,0 +1,6 @@
|
||||
@Composable |
||||
fun NewsStory() { |
||||
Text("A day in Shark Fin Cove") |
||||
Text("Davenport, California") |
||||
Text("December 2018") |
||||
} |
@ -0,0 +1,5 @@
|
||||
<html> |
||||
<body> |
||||
This intention surrounds selected compose code with a widget. |
||||
</body> |
||||
</html> |
@ -0,0 +1,6 @@
|
||||
@Composable |
||||
fun NewsStory() { |
||||
Text("A day in Shark Fin Cove") |
||||
Text("Davenport, California") |
||||
Text("December 2018") |
||||
} |
@ -0,0 +1,8 @@
|
||||
@Composable |
||||
fun NewsStory() { |
||||
Row { |
||||
Text("A day in Shark Fin Cove") |
||||
Text("Davenport, California") |
||||
Text("December 2018") |
||||
} |
||||
} |
@ -0,0 +1,20 @@
|
||||
<!-- |
||||
~ Copyright (C) 2020 The Android Open Source Project |
||||
~ |
||||
~ Licensed under the Apache License, Version 2.0 (the "License"); |
||||
~ you may not use this file except in compliance with the License. |
||||
~ You may obtain a copy of the License at |
||||
~ |
||||
~ http://www.apache.org/licenses/LICENSE-2.0 |
||||
~ |
||||
~ Unless required by applicable law or agreed to in writing, software |
||||
~ distributed under the License is distributed on an "AS IS" BASIS, |
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
~ See the License for the specific language governing permissions and |
||||
~ limitations under the License. |
||||
--> |
||||
<html> |
||||
<body> |
||||
This intention removes Composable wrapper like Row or Column |
||||
</body> |
||||
</html> |
@ -0,0 +1,7 @@
|
||||
@Composable |
||||
fun NewsStory() { |
||||
Modifier |
||||
.padding(padding) |
||||
.clickable(onClick = onClick) |
||||
.fillMaxWidth() |
||||
} |
@ -0,0 +1,4 @@
|
||||
@Composable |
||||
fun NewsStory() { |
||||
Modifier.padding(padding).clickable(onClick = onClick).fillMaxWidth() |
||||
} |
@ -0,0 +1,20 @@
|
||||
<!-- |
||||
~ Copyright (C) 2020 The Android Open Source Project |
||||
~ |
||||
~ Licensed under the Apache License, Version 2.0 (the "License"); |
||||
~ you may not use this file except in compliance with the License. |
||||
~ You may obtain a copy of the License at |
||||
~ |
||||
~ http://www.apache.org/licenses/LICENSE-2.0 |
||||
~ |
||||
~ Unless required by applicable law or agreed to in writing, software |
||||
~ distributed under the License is distributed on an "AS IS" BASIS, |
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
~ See the License for the specific language governing permissions and |
||||
~ limitations under the License. |
||||
--> |
||||
<html> |
||||
<body> |
||||
Wraps Modifier chain that is two modifiers or longer, in one modifier per line. |
||||
</body> |
||||
</html> |
@ -0,0 +1,29 @@
|
||||
create.preview=Create Preview |
||||
surround.with.widget.intention.text=Surround with widget |
||||
surround.with.box.intention.text=Surround with Container |
||||
surround.with.row.intention.text=Surround with Row |
||||
surround.with.column.intention.text=Surround with Column |
||||
import.compose.state=Import {0} |
||||
compose.enable.insertion.handler=Enable enhanced auto-completion when using Jetpack Compose |
||||
compose.enable.formatting.for.modifiers=Enable Compose formatting for Modifiers |
||||
create.composable.function=Create @Composable function |
||||
remove.wrapper=Remove wrapper |
||||
wrap.modifiers=Wrap modifiers |
||||
compose=Compose |
||||
rename.file=Rename File |
||||
file.name=File name |
||||
rename.files.with.following.names=Rename files with the following names to: |
||||
errors.composable_invocation=@Composable invocations can only happen from the context of a @Composable function |
||||
errors.composable_expected=Functions which invoke @Composable functions must be marked with the @Composable annotation |
||||
errors.captured_composable_invocation=Composable calls are not allowed inside the {0} parameter of {1} |
||||
errors.composable_property_backing_field=Composable properties are not able to have backing fields |
||||
errors.composable_var=Composable properties are not able to have backing fields |
||||
errors.composable_suspend_fun=Suspend functions cannot be made Composable |
||||
errors.illegal_try_catch_around_composable=Try catch is not supported around composable function invocations. |
||||
errors.composable_function_reference=Function References of @Composable functions are not currently supported |
||||
errors.conflicting_overloads=Conflicting overloads: {0} |
||||
errors.type_mismatch=Type inference failed. Expected type mismatch: inferred type is {1} but {0} was expected |
||||
errors.missing_disallow_composable_calls_annotation=Parameter {0} cannot be inlined inside of lambda argument {1} of {2} without also being annotated with @DisallowComposableCalls |
||||
errors.nonreadonly_call_in_readonly_composable=Composables marked with @ReadOnlyComposable can only call other @ReadOnlyComposable composables |
||||
errors.composable_fun_main=Composable main functions are not currently supported |
||||
filter.ignore.compose.runtime.classes=Do not step into Compose internal classes |
Loading…
Reference in new issue