From d292b8bdcdc2903a780f559488fb948ddf227a9c Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 10 Sep 2022 10:09:29 +0530 Subject: [PATCH] Added attribute completion provider for XML drawables and animations --- changelogs/v2.1.1-beta.md | 7 ++ .../xml/providers/XmlCompletionProvider.kt | 32 +++++- .../common/CommonAttrCompletionProvider.kt | 53 ++++++++++ .../ManifestAttrCompletionProvider.kt | 3 +- .../manifest/ManifestTagCompletionProvider.kt | 2 +- .../completion/manifestFileCompletions.kt | 57 +---------- .../providers/completion/xmlCompletions.kt | 57 ++++++++++- .../androidide/lsp/xml/utils/tagTransforms.kt | 88 +++++++++++++++++ .../CommonAttrCompletionProviderTest.kt | 49 +++++++++ .../lsp/xml/utils/TagTransformsTest.kt | 93 ++++++++++++++++++ tests/test-home/.androidide/init/model.jar | Bin 59879 -> 59879 bytes .../drawable/TestDrawableAttrs_template.xml | 4 + 12 files changed, 381 insertions(+), 64 deletions(-) create mode 100644 changelogs/v2.1.1-beta.md create mode 100644 lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/CommonAttrCompletionProvider.kt create mode 100644 lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/tagTransforms.kt create mode 100644 lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/CommonAttrCompletionProviderTest.kt create mode 100644 lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/utils/TagTransformsTest.kt create mode 100644 tests/test-project/app/src/main/res/drawable/TestDrawableAttrs_template.xml diff --git a/changelogs/v2.1.1-beta.md b/changelogs/v2.1.1-beta.md new file mode 100644 index 000000000..97b76038c --- /dev/null +++ b/changelogs/v2.1.1-beta.md @@ -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. \ No newline at end of file diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt index de0da850f..4d79e612e 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt @@ -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 diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/CommonAttrCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/CommonAttrCompletionProvider.kt new file mode 100644 index 000000000..c3c5be213 --- /dev/null +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/CommonAttrCompletionProvider.kt @@ -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 . + */ + +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 { + 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() + } +} diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt index d58ecd72f..0eeb8c80c 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt @@ -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 { 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) } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestTagCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestTagCompletionProvider.kt index 8a06f4748..414f68d8b 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestTagCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestTagCompletionProvider.kt @@ -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) { diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifestFileCompletions.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifestFileCompletions.kt index 56801589b..2ca1eb594 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifestFileCompletions.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifestFileCompletions.kt @@ -38,59 +38,4 @@ fun manifestResourceTable(): Set { return setOf( 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() -} +} \ No newline at end of file diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt index b1695af59..ca04d9b9f 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt @@ -55,4 +55,59 @@ fun findAllNamespaces(node: DOMNode): MutableSet> { curr = curr.parentNode } return namespaces -} \ No newline at end of file +} + +/** + * 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() +} diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/tagTransforms.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/tagTransforms.kt new file mode 100644 index 000000000..2a15ec170 --- /dev/null +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/utils/tagTransforms.kt @@ -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 . + */ + +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) +} diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/CommonAttrCompletionProviderTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/CommonAttrCompletionProviderTest.kt new file mode 100644 index 000000000..89ccdc236 --- /dev/null +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/CommonAttrCompletionProviderTest.kt @@ -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 . + */ + +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") + } + } +} \ No newline at end of file diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/utils/TagTransformsTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/utils/TagTransformsTest.kt new file mode 100644 index 000000000..41f58ef49 --- /dev/null +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/utils/TagTransformsTest.kt @@ -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 . + */ + +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) + } + } +} diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index 1728e1b6975931a37b4b40ee4fc86936c8189d4d..6f729e921cf1ffdb59c48a858475033c77882f48 100644 GIT binary patch delta 33 ncmaEUnfdu;X5IjAW)?061`ZAew*ak;yy + +