Added attribute completion provider for XML drawables and animations

This commit is contained in:
Akash Yadav
2022-09-10 10:09:29 +05:30
parent 7e4fac4fe8
commit d292b8bdcd
12 changed files with 381 additions and 64 deletions
+7
View File
@@ -0,0 +1,7 @@
# **AndroidIDE v2.1.1-beta**
This is a patch release which fixes a bug.
## Fixes
- Bug which caused AbstractMethodError in Tooling API implementation.
@@ -18,6 +18,8 @@
package com.itsaky.androidide.lsp.xml.providers
import com.android.SdkConstants.ANDROID_MANIFEST_XML
import com.android.aaptcompiler.AaptResourceType.ANIM
import com.android.aaptcompiler.AaptResourceType.DRAWABLE
import com.android.aaptcompiler.AaptResourceType.LAYOUT
import com.android.aaptcompiler.ResourcePathData
import com.android.aaptcompiler.extractPathData
@@ -27,14 +29,19 @@ import com.itsaky.androidide.lsp.api.IServerSettings
import com.itsaky.androidide.lsp.models.CompletionParams
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.CompletionResult.Companion.EMPTY
import com.itsaky.androidide.lsp.xml.providers.completion.IXmlCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.AttrValueCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.IXmlCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.canCompleteManifest
import com.itsaky.androidide.lsp.xml.providers.completion.common.CommonAttrCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.layout.LayoutAttrCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.layout.LayoutTagCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestAttrCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestAttrValueCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestTagCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.canCompleteManifest
import com.itsaky.androidide.lsp.xml.utils.AnimTagTransformer
import com.itsaky.androidide.lsp.xml.utils.DrawableTagTransformer
import com.itsaky.androidide.lsp.xml.utils.ITagTransformer
import com.itsaky.androidide.lsp.xml.utils.NoOpTagTransformer
import com.itsaky.androidide.lsp.xml.utils.XmlUtils
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.ATTRIBUTE
@@ -46,10 +53,10 @@ import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.StopWatch
import com.itsaky.xml.INamespace
import io.github.rosemoe.sora.text.ContentReference
import java.io.IOException
import java.io.Reader
import org.eclipse.lemminx.dom.DOMParser
import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager
import java.io.IOException
import java.io.Reader
import kotlin.io.path.name
/**
@@ -131,10 +138,25 @@ class XmlCompletionProvider(settings: IServerSettings) :
return when (pathData.type) {
LAYOUT -> createLayoutCompleter(type)
null -> createNullTypeCompleter(pathData, type)
else -> createCommonCompleter(pathData, type)
}
}
private fun createCommonCompleter(pathData: ResourcePathData, type: NodeType): IXmlCompletionProvider? {
return when(type) {
ATTRIBUTE -> CommonAttrCompletionProvider( tagTransformerFor(pathData),this)
else -> null
}
}
private fun tagTransformerFor(pathData: ResourcePathData): ITagTransformer {
return when(pathData.type) {
ANIM -> AnimTagTransformer
DRAWABLE -> DrawableTagTransformer
else -> NoOpTagTransformer
}
}
private fun createNullTypeCompleter(
pathData: ResourcePathData,
type: NodeType
@@ -0,0 +1,53 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.xml.providers.completion.common
import com.android.aaptcompiler.ConfigDescription
import com.android.aaptcompiler.ResourceGroup
import com.android.aaptcompiler.Styleable
import com.itsaky.androidide.lsp.api.ICompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.AttrCompletionProvider
import com.itsaky.androidide.lsp.xml.utils.ITagTransformer
import org.eclipse.lemminx.dom.DOMNode
/**
* Provides attribute completion for all other resource types.
*
* @property tagTransform A function which returns the styleable entry name for the given tag name
* (first param) and its parent's tag name (second param).
* @author Akash Yadav
*/
internal class CommonAttrCompletionProvider(
private val tagTransform: ITagTransformer,
provider: ICompletionProvider
) : AttrCompletionProvider(provider) {
override fun findNodeStyleables(node: DOMNode, styleables: ResourceGroup): Set<Styleable> {
val name = node.nodeName
val styleable =
styleables
.findEntry(tagTransform.transform(name, nodeAtCursor.parentNode?.nodeName ?: ""))
?.findValue(ConfigDescription())
?.value
if (styleable != null && styleable is Styleable) {
return setOf(styleable)
}
return emptySet()
}
}
@@ -23,6 +23,7 @@ import com.android.aaptcompiler.ResourcePathData
import com.android.aaptcompiler.Styleable
import com.itsaky.androidide.lsp.api.ICompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.AttrCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.MANIFEST_TAG_PREFIX
import com.itsaky.androidide.lsp.xml.providers.completion.canCompleteManifest
import com.itsaky.androidide.lsp.xml.providers.completion.manifestResourceTable
import com.itsaky.androidide.lsp.xml.providers.completion.transformToEntryName
@@ -46,7 +47,7 @@ class ManifestAttrCompletionProvider(provider: ICompletionProvider) :
override fun findNodeStyleables(node: DOMNode, styleables: ResourceGroup): Set<Styleable> {
val name = node.nodeName
val styleable =
styleables.findEntry(transformToEntryName(name))?.findValue(ConfigDescription())?.value
styleables.findEntry(transformToEntryName(name, MANIFEST_TAG_PREFIX))?.findValue(ConfigDescription())?.value
if (styleable != null && styleable is Styleable) {
return setOf(styleable)
}
@@ -74,7 +74,7 @@ class ManifestTagCompletionProvider(provider: ICompletionProvider) :
styleables
.findEntries { it.startsWith(MANIFEST_TAG_PREFIX) }
.map { transformToTagName(it.name) }
.map { transformToTagName(it.name, MANIFEST_TAG_PREFIX) }
.forEach {
val match = matchLevel(it, newPrefix)
if (match == NO_MATCH) {
@@ -39,58 +39,3 @@ fun manifestResourceTable(): Set<ResourceTable> {
Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES) ?: return emptySet()
)
}
/**
* Transforms entry name to tag name.
*
* For example: `AndroidManifestUsesPermission` -> `uses-permission`
*/
fun transformToTagName(entryName: String): String {
val name = StringBuilder()
var index = MANIFEST_TAG_PREFIX.length
while (index < entryName.length) {
var c = entryName[index]
if (c.isUpperCase()) {
if (index != MANIFEST_TAG_PREFIX.length) {
name.append('-')
}
c = c.lowercaseChar()
}
name.append(c)
++index
}
return name.toString()
}
/**
* Transforms entry name to tag name.
*
* For example: `uses-permission` -> `AndroidManifestUsesPermission`
*/
fun transformToEntryName(tagName: String): String {
if (tagName == "manifest") {
return MANIFEST_TAG_PREFIX
}
val name = StringBuilder(MANIFEST_TAG_PREFIX)
var index = 0
var capitalize = false
while (index < tagName.length) {
var c = tagName[index]
if (c == '-') {
capitalize = true
++index
continue
}
if (index == 0 || capitalize) {
c = c.uppercaseChar()
capitalize = false
}
name.append(c)
++index
}
return name.toString()
}
@@ -56,3 +56,58 @@ fun findAllNamespaces(node: DOMNode): MutableSet<Pair<String, String>> {
}
return namespaces
}
/**
* Transforms entry name to tag name.
*
* For example: `AndroidManifestUsesPermission` -> `uses-permission`
*/
fun transformToTagName(entryName: String, prefix: String = ""): String {
val name = StringBuilder()
var index = prefix.length
while (index < entryName.length) {
var c = entryName[index]
if (c.isUpperCase()) {
if (index != prefix.length) {
name.append('-')
}
c = c.lowercaseChar()
}
name.append(c)
++index
}
return name.toString()
}
/**
* Transforms entry name to tag name.
*
* For example: `uses-permission` -> `AndroidManifestUsesPermission`
*/
fun transformToEntryName(tagName: String, prefix: String = ""): String {
if (tagName == "manifest") {
return MANIFEST_TAG_PREFIX
}
val name = StringBuilder(prefix)
var index = 0
var capitalize = false
while (index < tagName.length) {
var c = tagName[index]
if (c == '-') {
capitalize = true
++index
continue
}
if (index == 0 || capitalize) {
c = c.uppercaseChar()
capitalize = false
}
name.append(c)
++index
}
return name.toString()
}
@@ -0,0 +1,88 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.xml.utils
import com.itsaky.androidide.lsp.xml.providers.completion.transformToEntryName
/** Transforms tag names to styleable entry names. */
internal interface ITagTransformer {
/** Returns the styleable entry name for the given tag name and its parent's tag name. */
fun transform(tag: String, parent: String = ""): String
}
internal object NoOpTagTransformer : ITagTransformer {
override fun transform(tag: String, parent: String): String {
return ""
}
}
internal object DrawableTagTransformer : ITagTransformer {
override fun transform(tag: String, parent: String): String {
if (tag == "corners") {
return "DrawableCorners"
}
val prefix =
if (parent.isNotEmpty()) {
toEntry(parent).run { if (startsWith('#')) toEntry(tag) else this }
} else tag
val suffix =
if (parent.isNotEmpty() && !parent.startsWith('#')) transformToEntryName(tag) else ""
return "${prefix}Drawable${suffix}"
}
}
internal object AnimTagTransformer : ITagTransformer {
override fun transform(tag: String, parent: String): String {
if (tag == "set") {
return "AnimationSet"
}
return "${toEntry(tag)}${animEntrySuffix(tag)}"
}
/** Returns the suffix that is required to get the styleable entry for an anim resource. */
private fun animEntrySuffix(tag: String): String {
return when {
tag.endsWith("Animation") -> ""
tag.endsWith("Interpolator") -> ""
else -> "Animation"
}
}
}
/**
* Convert the tag name to entry name.
*
* @param tag The tag name.
*/
private fun toEntry(tag: String): String {
if (tag.startsWith('#')) {
return tag
}
when (tag) {
"selector" -> return "StateList"
"animated-selector" -> return "AnimatedStateList"
"animation" -> return "Animation"
"shape" -> return "Gradient"
}
return transformToEntryName(tag)
}
@@ -0,0 +1,49 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.xml.providers.completion
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lsp.xml.CompletionHelper
import com.itsaky.androidide.lsp.xml.CompletionHelperImpl
import com.itsaky.androidide.lsp.xml.XMLLSPTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/**
* @author Akash Yadav
*/
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class CommonAttrCompletionProviderTest : CompletionHelper by CompletionHelperImpl() {
@Before
fun setup() = XMLLSPTest.initProjectIfNeeded()
@Test
fun `test simple drawable attributes`() {
XMLLSPTest.apply {
openFile("../res/drawable/TestDrawableAttrs")
val (incomplete, items) = complete()
assertThat(incomplete).isFalse()
assertThat(items).contains("android:shape")
}
}
}
@@ -0,0 +1,93 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.xml.utils
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/** @author Akash Yadav */
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class TagTransformsTest {
@Test
fun `test drawable tag transforms`() {
val tagMap =
mapOf(
"selector" to "StateListDrawable",
"animated-selector" to "AnimatedStateListDrawable",
"animation" to "AnimationDrawable",
"shape" to "GradientDrawable",
"corners" to "DrawableCorners",
"layer" to "LayerDrawable",
"level-list" to "LevelListDrawable",
"rotate" to "RotateDrawable",
"animated-rotate" to "AnimatedRotateDrawable",
"inset" to "InsetDrawable",
"bitmap" to "BitmapDrawable",
"nine-patch" to "NinePatchDrawable",
"adaptive-icon" to "AdaptiveIconDrawable",
"ripple" to "RippleDrawable",
"scale" to "ScaleDrawable",
"clip" to "ClipDrawable",
"vector" to "VectorDrawable",
"animated-vector" to "AnimatedVectorDrawable"
)
tagMap.forEach { (tag, entry) ->
assertThat(DrawableTagTransformer.transform(tag, "#document")).isEqualTo(entry)
}
}
@Test
fun `test drawable sub tag transforms`() {
val tags =
listOf(
Triple("item", "selector", "StateListDrawableItem"),
Triple("item", "animated-selector", "AnimatedStateListDrawableItem"),
Triple("transition", "animated-selector", "AnimatedStateListDrawableTransition"),
Triple("size", "shape", "GradientDrawableSize"),
Triple("padding", "shape", "GradientDrawablePadding"),
Triple("gradient", "shape", "GradientDrawableGradient")
)
tags.forEach {
assertThat(DrawableTagTransformer.transform(it.first, it.second)).isEqualTo(it.third)
}
}
@Test
fun `test animation tag transforms`() {
val tagMap =
mapOf(
"set" to "AnimationSet",
"rotate" to "RotateAnimation",
"scale" to "ScaleAnimation",
"gridLayoutAnimation" to "GridLayoutAnimation",
"accelerateInterpolator" to "AccelerateInterpolator",
"accelerateDecelerateInterpolator" to "AccelerateDecelerateInterpolator"
)
tagMap.forEach { (tag, entry) ->
assertThat(AnimTagTransformer.transform(tag)).isEqualTo(entry)
}
}
}
Binary file not shown.
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
sh@@cursor@@>
</shape>