diff --git a/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar b/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar index ca86c4f5d..5e8cb6631 100644 Binary files a/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar and b/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar differ diff --git a/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt b/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt index 7b134ebac..e08963b41 100644 --- a/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt +++ b/lsp/api/src/main/java/com/itsaky/lsp/models/Completions.kt @@ -45,20 +45,25 @@ data class CompletionParams(var position: Position, var file: Path) { } } -open class CompletionResult(var isIncomplete: Boolean, items: List) { +open class CompletionResult(items: List) { + var items: List = 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()) + 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), diff --git a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CompletionProvider.java b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CompletionProvider.java index 2552d4269..779baee3f 100644 --- a/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CompletionProvider.java +++ b/lsp/java/src/main/java/com/itsaky/lsp/java/providers/CompletionProvider.java @@ -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; } } diff --git a/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/XmlCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/XmlCompletionProvider.kt index c84b8374c..e2538cf5f 100644 --- a/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/XmlCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/XmlCompletionProvider.kt @@ -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() + 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() 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 } diff --git a/lsp/xml/src/test/java/com/itsaky/lsp/xml/providers/XMLCompletionProviderTest.kt b/lsp/xml/src/test/java/com/itsaky/lsp/xml/providers/XMLCompletionProviderTest.kt index 50c1b1dce..dfe3e19ac 100644 --- a/lsp/xml/src/test/java/com/itsaky/lsp/xml/providers/XMLCompletionProviderTest.kt +++ b/lsp/xml/src/test/java/com/itsaky/lsp/xml/providers/XMLCompletionProviderTest.kt @@ -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}") } diff --git a/lsp/xml/src/test/resources/completion/AttributeCompletion_template.xml b/lsp/xml/src/test/resources/completion/AttributeCompletion_template.xml index d171f1f5c..d94b632b6 100644 --- a/lsp/xml/src/test/resources/completion/AttributeCompletion_template.xml +++ b/lsp/xml/src/test/resources/completion/AttributeCompletion_template.xml @@ -19,12 +19,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - diff --git a/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java b/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java index 936bd3443..c7d17ae9b 100755 --- a/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java +++ b/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java @@ -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 attrs.xml. This parser maps the attributes with the declared @@ -49,14 +49,14 @@ public class AttrInfo { /** Styles mapped by names; */ public final Map styles = new HashMap<>(); - public final Map attributes = new HashMap<>(); + public final Map attributes = new TreeMap<>(); public final Styleable NO_PARENT; public AttrInfo(@NonNull final Resources resources) throws Exception { NO_PARENT = new Styleable(""); - + 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); - } }