Added utility method to find the module of a file

This commit is contained in:
Akash Yadav
2022-05-09 15:15:49 +05:30
parent 5f113f6520
commit 10e5551eca
14 changed files with 271 additions and 87 deletions
@@ -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);
@@ -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;
@@ -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
}
}
@@ -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
}
+10 -7
View File
@@ -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 ()
}
@@ -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) {
@@ -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/");
}
@@ -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)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 "<this-will-not-match>"
}
}
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
}
}
@@ -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", "<namespace declarator>");
/** 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.
@@ -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<UiNamespace> 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);
}
}
@@ -43,7 +43,7 @@ public abstract class BaseView implements IView {
public static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
protected final Set<INamespace> namespaces =
new TreeSet<>(Comparator.comparing(INamespace::getName));
new TreeSet<>(Comparator.comparing(INamespace::getPrefix));
protected final List<IAttribute> attributes = new ArrayList<>();
protected final Set<IAttributeAdapter> 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());