mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 15:56:48 +08:00
Implemented snippet support in LSP
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.androidide.lsp.api
|
||||
|
||||
import com.itsaky.androidide.lsp.models.SnippetDescription
|
||||
|
||||
fun describeSnippet(prefix: String) = SnippetDescription(prefix.length)
|
||||
+3
@@ -18,6 +18,7 @@
|
||||
package com.itsaky.androidide.lsp.java.providers.completion
|
||||
|
||||
import com.itsaky.androidide.lsp.api.IServerSettings
|
||||
import com.itsaky.androidide.lsp.api.describeSnippet
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
|
||||
import com.itsaky.androidide.lsp.java.edits.ClassImportEditHandler
|
||||
@@ -157,6 +158,7 @@ abstract class IJavaCompletionProvider(
|
||||
overloads: List<ExecutableElement>,
|
||||
addParens: Boolean,
|
||||
matchLevel: MatchLevel,
|
||||
partial: String
|
||||
): CompletionItem {
|
||||
abortIfCancelled()
|
||||
abortCompletionIfCancelled()
|
||||
@@ -181,6 +183,7 @@ abstract class IJavaCompletionProvider(
|
||||
item.command = Command("Trigger Parameter Hints", Command.TRIGGER_PARAMETER_HINTS)
|
||||
}
|
||||
item.insertTextFormat = SNIPPET // Snippet
|
||||
item.snippetDescription = describeSnippet(partial)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
+3
-3
@@ -262,7 +262,7 @@ class ImportCompletionProvider(
|
||||
task: CompileTask,
|
||||
type: TypeElement,
|
||||
path: TreePath,
|
||||
incomplete: String
|
||||
partial: String
|
||||
): MutableList<CompletionItem> {
|
||||
|
||||
abortIfCancelled()
|
||||
@@ -289,7 +289,7 @@ class ImportCompletionProvider(
|
||||
continue
|
||||
}
|
||||
|
||||
val match = matchLevel(member.simpleName, incomplete)
|
||||
val match = matchLevel(member.simpleName, partial)
|
||||
if (match == NO_MATCH) {
|
||||
continue
|
||||
}
|
||||
@@ -313,7 +313,7 @@ class ImportCompletionProvider(
|
||||
}
|
||||
|
||||
if (member.kind == METHOD) {
|
||||
list.add(method(task, listOf(member as MethodSymbol), false, match))
|
||||
list.add(method(task, listOf(member as MethodSymbol), false, match, partial))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ class MemberReferenceCompletionProvider(
|
||||
continue
|
||||
}
|
||||
|
||||
list.add(method(task, value, false, matchLevel))
|
||||
list.add(method(task, value, false, matchLevel, partial))
|
||||
}
|
||||
|
||||
if (isStatic) {
|
||||
|
||||
+6
-6
@@ -131,7 +131,7 @@ class MemberSelectCompletionProvider(
|
||||
scope: Scope,
|
||||
type: DeclaredType,
|
||||
isStatic: Boolean,
|
||||
partialName: String,
|
||||
partial: String,
|
||||
endsWithParen: Boolean,
|
||||
): CompletionResult {
|
||||
val trees = Trees.instance(task.task)
|
||||
@@ -147,7 +147,7 @@ class MemberSelectCompletionProvider(
|
||||
if (member.kind == CONSTRUCTOR) {
|
||||
continue
|
||||
}
|
||||
val matchLevel = matchLevel(member.simpleName, partialName)
|
||||
val matchLevel = matchLevel(member.simpleName, partial)
|
||||
if (matchLevel == NO_MATCH) {
|
||||
continue
|
||||
}
|
||||
@@ -176,16 +176,16 @@ class MemberSelectCompletionProvider(
|
||||
continue
|
||||
}
|
||||
|
||||
list.add(method(task, value, !endsWithParen, matchLevel))
|
||||
list.add(method(task, value, !endsWithParen, matchLevel, partial))
|
||||
}
|
||||
|
||||
if (isStatic) {
|
||||
list.add(keyword("class", partialName, 100))
|
||||
list.add(keyword("class", partial, 100))
|
||||
}
|
||||
|
||||
if (!isStatic && isEnclosingClass(type, scope)) {
|
||||
list.add(keyword("this", partialName, 100))
|
||||
list.add(keyword("super", partialName, 100))
|
||||
list.add(keyword("this", partial, 100))
|
||||
list.add(keyword("super", partial, 100))
|
||||
}
|
||||
|
||||
return CompletionResult(list)
|
||||
|
||||
+8
-6
@@ -32,6 +32,7 @@ import com.itsaky.androidide.lsp.java.utils.ScopeHelper
|
||||
import com.itsaky.androidide.lsp.models.CompletionItem
|
||||
import com.itsaky.androidide.lsp.models.CompletionResult
|
||||
import com.itsaky.androidide.lsp.models.InsertTextFormat.SNIPPET
|
||||
import com.itsaky.androidide.lsp.models.MatchLevel
|
||||
import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
|
||||
import com.itsaky.androidide.progress.ProgressManager.Companion.abortIfCancelled
|
||||
import com.itsaky.androidide.projects.FileManager
|
||||
@@ -101,7 +102,7 @@ class ScopeCompletionProvider(
|
||||
if (member.kind == METHOD) {
|
||||
val method = member as ExecutableElement
|
||||
val parentPath = path.parentPath /*method*/.parentPath /*class*/
|
||||
list.add(overrideIfPossible(task, parentPath, method, endsWithParen, matchLevel))
|
||||
list.add(overrideIfPossible(task, parentPath, method, endsWithParen, matchLevel, partial))
|
||||
} else {
|
||||
list.add(item(task, member, matchLevel))
|
||||
}
|
||||
@@ -126,11 +127,12 @@ class ScopeCompletionProvider(
|
||||
parentPath: TreePath,
|
||||
method: ExecutableElement,
|
||||
endsWithParen: Boolean,
|
||||
matchLevel: com.itsaky.androidide.lsp.models.MatchLevel,
|
||||
matchLevel: MatchLevel,
|
||||
partial: String
|
||||
): CompletionItem {
|
||||
if (parentPath.leaf.kind != CLASS) {
|
||||
// Can only override if the cursor is directly in a class declaration
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel)
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel, partial)
|
||||
}
|
||||
|
||||
abortIfCancelled()
|
||||
@@ -139,7 +141,7 @@ class ScopeCompletionProvider(
|
||||
val parentElement =
|
||||
Trees.instance(task.task).getElement(parentPath)
|
||||
?: // Can't get further information for overriding this method
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel)
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel, partial)
|
||||
val type = parentElement.asType() as DeclaredType
|
||||
val enclosing = method.enclosingElement
|
||||
val isFinalClass = enclosing.modifiers.contains(FINAL)
|
||||
@@ -154,7 +156,7 @@ class ScopeCompletionProvider(
|
||||
parentPath.leaf !is ClassTree
|
||||
) {
|
||||
// Override is not possible
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel)
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel, partial)
|
||||
}
|
||||
|
||||
// Print the method details and the annotations
|
||||
@@ -166,7 +168,7 @@ class ScopeCompletionProvider(
|
||||
builder = buildMethod(method, types, type)
|
||||
} catch (error: Throwable) {
|
||||
log.error("Cannot override method:", method.simpleName, error.message)
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel)
|
||||
return method(task, listOf(method), !endsWithParen, matchLevel, partial)
|
||||
}
|
||||
|
||||
val imports = mutableSetOf<String>()
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ class StaticImportCompletionProvider(
|
||||
continue
|
||||
}
|
||||
|
||||
list.add(method(task, value, !endsWithParen, matchLevel))
|
||||
list.add(method(task, value, !endsWithParen, matchLevel, partial))
|
||||
}
|
||||
|
||||
log.info("...found " + list.size + " static imports")
|
||||
|
||||
+1
-2
@@ -19,6 +19,7 @@ package com.itsaky.androidide.lsp.java.providers.completion
|
||||
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.lsp.api.ICompletionCancelChecker
|
||||
import com.itsaky.androidide.lsp.api.describeSnippet
|
||||
import com.itsaky.androidide.lsp.java.parser.ParseTask
|
||||
import com.itsaky.androidide.lsp.models.CompletionItem
|
||||
import com.itsaky.androidide.lsp.models.CompletionItemKind.SNIPPET
|
||||
@@ -67,8 +68,6 @@ class TopLevelSnippetsProvider {
|
||||
return snippetItem("class $name", "class $name {\n $0\n}", describeSnippet(prefix))
|
||||
}
|
||||
|
||||
private fun describeSnippet(prefix: String) = SnippetDescription(prefix.length)
|
||||
|
||||
private fun packageSnippet(prefix: String, file: Path): CompletionItem? {
|
||||
abortIfCancelled()
|
||||
abortCompletionIfCancelled()
|
||||
|
||||
-356
@@ -1,356 +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.androidide.lsp.xml.providers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.itsaky.androidide.app.BaseApplication;
|
||||
import com.itsaky.androidide.lexers.xml.XMLLexer;
|
||||
import com.itsaky.androidide.utils.CharSequenceReader;
|
||||
import com.itsaky.androidide.utils.ILogger;
|
||||
import com.itsaky.attrinfo.models.Attr;
|
||||
import com.itsaky.androidide.lsp.api.AbstractServiceProvider;
|
||||
import com.itsaky.androidide.lsp.api.ICompletionProvider;
|
||||
import com.itsaky.androidide.lsp.api.IServerSettings;
|
||||
import com.itsaky.androidide.lsp.models.Command;
|
||||
import com.itsaky.androidide.lsp.models.CompletionData;
|
||||
import com.itsaky.androidide.lsp.models.CompletionItem;
|
||||
import com.itsaky.androidide.lsp.models.CompletionItemKind;
|
||||
import com.itsaky.androidide.lsp.models.CompletionParams;
|
||||
import com.itsaky.androidide.lsp.models.CompletionResult;
|
||||
import com.itsaky.androidide.lsp.models.InsertTextFormat;
|
||||
import com.itsaky.androidide.lsp.xml.R;
|
||||
import com.itsaky.sdk.SDKInfo;
|
||||
import com.itsaky.widgets.models.Widget;
|
||||
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
|
||||
|
||||
/**
|
||||
* Completion provider for the XML Language
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class CompletionProvider extends AbstractServiceProvider implements ICompletionProvider {
|
||||
|
||||
private static final ILogger LOG = ILogger.newInstance("XMLCompletionProvider");
|
||||
private static final String APP_NS = "http://schemas.android.com/apk/res/auto";
|
||||
private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android";
|
||||
private static final String TOOLS_NS = "http://schemas.android.com/tools";
|
||||
|
||||
private final SDKInfo sdkInfo;
|
||||
|
||||
public CompletionProvider(SDKInfo sdkInfo, IServerSettings settings) {
|
||||
Objects.requireNonNull(sdkInfo);
|
||||
Objects.requireNonNull(settings);
|
||||
super.applySettings(settings);
|
||||
|
||||
this.sdkInfo = sdkInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canComplete(Path file) {
|
||||
return ICompletionProvider.super.canComplete(file) && file.toFile().getName().endsWith(".xml");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public CompletionResult complete(@NonNull CompletionParams params) {
|
||||
final var index = params.getPosition().requireIndex();
|
||||
final var contents = params.requireContents();
|
||||
final var prefix = params.requirePrefix();
|
||||
try {
|
||||
return complete(contents, prefix, getFileType(params.getFile()), index);
|
||||
} catch (Exception e) {
|
||||
// Do not log if completion request was cancelled
|
||||
if (!(e instanceof CompletionCancelledException)) {
|
||||
LOG.error("Unable to provide XML completions");
|
||||
}
|
||||
return CompletionResult.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CompletionResult complete(
|
||||
final CharSequence contents,
|
||||
@NonNull final String prefix,
|
||||
final String fileType,
|
||||
final int index) {
|
||||
if (fileType == null) {
|
||||
return CompletionResult.EMPTY;
|
||||
}
|
||||
|
||||
final var matchLower = getSettings().shouldMatchAllLowerCase();
|
||||
final var result = new CompletionResult();
|
||||
final var attrs = sdkInfo.getAttrInfo();
|
||||
// final var parser = XmlUtils.newParser (contents);
|
||||
|
||||
if (prefix.startsWith("<") || prefix.startsWith("</")) {
|
||||
if ("layout".equals(fileType)) {
|
||||
var newPrefix = prefix.substring(1);
|
||||
var slash = false;
|
||||
if (newPrefix.startsWith("/")) {
|
||||
slash = true;
|
||||
newPrefix = newPrefix.substring(1);
|
||||
}
|
||||
|
||||
addLayoutXmlTags(result, newPrefix, slash);
|
||||
}
|
||||
} else {
|
||||
final IsInValueScanner scanner = new IsInValueScanner(index);
|
||||
final String name = scanner.scan(contents);
|
||||
if (name != null) {
|
||||
final String attrName = name.contains(":") ? name.substring(name.indexOf(":") + 1) : name;
|
||||
if (attrs.getAttributes().containsKey(attrName)) {
|
||||
Attr attr = attrs.getAttributes().get(attrName);
|
||||
if (attr != null && attr.hasPossibleValues()) {
|
||||
Set<String> values = attr.possibleValues;
|
||||
for (String value : values) {
|
||||
if (matchesPartialName(value, prefix, matchLower)) {
|
||||
result.getItems().add(valueAsCompletion(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (prefix.length() > 0) {
|
||||
for (Map.Entry<String, Attr> entry : attrs.getAttributes().entrySet()) {
|
||||
Attr attr = entry.getValue();
|
||||
if (matchesPartialName(attr.name, prefix, matchLower)) {
|
||||
result.getItems().add(attrAsCompletion(attr));
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcuts for automatically declaring namespaces
|
||||
// These completions are proposed if you type 'androidNs', 'appNs' or 'toolsNs'
|
||||
// Idea is shamelessly copied from Android Studio 😂
|
||||
if (matchesPartialName("android", prefix, matchLower)) {
|
||||
result.getItems().add(createNamespaceCompletion("android", ANDROID_NS));
|
||||
}
|
||||
|
||||
if (matchesPartialName("app", prefix, matchLower)) {
|
||||
result.getItems().add(createNamespaceCompletion("app", APP_NS));
|
||||
}
|
||||
|
||||
if (matchesPartialName("tools", prefix, matchLower)) {
|
||||
result.getItems().add(createNamespaceCompletion("tools", TOOLS_NS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addLayoutXmlTags(CompletionResult result, String prefix, boolean slash) {
|
||||
prefix = prefix.toLowerCase(Locale.ROOT);
|
||||
for (var widget : this.sdkInfo.getWidgetInfo().getWidgets()) {
|
||||
var name = widget.simpleName.toLowerCase(Locale.ROOT);
|
||||
if (matchesPartialName(name, prefix, getSettings().shouldMatchAllLowerCase())) {
|
||||
result.getItems().add(widgetNameAsCompletion(widget, slash));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesPartialName(String candidate, String partialName, boolean matchLower) {
|
||||
if (candidate == null || partialName == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate.length() < partialName.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchLower) {
|
||||
candidate = candidate.toLowerCase(Locale.ROOT);
|
||||
partialName = partialName.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
return candidate.startsWith(partialName);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CompletionItem widgetNameAsCompletion(@NonNull Widget view, boolean slash) {
|
||||
CompletionItem item = new CompletionItem();
|
||||
item.setLabel(view.simpleName);
|
||||
item.setDetail(view.name);
|
||||
item.setInsertText(createTagInsertText(view, slash));
|
||||
item.setInsertTextFormat(InsertTextFormat.PLAIN_TEXT);
|
||||
item.setSortText("2" + view.simpleName);
|
||||
item.setKind(CompletionItemKind.CLASS);
|
||||
|
||||
// Required to show API information in completion list
|
||||
final var data = new CompletionData();
|
||||
data.setClassName(view.name);
|
||||
|
||||
item.setData(data);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String createTagInsertText(@NonNull Widget view, boolean closing) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// Don't append leading '<' and trailing '>'
|
||||
// They may have been already inserted by editor upon typing '<'
|
||||
|
||||
sb.append(view.simpleName);
|
||||
|
||||
if (!closing) {
|
||||
sb.append("$0/");
|
||||
} else {
|
||||
sb.append(">$0");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CompletionItem valueAsCompletion(String value) {
|
||||
CompletionItem item = new CompletionItem();
|
||||
item.setLabel(value);
|
||||
item.setDetail("Attribute value");
|
||||
item.setInsertText(value);
|
||||
item.setInsertTextFormat(InsertTextFormat.PLAIN_TEXT);
|
||||
item.setSortText("0" + value);
|
||||
item.setKind(CompletionItemKind.VALUE);
|
||||
return item;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CompletionItem attrAsCompletion(@NonNull Attr attr) {
|
||||
CompletionItem item = new CompletionItem();
|
||||
item.setLabel(attr.name);
|
||||
item.setDetail("Attribute");
|
||||
item.setInsertText(createAttributeInsertText(attr));
|
||||
item.setInsertTextFormat(InsertTextFormat.PLAIN_TEXT);
|
||||
item.setSortText("1" + attr.name);
|
||||
item.setKind(CompletionItemKind.SNIPPET);
|
||||
item.setCommand(new Command("Trigger completion request", Command.TRIGGER_COMPLETION));
|
||||
return item;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String createAttributeInsertText(@NonNull Attr attr) {
|
||||
StringBuilder xml = new StringBuilder();
|
||||
xml.append(attr.namespace.getPrefix());
|
||||
xml.append(":");
|
||||
xml.append(attr.name);
|
||||
xml.append("=");
|
||||
xml.append("\"");
|
||||
|
||||
if (attr.namespace.getPrefix().equals("android") && attr.name.equals("id")) {
|
||||
xml.append("@+id/");
|
||||
}
|
||||
|
||||
xml.append("$0");
|
||||
xml.append("\"");
|
||||
|
||||
return xml.toString();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Contract(pure = true)
|
||||
private static CompletionItem createNamespaceCompletion(String name, String value) {
|
||||
final var item = new CompletionItem();
|
||||
item.setLabel(name + "Ns");
|
||||
item.setDetail(
|
||||
BaseApplication.getBaseInstance().getString(R.string.msg_add_namespace_decl, name));
|
||||
item.setInsertText(String.format("xmlns:%1$s=\"%2$s\"", name, value));
|
||||
item.setKind(CompletionItemKind.SNIPPET);
|
||||
item.setSortText("1000" + item.getLabel());
|
||||
// This item is expected to be at the last of the
|
||||
// completion list
|
||||
return item;
|
||||
}
|
||||
|
||||
private String getFileType(Path path) {
|
||||
final var file = path.toFile();
|
||||
if (file.getName().equals("AndroidManifest.xml")) {
|
||||
return "manifest";
|
||||
}
|
||||
|
||||
final var resPattern = Pattern.compile(".*/src/.*/res");
|
||||
//noinspection ConstantConditions
|
||||
if (!resPattern.matcher(file.getParentFile().getParentFile().getAbsolutePath()).matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final var parent = file.getParentFile().getName();
|
||||
|
||||
if (parent.startsWith("drawable")
|
||||
|| parent.startsWith("mipmap")
|
||||
|| parent.startsWith("color")) {
|
||||
return "drawable";
|
||||
} else if (parent.startsWith("values")) {
|
||||
return "values";
|
||||
} else if (parent.startsWith("layout")) {
|
||||
return "layout";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class IsInValueScanner {
|
||||
|
||||
private final int cursorIndex;
|
||||
|
||||
public IsInValueScanner(int cursorIndex) {
|
||||
this.cursorIndex = cursorIndex;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String scan(CharSequence xmlContent) {
|
||||
try {
|
||||
XMLLexer lexer = new XMLLexer(CharStreams.fromReader(new CharSequenceReader(xmlContent)));
|
||||
Token token;
|
||||
String attrName = null;
|
||||
while ((token = lexer.nextToken()) != null && token.getType() != XMLLexer.EOF) {
|
||||
if (token.getType() == XMLLexer.STRING && containsCursor(token)) {
|
||||
String text = token.getText();
|
||||
if (text.startsWith("\"")) return attrName;
|
||||
} else if (token.getType() == XMLLexer.Name) {
|
||||
attrName = token.getText();
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Do not log if completion was cancelled
|
||||
if (!(e instanceof CompletionCancelledException)) {
|
||||
LOG.error("An error occurred on checking if cursor is in string", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean containsCursor(@NonNull Token token) {
|
||||
int start = token.getStartIndex();
|
||||
int end = token.getStopIndex();
|
||||
return start <= cursorIndex && cursorIndex <= end;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -158,7 +158,8 @@ open class AttrCompletionProvider(provider: ICompletionProvider) :
|
||||
attr = ref,
|
||||
resPkg = pck,
|
||||
nsPrefix = pckPrefix,
|
||||
matchLevel = matchLevel
|
||||
matchLevel = matchLevel,
|
||||
partial = prefix
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+3
@@ -22,6 +22,7 @@ import com.android.aaptcompiler.ResourcePathData
|
||||
import com.android.aaptcompiler.ResourceTable
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.lsp.api.ICompletionProvider
|
||||
import com.itsaky.androidide.lsp.api.describeSnippet
|
||||
import com.itsaky.androidide.lsp.models.Command
|
||||
import com.itsaky.androidide.lsp.models.CompletionData
|
||||
import com.itsaky.androidide.lsp.models.CompletionItem
|
||||
@@ -152,6 +153,7 @@ abstract class IXmlCompletionProvider(private val provider: ICompletionProvider)
|
||||
*/
|
||||
protected open fun createAttrCompletionItem(
|
||||
attr: Reference,
|
||||
partial: String,
|
||||
resPkg: String,
|
||||
nsPrefix: String,
|
||||
matchLevel: MatchLevel
|
||||
@@ -170,6 +172,7 @@ abstract class IXmlCompletionProvider(private val provider: ICompletionProvider)
|
||||
this.detail = "From package '$resPkg'"
|
||||
this.insertText = "$title=\"$0\""
|
||||
this.insertTextFormat = SNIPPET
|
||||
this.snippetDescription = describeSnippet(partial)
|
||||
this.sortText = label.toString()
|
||||
this.matchLevel = matchLevel
|
||||
this.command = Command("Trigger completion request", Command.TRIGGER_COMPLETION)
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user