Added completion provider for manifest tags

This commit is contained in:
Akash Yadav
2022-08-28 12:29:50 +05:30
parent 5ef38ce963
commit 4ca3c54615
13 changed files with 288 additions and 21 deletions
@@ -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)
}
}
}
@@ -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
}
@@ -33,9 +33,10 @@ interface ResourceTableRegistry : XmlRegistry<ResourceTable> {
companion object {
const val PCK_ANDROID = "android"
@JvmStatic val COMPLETION_MODULE_RES_LOOKUP_KEY = Lookup.Key<Set<ResourceTable>>()
@JvmStatic val COMPLETION_DEP_RES_LOOKUP_KEY = Lookup.Key<Set<ResourceTable>>()
@JvmStatic val COMPLETION_FRAMEWORK_RES_LOOKUP_KEY = Lookup.Key<ResourceTable>()
@JvmStatic val COMPLETION_MODULE_RES = Lookup.Key<Set<ResourceTable>>()
@JvmStatic val COMPLETION_DEP_RES = Lookup.Key<Set<ResourceTable>>()
@JvmStatic val COMPLETION_FRAMEWORK_RES = Lookup.Key<ResourceTable>()
@JvmStatic val COMPLETION_MANIFEST_ATTR_RES = Lookup.Key<ResourceTable>()
@JvmStatic fun getInstance(): ResourceTableRegistry = DefaultResourceTableRegistry
}
@@ -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
}
}
@@ -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<ResourceTable> {
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<ResourceTable>().also {
it.addAll(sourceResTables)
it.addAll(depResTables)
@@ -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
*/
@@ -15,21 +15,34 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<CompletionItem>()
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()
}
}
@@ -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)
@@ -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 <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 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")
}
}
}
Binary file not shown.
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.itsaky.test.app">
<perm@@cursor@@>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.itsaky.test.app">
<uses@@cursor@@>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>