Implemented snippet support in LSP

This commit is contained in:
Akash Yadav
2022-09-04 23:31:03 +05:30
parent ac8c268c13
commit d5e77be04e
12 changed files with 50 additions and 376 deletions
@@ -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)
@@ -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
}
@@ -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
}
@@ -168,7 +168,7 @@ class MemberReferenceCompletionProvider(
continue
}
list.add(method(task, value, false, matchLevel))
list.add(method(task, value, false, matchLevel, partial))
}
if (isStatic) {
@@ -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)
@@ -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>()
@@ -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")
@@ -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()
@@ -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;
}
}
}
@@ -158,7 +158,8 @@ open class AttrCompletionProvider(provider: ICompletionProvider) :
attr = ref,
resPkg = pck,
nsPrefix = pckPrefix,
matchLevel = matchLevel
matchLevel = matchLevel,
partial = prefix
)
)
}
@@ -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.