Added completion provider for manifest attributes

This commit is contained in:
Akash Yadav
2022-08-28 18:23:45 +05:30
parent 4ca3c54615
commit 2f1f4e58af
13 changed files with 269 additions and 82 deletions
@@ -29,10 +29,11 @@ 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.common.AttrValueCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.layout.LayoutAttributeCompletionProvider
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.ManifestCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestAttrCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestTagCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.manifest.canCompleteManifest
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
@@ -134,7 +135,7 @@ class XmlCompletionProvider(settings: IServerSettings) :
): IXmlCompletionProvider? {
// In test cases
if (ManifestCompletionProvider.canComplete(pathData, type)) {
if (canCompleteManifest(pathData, type)) {
return createManifestCompleter(type)
}
@@ -147,6 +148,7 @@ class XmlCompletionProvider(settings: IServerSettings) :
private fun createManifestCompleter(type: NodeType): IXmlCompletionProvider? {
return when (type) {
TAG -> ManifestTagCompletionProvider(this)
ATTRIBUTE -> ManifestAttrCompletionProvider(this)
else -> null
}
}
@@ -154,7 +156,7 @@ class XmlCompletionProvider(settings: IServerSettings) :
private fun createLayoutCompleter(type: NodeType): IXmlCompletionProvider? {
return when (type) {
TAG -> LayoutTagCompletionProvider(this)
ATTRIBUTE -> LayoutAttributeCompletionProvider(this)
ATTRIBUTE -> LayoutAttrCompletionProvider(this)
ATTRIBUTE_VALUE -> AttrValueCompletionProvider(this)
else -> null
}
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.lsp.xml.providers.completion.layout
package com.itsaky.androidide.lsp.xml.providers.completion
import com.android.aaptcompiler.AaptResourceType.STYLEABLE
import com.android.aaptcompiler.ConfigDescription
@@ -29,8 +29,8 @@ 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.MatchLevel.NO_MATCH
import com.itsaky.androidide.lsp.xml.providers.completion.AttributeCompletionProvider
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.ATTRIBUTE
import com.itsaky.androidide.xml.widgets.Widget
import com.itsaky.androidide.xml.widgets.WidgetTable
import org.eclipse.lemminx.dom.DOMDocument
@@ -41,9 +41,13 @@ import org.eclipse.lemminx.dom.DOMNode
*
* @author Akash Yadav
*/
class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
AttributeCompletionProvider(provider) {
open class AttrCompletionProvider(provider: ICompletionProvider) :
IXmlCompletionProvider(provider) {
override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean {
return super.canProvideCompletions(pathData, type) && type == ATTRIBUTE
}
override fun doComplete(
params: CompletionParams,
pathData: ResourcePathData,
@@ -96,7 +100,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
return CompletionResult(list)
}
private fun completeForNamespace(
protected open fun completeForNamespace(
namespace: String,
nsPrefix: String,
node: DOMNode,
@@ -125,7 +129,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
}
}
private fun addFromPackage(
protected open fun addFromPackage(
tablePackage: ResourceTablePackage?,
node: DOMNode,
pck: String,
@@ -148,7 +152,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
)
}
private fun addFromStyleables(
protected open fun addFromStyleables(
styleables: Set<Styleable>,
pck: String,
pckPrefix: String,
@@ -173,7 +177,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
}
}
private fun findNodeStyleables(node: DOMNode, styleables: ResourceGroup): Set<Styleable> {
protected open fun findNodeStyleables(node: DOMNode, styleables: ResourceGroup): Set<Styleable> {
val nodeName = node.nodeName
val widgets = Lookup.DEFAULT.lookup(WidgetTable.COMPLETION_LOOKUP_KEY) ?: return emptySet()
@@ -199,7 +203,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
return emptySet()
}
private fun findStyleablesForName(
protected open fun findStyleablesForName(
styleables: ResourceGroup,
node: DOMNode,
addFromParent: Boolean = false,
@@ -231,7 +235,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
return result
}
private fun findLayoutParams(styleables: ResourceGroup, parentNode: DOMNode): Set<Styleable> {
protected open fun findLayoutParams(styleables: ResourceGroup, parentNode: DOMNode): Set<Styleable> {
val result = mutableSetOf<Styleable>()
// Add layout params common for all view groups and the ones supporting child margins
@@ -248,7 +252,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
return result
}
private fun findStyleablesForWidget(
protected open fun findStyleablesForWidget(
styleables: ResourceGroup,
widgets: WidgetTable,
widget: Widget,
@@ -293,7 +297,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
return result
}
private fun addWidgetStyleable(
protected open fun addWidgetStyleable(
styleables: ResourceGroup,
widget: Widget,
result: MutableSet<Styleable>,
@@ -302,7 +306,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
addWidgetStyleable(styleables, widget.simpleName, result, suffix)
}
private fun addWidgetStyleable(
protected open fun addWidgetStyleable(
styleables: ResourceGroup,
widget: String,
result: MutableSet<Styleable>,
@@ -314,7 +318,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
}
}
private fun addSuperclassStyleables(
protected open fun addSuperclassStyleables(
styleables: ResourceGroup,
widgets: WidgetTable,
widget: Widget,
@@ -332,7 +336,7 @@ class LayoutAttributeCompletionProvider(provider: ICompletionProvider) :
}
}
private fun findStyleableEntry(styleables: ResourceGroup, name: String): Styleable? {
protected open fun findStyleableEntry(styleables: ResourceGroup, name: String): Styleable? {
val value = styleables.findEntry(name)?.findValue(ConfigDescription())?.value
if (value !is Styleable) {
return null
@@ -45,7 +45,7 @@ import org.eclipse.lemminx.dom.DOMDocument
*/
abstract class IXmlCompletionProvider(private val provider: ICompletionProvider) {
protected val log = ILogger.newInstance("XmlCompletionProvider")
protected val log: ILogger = ILogger.newInstance("XmlCompletionProvider")
companion object {
const val NAMESPACE_PREFIX = "http://schemas.android.com/apk/res/"
@@ -17,20 +17,20 @@
package com.itsaky.androidide.lsp.xml.providers.completion.layout
import com.android.aaptcompiler.AaptResourceType.LAYOUT
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.providers.completion.AttrCompletionProvider
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType
/**
* Base class for completion providers providing completions in a layout file.
* Attribute completion provider for layout files.
*
* @author Akash Yadav
*/
abstract class LayoutCompletionProvider(provider: ICompletionProvider) : IXmlCompletionProvider(provider) {
class LayoutAttrCompletionProvider(provider: ICompletionProvider) :
AttrCompletionProvider(provider) {
override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean {
return super.canProvideCompletions(pathData, type) && pathData.type == LAYOUT
return super.canProvideCompletions(pathData, type) && canCompleteLayout(pathData, type)
}
}
@@ -35,10 +35,10 @@ import org.eclipse.lemminx.dom.DOMDocument
* @author Akash Yadav
*/
open class LayoutTagCompletionProvider(val provider: ICompletionProvider) :
LayoutCompletionProvider(provider) {
IXmlCompletionProvider(provider) {
override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean {
return super.canProvideCompletions(pathData, type) && type == TAG
return super.canProvideCompletions(pathData, type) && canCompleteLayout(pathData, type) && type == TAG
}
override fun doComplete(
@@ -15,22 +15,16 @@
* 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.layout
import com.android.aaptcompiler.AaptResourceType.LAYOUT
import com.android.aaptcompiler.ResourcePathData
import com.itsaky.androidide.lsp.api.ICompletionProvider
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.ATTRIBUTE
import java.nio.file.Path
/**
* Base class for providing attribute completions in XML file.
*
* @author Akash Yadav
*/
abstract class AttributeCompletionProvider(provider: ICompletionProvider) : IXmlCompletionProvider(provider) {
override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean {
return super.canProvideCompletions(pathData, type) && type == ATTRIBUTE
}
}
fun canCompleteLayout(pathData: ResourcePathData, type: NodeType) : Boolean {
return pathData.type == LAYOUT
}
@@ -17,32 +17,44 @@
package com.itsaky.androidide.lsp.xml.providers.completion.manifest
import com.android.SdkConstants
import com.android.aaptcompiler.ConfigDescription
import com.android.aaptcompiler.ResourceGroup
import com.android.aaptcompiler.ResourcePathData
import com.android.aaptcompiler.ResourceTable
import com.android.aaptcompiler.Styleable
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.api.ICompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.IXmlCompletionProvider
import com.itsaky.androidide.lsp.xml.providers.completion.AttrCompletionProvider
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType
import com.itsaky.androidide.utils.VMUtils
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
import org.eclipse.lemminx.dom.DOMNode
/**
* Base class for completers which provide completions in Android manifest.
* Provides attribution completion for AndroidManifest.
*
* @author Akash Yadav
*/
abstract class ManifestCompletionProvider(provider: ICompletionProvider) :
IXmlCompletionProvider(provider) {
class ManifestAttrCompletionProvider(provider: ICompletionProvider) :
AttrCompletionProvider(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) && canComplete(pathData, type)
return super.canProvideCompletions(pathData, type) && canCompleteManifest(pathData, type)
}
override fun findResourceTables(nsUri: String): Set<ResourceTable> {
return setOf(
Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES) ?: return emptySet()
)
}
override fun findNodeStyleables(node: DOMNode, styleables: ResourceGroup): Set<Styleable> {
val name = node.nodeName
val styleable =
styleables.findEntry(transformToEntryName(name))?.findValue(ConfigDescription())?.value
if (styleable != null && styleable is Styleable) {
return setOf(styleable)
}
return emptySet()
}
}
@@ -27,6 +27,7 @@ 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.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.xml.resources.ResourceTableRegistry
@@ -38,14 +39,12 @@ import org.eclipse.lemminx.dom.DOMDocument
* @author Akash Yadav
*/
class ManifestTagCompletionProvider(provider: ICompletionProvider) :
ManifestCompletionProvider(provider) {
companion object {
private const val MANIFEST_TAG_PREFIX = "AndroidManifest"
}
IXmlCompletionProvider(provider) {
override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean {
return super.canProvideCompletions(pathData, type) && type == TAG
return super.canProvideCompletions(pathData, type) &&
canCompleteManifest(pathData, type) &&
type == TAG
}
override fun doComplete(
@@ -72,7 +71,7 @@ class ManifestTagCompletionProvider(provider: ICompletionProvider) :
styleables
.findEntries { it.startsWith(MANIFEST_TAG_PREFIX) }
.map { transformTagName(it.name) }
.map { transformToTagName(it.name) }
.forEach {
val match = matchLevel(it, newPrefix)
if (match == NO_MATCH) {
@@ -84,23 +83,4 @@ class ManifestTagCompletionProvider(provider: ICompletionProvider) :
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()
}
}
@@ -0,0 +1,87 @@
/*
* 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.SdkConstants.ANDROID_MANIFEST_XML
import com.android.aaptcompiler.ResourcePathData
import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType
import com.itsaky.androidide.utils.VMUtils
const val MANIFEST_TAG_PREFIX = "AndroidManifest"
fun canCompleteManifest(pathData: ResourcePathData, type: NodeType): Boolean {
return pathData.file.name == ANDROID_MANIFEST_XML ||
(VMUtils.isJvm() &&
pathData.file.name.startsWith("Manifest") &&
pathData.file.name.endsWith("_template.xml"))
}
/**
* 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()
}
@@ -0,0 +1,64 @@
/*
* 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 ManifestAttrCompletionProviderTest : CompletionHelper by CompletionHelperImpl() {
@Before fun setup() = XMLLSPTest.initProjectIfNeeded()
@Test // prefix: 'version'
fun `test manifest tag attributes`() {
XMLLSPTest.apply {
openFile("completion/ManifestAttrTest")
val (incomplete, items) = complete()
assertThat(incomplete).isFalse()
assertThat(items)
.containsAtLeast("android:versionName", "android:versionCode", "android:versionCodeMajor")
}
}
@Test // prefix: 'allow'
fun `test manifest application tag attributes`() {
XMLLSPTest.apply {
openFile("completion/ManifestApplicationAttrTest")
val (incomplete, items) = complete()
assertThat(incomplete).isFalse()
assertThat(items)
.containsAtLeast(
"android:allowBackup",
"android:allowClearUserDataOnFailedRestore",
"android:allowAudioPlaybackCapture",
"android:allowNativeHeapPointerTagging",
"android:allowAutoRevokePermissionsExemption"
)
}
}
}
Binary file not shown.
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.itsaky.test.app">
<application
allow@@cursor@@
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,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.itsaky.test.app"
version@@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>