mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Added attribute completion provider for XML drawables and animations
This commit is contained in:
@@ -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.
|
||||
+27
-5
@@ -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
|
||||
|
||||
+53
@@ -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()
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-56
@@ -38,59 +38,4 @@ fun manifestResourceTable(): Set<ResourceTable> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
+56
-1
@@ -55,4 +55,59 @@ fun findAllNamespaces(node: DOMNode): MutableSet<Pair<String, String>> {
|
||||
curr = curr.parentNode
|
||||
}
|
||||
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)
|
||||
}
|
||||
+49
@@ -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>
|
||||
Reference in New Issue
Block a user