mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Provide attribute completions for XML files
This commit is contained in:
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user