diff --git a/build.gradle.kts b/build.gradle.kts index 97bda1ab3b..b0f6438ace 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -298,11 +298,18 @@ spotless { trimTrailingWhitespace() endWithNewline() - // Eclipse WTP splits strings.xml entries across lines, so exclude it. + // Eclipse WTP splits strings.xml entries across lines, so exclude it. LSP test + // fixtures (testing/resources/test-project/**/*_template.xml) embed an + // "@@cursor@@" marker inside attribute-like text; the formatter splits it + // across lines too, breaking the marker lookup, so exclude those as well. target( spotlessTarget( "**/src/*/res/**/*.xml", - extraExcludes = arrayOf("**/src/*/res/values*/strings.xml"), + extraExcludes = + arrayOf( + "**/src/*/res/values*/strings.xml", + "testing/resources/test-project/**/*_template.xml", + ), ), ) } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrCompletionProvider.kt index 329f8b3c48..af954c6422 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrCompletionProvider.kt @@ -32,6 +32,7 @@ import com.itsaky.androidide.lsp.models.CompletionResult 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.ATTRIBUTE +import com.itsaky.androidide.xml.resources.ResourceTableRegistry import com.itsaky.androidide.xml.widgets.Widget import com.itsaky.androidide.xml.widgets.WidgetTable import org.eclipse.lemminx.dom.DOMDocument @@ -42,310 +43,331 @@ import org.eclipse.lemminx.dom.DOMNode * * @author Akash Yadav */ -open class AttrCompletionProvider(provider: ICompletionProvider) : - IXmlCompletionProvider(provider) { - - private var attrHasNamespace = false - - override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean { - return super.canProvideCompletions(pathData, type) && type == ATTRIBUTE - } - - override fun doComplete( - params: CompletionParams, - pathData: ResourcePathData, - document: DOMDocument, - type: NodeType, - prefix: String - ): CompletionResult { - val list = mutableListOf() - - val newPrefix = - if (attrAtCursor.name.contains(':')) { - attrAtCursor.name.substringAfterLast(':') - } else attrAtCursor.name - - attrHasNamespace = newPrefix != attrAtCursor.name - - val namespace = - attrAtCursor.namespaceURI - ?: run { - return completeFromAllNamespaces(nodeAtCursor, list, newPrefix) - } - - val nsPrefix = attrAtCursor.nodeName.substringBefore(':') - completeForNamespace(namespace, nsPrefix, nodeAtCursor, newPrefix, list) - - return CompletionResult(list) - } - - private fun completeFromAllNamespaces( - node: DOMNode, - list: MutableList, - newPrefix: String - ): CompletionResult { - val namespaces = findAllNamespaces(node) - namespaces.forEach { completeForNamespace(it.second, it.first, node, newPrefix, list) } - - return CompletionResult(list) - } - - protected open fun completeForNamespace( - namespace: String?, - nsPrefix: String, - node: DOMNode, - newPrefix: String, - list: MutableList - ) { - if (namespace == null) { - log.warn("Namespace is null. Cannot compute completions for namespace prefix: {}.", nsPrefix) - return - } - val tables = findResourceTables(namespace) - if (tables.isEmpty()) { - log.warn("No resource tables found for namespace: {}", namespace) - return - } - - val pck = namespace.substringAfter(NAMESPACE_PREFIX) - val packages = mutableSetOf() - for (table in tables) { - if (namespace == NAMESPACE_AUTO) { - packages.addAll(table.packages.filter { it.name.isNotBlank() }) - } else { - val tablePackage = table.findPackage(pck) - tablePackage?.also { packages.add(it) } - } - } - - for (tablePackage in packages) { - addFromPackage(tablePackage, node, tablePackage.name, nsPrefix, newPrefix, list) - } - } - - protected open fun addFromPackage( - tablePackage: ResourceTablePackage?, - node: DOMNode, - pck: String, - nsPrefix: String, - newPrefix: String, - list: MutableList - ) { - val styleables = tablePackage?.findGroup(STYLEABLE) ?: return - val nodeStyleables = findNodeStyleables(node, styleables) - if (nodeStyleables.isEmpty()) { - return - } - - addFromStyleables( - styleables = nodeStyleables, - pck = pck, - pckPrefix = nsPrefix, - prefix = newPrefix, - list = list - ) - } - - protected open fun addFromStyleables( - styleables: Set, - pck: String, - pckPrefix: String, - prefix: String, - list: MutableList - ) { - for (nodeStyleable in styleables) { - for (ref in nodeStyleable.entries) { - val matchLevel = matchLevel(ref.name.entry!!, prefix) - if (matchLevel == NO_MATCH || hasAttr(pckPrefix, ref)) { - continue - } - list.add( - createAttrCompletionItem( - attr = ref, - resPkg = pck, - nsPrefix = pckPrefix, - hasNamespace = attrHasNamespace, - matchLevel = matchLevel, - partial = prefix - ) - ) - } - } - } - - protected open fun hasAttr(prefix: String, ref: Reference): Boolean { - return this.nodeAtCursor.hasAttribute("${prefix}:${ref.name.entry}") - } - - protected open fun findNodeStyleables(node: DOMNode, styleables: ResourceGroup): Set { - val nodeName = node.nodeName - val widgets = Lookup.getDefault().lookup(WidgetTable.COMPLETION_LOOKUP_KEY) ?: return emptySet() - - // Find the widget - val widget = - if (nodeName.contains(".")) { - widgets.getWidget(nodeName) - } else { - widgets.findWidgetWithSimpleName(nodeName) - } - - if (widget != null) { - // This is a widget from the Android SDK - // we can get its superclasses and other stuff - return findStyleablesForWidget(styleables, widgets, widget, node) - } else if (nodeName.contains('.')) { - // Probably a custom view or a view from libraries - // If the developer follows the naming convention then only the completions will be provided - // This must be called if and only if the tag name is qualified - return findStyleablesForName(styleables, node, true) - } - - log.info("Cannot find styleable entries for tag: null") - return emptySet() - } - - protected open fun findStyleablesForName( - styleables: ResourceGroup, - node: DOMNode, - addFromParent: Boolean = false, - suffix: String = "" - ): Set { - val result = mutableSetOf() - - // Styles must be defined by the View class' simple name - var name = node.nodeName - if (name.contains('.')) { - name = name.substringAfterLast('.') - } - - // Common attributes for all views - addWidgetStyleable(styleables = styleables, widget = "View", result = result) - - // Find the declared styleable - val entry = findStyleableEntry(styleables, "$name$suffix") - if (entry != null) { - result.add(entry) - } - - // If the layout params from the parent must be added, check for parent and then add them - // Layout param attributes must be added only from the direct parent - if (addFromParent) { - node.parentNode?.also { result.addAll(findLayoutParams(styleables, node.parentNode)) } - } - - return result - } - - protected open fun findLayoutParams( - styleables: ResourceGroup, - parentNode: DOMNode - ): Set { - val result = mutableSetOf() - - // Add layout params common for all view groups and the ones supporting child margins - addWidgetStyleable(styleables, "ViewGroup", result, suffix = "_Layout") - addWidgetStyleable(styleables, "ViewGroup", result, suffix = "_MarginLayout") - - var name = parentNode.nodeName - if (name.contains('.')) { - name = name.substringAfterLast('.') - } - - addWidgetStyleable(styleables, name, result, "_Layout") - - return result - } - - protected open fun findStyleablesForWidget( - styleables: ResourceGroup, - widgets: WidgetTable, - widget: Widget, - node: DOMNode, - adddFromParent: Boolean = true, - suffix: String = "" - ): Set { - val result = mutableSetOf() - - // Find the for the widget in the resource group - addWidgetStyleable(styleables, widget, result, suffix = suffix) - - // Find styleables for all the superclasses - addSuperclassStyleables(styleables, widgets, widget, result, suffix = suffix) - - // Add attributes provided by the layout params - if (adddFromParent && node.parentNode != null) { - val parentName = node.parentNode.nodeName - val parentWidget = - if (parentName.contains(".")) { - widgets.getWidget(parentName) - } else { - widgets.findWidgetWithSimpleName(parentName) - } - - if (parentWidget != null) { - result.addAll( - findStyleablesForWidget( - styleables, - widgets, - parentWidget, - node.parentNode, - false, - "_Layout" - ) - ) - } else { - result.addAll(findLayoutParams(styleables, node.parentNode)) - } - } - - return result - } - - protected open fun addWidgetStyleable( - styleables: ResourceGroup, - widget: Widget, - result: MutableSet, - suffix: String = "" - ) { - addWidgetStyleable(styleables, widget.simpleName, result, suffix) - } - - protected open fun addWidgetStyleable( - styleables: ResourceGroup, - widget: String, - result: MutableSet, - suffix: String = "" - ) { - val entry = findStyleableEntry(styleables, "${widget}${suffix}") - if (entry != null) { - result.add(entry) - } - } - - protected open fun addSuperclassStyleables( - styleables: ResourceGroup, - widgets: WidgetTable, - widget: Widget, - result: MutableSet, - suffix: String = "" - ) { - for (superclass in widget.superclasses) { - // When a ViewGroup is encountered in the superclasses, add the margin layout params - if ("android.view.ViewGroup" == superclass) { - addWidgetStyleable(styleables, "ViewGroup", result, suffix = "_MarginLayout") - } - - val superr = widgets.getWidget(superclass) ?: continue - addWidgetStyleable(styleables, superr.simpleName, result, suffix = suffix) - } - } - - protected open fun findStyleableEntry(styleables: ResourceGroup, name: String): Styleable? { - val value = styleables.findEntry(name)?.findValue(ConfigDescription())?.value - if (value !is Styleable) { - log.warn("Cannot find styleable for {}", name) - return null - } - return value - } +open class AttrCompletionProvider( + provider: ICompletionProvider, +) : IXmlCompletionProvider(provider) { + private var attrHasNamespace = false + + companion object { + // AOSP keeps these as unused placeholders for removed attribute IDs (b/131100106); + // they have no entry, so they must never surface in completions. + private val REMOVED_ATTR_NAME = Regex("^__removed\\d+$") + } + + override fun canProvideCompletions( + pathData: ResourcePathData, + type: NodeType, + ): Boolean = super.canProvideCompletions(pathData, type) && type == ATTRIBUTE + + override fun doComplete( + params: CompletionParams, + pathData: ResourcePathData, + document: DOMDocument, + type: NodeType, + prefix: String, + ): CompletionResult { + val list = mutableListOf() + + val newPrefix = + if (attrAtCursor.name.contains(':')) { + attrAtCursor.name.substringAfterLast(':') + } else { + attrAtCursor.name + } + + attrHasNamespace = newPrefix != attrAtCursor.name + + val namespace = + attrAtCursor.namespaceURI + ?: run { + return completeFromAllNamespaces(nodeAtCursor, list, newPrefix) + } + + val nsPrefix = attrAtCursor.nodeName.substringBefore(':') + completeForNamespace(namespace, nsPrefix, nodeAtCursor, newPrefix, list) + + return CompletionResult(list) + } + + private fun completeFromAllNamespaces( + node: DOMNode, + list: MutableList, + newPrefix: String, + ): CompletionResult { + val namespaces = findAllNamespaces(node) + namespaces.forEach { completeForNamespace(it.second, it.first, node, newPrefix, list) } + + return CompletionResult(list) + } + + protected open fun completeForNamespace( + namespace: String?, + nsPrefix: String, + node: DOMNode, + newPrefix: String, + list: MutableList, + ) { + if (namespace == null) { + log.warn("Namespace is null. Cannot compute completions for namespace prefix: {}.", nsPrefix) + return + } + val tables = findResourceTables(namespace) + if (tables.isEmpty()) { + log.warn("No resource tables found for namespace: {}", namespace) + return + } + + val pck = namespace.substringAfter(NAMESPACE_PREFIX) + val packages = mutableSetOf() + for (table in tables) { + if (namespace == NAMESPACE_AUTO) { + packages.addAll(table.packages.filter { it.name.isNotBlank() }) + } else { + val tablePackage = table.findPackage(pck) + tablePackage?.also { packages.add(it) } + } + } + + for (tablePackage in packages) { + addFromPackage(tablePackage, node, tablePackage.name, nsPrefix, newPrefix, list) + } + } + + protected open fun addFromPackage( + tablePackage: ResourceTablePackage?, + node: DOMNode, + pck: String, + nsPrefix: String, + newPrefix: String, + list: MutableList, + ) { + val styleables = tablePackage?.findGroup(STYLEABLE) ?: return + val nodeStyleables = findNodeStyleables(node, styleables) + if (nodeStyleables.isEmpty()) { + return + } + + addFromStyleables( + styleables = nodeStyleables, + pck = pck, + pckPrefix = nsPrefix, + prefix = newPrefix, + list = list, + ) + } + + protected open fun addFromStyleables( + styleables: Set, + pck: String, + pckPrefix: String, + prefix: String, + list: MutableList, + ) { + for (nodeStyleable in styleables) { + for (ref in nodeStyleable.entries) { + val name = ref.name.entry!! + if (pck == ResourceTableRegistry.PCK_ANDROID && REMOVED_ATTR_NAME.matches(name)) { + continue + } + + val matchLevel = matchLevel(name, prefix) + if (matchLevel == NO_MATCH || hasAttr(pckPrefix, ref)) { + continue + } + list.add( + createAttrCompletionItem( + attr = ref, + resPkg = pck, + nsPrefix = pckPrefix, + hasNamespace = attrHasNamespace, + matchLevel = matchLevel, + partial = prefix, + ), + ) + } + } + } + + protected open fun hasAttr( + prefix: String, + ref: Reference, + ): Boolean = this.nodeAtCursor.hasAttribute("$prefix:${ref.name.entry}") + + protected open fun findNodeStyleables( + node: DOMNode, + styleables: ResourceGroup, + ): Set { + val nodeName = node.nodeName + val widgets = Lookup.getDefault().lookup(WidgetTable.COMPLETION_LOOKUP_KEY) ?: return emptySet() + + // Find the widget + val widget = + if (nodeName.contains(".")) { + widgets.getWidget(nodeName) + } else { + widgets.findWidgetWithSimpleName(nodeName) + } + + if (widget != null) { + // This is a widget from the Android SDK + // we can get its superclasses and other stuff + return findStyleablesForWidget(styleables, widgets, widget, node) + } else if (nodeName.contains('.')) { + // Probably a custom view or a view from libraries + // If the developer follows the naming convention then only the completions will be provided + // This must be called if and only if the tag name is qualified + return findStyleablesForName(styleables, node, true) + } + + log.info("Cannot find styleable entries for tag: null") + return emptySet() + } + + protected open fun findStyleablesForName( + styleables: ResourceGroup, + node: DOMNode, + addFromParent: Boolean = false, + suffix: String = "", + ): Set { + val result = mutableSetOf() + + // Styles must be defined by the View class' simple name + var name = node.nodeName + if (name.contains('.')) { + name = name.substringAfterLast('.') + } + + // Common attributes for all views + addWidgetStyleable(styleables = styleables, widget = "View", result = result) + + // Find the declared styleable + val entry = findStyleableEntry(styleables, "$name$suffix") + if (entry != null) { + result.add(entry) + } + + // If the layout params from the parent must be added, check for parent and then add them + // Layout param attributes must be added only from the direct parent + if (addFromParent) { + node.parentNode?.also { result.addAll(findLayoutParams(styleables, node.parentNode)) } + } + + return result + } + + protected open fun findLayoutParams( + styleables: ResourceGroup, + parentNode: DOMNode, + ): Set { + val result = mutableSetOf() + + // Add layout params common for all view groups and the ones supporting child margins + addWidgetStyleable(styleables, "ViewGroup", result, suffix = "_Layout") + addWidgetStyleable(styleables, "ViewGroup", result, suffix = "_MarginLayout") + + var name = parentNode.nodeName + if (name.contains('.')) { + name = name.substringAfterLast('.') + } + + addWidgetStyleable(styleables, name, result, "_Layout") + + return result + } + + protected open fun findStyleablesForWidget( + styleables: ResourceGroup, + widgets: WidgetTable, + widget: Widget, + node: DOMNode, + adddFromParent: Boolean = true, + suffix: String = "", + ): Set { + val result = mutableSetOf() + + // Find the for the widget in the resource group + addWidgetStyleable(styleables, widget, result, suffix = suffix) + + // Find styleables for all the superclasses + addSuperclassStyleables(styleables, widgets, widget, result, suffix = suffix) + + // Add attributes provided by the layout params + if (adddFromParent && node.parentNode != null) { + val parentName = node.parentNode.nodeName + val parentWidget = + if (parentName.contains(".")) { + widgets.getWidget(parentName) + } else { + widgets.findWidgetWithSimpleName(parentName) + } + + if (parentWidget != null) { + result.addAll( + findStyleablesForWidget( + styleables, + widgets, + parentWidget, + node.parentNode, + false, + "_Layout", + ), + ) + } else { + result.addAll(findLayoutParams(styleables, node.parentNode)) + } + } + + return result + } + + protected open fun addWidgetStyleable( + styleables: ResourceGroup, + widget: Widget, + result: MutableSet, + suffix: String = "", + ) { + addWidgetStyleable(styleables, widget.simpleName, result, suffix) + } + + protected open fun addWidgetStyleable( + styleables: ResourceGroup, + widget: String, + result: MutableSet, + suffix: String = "", + ) { + val entry = findStyleableEntry(styleables, "${widget}$suffix") + if (entry != null) { + result.add(entry) + } + } + + protected open fun addSuperclassStyleables( + styleables: ResourceGroup, + widgets: WidgetTable, + widget: Widget, + result: MutableSet, + suffix: String = "", + ) { + for (superclass in widget.superclasses) { + // When a ViewGroup is encountered in the superclasses, add the margin layout params + if ("android.view.ViewGroup" == superclass) { + addWidgetStyleable(styleables, "ViewGroup", result, suffix = "_MarginLayout") + } + + val superr = widgets.getWidget(superclass) ?: continue + addWidgetStyleable(styleables, superr.simpleName, result, suffix = suffix) + } + } + + protected open fun findStyleableEntry( + styleables: ResourceGroup, + name: String, + ): Styleable? { + val value = styleables.findEntry(name)?.findValue(ConfigDescription())?.value + if (value !is Styleable) { + log.warn("Cannot find styleable for {}", name) + return null + } + return value + } } diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/LayoutAttributeCompletionProviderTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/LayoutAttributeCompletionProviderTest.kt index 674b69f5bb..d8025b227f 100644 --- a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/LayoutAttributeCompletionProviderTest.kt +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/LayoutAttributeCompletionProviderTest.kt @@ -29,155 +29,162 @@ import org.robolectric.RobolectricTestRunner /** @author Akash Yadav */ @RunWith(RobolectricTestRunner::class) class LayoutAttributeCompletionProviderTest : CompletionHelper by CompletionHelperImpl() { + @Before + fun setup() { + XMLLSPTest.initProjectIfNeeded() + } - @Before - fun setup() { - XMLLSPTest.initProjectIfNeeded() - } + @Test // prefix: 's' + fun `attributes from superclasses must be included`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsFromSuperclass") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items).isNotEmpty() + assertThat(items).contains("android:singleLine") // From TextView + assertThat(items).contains("android:scrollbars") // from View + } + } - @Test // prefix: 's' - fun `attributes from superclasses must be included`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsFromSuperclass") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isFalse() - assertThat(items).isNotEmpty() - assertThat(items).contains("android:singleLine") // From TextView - assertThat(items).contains("android:scrollbars") // from View - } - } + @Test // prefix: 'l' + fun `attributes from parent's margin layout params must be included`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsFromLayoutParams") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items).isNotEmpty() + assertThat(items).contains("android:lines") // From TextView + assertThat(items).contains("android:layout_weight") // from LinearLayout.LayoutParams + assertThat(items).contains("android:layout_margin") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginLeft") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginTop") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginRight") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginBottom") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginStart") // from ViewGroup.MarginLayoutParams + } + } - @Test // prefix: 'l' - fun `attributes from parent's margin layout params must be included`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsFromLayoutParams") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isFalse() - assertThat(items).isNotEmpty() - assertThat(items).contains("android:lines") // From TextView - assertThat(items).contains("android:layout_weight") // from LinearLayout.LayoutParams - assertThat(items).contains("android:layout_margin") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginLeft") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginTop") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginRight") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginBottom") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginStart") // from ViewGroup.MarginLayoutParams - } - } - - @Test // prefix: 'android:l' - fun `attributes must be completed when namespace is specified as well`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsWithNamespace") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isFalse() - assertThat(items).isNotEmpty() - assertThat(items).contains("android:lines") // From TextView - assertThat(items).contains("android:layout_weight") // from LinearLayout.LayoutParams - assertThat(items).contains("android:layout_margin") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginLeft") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginTop") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginRight") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginBottom") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginStart") // from ViewGroup.MarginLayoutParams - } - } - - @Test // prefix: 'android:margin' - fun `attributes must be completed with a partial prefix`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsWithPartialName") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isFalse() - assertThat(items).isNotEmpty() - assertThat(items).contains("android:layout_margin") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginLeft") // from ViewGroup.MarginLayoutParams - assertThat(items).contains("android:layout_marginTop") // from ViewGroup.MarginLayoutParams - } - } - - @Test // prefix: 'padding' - fun `duplicate attributes must not be included`() { - XMLLSPTest.apply { - openFile("../res/layout/TestNoDuplicateAttrs") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isFalse() - assertThat(items).isNotEmpty() - assertThat(items).contains("android:paddingLeft") // from View - assertThat(items).contains("android:paddingRight") // from View - assertThat(items).contains("android:paddingTop") // from View - assertThat(items).contains("android:paddingBottom") // from View - assertThat(items).contains("android:paddingStart") // from View - assertThat(items).contains("android:paddingEnd") // from View - assertThat(items).doesNotContain("android:padding") - } - } - - @Test // prefix: 'layout' - fun `attributes from all defined namespaces must be completed`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsWithMultipleNamespaces") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isTrue() - assertThat(items).isNotEmpty() - assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout - - } - } - - @Test // prefix: 'material:layout' - fun `attributes from the defined namespace must be completed`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsWithDefinedNamespace") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isTrue() - assertThat(items).isNotEmpty() - assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout - - // Attributes no other attributes must be included - assertThat(items.filter { !it.startsWith("material:") }).isEmpty() - } - } - - @Test // prefix: 'layout' - fun `attributes from defined auto namespace must be completed`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsWithMultipleNamespacesAuto") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isTrue() - assertThat(items).isNotEmpty() - assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout - - } - } - - @Test // prefix: 'material:layout' - fun `attributes from the defined auto namespace must be completed`() { - XMLLSPTest.apply { - openFile("../res/layout/TestAttrsWithDefinedNamespaceAuto") - val (isIncomplete, items) = complete() - assertThat(isIncomplete).isTrue() - assertThat(items).isNotEmpty() - assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout - assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout - - // Attributes no other attributes must be included - assertThat(items.filter { !it.startsWith("material:") }).isEmpty() - } - } + @Test // prefix: 'android:l' + fun `attributes must be completed when namespace is specified as well`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsWithNamespace") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items).isNotEmpty() + assertThat(items).contains("android:lines") // From TextView + assertThat(items).contains("android:layout_weight") // from LinearLayout.LayoutParams + assertThat(items).contains("android:layout_margin") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginLeft") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginTop") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginRight") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginBottom") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginStart") // from ViewGroup.MarginLayoutParams + } + } + + @Test // prefix: 'android:margin' + fun `attributes must be completed with a partial prefix`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsWithPartialName") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items).isNotEmpty() + assertThat(items).contains("android:layout_margin") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginLeft") // from ViewGroup.MarginLayoutParams + assertThat(items).contains("android:layout_marginTop") // from ViewGroup.MarginLayoutParams + } + } + + @Test // prefix: 'padding' + fun `duplicate attributes must not be included`() { + XMLLSPTest.apply { + openFile("../res/layout/TestNoDuplicateAttrs") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items).isNotEmpty() + assertThat(items).contains("android:paddingLeft") // from View + assertThat(items).contains("android:paddingRight") // from View + assertThat(items).contains("android:paddingTop") // from View + assertThat(items).contains("android:paddingBottom") // from View + assertThat(items).contains("android:paddingStart") // from View + assertThat(items).contains("android:paddingEnd") // from View + assertThat(items).doesNotContain("android:padding") + } + } + + @Test // prefix: 'android:__' + fun `removed placeholder attributes must not be included`() { + XMLLSPTest.apply { + openFile("../res/layout/TestNoRemovedPlaceholderAttrs") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isFalse() + assertThat(items.filter { it.startsWith("android:__removed") }).isEmpty() + } + } + + @Test // prefix: 'layout' + fun `attributes from all defined namespaces must be completed`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsWithMultipleNamespaces") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isTrue() + assertThat(items).isNotEmpty() + assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout + } + } + + @Test // prefix: 'material:layout' + fun `attributes from the defined namespace must be completed`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsWithDefinedNamespace") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isTrue() + assertThat(items).isNotEmpty() + assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout + + // Attributes no other attributes must be included + assertThat(items.filter { !it.startsWith("material:") }).isEmpty() + } + } + + @Test // prefix: 'layout' + fun `attributes from defined auto namespace must be completed`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsWithMultipleNamespacesAuto") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isTrue() + assertThat(items).isNotEmpty() + assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout + } + } + + @Test // prefix: 'material:layout' + fun `attributes from the defined auto namespace must be completed`() { + XMLLSPTest.apply { + openFile("../res/layout/TestAttrsWithDefinedNamespaceAuto") + val (isIncomplete, items) = complete() + assertThat(isIncomplete).isTrue() + assertThat(items).isNotEmpty() + assertThat(items).contains("material:layout_constraintEnd_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintEnd_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toEndOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintStart_toStartOf") // From ConstraintLayout + assertThat(items).contains("material:layout_constraintHorizontal_bias") // From ConstraintLayout + + // Attributes no other attributes must be included + assertThat(items.filter { !it.startsWith("material:") }).isEmpty() + } + } } diff --git a/testing/resources/test-project/app/src/main/res/layout/TestNoRemovedPlaceholderAttrs_template.xml b/testing/resources/test-project/app/src/main/res/layout/TestNoRemovedPlaceholderAttrs_template.xml new file mode 100644 index 0000000000..92ad30727b --- /dev/null +++ b/testing/resources/test-project/app/src/main/res/layout/TestNoRemovedPlaceholderAttrs_template.xml @@ -0,0 +1,31 @@ + + + + + +