Ilya Ryzhenkov
2 years ago
70 changed files with 5170 additions and 24 deletions
@ -0,0 +1,3 @@
|
||||
compiler-hosted-1.1.0-SNAPSHOT.jar |
||||
-This file comes form https://androidx.dev/snapshots/builds/7722085/artifacts |
||||
-It is meant to be a temporary solution until we figure out how to bundle compose-compiler in Studio properly. |
Binary file not shown.
@ -0,0 +1,91 @@
|
||||
/* |
||||
* 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.lang.annotation.AnnotationHolder |
||||
import com.intellij.lang.annotation.Annotator |
||||
import com.intellij.lang.annotation.HighlightSeverity |
||||
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors |
||||
import com.intellij.openapi.editor.colors.TextAttributesKey |
||||
import com.intellij.openapi.progress.ProcessCanceledException |
||||
import com.intellij.openapi.roots.ProjectFileIndex |
||||
import com.intellij.openapi.util.Key |
||||
import com.intellij.psi.PsiElement |
||||
import org.jetbrains.kotlin.analyzer.AnalysisResult |
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks |
||||
import org.jetbrains.kotlin.psi.KtCallExpression |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
import org.jetbrains.kotlin.resolve.BindingContext |
||||
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall |
||||
|
||||
// Used to apply styles for calls to @Composable functions. |
||||
class ComposableAnnotator : Annotator { |
||||
companion object TextAttributeRegistry { |
||||
val COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY: TextAttributesKey |
||||
val COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME = "ComposableCallTextAttributes" |
||||
private val ANALYSIS_RESULT_KEY = Key<AnalysisResult>( |
||||
"ComposableAnnotator.DidAnnotateKey" |
||||
) |
||||
private val CAN_CONTAIN_COMPOSABLE_KEY = Key<Boolean>( |
||||
"ComposableAnnotator.CanContainComposable" |
||||
) |
||||
|
||||
init { |
||||
COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY = TextAttributesKey.createTextAttributesKey( |
||||
COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME, |
||||
DefaultLanguageHighlighterColors.FUNCTION_CALL) |
||||
} |
||||
} |
||||
|
||||
override fun annotate(element: PsiElement, holder: AnnotationHolder) { |
||||
if (element !is KtCallExpression) return |
||||
|
||||
// AnnotationHolder.currentAnnotationSession applies to a single file. |
||||
var canContainComposable = holder.currentAnnotationSession.getUserData(CAN_CONTAIN_COMPOSABLE_KEY) |
||||
if (canContainComposable == null) { |
||||
// isComposeEnabled doesn't work for library sources, we check all kt library sources files. File check only once on opening. |
||||
canContainComposable = isComposeEnabled(element) || |
||||
(element.containingFile.virtualFile != null && |
||||
ProjectFileIndex.getInstance(element.project).isInLibrarySource(element.containingFile.virtualFile)) |
||||
holder.currentAnnotationSession.putUserData(CAN_CONTAIN_COMPOSABLE_KEY, canContainComposable) |
||||
} |
||||
|
||||
if (!canContainComposable) return |
||||
|
||||
// AnnotationHolder.currentAnnotationSession applies to a single file. |
||||
var analysisResult = holder.currentAnnotationSession.getUserData( |
||||
ANALYSIS_RESULT_KEY |
||||
) |
||||
if (analysisResult == null) { |
||||
val ktFile = element.containingFile as? KtFile ?: return |
||||
analysisResult = ktFile.analyzeWithAllCompilerChecks() |
||||
holder.currentAnnotationSession.putUserData( |
||||
ANALYSIS_RESULT_KEY, analysisResult |
||||
) |
||||
} |
||||
if (analysisResult.isError()) { |
||||
throw ProcessCanceledException(analysisResult.error) |
||||
} |
||||
if (!shouldStyleCall(analysisResult.bindingContext, element)) return |
||||
val elementToStyle = element.calleeExpression ?: return |
||||
holder.newSilentAnnotation(HighlightSeverity.INFORMATION).range(elementToStyle).textAttributes(COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY).create() |
||||
} |
||||
|
||||
private fun shouldStyleCall(bindingContext: BindingContext, element: KtCallExpression): Boolean { |
||||
return element.getResolvedCall(bindingContext)?.isComposableInvocation() == true |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
|
||||
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,119 @@
|
||||
/* |
||||
* 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. |
||||
*/ |
||||
package com.android.tools.compose |
||||
|
||||
import com.android.tools.idea.flags.StudioFlags.COMPOSE_AUTO_DOCUMENTATION |
||||
import com.android.tools.idea.flags.StudioFlags.COMPOSE_EDITOR_SUPPORT |
||||
import com.android.tools.modules.* |
||||
import com.intellij.codeInsight.CodeInsightSettings |
||||
import com.intellij.codeInsight.completion.CompletionService |
||||
import com.intellij.codeInsight.documentation.DocumentationManager |
||||
import com.intellij.codeInsight.lookup.Lookup |
||||
import com.intellij.codeInsight.lookup.LookupEvent |
||||
import com.intellij.codeInsight.lookup.LookupListener |
||||
import com.intellij.codeInsight.lookup.LookupManager |
||||
import com.intellij.codeInsight.lookup.impl.LookupImpl |
||||
import com.intellij.openapi.application.ApplicationManager |
||||
import com.intellij.openapi.fileEditor.FileDocumentManager |
||||
import com.intellij.openapi.module.ModuleUtilCore |
||||
import com.intellij.openapi.project.IndexNotReadyException |
||||
import com.intellij.openapi.project.Project |
||||
import com.intellij.openapi.startup.StartupActivity |
||||
import com.intellij.util.Alarm |
||||
import java.beans.PropertyChangeListener |
||||
|
||||
/** |
||||
* Automatically shows quick documentation for Compose functions during code completion |
||||
*/ |
||||
class ComposeAutoDocumentation(private val project: Project) { |
||||
private var documentationOpenedByCompose = false |
||||
|
||||
private val lookupListener = PropertyChangeListener { evt -> |
||||
if (COMPOSE_EDITOR_SUPPORT.get() && |
||||
COMPOSE_AUTO_DOCUMENTATION.get () && |
||||
LookupManager.PROP_ACTIVE_LOOKUP == evt.propertyName && |
||||
evt.newValue is Lookup) { |
||||
val lookup = evt.newValue as Lookup |
||||
|
||||
val moduleSystem = FileDocumentManager.getInstance().getFile(lookup.editor.document) |
||||
?.let { ModuleUtilCore.findModuleForFile(it, lookup.project) } |
||||
|
||||
if (moduleSystem?.isComposeModule() == true) { |
||||
lookup.addLookupListener(object : LookupListener { |
||||
override fun currentItemChanged(event: LookupEvent) { |
||||
showJavaDoc(lookup) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun onProjectOpened() { |
||||
if (COMPOSE_EDITOR_SUPPORT.get() && COMPOSE_AUTO_DOCUMENTATION.get() && !ApplicationManager.getApplication().isUnitTestMode) { |
||||
LookupManager.getInstance(project).addPropertyChangeListener(lookupListener) |
||||
} |
||||
} |
||||
|
||||
class MyStartupActivity : StartupActivity { |
||||
override fun runActivity(project: Project) = getInstance(project).onProjectOpened() |
||||
} |
||||
|
||||
companion object { |
||||
@JvmStatic |
||||
fun getInstance(project: Project): ComposeAutoDocumentation = project.getService(ComposeAutoDocumentation::class.java) |
||||
} |
||||
|
||||
private fun showJavaDoc(lookup: Lookup) { |
||||
if (LookupManager.getInstance(project).activeLookup !== lookup) { |
||||
return |
||||
} |
||||
|
||||
// If we open doc when lookup is not visible, doc will have wrong parent window (editor window instead of lookup). |
||||
if ((lookup as? LookupImpl)?.isVisible != true) { |
||||
Alarm().addRequest({ showJavaDoc(lookup) }, CodeInsightSettings.getInstance().JAVADOC_INFO_DELAY) |
||||
return |
||||
} |
||||
|
||||
val psiElement = lookup.currentItem?.psiElement ?: return |
||||
val docManager = DocumentationManager.getInstance(project) |
||||
if (!psiElement.isComposableFunction()) { |
||||
// Close documentation for not composable function if it was opened by [AndroidComposeAutoDocumentation]. |
||||
// Case docManager.docInfoHint?.isFocused == true: user clicked on doc window and after that clicked on lookup and selected another |
||||
// element. Due to bug docManager.docInfoHint?.isFocused == true even after clicking on lookup element, in that case if we close |
||||
// docManager.docInfoHint, lookup will be closed as well. |
||||
if (documentationOpenedByCompose && docManager.docInfoHint?.isFocused == false) { |
||||
docManager.docInfoHint?.cancel() |
||||
documentationOpenedByCompose = false |
||||
} |
||||
return |
||||
} |
||||
|
||||
// It's composable function and documentation already opened |
||||
if (docManager.docInfoHint != null) return // will auto-update |
||||
|
||||
val currentItem = lookup.currentItem |
||||
if (currentItem != null && currentItem.isValid && CompletionService.getCompletionService().currentCompletion != null) { |
||||
try { |
||||
docManager.showJavaDocInfo(lookup.editor, lookup.psiFile, false) { |
||||
documentationOpenedByCompose = false |
||||
} |
||||
documentationOpenedByCompose = true |
||||
} |
||||
catch (ignored: IndexNotReadyException) { |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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,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 |
||||
|
||||
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.* |
||||
|
||||
/** |
||||
* 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"), |
||||
settings.state::isComposeInsertHandlerEnabled) |
||||
|
||||
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[ComposableAnnotator.COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME] = |
||||
ComposableAnnotator.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", |
||||
ComposableAnnotator.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,73 @@
|
||||
/* |
||||
* 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.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 (!isComposeEnabled(diagnostic.psiElement)) 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,63 @@
|
||||
/* |
||||
* 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.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) || !isComposeEnabled(root)) { |
||||
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" |
||||
fun fqNameFor(cname: String) = FqName("$root.$cname") |
||||
val Composable = fqNameFor("Composable") |
||||
} |
||||
|
||||
val Composable = fqNameFor("Composable") |
||||
val DisallowComposableCalls = fqNameFor("DisallowComposableCalls") |
||||
val ReadOnlyComposable = fqNameFor("ReadOnlyComposable") |
||||
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,97 @@
|
||||
/* |
||||
* 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.name.* |
||||
|
||||
private const val UI_PACKAGE = "androidx.ui" |
||||
private const val COMPOSE_PACKAGE = "androidx.compose.ui" |
||||
|
||||
/** Preview element name */ |
||||
const val COMPOSE_PREVIEW_ANNOTATION_NAME = "Preview" |
||||
|
||||
const val COMPOSABLE_ANNOTATION_NAME = "Composable" |
||||
|
||||
const val COMPOSE_ALIGNMENT = "${COMPOSE_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" |
||||
|
||||
val COMPOSABLE_FQ_NAMES = setOf( |
||||
"androidx.compose.$COMPOSABLE_ANNOTATION_NAME", |
||||
"androidx.compose.runtime.$COMPOSABLE_ANNOTATION_NAME" |
||||
) |
||||
|
||||
/** |
||||
* Represents the Jetpack Compose library package name. The compose libraries will move from |
||||
* `androidx.ui` to `androidx.compose` and this enum encapsulates the naming for the uses in tools. |
||||
*/ |
||||
enum class ComposeLibraryNamespace( |
||||
val packageName: String, |
||||
/** Package containing the API preview definitions. Elements here will be referenced by the user. */ |
||||
val apiPreviewPackage: String = "$packageName.tooling.preview", |
||||
/** Package containing the preview implementation. Elements in this package are for use of tooling only. */ |
||||
val implementationPreviewPackage: String = apiPreviewPackage |
||||
) { |
||||
ANDROIDX_COMPOSE(COMPOSE_PACKAGE), |
||||
|
||||
/** New namespace where the API and implementation are split in two separate packages */ |
||||
ANDROIDX_COMPOSE_WITH_API(COMPOSE_PACKAGE, implementationPreviewPackage = "${COMPOSE_PACKAGE}.tooling"); |
||||
|
||||
/** |
||||
* Name of the `ComposeViewAdapter` object that is used by the preview surface to hold |
||||
* the previewed `@Composable`s. |
||||
*/ |
||||
val composableAdapterName: String = "$implementationPreviewPackage.ComposeViewAdapter" |
||||
|
||||
val composeModifierClassName: String = "$packageName.Modifier" |
||||
|
||||
/** Only composables with this annotations will be rendered to the surface. */ |
||||
val previewAnnotationName = "$apiPreviewPackage.$COMPOSE_PREVIEW_ANNOTATION_NAME" |
||||
|
||||
/** Same as [previewAnnotationName] but in [FqName] form. */ |
||||
val previewAnnotationNameFqName = FqName(previewAnnotationName) |
||||
|
||||
/** Annotation FQN for `Preview` annotated parameters. */ |
||||
val previewParameterAnnotationName = "$apiPreviewPackage.PreviewParameter" |
||||
|
||||
/** FqName of @Composable function that loads a string resource. **/ |
||||
val stringResourceFunctionFqName = "$packageName.res.stringResource" |
||||
|
||||
/** FqName of the Devices class for its corresponding `@Preview` parameter. */ |
||||
val composeDevicesClassName = "$apiPreviewPackage.Devices" |
||||
|
||||
val previewActivityName = "$implementationPreviewPackage.PreviewActivity" |
||||
} |
||||
|
||||
/** Only composables with this annotations will be rendered to the surface. */ |
||||
@JvmField |
||||
val COMPOSE_VIEW_ADAPTER_FQNS = setOf(ComposeLibraryNamespace.ANDROIDX_COMPOSE.composableAdapterName, |
||||
ComposeLibraryNamespace.ANDROIDX_COMPOSE_WITH_API.composableAdapterName) |
||||
|
||||
/** FQNs for the `@Preview` annotation. Only composables with this annotations will be rendered to the surface. */ |
||||
@JvmField |
||||
val PREVIEW_ANNOTATION_FQNS = setOf(ComposeLibraryNamespace.ANDROIDX_COMPOSE.previewAnnotationName, |
||||
ComposeLibraryNamespace.ANDROIDX_COMPOSE_WITH_API.previewAnnotationName) |
||||
|
||||
/** Annotations FQNs for `Preview` annotated parameters. */ |
||||
@JvmField |
||||
val PREVIEW_PARAMETER_FQNS = setOf(ComposeLibraryNamespace.ANDROIDX_COMPOSE.previewParameterAnnotationName, |
||||
ComposeLibraryNamespace.ANDROIDX_COMPOSE_WITH_API.previewParameterAnnotationName) |
||||
|
@ -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,45 @@
|
||||
/* |
||||
* 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 isComposeEnabled(element: PsiElement): Boolean = element.inComposeModule() ?: false |
||||
|
||||
fun isModifierChainLongerThanTwo(element: KtElement): Boolean { |
||||
if (element.getChildrenOfType<KtDotQualifiedExpression>().isNotEmpty()) { |
||||
val fqName = element.resolveToCall(BodyResolveMode.PARTIAL)?.getReturnType()?.fqName?.asString() |
||||
if (fqName == ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName) { |
||||
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(): ComposeSettings = 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,74 @@
|
||||
/* |
||||
* 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.fqNameMatches(fqName: String): Boolean { |
||||
// For inspiration, see IDELightClassGenerationSupport.KtUltraLightSupportImpl.findAnnotation in the Kotlin plugin. |
||||
val shortName = shortName?.asString() ?: return false |
||||
return fqName.endsWith(shortName) && fqName == getQualifiedName() |
||||
} |
||||
|
||||
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 { |
||||
if (this !is KtAnnotationEntry) return false |
||||
val fqName = this.getQualifiedName() ?: return false |
||||
return COMPOSABLE_FQ_NAMES.any { it == fqName } |
||||
} |
||||
|
||||
fun PsiElement.isInsideComposableCode(): Boolean { |
||||
// TODO: also handle composable lambdas. |
||||
return language == KotlinLanguage.INSTANCE && parentOfType<KtNamedFunction>()?.isComposableFunction() == true |
||||
} |
@ -0,0 +1,380 @@
|
||||
/* |
||||
* 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.castSafelyTo |
||||
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.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` |
||||
.castSafelyTo<DeclarationLookupObject>() |
||||
?.descriptor |
||||
?.castSafelyTo<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) |
||||
} |
||||
|
||||
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.compose.isComposeEnabled |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
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.caches.resolve.resolveImportReference |
||||
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName |
||||
import org.jetbrains.kotlin.idea.references.mainReference |
||||
import org.jetbrains.kotlin.idea.search.* |
||||
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() || !isComposeEnabled(elementToComplete) || 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 |
||||
.getInstance().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,297 @@
|
||||
/* |
||||
* 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.ComposeLibraryNamespace |
||||
import com.android.tools.compose.isComposeEnabled |
||||
import com.android.tools.idea.flags.StudioFlags |
||||
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.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 ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName |
||||
*/ |
||||
class ComposeModifierCompletionContributor : CompletionContributor() { |
||||
|
||||
companion object { |
||||
private val modifierFqName = ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName |
||||
} |
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, resultSet: CompletionResultSet) { |
||||
val element = parameters.position |
||||
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || !isComposeEnabled(element) || 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) |
||||
|
||||
ProgressManager.checkCanceled() |
||||
val (returnsModifier, others) = extensionFunctions.partition { it.returnType?.fqName?.asString() == modifierFqName } |
||||
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, 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 = $modifierFqName.call", originalFile) |
||||
return file.getChildOfType<KtProperty>()!!.getChildOfType<KtDotQualifiedExpression>()!!.lastChild as KtSimpleNameExpression |
||||
} |
||||
|
||||
private fun getExtensionFunctionsForModifier(nameExpression: KtSimpleNameExpression, |
||||
originalPosition: PsiElement): Collection<CallableDescriptor> { |
||||
val file = nameExpression.containingFile as KtFile |
||||
val searchScope = getResolveScope(file) |
||||
val resolutionFacade = file.getResolutionFacade() |
||||
|
||||
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS) |
||||
|
||||
val 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) |
||||
return indicesHelper.getCallableTopLevelExtensions(callTypeAndReceiver, nameExpression, bindingContext, null) { true } |
||||
} |
||||
|
||||
private val PsiElement.isModifierProperty: Boolean |
||||
get() { |
||||
val property = contextOfType<KtProperty>() ?: return false |
||||
return property.type()?.fqName?.asString() == modifierFqName |
||||
} |
||||
|
||||
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() == modifierFqName |
||||
} |
||||
|
||||
/** |
||||
* 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() == modifierFqName |
||||
} |
||||
|
||||
/** |
||||
* 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) |
||||
if (insertModifier) { |
||||
context.document.insertString(context.startOffset, callOnModifierObject) |
||||
context.offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, context.startOffset + callOnModifierObject.length) |
||||
psiDocumentManager.commitAllDocuments() |
||||
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document) |
||||
} |
||||
val ktFile = context.file as KtFile |
||||
val modifierDescriptor = ktFile.resolveImportReference(FqName(modifierFqName)).singleOrNull() |
||||
modifierDescriptor?.let { ImportInsertHelper.getInstance(context.project).importDescriptor(ktFile, it) } |
||||
psiDocumentManager.commitAllDocuments() |
||||
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document) |
||||
super.handleInsert(context) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,21 @@
|
||||
/* |
||||
* 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 { |
||||
const val ConstraintSets = "ConstraintSets" |
||||
const val Extends = "Extends" |
||||
} |
@ -0,0 +1,155 @@
|
||||
/* |
||||
* 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.intellij.codeInsight.completion.CompletionContributor |
||||
import com.intellij.codeInsight.completion.CompletionParameters |
||||
import com.intellij.codeInsight.completion.CompletionProvider |
||||
import com.intellij.codeInsight.completion.CompletionResultSet |
||||
import com.intellij.codeInsight.completion.CompletionType |
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder |
||||
import com.intellij.json.JsonElementTypes |
||||
import com.intellij.json.psi.JsonObject |
||||
import com.intellij.json.psi.JsonProperty |
||||
import com.intellij.json.psi.JsonReferenceExpression |
||||
import com.intellij.openapi.util.Key |
||||
import com.intellij.patterns.PlatformPatterns |
||||
import com.intellij.patterns.PsiElementPattern |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.SmartPointerManager |
||||
import com.intellij.psi.SmartPsiElementPointer |
||||
import com.intellij.util.ProcessingContext |
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType |
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType |
||||
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType |
||||
import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType |
||||
|
||||
/** |
||||
* [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 { |
||||
extend( |
||||
CompletionType.BASIC, |
||||
jsonPropertyName().withConstraintSetsParentAtLevel(6), |
||||
ConstraintIdsProvider |
||||
) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* [SmartPsiElementPointer] to the [JsonProperty] corresponding to the ConstraintSets property. |
||||
*/ |
||||
private typealias ConstraintSetsPropertyPointer = SmartPsiElementPointer<JsonProperty> |
||||
|
||||
private val constraintSetsPropertyKey = |
||||
Key.create<ConstraintSetsPropertyPointer>("compose.json.autocomplete.constraint.sets.property") |
||||
|
||||
/** |
||||
* Completion provider that looks for the 'ConstraintSets' declaration and caches it, provides useful functions for inheritors that want to |
||||
* provide completions based con the contents of the 'ConstraintSets' [JsonProperty]. |
||||
*/ |
||||
private abstract class ConstraintSetCompletionProvider : CompletionProvider<CompletionParameters>() { |
||||
final override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { |
||||
val setsProperty = if (context[constraintSetsPropertyKey] != null) { |
||||
context[constraintSetsPropertyKey]!! |
||||
} |
||||
else { |
||||
parameters.position.getTopmostParentOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.firstOrNull { |
||||
it.name == KeyWords.ConstraintSets |
||||
}?.let { |
||||
val pointer = SmartPointerManager.createPointer(it) |
||||
context.put(constraintSetsPropertyKey, pointer) |
||||
return@let pointer |
||||
} |
||||
} |
||||
addCompletions(setsProperty, parameters, result) |
||||
} |
||||
|
||||
/** |
||||
* Inheritors should implement this function that may pass a reference to the ConstraintSets property. |
||||
*/ |
||||
abstract fun addCompletions( |
||||
constraintSetsProperty: ConstraintSetsPropertyPointer?, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) |
||||
|
||||
/** |
||||
* Returns the available constraint IDs for the given [constraintSetName], this is done by reading all IDs in all ConstraintSets and |
||||
* subtracting the IDs already present in [constraintSetName]. |
||||
*/ |
||||
protected fun ConstraintSetsPropertyPointer.findConstraintIdsForSet(constraintSetName: String): List<String> { |
||||
val availableNames = mutableSetOf(KeyWords.Extends) |
||||
val usedNames = mutableSetOf<String>() |
||||
this.element?.getChildOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.forEach { cSetProperty -> |
||||
cSetProperty.getChildOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.forEach { constraintNameProperty -> |
||||
if (cSetProperty.name == constraintSetName) { |
||||
usedNames.add(constraintNameProperty.name) |
||||
} |
||||
else { |
||||
availableNames.add(constraintNameProperty.name) |
||||
} |
||||
} |
||||
} |
||||
availableNames.removeAll(usedNames) |
||||
return availableNames.toList() |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides options to autocomplete constraint IDs for constraint set declarations, based on the IDs already defined by the user in other |
||||
* constraint sets. |
||||
*/ |
||||
private object ConstraintIdsProvider : ConstraintSetCompletionProvider() { |
||||
override fun addCompletions(constraintSetsProperty: SmartPsiElementPointer<JsonProperty>?, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet) { |
||||
val parentName = parameters.position.getParentOfType<JsonProperty>(true)?.getParentOfType<JsonProperty>(true)?.name |
||||
if (constraintSetsProperty != null && parentName != null) { |
||||
constraintSetsProperty.findConstraintIdsForSet(parentName).forEach { |
||||
val template = if (it == KeyWords.Extends) JsonStringValueTemplate else JsonNewObjectTemplate |
||||
result.addLookupElement(name = it, tailText = null, template) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun jsonPropertyName() = PlatformPatterns.psiElement(JsonElementTypes.IDENTIFIER) |
||||
|
||||
private inline fun <reified T : PsiElement> psiElement() = PlatformPatterns.psiElement(T::class.java) |
||||
|
||||
private fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, name: String) = |
||||
this.withSuperParent(level, psiElement<JsonProperty>().withChild(psiElement<JsonReferenceExpression>().withText(name))) |
||||
|
||||
private fun PsiElementPattern<*, *>.withConstraintSetsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, "ConstraintSets") |
||||
|
||||
private fun CompletionResultSet.addLookupElement(name: String, tailText: String? = null, format: InsertionFormat? = null) { |
||||
var lookupBuilder = if (format == null) { |
||||
LookupElementBuilder.create(name) |
||||
} |
||||
else { |
||||
LookupElementBuilder.create(format, name).withInsertHandler(InsertionFormatHandler) |
||||
} |
||||
lookupBuilder = lookupBuilder.withCaseSensitivity(false) |
||||
if (tailText != null) { |
||||
lookupBuilder = lookupBuilder.withTailText(tailText, true) |
||||
} |
||||
addElement(lookupBuilder) |
||||
} |
@ -0,0 +1,43 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
/** |
||||
* Describes a string that may be automatically inserted when selecting an autocomplete option. |
||||
*/ |
||||
internal 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. |
||||
*/ |
||||
internal 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. |
||||
*/ |
||||
internal class LiteralNewLineFormat(literalFormat: String) : InsertionFormat(literalFormat) |
||||
|
||||
internal val JsonStringValueTemplate = LiteralWithCaretFormat(": '|',") |
||||
|
||||
internal val JsonNewObjectTemplate = LiteralNewLineFormat(": {\n}") |
@ -0,0 +1,91 @@
|
||||
/* |
||||
* 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.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 |
||||
|
||||
/** |
||||
* An [InsertHandler] to handle [InsertionFormat]. |
||||
* |
||||
* The [InsertionFormat] object needs to be present in [LookupElement.getObject] to be handled here. |
||||
*/ |
||||
internal object InsertionFormatHandler : InsertHandler<LookupElement> { |
||||
override fun handleInsert(context: InsertionContext, item: LookupElement) { |
||||
val format = item.`object` as? InsertionFormat ?: return |
||||
when (format) { |
||||
is LiteralWithCaretFormat -> handleCaretInsertion(context, format) |
||||
is LiteralNewLineFormat -> handleNewLineInsertion(context, format) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles insertions of [LiteralWithCaretFormat], moving the caret at the position specified by the '|' character. |
||||
*/ |
||||
private fun handleCaretInsertion(context: InsertionContext, format: LiteralWithCaretFormat) { |
||||
with(context) { |
||||
val isMoveCaret = format.insertableString.contains('|') |
||||
val stringToInsert = format.insertableString.replace("|", "") |
||||
|
||||
// Insert the string without the reserved character: | |
||||
EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true) |
||||
PsiDocumentManager.getInstance(project).commitDocument(document) |
||||
|
||||
// Move caret to the position indicated by '|' |
||||
EditorActionUtil.moveCaretToLineEnd(editor, false, true) |
||||
if (isMoveCaret && stringToInsert.isNotEmpty()) { |
||||
val caretPosition = format.insertableString.indexOf('|').coerceAtLeast(0) |
||||
EditorModificationUtil.moveCaretRelatively(editor, caretPosition - stringToInsert.length) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handles insertions of [LiteralNewLineFormat], applying the new line with the [IdeActions.ACTION_EDITOR_ENTER] and moving the caret at |
||||
* the end of the new line. |
||||
*/ |
||||
private fun handleNewLineInsertion(context: InsertionContext, format: LiteralNewLineFormat) { |
||||
val literal = format.insertableString |
||||
with(context) { |
||||
val newLineOffset = literal.indexOf('\n') |
||||
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,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,145 @@
|
||||
/* |
||||
* 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.* |
||||
import com.intellij.debugger.engine.* |
||||
import com.intellij.debugger.engine.evaluation.* |
||||
import com.intellij.debugger.jdi.* |
||||
import com.intellij.debugger.requests.* |
||||
import com.intellij.openapi.application.* |
||||
import com.intellij.openapi.fileTypes.* |
||||
import com.intellij.util.* |
||||
import com.intellij.xdebugger.frame.* |
||||
import com.sun.jdi.* |
||||
import com.sun.jdi.request.* |
||||
import org.jetbrains.kotlin.fileClasses.* |
||||
import org.jetbrains.kotlin.idea.* |
||||
import org.jetbrains.kotlin.idea.debugger.* |
||||
import org.jetbrains.kotlin.load.kotlin.* |
||||
import org.jetbrains.kotlin.psi.* |
||||
|
||||
/** |
||||
* 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.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.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.isComposeEnabled |
||||
import com.android.tools.compose.isModifierChainLongerThanTwo |
||||
import com.android.tools.compose.settings.ComposeCustomCodeStyleSettings |
||||
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 && |
||||
isComposeEnabled(psiElement) && |
||||
!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,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.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 |
||||
|
||||
|
||||
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, unresolvedCall)) } |
||||
|
||||
// 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,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.intentions |
||||
|
||||
import com.android.tools.compose.* |
||||
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( |
||||
"@" + ComposeLibraryNamespace.ANDROIDX_COMPOSE.previewAnnotationName) |
||||
|
||||
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,147 @@
|
||||
/* |
||||
* 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.psi.PsiFile |
||||
import com.intellij.ui.popup.list.ListPopupImpl |
||||
import org.jetbrains.kotlin.idea.codeInsight.surroundWith.statement.KotlinStatementSurroundDescriptor |
||||
import org.jetbrains.kotlin.psi.KtFile |
||||
|
||||
/** |
||||
* 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") |
||||
} |
||||
|
||||
/** |
||||
* 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 |
||||
|
||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { |
||||
when { |
||||
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> return false |
||||
file == null || editor == null -> return false |
||||
!file.isWritable || file !is KtFile || !editor.selectionModel.hasSelection() -> return false |
||||
else -> { |
||||
val element = file.findElementAt(editor.caretModel.offset) ?: return false |
||||
if (!element.isInsideComposableCode()) return false |
||||
|
||||
val statements = KotlinStatementSurroundDescriptor() |
||||
.getElementsToSurround(file, editor.selectionModel.selectionStart, editor.selectionModel.selectionEnd) |
||||
|
||||
return statements.isNotEmpty() |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected abstract fun getTemplate(): TemplateImpl? |
||||
|
||||
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { |
||||
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,31 @@
|
||||
/* |
||||
* 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.configurationStore.* |
||||
import com.intellij.psi.codeStyle.* |
||||
|
||||
class ComposeCustomCodeStyleSettings(settings: CodeStyleSettings) : CustomCodeStyleSettings("ComposeCustomCodeStyleSettings", settings) { |
||||
@Property(externalName = "use_custom_formatting_for_modifiers") |
||||
@JvmField |
||||
var USE_CUSTOM_FORMATTING_FOR_MODIFIERS = true |
||||
|
||||
companion object { |
||||
fun getInstance(settings: CodeStyleSettings): ComposeCustomCodeStyleSettings { |
||||
return settings.getCustomSettings(ComposeCustomCodeStyleSettings::class.java) |
||||
} |
||||
} |
||||
} |
@ -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,21 @@
|
||||
/* |
||||
* 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) |
||||
} |
||||
|
||||
class Flag<T>(val value: T) { |
||||
fun get(): T = value |
||||
} |
@ -0,0 +1,12 @@
|
||||
/* |
||||
* 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.intellij.openapi.module.Module |
||||
import com.intellij.psi.* |
||||
|
||||
fun PsiElement.inComposeModule() = true |
||||
fun Module.isComposeModule() = true |
@ -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