Ilya Ryzhenkov
2 years ago
33 changed files with 1607 additions and 246 deletions
Binary file not shown.
Binary file not shown.
@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright (C) 2022 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion.constraintlayout |
||||
|
||||
import com.intellij.codeInsight.completion.CompletionParameters |
||||
import com.intellij.json.psi.JsonProperty |
||||
import com.intellij.psi.util.parentOfType |
||||
|
||||
/** |
||||
* From the element being invoked, returns the [JsonProperty] parent that also includes the [JsonProperty] from which completion is |
||||
* triggered. |
||||
*/ |
||||
internal fun getJsonPropertyParent(parameters: CompletionParameters): JsonProperty? = |
||||
parameters.position.parentOfType<JsonProperty>(withSelf = true)?.parentOfType<JsonProperty>(withSelf = false) |
@ -0,0 +1,101 @@
|
||||
/* |
||||
* Copyright (C) 2022 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion.constraintlayout |
||||
|
||||
import com.intellij.json.JsonElementTypes |
||||
import com.intellij.json.psi.JsonArray |
||||
import com.intellij.json.psi.JsonProperty |
||||
import com.intellij.json.psi.JsonReferenceExpression |
||||
import com.intellij.json.psi.JsonStringLiteral |
||||
import com.intellij.json.psi.JsonValue |
||||
import com.intellij.patterns.PatternCondition |
||||
import com.intellij.patterns.PlatformPatterns |
||||
import com.intellij.patterns.PsiElementPattern |
||||
import com.intellij.patterns.StandardPatterns |
||||
import com.intellij.patterns.StringPattern |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.util.ProcessingContext |
||||
|
||||
// region ConstraintLayout Pattern Helpers |
||||
internal fun jsonPropertyName() = PlatformPatterns.psiElement(JsonElementTypes.IDENTIFIER) |
||||
|
||||
internal fun jsonStringValue() = |
||||
PlatformPatterns.psiElement(JsonElementTypes.SINGLE_QUOTED_STRING).withParent<JsonStringLiteral>() |
||||
|
||||
internal fun PsiElementPattern<*, *>.withConstraintSetsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, KeyWords.ConstraintSets) |
||||
internal fun PsiElementPattern<*, *>.withTransitionsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, KeyWords.Transitions) |
||||
|
||||
internal fun PsiElementPattern<*, *>.insideClearArray() = inArrayWithinConstraintBlockProperty { |
||||
// For the 'clear' constraint block property |
||||
matches(KeyWords.Clear) |
||||
} |
||||
|
||||
internal fun PsiElementPattern<*, *>.insideConstraintArray() = inArrayWithinConstraintBlockProperty { |
||||
// The parent property name may only be a StandardAnchor |
||||
oneOf(StandardAnchor.values().map { it.keyWord }) |
||||
} |
||||
|
||||
/** |
||||
* [PsiElementPattern] that matches an element in a [JsonArray] within a Constraint block. Where the property the array is assigned to, has |
||||
* a name that is matched by [matchPropertyName]. |
||||
*/ |
||||
internal fun PsiElementPattern<*, *>.inArrayWithinConstraintBlockProperty(matchPropertyName: StringPattern.() -> StringPattern) = |
||||
withSuperParent(2, psiElement<JsonArray>()) |
||||
.withSuperParent( |
||||
BASE_DEPTH_FOR_LITERAL_IN_PROPERTY + 1, // JsonArray adds one level |
||||
psiElement<JsonProperty>().withChild( |
||||
// The first expression in a JsonProperty corresponds to the name of the property |
||||
psiElement<JsonReferenceExpression>().withText(StandardPatterns.string().matchPropertyName()) |
||||
) |
||||
) |
||||
.withConstraintSetsParentAtLevel(CONSTRAINT_BLOCK_PROPERTY_DEPTH + 1) // JsonArray adds one level |
||||
// endregion |
||||
|
||||
// region Kotlin Syntax Helpers |
||||
internal inline fun <reified T : PsiElement> psiElement(): PsiElementPattern<T, PsiElementPattern.Capture<T>> = |
||||
PlatformPatterns.psiElement(T::class.java) |
||||
|
||||
internal inline fun <reified T : PsiElement> PsiElementPattern<*, *>.withParent() = this.withParent(T::class.java) |
||||
|
||||
/** |
||||
* Pattern such that when traversing up the tree from the current element, the element at [level] is a [JsonProperty]. And its name matches |
||||
* the given [name]. |
||||
*/ |
||||
internal fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, name: String) = |
||||
withPropertyParentAtLevel(level, listOf(name)) |
||||
|
||||
/** |
||||
* Pattern such that when traversing up the tree from the current element, the element at [level] is a [JsonProperty]. Which name matches |
||||
* one of the given [names]. |
||||
*/ |
||||
internal fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, names: Collection<String>) = |
||||
this.withSuperParent(level, psiElement<JsonProperty>().withChild( |
||||
psiElement<JsonReferenceExpression>().withText(StandardPatterns.string().oneOf(names))) |
||||
) |
||||
|
||||
/** |
||||
* Verifies that the current element is at the given [index] of the elements contained by its [JsonArray] parent. |
||||
*/ |
||||
internal fun <T : JsonValue> PsiElementPattern<T, PsiElementPattern.Capture<T>>.atIndexOfJsonArray(index: Int) = |
||||
with(object : PatternCondition<T>("atIndexOfJsonArray") { |
||||
override fun accepts(element: T, context: ProcessingContext?): Boolean { |
||||
val parent = element.context as? JsonArray ?: return false |
||||
val children = parent.valueList |
||||
val indexOfSelf = children.indexOf(element) |
||||
return index == indexOfSelf |
||||
} |
||||
}) |
||||
// endregion |
@ -0,0 +1,433 @@
|
||||
/* |
||||
* Copyright (C) 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion.constraintlayout.provider |
||||
|
||||
import com.android.tools.compose.code.completion.constraintlayout.ClearAllTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.ClearOption |
||||
import com.android.tools.compose.code.completion.constraintlayout.ConstrainAnchorTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.ConstraintLayoutKeyWord |
||||
import com.android.tools.compose.code.completion.constraintlayout.Dimension |
||||
import com.android.tools.compose.code.completion.constraintlayout.JsonNewObjectTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.JsonNumericValueTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.JsonObjectArrayTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.JsonStringArrayTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.JsonStringValueTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.KeyCycleField |
||||
import com.android.tools.compose.code.completion.constraintlayout.KeyFrameChildCommonField |
||||
import com.android.tools.compose.code.completion.constraintlayout.KeyFrameField |
||||
import com.android.tools.compose.code.completion.constraintlayout.KeyPositionField |
||||
import com.android.tools.compose.code.completion.constraintlayout.KeyWords |
||||
import com.android.tools.compose.code.completion.constraintlayout.OnSwipeField |
||||
import com.android.tools.compose.code.completion.constraintlayout.RenderTransform |
||||
import com.android.tools.compose.code.completion.constraintlayout.SpecialAnchor |
||||
import com.android.tools.compose.code.completion.constraintlayout.StandardAnchor |
||||
import com.android.tools.compose.code.completion.constraintlayout.TransitionField |
||||
import com.android.tools.compose.code.completion.constraintlayout.buildJsonNumberArrayTemplate |
||||
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent |
||||
import com.android.tools.compose.code.completion.constraintlayout.provider.model.ConstraintSetModel |
||||
import com.android.tools.compose.code.completion.constraintlayout.provider.model.ConstraintSetsPropertyModel |
||||
import com.android.tools.compose.code.completion.constraintlayout.provider.model.JsonPropertyModel |
||||
import com.android.tools.compose.completion.addLookupElement |
||||
import com.android.tools.compose.completion.inserthandler.InsertionFormat |
||||
import com.intellij.codeInsight.completion.CompletionParameters |
||||
import com.intellij.codeInsight.completion.CompletionProvider |
||||
import com.intellij.codeInsight.completion.CompletionResultSet |
||||
import com.intellij.json.psi.JsonArray |
||||
import com.intellij.json.psi.JsonObject |
||||
import com.intellij.json.psi.JsonProperty |
||||
import com.intellij.json.psi.JsonStringLiteral |
||||
import com.intellij.openapi.diagnostic.thisLogger |
||||
import com.intellij.openapi.progress.ProgressManager |
||||
import com.intellij.psi.PsiElement |
||||
import com.intellij.psi.util.parentOfType |
||||
import com.intellij.util.ProcessingContext |
||||
import kotlin.reflect.KClass |
||||
|
||||
/** |
||||
* Completion provider that looks for the 'ConstraintSets' declaration and passes a model that provides useful functions for inheritors that |
||||
* want to provide completions based on the contents of the 'ConstraintSets' [JsonProperty]. |
||||
*/ |
||||
internal abstract class BaseConstraintSetsCompletionProvider : CompletionProvider<CompletionParameters>() { |
||||
final override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { |
||||
val constraintSetsModel = createConstraintSetsModel(initialElement = parameters.position) |
||||
if (constraintSetsModel != null) { |
||||
ProgressManager.checkCanceled() |
||||
addCompletions(constraintSetsModel, parameters, result) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Inheritors should implement this function that may pass a reference to the ConstraintSets property. |
||||
*/ |
||||
abstract fun addCompletions( |
||||
constraintSetsPropertyModel: ConstraintSetsPropertyModel, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) |
||||
|
||||
/** |
||||
* Finds the [JsonProperty] for the 'ConstraintSets' declaration and returns its model. |
||||
* |
||||
* The `ConstraintSets` property is expected to be a property of the root [JsonObject]. |
||||
*/ |
||||
private fun createConstraintSetsModel(initialElement: PsiElement): ConstraintSetsPropertyModel? { |
||||
// Start with the closest JsonObject towards the root |
||||
var currentJsonObject: JsonObject? = initialElement.parentOfType<JsonObject>(withSelf = true) ?: return null |
||||
lateinit var topLevelJsonObject: JsonObject |
||||
|
||||
// Then find the top most JsonObject while checking for cancellation |
||||
while (currentJsonObject != null) { |
||||
topLevelJsonObject = currentJsonObject |
||||
currentJsonObject = currentJsonObject.parentOfType<JsonObject>(withSelf = false) |
||||
|
||||
ProgressManager.checkCanceled() |
||||
} |
||||
|
||||
// The last non-null JsonObject is the topmost, the ConstraintSets property is expected within this element |
||||
val constraintSetsProperty = topLevelJsonObject.findProperty(KeyWords.ConstraintSets) ?: return null |
||||
// TODO(b/207030860): Consider creating the model even if there's no property that is explicitly called 'ConstraintSets' |
||||
// ie: imply that the root JsonObject is the ConstraintSets object, with the downside that figuring out the correct context would |
||||
// be much more difficult |
||||
return ConstraintSetsPropertyModel(constraintSetsProperty) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides options to autocomplete constraint IDs for constraint set declarations, based on the IDs already defined by the user in other |
||||
* constraint sets. |
||||
*/ |
||||
internal object ConstraintSetFieldsProvider : BaseConstraintSetsCompletionProvider() { |
||||
override fun addCompletions( |
||||
constraintSetsPropertyModel: ConstraintSetsPropertyModel, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) { |
||||
val currentConstraintSet = ConstraintSetModel.getModelForCompletionOnConstraintSetProperty(parameters) ?: return |
||||
val currentSetName = currentConstraintSet.name ?: return |
||||
constraintSetsPropertyModel.getRemainingFieldsForConstraintSet(currentSetName).forEach { fieldName -> |
||||
val template = if (fieldName == KeyWords.Extends) JsonStringValueTemplate else JsonNewObjectTemplate |
||||
result.addLookupElement(lookupString = fieldName, tailText = null, template) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Autocomplete options with the names of all available ConstraintSets, except from the one the autocomplete was invoked from. |
||||
*/ |
||||
internal object ConstraintSetNamesProvider : BaseConstraintSetsCompletionProvider() { |
||||
override fun addCompletions( |
||||
constraintSetsPropertyModel: ConstraintSetsPropertyModel, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) { |
||||
val currentConstraintSet = ConstraintSetModel.getModelForCompletionOnConstraintSetProperty(parameters) |
||||
val currentSetName = currentConstraintSet?.name |
||||
val names = constraintSetsPropertyModel.getConstraintSetNames().toMutableSet() |
||||
if (currentSetName != null) { |
||||
names.remove(currentSetName) |
||||
} |
||||
names.forEach(result::addLookupElement) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Autocomplete options used to define the constraints of a widget (defined by the ID) within a ConstraintSet |
||||
*/ |
||||
internal object ConstraintsProvider : BaseConstraintSetsCompletionProvider() { |
||||
override fun addCompletions( |
||||
constraintSetsPropertyModel: ConstraintSetsPropertyModel, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) { |
||||
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return |
||||
val existingFieldsSet = parentPropertyModel.declaredFieldNamesSet |
||||
StandardAnchor.values().forEach { |
||||
if (!existingFieldsSet.contains(it.keyWord)) { |
||||
result.addLookupElement(lookupString = it.keyWord, tailText = " [...]", format = ConstrainAnchorTemplate) |
||||
} |
||||
} |
||||
if (!existingFieldsSet.contains(KeyWords.Visibility)) { |
||||
result.addLookupElement(lookupString = KeyWords.Visibility, format = JsonStringValueTemplate) |
||||
} |
||||
result.addEnumKeyWordsWithStringValueTemplate<SpecialAnchor>(existingFieldsSet) |
||||
result.addEnumKeyWordsWithNumericValueTemplate<Dimension>(existingFieldsSet) |
||||
result.addEnumKeyWordsWithNumericValueTemplate<RenderTransform>(existingFieldsSet) |
||||
|
||||
// Complete 'clear' if the containing ConstraintSet has `extendsFrom` |
||||
val containingConstraintSetModel = parentPropertyModel.getParentProperty()?.let { |
||||
ConstraintSetModel(it) |
||||
} |
||||
if (containingConstraintSetModel?.extendsFrom != null) { |
||||
// Add an option with an empty string array and another one with all clear options |
||||
result.addLookupElement(lookupString = KeyWords.Clear, format = JsonStringArrayTemplate) |
||||
result.addLookupElement(lookupString = KeyWords.Clear, format = ClearAllTemplate, tailText = " [<all>]") |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides IDs when autocompleting a constraint array. |
||||
* |
||||
* The ID may be either 'parent' or any of the declared IDs in all ConstraintSets, except the ID of the constraints block from which this |
||||
* provider was invoked. |
||||
*/ |
||||
internal object ConstraintIdsProvider : BaseConstraintSetsCompletionProvider() { |
||||
override fun addCompletions( |
||||
constraintSetsPropertyModel: ConstraintSetsPropertyModel, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) { |
||||
val possibleIds = constraintSetsPropertyModel.constraintSets.flatMap { it.declaredIds }.toCollection(HashSet()) |
||||
// Parent ID should always be present |
||||
possibleIds.add(KeyWords.ParentId) |
||||
// Remove the current ID |
||||
getJsonPropertyParent(parameters)?.name?.let(possibleIds::remove) |
||||
|
||||
possibleIds.forEach { id -> |
||||
result.addLookupElement(lookupString = id) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides the appropriate anchors when completing a constraint array. |
||||
* |
||||
* [StandardAnchor.verticalAnchors] can only be constrained to other vertical anchors. Same logic for [StandardAnchor.horizontalAnchors]. |
||||
*/ |
||||
internal object AnchorablesProvider : BaseConstraintSetsCompletionProvider() { |
||||
override fun addCompletions( |
||||
constraintSetsPropertyModel: ConstraintSetsPropertyModel, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) { |
||||
val currentAnchorKeyWord = parameters.position.parentOfType<JsonProperty>(withSelf = true)?.name ?: return |
||||
|
||||
val possibleAnchors = when { |
||||
StandardAnchor.isVertical(currentAnchorKeyWord) -> StandardAnchor.verticalAnchors |
||||
StandardAnchor.isHorizontal(currentAnchorKeyWord) -> StandardAnchor.horizontalAnchors |
||||
else -> emptyList() |
||||
} |
||||
possibleAnchors.forEach { result.addLookupElement(lookupString = it.keyWord) } |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides the appropriate options when completing string literals within a `clear` array. |
||||
* |
||||
* @see ClearOption |
||||
*/ |
||||
internal object ClearOptionsProvider : BaseConstraintSetsCompletionProvider() { |
||||
override fun addCompletions( |
||||
constraintSetsPropertyModel: ConstraintSetsPropertyModel, |
||||
parameters: CompletionParameters, |
||||
result: CompletionResultSet |
||||
) { |
||||
val existing = parameters.position.parentOfType<JsonArray>(withSelf = false)?.valueList |
||||
?.filterIsInstance<JsonStringLiteral>() |
||||
?.map { it.value } |
||||
?.toSet() ?: emptySet() |
||||
addEnumKeywords<ClearOption>(result, existing) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides completion for the fields of a `Transition`. |
||||
* |
||||
* @see TransitionField |
||||
*/ |
||||
internal object TransitionFieldsProvider : CompletionProvider<CompletionParameters>() { |
||||
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { |
||||
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return |
||||
TransitionField.values().forEach { |
||||
if (parentPropertyModel.containsPropertyOfName(it.keyWord)) { |
||||
// skip |
||||
return@forEach |
||||
} |
||||
when (it) { |
||||
TransitionField.OnSwipe, |
||||
TransitionField.KeyFrames -> { |
||||
result.addLookupElement(lookupString = it.keyWord, format = JsonNewObjectTemplate) |
||||
} |
||||
else -> { |
||||
result.addLookupElement(lookupString = it.keyWord, format = JsonStringValueTemplate) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides completion for the fields of an `OnSwipe` block. |
||||
* |
||||
* @see OnSwipeField |
||||
*/ |
||||
internal object OnSwipeFieldsProvider : CompletionProvider<CompletionParameters>() { |
||||
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { |
||||
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return |
||||
result.addEnumKeyWordsWithStringValueTemplate<OnSwipeField>(parentPropertyModel.declaredFieldNamesSet) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides completion for the fields of a `KeyFrames` block. |
||||
* |
||||
* @see KeyFrameField |
||||
*/ |
||||
internal object KeyFramesFieldsProvider : CompletionProvider<CompletionParameters>() { |
||||
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { |
||||
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return |
||||
addEnumKeywords<KeyFrameField>( |
||||
result = result, |
||||
format = JsonObjectArrayTemplate, |
||||
existing = parentPropertyModel.declaredFieldNamesSet |
||||
) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides completion for the fields of KeyFrame children. A KeyFrame child can be any of [KeyFrameField]. |
||||
*/ |
||||
internal object KeyFrameChildFieldsCompletionProvider : CompletionProvider<CompletionParameters>() { |
||||
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { |
||||
// TODO(b/207030860): For consistency, make it so that JsonPropertyModel may be used here. It currently won't work because the model |
||||
// doesn't consider a property defined by an array of objects. |
||||
|
||||
// Obtain existing list of existing properties |
||||
val parentObject = parameters.position.parentOfType<JsonObject>(withSelf = false) ?: return |
||||
val existingFieldsSet = parentObject.propertyList.map { it.name }.toSet() |
||||
|
||||
// We have to know the type of KeyFrame we are autocompleting for (KeyPositions, KeyAttributes, etc) |
||||
val keyFrameTypeName = parentObject.parentOfType<JsonProperty>(withSelf = false)?.name ?: return |
||||
|
||||
// Look for the `frames` property, we want to know the size of its array (if present), since all other numeric properties should have an |
||||
// array of the same size |
||||
val framesProperty = parentObject.findProperty(KeyFrameChildCommonField.Frames.keyWord) |
||||
val arrayCountInFramesProperty = (framesProperty?.value as? JsonArray)?.valueList?.size ?: 1 |
||||
|
||||
// Create the template that will be used by any numeric property we autocomplete |
||||
val jsonNumberArrayTemplate = buildJsonNumberArrayTemplate(count = arrayCountInFramesProperty) |
||||
|
||||
// We've done some read operations, check for cancellation |
||||
ProgressManager.checkCanceled() |
||||
|
||||
// Common fields for any type of KeyFrame |
||||
KeyFrameChildCommonField.values().forEach { |
||||
if (existingFieldsSet.contains(it.keyWord)) { |
||||
return@forEach |
||||
} |
||||
when (it) { |
||||
KeyFrameChildCommonField.Frames -> result.addLookupElement(lookupString = it.keyWord, format = jsonNumberArrayTemplate) |
||||
else -> result.addLookupElement(lookupString = it.keyWord, format = JsonStringValueTemplate) |
||||
} |
||||
} |
||||
|
||||
// Figure out which type of KeyFrame the completion is being called on, and offer completion for their respective fields |
||||
when (keyFrameTypeName) { |
||||
KeyFrameField.Positions.keyWord -> { |
||||
addKeyPositionFields(result, existingFieldsSet) { |
||||
// Some KeyPosition fields take either a Number Array value or a String value |
||||
if (isNumberArrayType(it)) jsonNumberArrayTemplate else JsonStringValueTemplate |
||||
} |
||||
} |
||||
KeyFrameField.Attributes.keyWord -> { |
||||
// KeyAttributes properties are the same as the RenderTransform fields |
||||
addEnumKeywords<RenderTransform>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet) |
||||
} |
||||
KeyFrameField.Cycles.keyWord -> { |
||||
// KeyCycles properties are a mix of RenderTransform fields and KeyCycles specific fields |
||||
addEnumKeywords<RenderTransform>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet) |
||||
addEnumKeywords<KeyCycleField>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet) |
||||
} |
||||
else -> { |
||||
thisLogger().warn("Completion on unknown KeyFrame type: $keyFrameTypeName") |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Add LookupElements to the [result] for each non-repeated [KeyPositionField] using the [InsertionFormat] returned by [templateProvider]. |
||||
*/ |
||||
private fun addKeyPositionFields( |
||||
result: CompletionResultSet, |
||||
existing: Set<String>, |
||||
templateProvider: (KeyPositionField) -> InsertionFormat |
||||
) { |
||||
KeyPositionField.values().forEach { keyPositionField -> |
||||
if (existing.contains(keyPositionField.keyWord)) { |
||||
// Skip repeated fields |
||||
return@forEach |
||||
} |
||||
result.addLookupElement(lookupString = keyPositionField.keyWord, format = templateProvider(keyPositionField)) |
||||
} |
||||
} |
||||
|
||||
private fun isNumberArrayType(keyPositionField: KeyPositionField) = |
||||
when (keyPositionField) { |
||||
// Only some KeyPosition fields receive a Number value |
||||
KeyPositionField.PercentX, |
||||
KeyPositionField.PercentY, |
||||
KeyPositionField.PercentWidth, |
||||
KeyPositionField.PercentHeight -> true |
||||
else -> false |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Provides plaint-text completion for each of the elements in the Enum. |
||||
* |
||||
* The provided values come from [ConstraintLayoutKeyWord.keyWord]. |
||||
*/ |
||||
internal class EnumValuesCompletionProvider<E>(private val enumClass: KClass<E>) |
||||
: CompletionProvider<CompletionParameters>() where E : Enum<E>, E : ConstraintLayoutKeyWord { |
||||
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { |
||||
enumClass.java.enumConstants.forEach { |
||||
result.addLookupElement(lookupString = it.keyWord) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Add the [ConstraintLayoutKeyWord.keyWord] of the enum constants as a completion result that takes a string for its value. |
||||
*/ |
||||
private inline fun <reified E> CompletionResultSet.addEnumKeyWordsWithStringValueTemplate( |
||||
existing: Set<String> |
||||
) where E : Enum<E>, E : ConstraintLayoutKeyWord { |
||||
addEnumKeywords<E>(result = this, existing = existing, format = JsonStringValueTemplate) |
||||
} |
||||
|
||||
/** |
||||
* Add the [ConstraintLayoutKeyWord.keyWord] of the enum constants as a completion result that takes a number for its value. |
||||
*/ |
||||
private inline fun <reified E> CompletionResultSet.addEnumKeyWordsWithNumericValueTemplate( |
||||
existing: Set<String> |
||||
) where E : Enum<E>, E : ConstraintLayoutKeyWord { |
||||
addEnumKeywords<E>(result = this, existing = existing, format = JsonNumericValueTemplate) |
||||
} |
||||
|
||||
/** |
||||
* Helper function to simplify adding enum constant members to the completion result. |
||||
*/ |
||||
private inline fun <reified E> addEnumKeywords( |
||||
result: CompletionResultSet, |
||||
existing: Set<String> = emptySet(), |
||||
format: InsertionFormat? = null |
||||
) where E : Enum<E>, E : ConstraintLayoutKeyWord { |
||||
E::class.java.enumConstants.forEach { constant -> |
||||
if (!existing.contains(constant.keyWord)) { |
||||
result.addLookupElement(lookupString = constant.keyWord, format = format) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,101 @@
|
||||
/* |
||||
* Copyright (C) 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion.constraintlayout.provider.model |
||||
|
||||
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent |
||||
import com.intellij.codeInsight.completion.CompletionParameters |
||||
import com.intellij.json.psi.JsonElement |
||||
import com.intellij.json.psi.JsonObject |
||||
import com.intellij.json.psi.JsonProperty |
||||
import com.intellij.openapi.progress.ProgressManager |
||||
import com.intellij.psi.SmartPointerManager |
||||
import com.intellij.psi.util.parentOfType |
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType |
||||
|
||||
/** |
||||
* Base model for [JsonElement], sets a pointer to avoid holding to the element itself. |
||||
*/ |
||||
internal abstract class BaseJsonElementModel<E: JsonElement>(element: E) { |
||||
protected val elementPointer = SmartPointerManager.createPointer(element) |
||||
} |
||||
|
||||
/** |
||||
* Base model for a [JsonProperty]. |
||||
* |
||||
* Populates some common fields and provides useful function while avoiding holding to PsiElement instances. |
||||
*/ |
||||
internal open class JsonPropertyModel(element: JsonProperty): BaseJsonElementModel<JsonProperty>(element) { |
||||
/** |
||||
* The [JsonObject] that describes this [JsonProperty]. |
||||
*/ |
||||
private val innerJsonObject: JsonObject? = elementPointer.element?.getChildOfType<JsonObject>() |
||||
|
||||
/** |
||||
* A mapping of the containing [JsonProperty]s by their declare name. |
||||
*/ |
||||
private val propertiesByName: Map<String, JsonProperty> = |
||||
innerJsonObject?.propertyList?.associateBy { it.name } ?: emptyMap() |
||||
|
||||
/** |
||||
* [List] of all the children of this element that are [JsonProperty]. |
||||
*/ |
||||
protected val innerProperties: Collection<JsonProperty> = propertiesByName.values |
||||
|
||||
/** |
||||
* Name of the [JsonProperty]. |
||||
*/ |
||||
val name: String? |
||||
get() = elementPointer.element?.name |
||||
|
||||
/** |
||||
* A set of names for all declared properties in this [JsonProperty]. |
||||
*/ |
||||
val declaredFieldNamesSet: Set<String> = propertiesByName.keys |
||||
|
||||
/** |
||||
* For the children of the current element, returns the [JsonProperty] which name matches the given [name]. Null if none of them does. |
||||
*/ |
||||
protected fun findProperty(name: String): JsonProperty? = propertiesByName[name] |
||||
|
||||
/** |
||||
* Returns true if this [JsonProperty] contains another [JsonProperty] declared by the given [name]. |
||||
*/ |
||||
fun containsPropertyOfName(name: String): Boolean = propertiesByName.containsKey(name) |
||||
|
||||
/** |
||||
* Returns the containing [JsonProperty]. |
||||
* |
||||
* May return null if this model is for a top level [JsonProperty]. |
||||
*/ |
||||
fun getParentProperty(): JsonProperty? = elementPointer.element?.parentOfType<JsonProperty>(withSelf = false) |
||||
|
||||
companion object { |
||||
/** |
||||
* Returns the [JsonPropertyModel] where the completion is performed on an inner [JsonProperty], including if the completion is on the |
||||
* value side of the inner [JsonProperty]. |
||||
* |
||||
* In other words, the model of the second [JsonProperty] parent if the element on [CompletionParameters.getPosition] is NOT a |
||||
* [JsonProperty]. |
||||
* |
||||
* Or the model of the first [JsonProperty] parent if the element on [CompletionParameters.getPosition] is a [JsonProperty]. |
||||
*/ |
||||
fun getModelForCompletionOnInnerJsonProperty(parameters: CompletionParameters): JsonPropertyModel? { |
||||
val parentJsonProperty = getJsonPropertyParent(parameters) ?: return null |
||||
ProgressManager.checkCanceled() |
||||
return JsonPropertyModel(parentJsonProperty) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,70 @@
|
||||
/* |
||||
* Copyright (C) 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion.constraintlayout.provider.model |
||||
|
||||
import com.android.tools.compose.code.completion.constraintlayout.KeyWords |
||||
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent |
||||
import com.intellij.codeInsight.completion.CompletionParameters |
||||
import com.intellij.json.psi.JsonProperty |
||||
import com.intellij.json.psi.JsonStringLiteral |
||||
import com.intellij.openapi.progress.ProgressManager |
||||
|
||||
/** |
||||
* Model for the JSON block corresponding to a single ConstraintSet. |
||||
* |
||||
* A ConstraintSet is a state that defines a specific layout of the contents in a ConstraintLayout. |
||||
*/ |
||||
internal class ConstraintSetModel(jsonProperty: JsonProperty) : JsonPropertyModel(jsonProperty) { |
||||
/** |
||||
* List of properties that have a constraint block assigned to it. |
||||
*/ |
||||
private val propertiesWithConstraints = innerProperties.filter { it.name != KeyWords.Extends } |
||||
|
||||
/** |
||||
* Name of the ConstraintSet this is extending constraints from. |
||||
*/ |
||||
val extendsFrom: String? = (findProperty(KeyWords.Extends)?.value as? JsonStringLiteral)?.value |
||||
|
||||
/** |
||||
* List of IDs declared in this ConstraintSet. |
||||
*/ |
||||
val declaredIds = propertiesWithConstraints.map { it.name } |
||||
|
||||
/** |
||||
* The constraints (by widget ID) explicitly declared in this ConstraintSet. |
||||
* |
||||
* Note that it does not resolve constraints inherited from [extendsFrom]. |
||||
*/ |
||||
val constraintsById: Map<String, ConstraintsModel> = |
||||
propertiesWithConstraints.associate { property -> |
||||
property.name to ConstraintsModel(property) |
||||
} |
||||
|
||||
// TODO(b/207030860): Add a method that can pull all resolved constraints for each widget ID, it could be useful to make sure we are not |
||||
// offering options that are implicitly present from the 'Extends' ConstraintSet |
||||
|
||||
companion object { |
||||
/** |
||||
* Returns a [ConstraintSetModel], for when the completion is performed on a property or the value of a property within a ConstraintSet |
||||
* declaration. |
||||
*/ |
||||
fun getModelForCompletionOnConstraintSetProperty(parameters: CompletionParameters): ConstraintSetModel? { |
||||
val parentJsonProperty = getJsonPropertyParent(parameters) ?: return null |
||||
ProgressManager.checkCanceled() |
||||
return ConstraintSetModel(parentJsonProperty) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,66 @@
|
||||
/* |
||||
* Copyright (C) 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion.constraintlayout.provider.model |
||||
|
||||
import com.android.tools.compose.code.completion.constraintlayout.KeyWords |
||||
import com.intellij.json.psi.JsonProperty |
||||
|
||||
/** |
||||
* Model for the `ConstraintSets` Json block. |
||||
* |
||||
* The `ConstraintSets` Json block, is a collection of different ConstraintSets, each of which describes a state of the layout by defining |
||||
* properties of each of its widgets such as width, height or their layout constraints. |
||||
* |
||||
* @param constraintSetsElement The PSI element of the `ConstraintSets` Json property |
||||
*/ |
||||
internal class ConstraintSetsPropertyModel( |
||||
constraintSetsElement: JsonProperty |
||||
) : JsonPropertyModel(constraintSetsElement) { |
||||
// TODO(b/209839226): Explore how we could use these models to validate the syntax or structure of the JSON as well as to check logic |
||||
// correctness through Inspections/Lint |
||||
/** |
||||
* List of all ConstraintSet elements in the Json block. |
||||
*/ |
||||
val constraintSets: List<ConstraintSetModel> = innerProperties.map { ConstraintSetModel(it) } |
||||
|
||||
/** |
||||
* The names of all ConstraintSets in this block. |
||||
*/ |
||||
fun getConstraintSetNames(): Collection<String> { |
||||
return declaredFieldNamesSet |
||||
} |
||||
|
||||
/** |
||||
* Returns the remaining possible fields for the given [constraintSetName], this is done by reading all fields in all ConstraintSets and |
||||
* subtracting the fields already present in [constraintSetName]. Most of these should be the IDs that represent constrained widgets. |
||||
*/ |
||||
fun getRemainingFieldsForConstraintSet(constraintSetName: String): List<String> { |
||||
val availableNames = mutableSetOf(KeyWords.Extends) |
||||
val usedNames = mutableSetOf<String>() |
||||
constraintSets.forEach { constraintSet -> |
||||
constraintSet.declaredFieldNamesSet.forEach { propertyName -> |
||||
if (constraintSet.name == constraintSetName) { |
||||
usedNames.add(propertyName) |
||||
} |
||||
else { |
||||
availableNames.add(propertyName) |
||||
} |
||||
} |
||||
} |
||||
availableNames.removeAll(usedNames) |
||||
return availableNames.toList() |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
/* |
||||
* Copyright (C) 2021 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.code.completion.constraintlayout.provider.model |
||||
|
||||
import com.intellij.json.psi.JsonProperty |
||||
|
||||
/** |
||||
* Model for the JSON block that corresponds to the constraints applied on a widget (defined by an ID). |
||||
* |
||||
* Constraints are a set of instructions that define the widget's dimensions, position with respect to other widgets and render-time |
||||
* transforms. |
||||
*/ |
||||
internal class ConstraintsModel(jsonProperty: JsonProperty): JsonPropertyModel(jsonProperty) { |
||||
// TODO(b/207030860): Fill the contents of this model as is necessary, keeping in mind that it would be useful to have fields like |
||||
// 'verticalConstraints', 'hasBaseline', 'dimensionBehavior', etc... |
||||
} |
@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright (C) 2022 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.completion |
||||
|
||||
import com.android.tools.compose.completion.inserthandler.FormatWithCaretInsertHandler |
||||
import com.android.tools.compose.completion.inserthandler.FormatWithLiveTemplateInsertHandler |
||||
import com.android.tools.compose.completion.inserthandler.FormatWithNewLineInsertHandler |
||||
import com.android.tools.compose.completion.inserthandler.InsertionFormat |
||||
import com.android.tools.compose.completion.inserthandler.LiteralNewLineFormat |
||||
import com.android.tools.compose.completion.inserthandler.LiteralWithCaretFormat |
||||
import com.android.tools.compose.completion.inserthandler.LiveTemplateFormat |
||||
import com.intellij.codeInsight.completion.CompletionResultSet |
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder |
||||
|
||||
/** |
||||
* Utility function to simplify adding [com.intellij.codeInsight.lookup.LookupElement]s with [InsertionFormat] support. |
||||
* |
||||
* Note that the added lookup element is case-sensitive. |
||||
* |
||||
* @param lookupString The base text to autocomplete, also used to match the user input with a completion result. |
||||
* @param tailText Grayed out text shown after the LookupElement name, not part of the actual completion. |
||||
* @param format InsertionFormat to handle the rest of the completion. See different implementations of [InsertionFormat] for more. |
||||
*/ |
||||
fun CompletionResultSet.addLookupElement(lookupString: String, tailText: String? = null, format: InsertionFormat? = null) { |
||||
// Populate the lookupObject param to allow multiple LookupElements with the same lookupString, differentiated by the tailText. |
||||
var lookupBuilder = LookupElementBuilder.create(tailText ?: lookupString, lookupString) |
||||
if (format != null) { |
||||
val insertionHandler = when (format) { |
||||
is LiteralWithCaretFormat -> FormatWithCaretInsertHandler(format) |
||||
is LiteralNewLineFormat -> FormatWithNewLineInsertHandler(format) |
||||
is LiveTemplateFormat -> FormatWithLiveTemplateInsertHandler(format) |
||||
} |
||||
lookupBuilder = lookupBuilder.withInsertHandler(insertionHandler) |
||||
} |
||||
lookupBuilder = lookupBuilder.withCaseSensitivity(true) |
||||
if (tailText != null) { |
||||
lookupBuilder = lookupBuilder.withTailText(tailText, true) |
||||
} |
||||
addElement(lookupBuilder) |
||||
} |
@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright (C) 2022 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.completion.inserthandler |
||||
|
||||
import com.intellij.codeInsight.completion.InsertHandler |
||||
import com.intellij.codeInsight.completion.InsertionContext |
||||
import com.intellij.codeInsight.lookup.LookupElement |
||||
import com.intellij.openapi.editor.EditorModificationUtil |
||||
import com.intellij.openapi.editor.actions.EditorActionUtil |
||||
import com.intellij.psi.PsiDocumentManager |
||||
|
||||
/** |
||||
* Handles insertions of an [InsertionFormat], moving the caret at the position specified by the '|' character. |
||||
*/ |
||||
class FormatWithCaretInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> { |
||||
override fun handleInsert(context: InsertionContext, item: LookupElement) { |
||||
with(context) { |
||||
val isMoveCaret = format.insertableString.contains('|') |
||||
val stringToInsert = format.insertableString.replace("|", "") |
||||
|
||||
// Insert the string without the reserved character: | |
||||
EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true) |
||||
PsiDocumentManager.getInstance(project).commitDocument(document) |
||||
|
||||
// Move caret to the position indicated by '|' |
||||
EditorActionUtil.moveCaretToLineEnd(editor, false, true) |
||||
if (isMoveCaret && stringToInsert.isNotEmpty()) { |
||||
val caretPosition = format.insertableString.indexOf('|').coerceAtLeast(0) |
||||
EditorModificationUtil.moveCaretRelatively(editor, caretPosition - stringToInsert.length) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,86 @@
|
||||
/* |
||||
* Copyright (C) 2022 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.completion.inserthandler |
||||
|
||||
import com.intellij.codeInsight.completion.InsertHandler |
||||
import com.intellij.codeInsight.completion.InsertionContext |
||||
import com.intellij.codeInsight.daemon.impl.quickfix.EmptyExpression |
||||
import com.intellij.codeInsight.lookup.LookupElement |
||||
import com.intellij.codeInsight.template.TemplateManager |
||||
import com.intellij.codeInsight.template.impl.ConstantNode |
||||
|
||||
/** |
||||
* Handles insertions of an [InsertionFormat] using the [TemplateManager], stopping at every '<>' for user input. |
||||
*/ |
||||
class FormatWithLiveTemplateInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> { |
||||
override fun handleInsert(context: InsertionContext, item: LookupElement) { |
||||
val templateManager = TemplateManager.getInstance(context.project) |
||||
val template = templateManager.createTemplate("", "") |
||||
|
||||
// Create template from the given format |
||||
getTemplateSegments(format).forEach { segment -> |
||||
val text = segment.textSegment |
||||
if (segment.takesUserInput) { |
||||
if (text.isNotEmpty()) { |
||||
template.addVariable(ConstantNode(text), true) |
||||
} |
||||
else { |
||||
template.addVariable(EmptyExpression(), true) |
||||
} |
||||
} |
||||
else { |
||||
template.addTextSegment(text) |
||||
} |
||||
} |
||||
|
||||
templateManager.startTemplate(context.editor, template) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Extracts the insertable text segments from the [InsertionFormat] indicating whether each segment is simple text or if it expects user |
||||
* input. |
||||
*/ |
||||
private fun getTemplateSegments(format: InsertionFormat): List<LiveTemplateSegment> { |
||||
val segments = mutableListOf<LiveTemplateSegment>() |
||||
val templateText = format.insertableString |
||||
var start = 0 |
||||
var end = 0 |
||||
// Normal text does not take user input |
||||
var isNormalText = true |
||||
|
||||
while (end < templateText.length) { |
||||
val currentChar = templateText.elementAtOrNull(end) |
||||
if (currentChar == '<' || currentChar == '>') { |
||||
// Stop at the marker characters and add any pending segment |
||||
segments.add(LiveTemplateSegment(takesUserInput = !isNormalText, templateText.substring(start, end))) |
||||
isNormalText = currentChar == '>' |
||||
start = end + 1 // update start but skip this char |
||||
} |
||||
end++ |
||||
} |
||||
if (end - start > 1) { |
||||
// Add the last segment if not empty (end index is exclusive) |
||||
segments.add(LiveTemplateSegment(takesUserInput = !isNormalText, templateText.substring(start, end))) |
||||
} |
||||
|
||||
return segments |
||||
} |
||||
|
||||
private data class LiveTemplateSegment( |
||||
val takesUserInput: Boolean, |
||||
val textSegment: String |
||||
) |
@ -0,0 +1,50 @@
|
||||
/* |
||||
* Copyright (C) 2022 The Android Open Source Project |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.android.tools.compose.completion.inserthandler |
||||
|
||||
/** |
||||
* Describes a string that may be automatically inserted when selecting an autocomplete option. |
||||
*/ |
||||
sealed class InsertionFormat( |
||||
val insertableString: String |
||||
) |
||||
|
||||
/** |
||||
* Inserts the string after the auto-completed value. |
||||
* |
||||
* The caret will be moved to the position marked by the '|' character. |
||||
*/ |
||||
class LiteralWithCaretFormat(literalFormat: String) : InsertionFormat(literalFormat) |
||||
|
||||
/** |
||||
* Inserts the string after the auto-complete value. |
||||
* |
||||
* It will insert a new line as if it was done by an ENTER keystroke, marked by the '\n' character. |
||||
* |
||||
* Note that it will only apply the new line on the first '\n' character. |
||||
*/ |
||||
class LiteralNewLineFormat(literalFormat: String) : InsertionFormat(literalFormat) |
||||
|
||||
/** |
||||
* Inserts a string driven by Live templates. The string is inserted after the auto-completed value. |
||||
* |
||||
* Use '<' and '>' to delimit a range of text the user is expected to edit, may contain multiple instances of these delimiters. |
||||
* |
||||
* Eg: For the string `"<0123>, <text>"`. The '0123' will be selected in the editor for the user to modify, once they press Enter, it |
||||
* will select 'text' for the user to modify until all marked snippets of the strings are handled or the user presses ESC to keep the text |
||||
* as is. |
||||
*/ |
||||
class LiveTemplateFormat(templateFormat: String) : InsertionFormat(templateFormat) |
Loading…
Reference in new issue