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());