Provide attribute completions for XML files

This commit is contained in:
Akash Yadav
2022-05-14 23:12:02 +05:30
parent 3edd74a2d3
commit 46b8694cfc
7 changed files with 101 additions and 71 deletions
@@ -45,20 +45,25 @@ data class CompletionParams(var position: Position, var file: Path) {
}
}
open class CompletionResult(var isIncomplete: Boolean, items: List<CompletionItem>) {
open class CompletionResult(items: List<CompletionItem>) {
var items: List<CompletionItem> = run {
var temp = items.toMutableList()
if (temp.size > MAX_ITEMS) {
temp = temp.subList(0, MAX_ITEMS)
}
var items =
items
.subList(0, if (items.size < MAX_ITEMS) items.size else MAX_ITEMS)
.toMutableList()
.apply { sort() }
temp.sort()
return@run temp
}
val isIncomplete = this.items.size < items.size
companion object {
const val MAX_ITEMS = 50
@JvmField val EMPTY = CompletionResult()
@JvmField val EMPTY = CompletionResult(listOf())
}
constructor() : this(false, ArrayList<CompletionItem>())
constructor() : this(listOf())
override fun toString(): String {
return android.text.TextUtils.join("\n", items)
@@ -114,7 +119,6 @@ open class CompletionItem(
companion object {
private val LOG = ILogger.newInstance("CompletionItem")
@JvmStatic fun sortTextForMatchRatio(ratio: Int, text: CharSequence) = "${100-ratio}$text"
}
@@ -130,13 +134,12 @@ open class CompletionItem(
override fun performCompletion(editor: CodeEditor, text: Content, line: Int, column: Int) {
val start = getIdentifierStart(text.getLine(line), column)
val insert = if (insertText == null) label else insertText
val shift = insert!!.contains("$0")
val shift = insertText.contains("$0")
text.delete(line, start, line, column)
if (text.contains("\n")) {
val lines = insert.split("\\\n")
val lines = insertText.split("\\\n")
var i = 0
lines.forEach {
var commit = it
@@ -277,7 +280,7 @@ data class Command(var title: String, var command: String) {
}
}
enum class CompletionItemKind private constructor(val sortIndex: Int) {
enum class CompletionItemKind(val sortIndex: Int) {
CLASS(7),
INTERFACE(7),
ANNOTATION_TYPE(7),
@@ -92,7 +92,7 @@ import javax.lang.model.util.Types;
public class CompletionProvider extends AbstractServiceProvider implements ICompletionProvider {
public static final int MAX_COMPLETION_ITEMS = 50;
public static final int MAX_COMPLETION_ITEMS = CompletionResult.MAX_ITEMS;
private static final String[] TOP_LEVEL_KEYWORDS = {
"package",
"import",
@@ -435,7 +435,6 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
}
if (list.getItems().size() + methods.size() > MAX_COMPLETION_ITEMS) {
list.setIncomplete(true);
break outer;
}
}
@@ -499,7 +498,6 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
}
if (list.getItems().size() > MAX_COMPLETION_ITEMS) {
list.setIncomplete(true);
break;
}
@@ -525,7 +523,6 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
final String name = packageName + "." + c.getSimpleName();
list.getItems().add(classItem(name, matchRatio));
if (list.getItems().size() > MAX_COMPLETION_ITEMS) {
list.setIncomplete(true);
break;
}
}
@@ -654,7 +651,7 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
list.add(keyword("super", 100));
}
return new CompletionResult(false, list);
return new CompletionResult(list);
}
private boolean isEnclosingClass(DeclaredType type, Scope start) {
@@ -778,7 +775,7 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
list.add(keyword("new", 100));
}
return new CompletionResult(false, list);
return new CompletionResult(list);
}
private void putMethod(
@@ -839,7 +836,7 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
list.add(item(task, member, matchRatio));
}
return new CompletionResult(false, list);
return new CompletionResult(list);
}
private CompletionResult completeImport(String path) {
@@ -876,7 +873,6 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
}
if (list.getItems().size() > MAX_COMPLETION_ITEMS) {
list.setIncomplete(true);
return list;
}
}
@@ -20,22 +20,25 @@ package com.itsaky.lsp.xml.providers
import com.itsaky.androidide.tooling.api.model.IdeAndroidModule
import com.itsaky.androidide.utils.CharSequenceReader
import com.itsaky.androidide.utils.ILogger
import com.itsaky.attrinfo.models.Attr
import com.itsaky.lsp.api.AbstractServiceProvider
import com.itsaky.lsp.api.ICompletionProvider
import com.itsaky.lsp.api.IServerSettings
import com.itsaky.lsp.models.Command
import com.itsaky.lsp.models.CompletionData
import com.itsaky.lsp.models.CompletionItem
import com.itsaky.lsp.models.CompletionItem.Companion.sortTextForMatchRatio
import com.itsaky.lsp.models.CompletionItemKind.CLASS
import com.itsaky.lsp.models.CompletionItemKind.FIELD
import com.itsaky.lsp.models.CompletionItemKind.VALUE
import com.itsaky.lsp.models.CompletionParams
import com.itsaky.lsp.models.CompletionResult
import com.itsaky.lsp.models.CompletionResult.Companion.EMPTY
import com.itsaky.lsp.models.InsertTextFormat.PLAIN_TEXT
import com.itsaky.lsp.models.Position
import com.itsaky.lsp.util.StringUtils
import com.itsaky.lsp.xml.utils.XmlUtils
import com.itsaky.lsp.xml.utils.XmlUtils.NodeType
import com.itsaky.lsp.xml.utils.XmlUtils.NodeType.ATTRIBUTE
import com.itsaky.lsp.xml.utils.XmlUtils.NodeType.ATTRIBUTE_VALUE
import com.itsaky.lsp.xml.utils.XmlUtils.NodeType.TAG
import com.itsaky.lsp.xml.utils.XmlUtils.NodeType.UNKNOWN
@@ -55,14 +58,13 @@ import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager
*
* @author Akash Yadav
*/
class XmlCompletionProvider(val sdkInfo: SDKInfo, settings: IServerSettings) :
class XmlCompletionProvider(private val sdkInfo: SDKInfo, settings: IServerSettings) :
AbstractServiceProvider(), ICompletionProvider {
init {
super.applySettings(settings)
}
private val cachedResult: CompletionResult? = null
private val log = ILogger.newInstance(javaClass.simpleName)
override fun complete(params: CompletionParams): CompletionResult {
@@ -128,20 +130,48 @@ class XmlCompletionProvider(val sdkInfo: SDKInfo, settings: IServerSettings) :
} else {
prefix
})
// ATTRIBUTE -> completeAttributes(document, prefix)
ATTRIBUTE -> completeAttributes(document, params.position)
ATTRIBUTE_VALUE -> completeAttributeValue(document, prefix, params.position)
else -> EMPTY
}
}
private fun completeAttributes(document: DOMDocument, position: Position): CompletionResult {
// TODO Provide attributes based on current node and it's direct parent node
// For example, if the current node is a 'TextView', provide attributes applicable to
// TextView only. Also, if the parent of this TextView is a LinearLayout, then add
// attributes related to LinearLayout LayoutParams.
// TODO Provided attributes from declared namespaces only
val attr = document.findAttrAt(position.requireIndex())
val list = mutableListOf<CompletionItem>()
for (attribute in sdkInfo.attrInfo.attributes.values) {
val matchRatio =
StringUtils.fuzzySearchRatio(
attribute.name, attr.name, settings.shouldMatchAllLowerCase())
if (matchRatio > 0) {
list.add(createAttrCompletionItem(attribute, matchRatio))
}
}
return CompletionResult(list)
}
private fun createAttrCompletionItem(attr: Attr, matchRatio: Int): CompletionItem =
CompletionItem().apply {
label = attr.name
kind = FIELD
detail = "From package '${attr.namespace.packageName}'"
sortText = sortTextForMatchRatio(matchRatio, label)
command = Command("Trigger completion request", Command.TRIGGER_COMPLETION)
}
private fun completeAttributeValue(
document: DOMDocument,
prefix: String,
position: Position
): CompletionResult {
val resolver = XmlUtils.getNamespaceResolver(document)
val attr = document.findAttrAt(position.requireIndex())
val uri = attr.namespaceURI
// TODO Provide attribute values based on namespace URI
// For example, if the namespace of this attribute refers to a library dependency/module,
@@ -165,7 +195,7 @@ class XmlCompletionProvider(val sdkInfo: SDKInfo, settings: IServerSettings) :
}
}
return CompletionResult(false, items)
return CompletionResult(items)
}
private fun createAttrValueCompletionItem(
@@ -178,14 +208,11 @@ class XmlCompletionProvider(val sdkInfo: SDKInfo, settings: IServerSettings) :
detail = "Value for '$attrName'"
kind = VALUE
sortText = sortTextForMatchRatio(matchRatio, label)
insertText = label.toString()
insertTextFormat = PLAIN_TEXT
}
}
private fun completeTags(prefix: String): CompletionResult {
val widgets = sdkInfo.widgetInfo.widgets
var isIncomplete = false
val result = mutableListOf<CompletionItem>()
for (widget in widgets) {
@@ -198,23 +225,16 @@ class XmlCompletionProvider(val sdkInfo: SDKInfo, settings: IServerSettings) :
if (simpleNameMatchRatio > 0 || nameMatchRatio > 0) {
result.add(
createTagCompletionItem(widget, max(simpleNameMatchRatio, nameMatchRatio)))
if (result.size == CompletionResult.MAX_ITEMS) {
isIncomplete = true
break
}
}
}
return CompletionResult(isIncomplete, result)
return CompletionResult(result)
}
private fun createTagCompletionItem(widget: Widget, matchRatio: Int): CompletionItem =
CompletionItem().apply {
label = widget.simpleName
detail = widget.name
insertText = label as String
insertTextFormat = PLAIN_TEXT
sortText = sortTextForMatchRatio(matchRatio, label)
kind = CLASS
data = CompletionData().apply { className = widget.name }
@@ -54,6 +54,18 @@ class XMLCompletionProviderTest : BaseXMLTest() {
assertThat(items).containsAtLeast("center", "fitCenter", "fitXY", "matrix")
}
@Test
fun testAttrCompletion() {
openFile("AttributeCompletion")
val (isIncomplete, items) = complete()
assertThat(isIncomplete).isTrue()
assertThat(items).isNotEmpty()
assertThat(items).containsAtLeast("text", "textSize", "textColor", "textStyle")
}
override fun openFile(fileName: String) {
super.openFile("completion/${fileName}")
}
@@ -19,12 +19,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
<TextView
android:id="@+id/icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/ic_android"
android:scaleType="@@cursor@@"
te@@cursor@@
android:padding="4dp"
android:layout_marginEnd="8dp"
android:layout_gravity="center_vertical"/>
@@ -17,7 +17,6 @@
package com.itsaky.attrinfo;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
@@ -37,6 +36,7 @@ import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
/**
* A parser for parsing <b>attrs.xml</b>. This parser maps the attributes with the declared
@@ -49,14 +49,14 @@ public class AttrInfo {
/** Styles mapped by names; */
public final Map<String, Styleable> styles = new HashMap<>();
public final Map<String, Attr> attributes = new HashMap<>();
public final Map<String, Attr> attributes = new TreeMap<>();
public final Styleable NO_PARENT;
public AttrInfo(@NonNull final Resources resources) throws Exception {
NO_PARENT = new Styleable("<unknown_parent>");
Objects.requireNonNull(resources, "Cannot read from null resources.");
final var in = resources.openRawResource(R.raw.attrs);
parseFromStream(in);
}
@@ -88,6 +88,29 @@ public class AttrInfo {
return styles;
}
@Override
public int hashCode() {
return Objects.hash(NO_PARENT, styles);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AttrInfo)) {
return false;
}
AttrInfo that = (AttrInfo) o;
return Objects.equals(NO_PARENT, that.NO_PARENT) && Objects.equals(styles, that.styles);
}
@NonNull
@Override
public String toString() {
return "AttrInfoCompat{" + "NO_PARENT=" + NO_PARENT + ", styles=" + styles + '}';
}
protected void parseFromStream(InputStream in) throws Exception {
Objects.requireNonNull(in, "Cannot read from null input stream");
@@ -204,27 +227,4 @@ public class AttrInfo {
throw new IllegalStateException("Element does not have 'name' attribute: " + child);
}
}
@NonNull
@Override
public String toString() {
return "AttrInfoCompat{" + "NO_PARENT=" + NO_PARENT + ", styles=" + styles + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AttrInfo)) {
return false;
}
AttrInfo that = (AttrInfo) o;
return Objects.equals(NO_PARENT, that.NO_PARENT) && Objects.equals(styles, that.styles);
}
@Override
public int hashCode() {
return Objects.hash(NO_PARENT, styles);
}
}