From 4ca3c546154de43bb0a9075fb8947d351dce39c1 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sun, 28 Aug 2022 12:29:50 +0530 Subject: [PATCH] Added completion provider for manifest tags --- .../language/CommonCompletionProvider.kt | 11 +- .../androidide/projects/api/AndroidModule.kt | 6 + .../xml/resources/ResourceTableRegistry.kt | 7 +- .../xml/providers/XmlCompletionProvider.kt | 31 ++++- .../completion/IXmlCompletionProvider.kt | 6 +- .../layout/LayoutTagCompletionProvider.kt | 3 +- .../ManifestCompletionProvider.kt} | 23 +++- .../manifest/ManifestTagCompletionProvider.kt | 106 ++++++++++++++++++ .../itsaky/androidide/lsp/xml/XMLLSPTest.kt | 11 +- .../ManifestTagCompletionProviderTest.kt | 59 ++++++++++ tests/test-home/.androidide/init/model.jar | Bin 59879 -> 59879 bytes .../ManifestPermTagTest_template.xml | 23 ++++ .../ManifestUsesTagTest_template.xml | 23 ++++ 13 files changed, 288 insertions(+), 21 deletions(-) rename lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/{TagCompletionProvider.kt => manifest/ManifestCompletionProvider.kt} (54%) create mode 100644 lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestTagCompletionProvider.kt create mode 100644 lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestTagCompletionProviderTest.kt create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestPermTagTest_template.xml create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestUsesTagTest_template.xml diff --git a/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.kt b/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.kt index 0da8e8b47..ed18860b5 100644 --- a/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.kt +++ b/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.kt @@ -114,17 +114,22 @@ class CommonCompletionProvider(private val server: ILanguageServer) { val frameworkResources = module.getFrameworkResourceTable() if (frameworkResources != null) { - lookup.update(ResourceTableRegistry.COMPLETION_FRAMEWORK_RES_LOOKUP_KEY, frameworkResources) + lookup.update(ResourceTableRegistry.COMPLETION_FRAMEWORK_RES, frameworkResources) } val moduleResources = module.getSourceResourceTables() if (moduleResources.isNotEmpty()) { - lookup.update(ResourceTableRegistry.COMPLETION_MODULE_RES_LOOKUP_KEY, moduleResources) + lookup.update(ResourceTableRegistry.COMPLETION_MODULE_RES, moduleResources) } val depResTables = module.getDependencyResourceTables() if (depResTables.isNotEmpty()) { - lookup.update(ResourceTableRegistry.COMPLETION_DEP_RES_LOOKUP_KEY, depResTables) + lookup.update(ResourceTableRegistry.COMPLETION_DEP_RES, depResTables) + } + + val manifestAttrTable = module.getManifestAttrTable() + if (manifestAttrTable != null) { + lookup.update(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES, manifestAttrTable) } } } diff --git a/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt b/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt index 91abae6ff..31e338cfc 100644 --- a/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt +++ b/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt @@ -352,5 +352,11 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this.. } } + /** Get the resource table for the attrs_manifest.xml file. */ + fun getManifestAttrTable(): ResourceTable? { + val platform = getPlatformDir() ?: return null + return ResourceTableRegistry.getInstance().getManifestAttrTable(platform) + } + private fun getPlatformDir() = bootClassPaths.firstOrNull { it.name == "android.jar" }?.parentFile } diff --git a/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt index 6814f6bca..c31903ffd 100644 --- a/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt +++ b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt @@ -33,9 +33,10 @@ interface ResourceTableRegistry : XmlRegistry { companion object { const val PCK_ANDROID = "android" - @JvmStatic val COMPLETION_MODULE_RES_LOOKUP_KEY = Lookup.Key>() - @JvmStatic val COMPLETION_DEP_RES_LOOKUP_KEY = Lookup.Key>() - @JvmStatic val COMPLETION_FRAMEWORK_RES_LOOKUP_KEY = Lookup.Key() + @JvmStatic val COMPLETION_MODULE_RES = Lookup.Key>() + @JvmStatic val COMPLETION_DEP_RES = Lookup.Key>() + @JvmStatic val COMPLETION_FRAMEWORK_RES = Lookup.Key() + @JvmStatic val COMPLETION_MANIFEST_ATTR_RES = Lookup.Key() @JvmStatic fun getInstance(): ResourceTableRegistry = DefaultResourceTableRegistry } 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 f5f3c267e..304438024 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 @@ -17,6 +17,7 @@ package com.itsaky.androidide.lsp.xml.providers +import com.android.SdkConstants.ANDROID_MANIFEST_XML import com.android.aaptcompiler.AaptResourceType.LAYOUT import com.android.aaptcompiler.ResourcePathData import com.android.aaptcompiler.extractPathData @@ -27,9 +28,11 @@ 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.layout.LayoutAttributeCompletionProvider import com.itsaky.androidide.lsp.xml.providers.completion.common.AttrValueCompletionProvider +import com.itsaky.androidide.lsp.xml.providers.completion.layout.LayoutAttributeCompletionProvider import com.itsaky.androidide.lsp.xml.providers.completion.layout.LayoutTagCompletionProvider +import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestCompletionProvider +import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestTagCompletionProvider 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 @@ -84,7 +87,7 @@ class XmlCompletionProvider(settings: IServerSettings) : if (prefix.isBlank() && type != ATTRIBUTE_VALUE) { return EMPTY } - + val pathData = extractPathData(params.file.toFile()) val completer = @@ -120,6 +123,30 @@ class XmlCompletionProvider(settings: IServerSettings) : private fun getCompleter(pathData: ResourcePathData, type: NodeType): IXmlCompletionProvider? { return when (pathData.type) { LAYOUT -> createLayoutCompleter(type) + null -> createNullTypeCompleter(pathData, type) + else -> null + } + } + + private fun createNullTypeCompleter( + pathData: ResourcePathData, + type: NodeType + ): IXmlCompletionProvider? { + + // In test cases + if (ManifestCompletionProvider.canComplete(pathData, type)) { + return createManifestCompleter(type) + } + + return when (pathData.file.name) { + ANDROID_MANIFEST_XML -> createManifestCompleter(type) + else -> null + } + } + + private fun createManifestCompleter(type: NodeType): IXmlCompletionProvider? { + return when (type) { + TAG -> ManifestTagCompletionProvider(this) else -> null } } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/IXmlCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/IXmlCompletionProvider.kt index d51e24c24..924b3a9c5 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/IXmlCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/IXmlCompletionProvider.kt @@ -229,7 +229,7 @@ abstract class IXmlCompletionProvider(private val provider: ICompletionProvider) if (pck == ResourceTableRegistry.PCK_ANDROID) { val platformResTable = - Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_FRAMEWORK_RES_LOOKUP_KEY) + Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_FRAMEWORK_RES) ?: run { log.debug("No platform resource table is set") return emptySet() @@ -251,9 +251,9 @@ abstract class IXmlCompletionProvider(private val provider: ICompletionProvider) protected open fun findAllModuleResourceTables(): Set { val lookup = Lookup.DEFAULT val sourceResTables = - lookup.lookup(ResourceTableRegistry.COMPLETION_MODULE_RES_LOOKUP_KEY) ?: emptySet() + lookup.lookup(ResourceTableRegistry.COMPLETION_MODULE_RES) ?: emptySet() val depResTables = - lookup.lookup(ResourceTableRegistry.COMPLETION_DEP_RES_LOOKUP_KEY) ?: emptySet() + lookup.lookup(ResourceTableRegistry.COMPLETION_DEP_RES) ?: emptySet() return mutableSetOf().also { it.addAll(sourceResTables) it.addAll(depResTables) diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt index f98f64bf6..a211a7ea0 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt @@ -24,14 +24,13 @@ import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.MatchLevel import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH import com.itsaky.androidide.lsp.xml.providers.completion.IXmlCompletionProvider -import com.itsaky.androidide.lsp.xml.providers.completion.TagCompletionProvider import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.TAG import kotlin.math.max import org.eclipse.lemminx.dom.DOMDocument /** - * [TagCompletionProvider] implementation for providing completing tags in an XML layout file. + * [LayoutCompletionProvider] implementation for providing completing tags in an XML layout file. * * @author Akash Yadav */ diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/TagCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestCompletionProvider.kt similarity index 54% rename from lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/TagCompletionProvider.kt rename to lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestCompletionProvider.kt index 9e20b1fd2..342c0cc97 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/TagCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestCompletionProvider.kt @@ -15,21 +15,34 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.lsp.xml.providers.completion +package com.itsaky.androidide.lsp.xml.providers.completion.manifest +import com.android.SdkConstants import com.android.aaptcompiler.ResourcePathData import com.itsaky.androidide.lsp.api.ICompletionProvider +import com.itsaky.androidide.lsp.xml.providers.completion.IXmlCompletionProvider import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType -import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.TAG +import com.itsaky.androidide.utils.VMUtils /** - * Base class for provider which provide XML tag completions. + * Base class for completers which provide completions in Android manifest. * * @author Akash Yadav */ -abstract class TagCompletionProvider(provider: ICompletionProvider) : IXmlCompletionProvider(provider) { +abstract class ManifestCompletionProvider(provider: ICompletionProvider) : + IXmlCompletionProvider(provider) { + companion object { + @JvmStatic + fun canComplete(pathData: ResourcePathData, type: NodeType) : Boolean { + return pathData.file.name == SdkConstants.ANDROID_MANIFEST_XML || + (VMUtils.isJvm() && + pathData.file.name.startsWith("Manifest") && + pathData.file.name.endsWith("_template.xml")) + } + } + override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean { - return super.canProvideCompletions(pathData, type) && type == TAG + return super.canProvideCompletions(pathData, type) && canComplete(pathData, type) } } 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 new file mode 100644 index 000000000..77bda6a73 --- /dev/null +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestTagCompletionProvider.kt @@ -0,0 +1,106 @@ +/* + * 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.manifest + +import com.android.aaptcompiler.AaptResourceType.STYLEABLE +import com.android.aaptcompiler.ResourcePathData +import com.itsaky.androidide.aapt.findEntries +import com.itsaky.androidide.lookup.Lookup +import com.itsaky.androidide.lsp.api.ICompletionProvider +import com.itsaky.androidide.lsp.models.CompletionItem +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.models.MatchLevel.NO_MATCH +import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType +import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.TAG +import com.itsaky.androidide.xml.resources.ResourceTableRegistry +import org.eclipse.lemminx.dom.DOMDocument + +/** + * Provides tag completion in AndroidManifest. + * + * @author Akash Yadav + */ +class ManifestTagCompletionProvider(provider: ICompletionProvider) : + ManifestCompletionProvider(provider) { + + companion object { + private const val MANIFEST_TAG_PREFIX = "AndroidManifest" + } + + override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean { + return super.canProvideCompletions(pathData, type) && type == TAG + } + + override fun doComplete( + params: CompletionParams, + pathData: ResourcePathData, + document: DOMDocument, + type: NodeType, + prefix: String + ): CompletionResult { + val newPrefix = + if (prefix.startsWith("<")) { + prefix.substring(1) + } else { + prefix + } + + val styleables = + Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES) + ?.findPackage(ResourceTableRegistry.PCK_ANDROID) + ?.findGroup(STYLEABLE) + ?: return EMPTY + + val result = mutableListOf() + + styleables + .findEntries { it.startsWith(MANIFEST_TAG_PREFIX) } + .map { transformTagName(it.name) } + .forEach { + val match = matchLevel(it, newPrefix) + if (match == NO_MATCH) { + return@forEach + } + + result.add(createTagCompletionItem(it, it, match)) + } + + return CompletionResult(result) + } + + private fun transformTagName(tag: String): String { + val name = StringBuilder() + var index = MANIFEST_TAG_PREFIX.length + while (index < tag.length) { + var c = tag[index] + if (c.isUpperCase()) { + if (index != MANIFEST_TAG_PREFIX.length) { + name.append('-') + } + c = c.lowercaseChar() + } + + name.append(c) + ++index + } + + return name.toString() + } +} diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt index e1edc340a..2679f24e7 100644 --- a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/XMLLSPTest.kt @@ -55,17 +55,22 @@ object XMLLSPTest : LSPTest() { val frameworkResources = module.getFrameworkResourceTable() if (frameworkResources != null) { - lookup.update(ResourceTableRegistry.COMPLETION_FRAMEWORK_RES_LOOKUP_KEY, frameworkResources) + lookup.update(ResourceTableRegistry.COMPLETION_FRAMEWORK_RES, frameworkResources) } val moduleResources = module.getSourceResourceTables() if (moduleResources.isNotEmpty()) { - lookup.update(ResourceTableRegistry.COMPLETION_MODULE_RES_LOOKUP_KEY, moduleResources) + lookup.update(ResourceTableRegistry.COMPLETION_MODULE_RES, moduleResources) } val depResTables = module.getDependencyResourceTables() if (depResTables.isNotEmpty()) { - lookup.update(ResourceTableRegistry.COMPLETION_DEP_RES_LOOKUP_KEY, depResTables) + lookup.update(ResourceTableRegistry.COMPLETION_DEP_RES, depResTables) + } + + val manifestAttrTable = module.getManifestAttrTable() + if (manifestAttrTable != null) { + lookup.update(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES, manifestAttrTable) } } catch (e: Throwable) { throw RuntimeException(e) diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestTagCompletionProviderTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestTagCompletionProviderTest.kt new file mode 100644 index 000000000..d9e2c0958 --- /dev/null +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestTagCompletionProviderTest.kt @@ -0,0 +1,59 @@ +/* + * 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 ManifestTagCompletionProviderTest : CompletionHelper by CompletionHelperImpl() { + + @Before + fun setup () = XMLLSPTest.initProjectIfNeeded() + + @Test // prefix: 'perm' + fun `simple tag completion test with prefix 'perm'` () { + XMLLSPTest.apply { + openFile("completion/ManifestPermTagTest") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items).containsAtLeast("permission", "permission-group", "permission-tree") + } + } + + @Test // prefix: 'uses' + fun `simple tag completion test with prefix 'uses'` () { + XMLLSPTest.apply { + openFile("completion/ManifestUsesTagTest") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items).containsAtLeast("uses-permission", "uses-sdk", "uses-configuration", "uses-feature") + } + } +} \ No newline at end of file diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index d0f060f33dd9a005fc02aa32d7d35835ef098ee2..8542db6e31894e8e858bd34dfb95b34c3b3b1ed2 100644 GIT binary patch delta 31 ncmaEUnfdu;W}X0VW)?061`ZB}ImsJ&GVU + + + + + + + + + + + + + diff --git a/tests/test-project/app/src/main/resources/completion/ManifestUsesTagTest_template.xml b/tests/test-project/app/src/main/resources/completion/ManifestUsesTagTest_template.xml new file mode 100644 index 000000000..f23e6e4ae --- /dev/null +++ b/tests/test-project/app/src/main/resources/completion/ManifestUsesTagTest_template.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + +