diff --git a/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrValueEditorSheet.java b/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrValueEditorSheet.java
index fecaec1e5..5a7a3990a 100644
--- a/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrValueEditorSheet.java
+++ b/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrValueEditorSheet.java
@@ -116,7 +116,7 @@ public class AttrValueEditorSheet extends BottomSheetDialogFragment
final var namespace = this.attribute.getNamespace();
final var formatText = Attr.createFormatText(attribute.getFormat());
this.binding.attributeName.setText(
- String.format("%s:%s", namespace.getName(), attribute.getAttributeName()));
+ String.format("%s:%s", namespace.getPrefix (), attribute.getAttributeName()));
this.binding.attributeFormat.setText(formatText);
setupTabs(formatText);
diff --git a/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.java b/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.java
index e74f91357..102262fef 100644
--- a/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.java
+++ b/app/src/main/java/com/itsaky/androidide/language/CommonCompletionProvider.java
@@ -19,6 +19,7 @@ package com.itsaky.androidide.language;
import androidx.annotation.NonNull;
+import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.lsp.api.ICompletionProvider;
import com.itsaky.lsp.api.ILanguageServer;
@@ -123,6 +124,7 @@ public class CommonCompletionProvider {
file);
params.setContent(content);
params.setPrefix(prefix);
+ params.setModule(ProjectManager.INSTANCE.findModuleForFile (file.toFile ()));
final var result = completer.complete(params);
Collections.sort(result.getItems());
return result;
diff --git a/app/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt b/app/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt
index 8c88fed5a..47db018c1 100644
--- a/app/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt
+++ b/app/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt
@@ -245,4 +245,38 @@ object ProjectManager {
android.variants.first { it.name == "debug" }.mainArtifact.generatedSourceFolders)
return sources
}
+
+ /**
+ * Finds the module in which the given file exists.
+ *
+ * @return The module in which the given file exists or `null` if the module cannot be found.
+ */
+ fun findModuleForFile(file: File): IdeModule? {
+ if (rootProject == null) {
+ log.warn(
+ "No root project instance is set. Is the project initialization process finished?")
+ return null
+ }
+
+ val path = file.canonicalPath
+ val modules = rootProject!!.getModules()
+
+ var longestPath = ""
+ var longestPathModule: IdeModule? = null
+ for (module in modules) {
+ val modulePath = module.projectDir!!.canonicalPath
+ if (path.startsWith(modulePath)) {
+ if (longestPath.length < modulePath.length && module is IdeModule) {
+ longestPathModule = module
+ longestPath = modulePath
+ }
+ }
+ }
+
+ if (longestPath.isEmpty()) {
+ return null
+ }
+
+ return longestPathModule
+ }
}
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 ca64b7b65..349de83a2 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/build-tools/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/model/IdeAndroidModule.kt b/build-tools/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/model/IdeAndroidModule.kt
index 650d7d1b4..51a0bb596 100644
--- a/build-tools/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/model/IdeAndroidModule.kt
+++ b/build-tools/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/model/IdeAndroidModule.kt
@@ -203,7 +203,7 @@ class IdeAndroidModule(
shouldLookupPackage = false
}
- val packageAttr = manifest.attributes.getNamedItem("package")]
+ val packageAttr = manifest.attributes.getNamedItem("package")
this.packageName = packageAttr.nodeValue
this.shouldLookupPackage = false
}
diff --git a/lsp/api/build.gradle b/lsp/api/build.gradle
index 52d4c2b4e..7d70359ab 100644
--- a/lsp/api/build.gradle
+++ b/lsp/api/build.gradle
@@ -20,16 +20,16 @@ apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
-
+
defaultConfig {
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
}
-
+
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
@@ -42,11 +42,14 @@ dependencies {
implementation common.editor
implementation "androidx.core:core-ktx:1.7.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-
- compileOnly project(':actions')
- compileOnly project(':common')
+
+ api project (':build-tools:tooling-api-model')
+
+ compileOnly project (':xml-inflater')
+ compileOnly project (':actions')
+ compileOnly project (':common')
}
repositories {
- mavenCentral()
+ mavenCentral ()
}
\ No newline at end of file
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 75c3b54ee..c4caf8693 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
@@ -17,6 +17,7 @@
package com.itsaky.lsp.models
+import com.itsaky.androidide.tooling.api.model.IdeModule
import com.itsaky.androidide.utils.ILogger
import io.github.rosemoe.sora.text.Content
import io.github.rosemoe.sora.widget.CodeEditor
@@ -25,6 +26,7 @@ import java.nio.file.Path
data class CompletionParams(var position: Position, var file: Path) {
var content: CharSequence? = null
var prefix: String? = null
+ var module: IdeModule? = null
fun requirePrefix(): String {
if (prefix == null) {
diff --git a/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/CompletionProvider.java b/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/CompletionProvider.java
index 2b46a19d3..e7291d0f9 100644
--- a/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/CompletionProvider.java
+++ b/lsp/xml/src/main/java/com/itsaky/lsp/xml/providers/CompletionProvider.java
@@ -248,13 +248,13 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
@NonNull
private String createAttributeInsertText(@NonNull Attr attr) {
StringBuilder xml = new StringBuilder();
- xml.append(attr.namespace.getName());
+ xml.append(attr.namespace.getPrefix ());
xml.append(":");
xml.append(attr.name);
xml.append("=");
xml.append("\"");
- if (attr.namespace.getName().equals("android") && attr.name.equals("id")) {
+ if (attr.namespace.getPrefix ().equals("android") && attr.name.equals("id")) {
xml.append("@+id/");
}
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 c1b84ce58..9fabce458 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
@@ -17,10 +17,16 @@
package com.itsaky.lsp.xml.providers
+import com.itsaky.androidide.tooling.api.model.IdeAndroidModule
import com.itsaky.androidide.utils.ILogger
import com.itsaky.lsp.api.ICompletionProvider
import com.itsaky.lsp.models.CompletionParams
import com.itsaky.lsp.models.CompletionResult
+import com.itsaky.lsp.xml.utils.XmlUtils
+import com.itsaky.lsp.xml.utils.XmlUtils.NodeType.UNKNOWN
+import com.itsaky.xml.INamespace
+import org.eclipse.lemminx.dom.DOMParser
+import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager
/**
* Completion provider for XMl files.
@@ -34,6 +40,25 @@ class XmlCompletionProvider : ICompletionProvider {
override fun complete(params: CompletionParams): CompletionResult {
return try {
+ if (params.module == null || params.module !is IdeAndroidModule) {
+ log.warn("Cannot provide completions for file:", params.file)
+ log.warn("Module provide in params is either null or is not an Android module")
+ return CompletionResult()
+ }
+
+ val contents = params.requireContents().toString()
+ val namespace =
+ INamespace.forPackageName((params.module as IdeAndroidModule).packageName)
+ val document =
+ DOMParser.getInstance()
+ .parse(contents, namespace.uri, URIResolverExtensionManager())
+ val type = XmlUtils.getNodeType(document, params.position.requireIndex())
+
+ if (type == UNKNOWN) {
+ log.warn("Unknown node type. CompletionParams:", params)
+ return CompletionResult()
+ }
+
CompletionResult()
} catch (error: Throwable) {
log.error("An error occurred while computing XML completions", error)
diff --git a/lsp/xml/src/main/java/com/itsaky/lsp/xml/utils/XmlUtils.java b/lsp/xml/src/main/java/com/itsaky/lsp/xml/utils/XmlUtils.java
deleted file mode 100644
index 900b92756..000000000
--- a/lsp/xml/src/main/java/com/itsaky/lsp/xml/utils/XmlUtils.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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 .
- */
-
-package com.itsaky.lsp.xml.utils;
-
-import androidx.annotation.NonNull;
-
-import com.itsaky.androidide.utils.CharSequenceReader;
-import com.itsaky.androidide.utils.ILogger;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-/**
- * @author Akash Yadav
- */
-public class XmlUtils {
-
- private static final XmlPullParserFactory parserFactory;
- private static final ILogger LOG = ILogger.newInstance("XmlUtils");
-
- static {
- try {
- parserFactory = XmlPullParserFactory.newInstance();
- } catch (XmlPullParserException e) {
- LOG.error("Unable to create pull parser factory");
- throw new RuntimeException(e);
- }
- }
-
- public static XmlPullParser newParser() throws XmlPullParserException {
- return parserFactory.newPullParser();
- }
-
- @NonNull
- public static XmlPullParser newParser(CharSequence contents) throws XmlPullParserException {
- final var parser = parserFactory.newPullParser();
- parser.setInput(new CharSequenceReader(contents));
- return parser;
- }
-}
diff --git a/lsp/xml/src/main/java/com/itsaky/lsp/xml/utils/XmlUtils.kt b/lsp/xml/src/main/java/com/itsaky/lsp/xml/utils/XmlUtils.kt
new file mode 100644
index 000000000..7dcfa451e
--- /dev/null
+++ b/lsp/xml/src/main/java/com/itsaky/lsp/xml/utils/XmlUtils.kt
@@ -0,0 +1,138 @@
+/*
+ * 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 .
+ */
+package com.itsaky.lsp.xml.utils
+
+import com.itsaky.androidide.lexers.xml.XMLLexer
+import com.itsaky.androidide.utils.CharSequenceReader
+import com.itsaky.androidide.utils.ILogger
+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 org.antlr.v4.runtime.CharStreams
+import org.antlr.v4.runtime.Token
+import org.eclipse.lemminx.dom.DOMDocument
+import org.eclipse.lemminx.dom.DOMElement
+import org.eclipse.lemminx.dom.DOMNode
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+import org.xmlpull.v1.XmlPullParserFactory
+
+/** @author Akash Yadav */
+object XmlUtils {
+ private val LOG = ILogger.newInstance("XmlUtils")
+ private val parserFactory: XmlPullParserFactory =
+ try {
+ XmlPullParserFactory.newInstance()
+ } catch (e: XmlPullParserException) {
+ LOG.error("Unable to create pull parser factory")
+ throw RuntimeException(e)
+ }
+
+ @Throws(XmlPullParserException::class)
+ fun newParser(): XmlPullParser {
+ return parserFactory.newPullParser()
+ }
+
+ @Throws(XmlPullParserException::class)
+ fun newParser(contents: CharSequence?): XmlPullParser {
+ val parser = parserFactory.newPullParser()
+ parser.setInput(CharSequenceReader(contents))
+ return parser
+ }
+
+ fun isTag(node: DOMNode, index: Int): Boolean {
+ var name = node.nodeName
+ if (name == null) {
+ name = ""
+ }
+ return node.start < index && index <= node.start + name.length + 1
+ }
+
+ fun isEndTag(node: DOMNode?, index: Int): Boolean {
+ if (node !is DOMElement) {
+ return false
+ }
+ val endOpenOffset = node.endTagOpenOffset
+ return if (endOpenOffset == -1) {
+ false
+ } else index >= endOpenOffset
+ }
+
+ fun isInAttributeValue(contents: String?, index: Int): Boolean {
+ val lexer = XMLLexer(CharStreams.fromString(contents))
+ var token: Token
+ while (lexer.nextToken().also { token = it } != null) {
+ val start = token.startIndex
+ val end = token.stopIndex
+ if (token.type == Token.EOF) {
+ break
+ }
+
+ if (index in start..end) {
+ return token.type == XMLLexer.STRING
+ }
+ if (end > index) {
+ break
+ }
+ }
+ return false
+ }
+
+ fun getPrefix(parsed: DOMDocument, index: Long, type: NodeType?): String? {
+ val text = parsed.text
+ when (type) {
+ TAG -> {
+ val nodeAt = parsed.findNodeAt(index.toInt()) ?: return null
+ return text.substring(nodeAt.start, index.toInt())
+ }
+ ATTRIBUTE -> {
+ val attr = parsed.findAttrAt(index.toInt()) ?: return null
+ return text.substring(attr.start, index.toInt())
+ }
+ ATTRIBUTE_VALUE -> {
+ val attrAt = parsed.findAttrAt(index.toInt()) ?: return null
+ var prefix = text.substring(attrAt.nodeAttrValue.start + 1, index.toInt())
+ if (prefix.contains("|")) {
+ prefix = prefix.substring(prefix.lastIndexOf('|') + 1)
+ }
+ return prefix
+ }
+ else -> return ""
+ }
+ }
+
+ fun getNodeType(parsed: DOMDocument, cursor: Int): NodeType {
+ val nodeAt = parsed.findNodeAt(cursor) ?: return NodeType.UNKNOWN
+
+ if (isTag(nodeAt, cursor) || isEndTag(nodeAt, cursor)) {
+ return TAG
+ }
+
+ return if (isInAttributeValue(parsed.textDocument.text, cursor)) {
+ ATTRIBUTE_VALUE
+ } else {
+ ATTRIBUTE
+ }
+ }
+
+ enum class NodeType {
+ UNKNOWN,
+ TAG,
+ ATTRIBUTE,
+ ATTRIBUTE_VALUE
+ }
+}
diff --git a/sdk-info/src/main/java/com/itsaky/xml/INamespace.java b/sdk-info/src/main/java/com/itsaky/xml/INamespace.java
index 11f9eb3eb..afaf8af21 100644
--- a/sdk-info/src/main/java/com/itsaky/xml/INamespace.java
+++ b/sdk-info/src/main/java/com/itsaky/xml/INamespace.java
@@ -31,11 +31,35 @@ import org.jetbrains.annotations.Contract;
*/
public interface INamespace extends Parcelable {
+ String URI_PREFIX = "http://schemas.android.com/apk/res/";
+
/** Used to represent the namespace declarator (xmlns) (don't know what else to call it). */
INamespace DECLARATOR = new UiNamespace("xmlns", "");
/** The android namespace. */
- INamespace ANDROID = new UiNamespace("android", "http://schemas.android.com/apk/res/android");
+ INamespace ANDROID =
+ new UiNamespace("android", "android", "http://schemas.android.com/apk/res/android");
+
+ /** The 'res-auto' namespace. */
+ INamespace RES_AUTO = new UiNamespace("", null, "http://schemas.android.com/apk/res-auto");
+
+ /** The tools namespace. */
+ INamespace TOOLS = new UiNamespace("tools", null, "http://schemas.android.com/tools");
+
+ static INamespace forPackageName(@NonNull String packageName) {
+ if (ANDROID.getPackageName().equals(packageName)) {
+ return ANDROID;
+ }
+
+ return new UiNamespace("", packageName, INamespace.URI_PREFIX + packageName);
+ }
+
+ /**
+ * Get the package name of this namespace (as used by aapt).
+ *
+ * @return The package name.
+ */
+ String getPackageName();
@NonNull
@Contract(value = "_ -> new", pure = true)
@@ -48,7 +72,7 @@ public interface INamespace extends Parcelable {
*
* @return The name of the namespace.
*/
- String getName();
+ String getPrefix();
/**
* Get the uri of this namespace.
diff --git a/sdk-info/src/main/java/com/itsaky/xml/impl/UiNamespace.java b/sdk-info/src/main/java/com/itsaky/xml/impl/UiNamespace.java
index 5cedc4404..8627fade6 100644
--- a/sdk-info/src/main/java/com/itsaky/xml/impl/UiNamespace.java
+++ b/sdk-info/src/main/java/com/itsaky/xml/impl/UiNamespace.java
@@ -31,9 +31,6 @@ import java.util.Objects;
*/
public class UiNamespace implements INamespace {
- private final String name;
- private final String uri;
-
public static final Creator CREATOR =
new Creator<>() {
@NonNull
@@ -50,21 +47,29 @@ public class UiNamespace implements INamespace {
return new UiNamespace[0];
}
};
+ private final String prefix;
+ private final String packageName;
+ private final String uri;
- public UiNamespace(String name, String uri) {
- this.name = name;
+ public UiNamespace(String prefix, String uri) {
+ this(prefix, null, uri);
+ }
+
+ public UiNamespace(String prefix, String packageName, String uri) {
+ this.prefix = prefix;
+ this.packageName = packageName;
this.uri = uri;
}
private UiNamespace(@NonNull Parcel in) {
- this.name = in.readString();
+ this.prefix = in.readString();
+ this.packageName = in.readString();
this.uri = in.readString();
}
- @NonNull
@Override
- public String toString() {
- return "UiNamespace{" + "name='" + name + '\'' + ", uri='" + uri + '\'' + '}';
+ public int hashCode() {
+ return Objects.hash(getUri());
}
@Override
@@ -84,14 +89,20 @@ public class UiNamespace implements INamespace {
return Objects.equals(getUri(), that.getUri());
}
+ @NonNull
@Override
- public int hashCode() {
- return Objects.hash(getUri());
+ public String toString() {
+ return "UiNamespace{" + "name='" + prefix + '\'' + ", uri='" + uri + '\'' + '}';
}
@Override
- public String getName() {
- return name;
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @Override
+ public String getPackageName() {
+ return packageName;
}
@Override
@@ -106,7 +117,8 @@ public class UiNamespace implements INamespace {
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeString(name);
+ dest.writeString(prefix);
+ dest.writeString(packageName);
dest.writeString(uri);
}
}
diff --git a/xml-inflater/src/main/java/com/itsaky/inflater/impl/BaseView.java b/xml-inflater/src/main/java/com/itsaky/inflater/impl/BaseView.java
index 70749cf7f..7fc669759 100644
--- a/xml-inflater/src/main/java/com/itsaky/inflater/impl/BaseView.java
+++ b/xml-inflater/src/main/java/com/itsaky/inflater/impl/BaseView.java
@@ -43,7 +43,7 @@ public abstract class BaseView implements IView {
public static final String XML_HEADER = "";
protected final Set namespaces =
- new TreeSet<>(Comparator.comparing(INamespace::getName));
+ new TreeSet<>(Comparator.comparing(INamespace::getPrefix));
protected final List attributes = new ArrayList<>();
protected final Set attrAdapters = new HashSet<>();
@@ -227,7 +227,7 @@ public abstract class BaseView implements IView {
public INamespace findRegisteredNamespace(@NonNull String name) {
final var namespaces =
this.namespaces.stream()
- .filter(ns -> name.equals(ns.getName()))
+ .filter(ns -> name.equals(ns.getPrefix ()))
.collect(Collectors.toSet());
if (namespaces.isEmpty()) {
@@ -269,8 +269,8 @@ public abstract class BaseView implements IView {
newLine(sb, indentCount + 1); // Attributes must be indented by one tab (4 spaces by
// default)
attr.getNamespace();
- if (attr.getNamespace() != null && attr.getNamespace().getName().length() > 0) {
- sb.append(attr.getNamespace().getName());
+ if (attr.getNamespace() != null && attr.getNamespace().getPrefix ().length() > 0) {
+ sb.append(attr.getNamespace().getPrefix ());
sb.append(":");
}
sb.append(attr.getAttributeName());