Skip to content

Commit

Permalink
feat: Support auto completion in dependencies closure (#970)
Browse files Browse the repository at this point in the history
  • Loading branch information
CsCherrYY authored Sep 2, 2021
1 parent 06d77e2 commit c14fdb7
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 1 deletion.
1 change: 1 addition & 0 deletions gradle-language-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies {
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0"
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0"
implementation "org.codehaus.groovy:groovy-eclipse-batch:3.0.8-01"
implementation "com.google.code.gson:gson:2.8.7"
}

ext.mainClass = "com.microsoft.gradle.GradleLanguageServer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.microsoft.gradle.semantictokens.TokenModifier;
import com.microsoft.gradle.semantictokens.TokenType;

import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.DocumentFilter;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
Expand Down Expand Up @@ -85,6 +86,8 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
textDocumentSyncOptions.setSave(new SaveOptions(Boolean.TRUE));
textDocumentSyncOptions.setChange(TextDocumentSyncKind.Incremental);
serverCapabilities.setTextDocumentSync(textDocumentSyncOptions);
CompletionOptions completionOptions = new CompletionOptions(false, Arrays.asList(".", ":"));
serverCapabilities.setCompletionProvider(completionOptions);
InitializeResult initializeResult = new InitializeResult(serverCapabilities);
return CompletableFuture.completedFuture(initializeResult);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@

import com.microsoft.gradle.compile.SemanticTokenVisitor;
import com.microsoft.gradle.compile.DocumentSymbolVisitor;
import com.microsoft.gradle.compile.CompletionVisitor;
import com.microsoft.gradle.compile.GradleCompilationUnit;
import com.microsoft.gradle.compile.CompletionVisitor.DependencyItem;
import com.microsoft.gradle.handlers.DependencyCompletionHandler;
import com.microsoft.gradle.manager.GradleFilesManager;
import com.microsoft.gradle.semantictokens.SemanticToken;
import com.microsoft.gradle.utils.LSPUtils;
Expand All @@ -34,6 +37,9 @@
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
Expand All @@ -54,18 +60,21 @@
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
import org.eclipse.lsp4j.util.Ranges;

public class GradleServices implements TextDocumentService, WorkspaceService, LanguageClientAware {

private LanguageClient client;
private GradleFilesManager gradleFilesManager;
private SemanticTokenVisitor semanticTokenVisitor;
private DocumentSymbolVisitor documentSymbolVisitor;
private CompletionVisitor completionVisitor;

public GradleServices() {
this.gradleFilesManager = new GradleFilesManager();
this.semanticTokenVisitor = new SemanticTokenVisitor();
this.documentSymbolVisitor = new DocumentSymbolVisitor();
this.completionVisitor = new CompletionVisitor();
}

@Override
Expand Down Expand Up @@ -182,4 +191,23 @@ public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> docume
}
return CompletableFuture.completedFuture(result);
}

@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params) {
URI uri = URI.create(params.getTextDocument().getUri());
GradleCompilationUnit unit = this.gradleFilesManager.getCompilationUnit(uri);
if (unit == null) {
return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList()));
}
this.completionVisitor.visitCompilationUnit(uri, unit);
List<DependencyItem> dependencies = this.completionVisitor.getDependencies(uri);
for (DependencyItem dependency : dependencies) {
if (Ranges.containsPosition(dependency.getRange(), params.getPosition())) {
DependencyCompletionHandler handler = new DependencyCompletionHandler();
return CompletableFuture
.completedFuture(Either.forLeft(handler.getDependencyCompletionItems(dependency, params.getPosition())));
}
}
return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*******************************************************************************
* Copyright (c) 2021 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/
package com.microsoft.gradle.compile;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.microsoft.gradle.utils.LSPUtils;

import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.GStringExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import org.eclipse.lsp4j.Range;

public class CompletionVisitor extends ClassCodeVisitorSupport {

public class DependencyItem {
private String text;
private Range range;

public DependencyItem(String text, Range range) {
this.text = text;
this.range = range;
}

public String getText() {
return this.text;
}

public Range getRange() {
return this.range;
}
}

private URI currentUri;
private Map<URI, List<DependencyItem>> dependencies = new HashMap<>();

public List<DependencyItem> getDependencies(URI uri) {
return this.dependencies.get(uri);
}

public void visitCompilationUnit(URI uri, GradleCompilationUnit compilationUnit) {
this.currentUri = uri;
compilationUnit.iterator().forEachRemaining(unit -> visitSourceUnit(uri, unit));
}

public void visitSourceUnit(URI uri, SourceUnit unit) {
ModuleNode moduleNode = unit.getAST();
if (moduleNode != null) {
this.dependencies.put(uri, new ArrayList<>());
visitModule(moduleNode);
}
}

public void visitModule(ModuleNode node) {
node.getClasses().forEach(classNode -> {
super.visitClass(classNode);
});
}

@Override
public void visitMethodCallExpression(MethodCallExpression node) {
if (node.getMethodAsString().equals("dependencies")) {
this.dependencies.put(currentUri, getDependencies(node));
}
super.visitMethodCallExpression(node);
}

private List<DependencyItem> getDependencies(MethodCallExpression expression) {
Expression argument = expression.getArguments();
if (argument instanceof ArgumentListExpression) {
return getDependencies((ArgumentListExpression) argument);
}
return Collections.emptyList();
}

private List<DependencyItem> getDependencies(ArgumentListExpression argumentListExpression) {
List<Expression> expressions = argumentListExpression.getExpressions();
List<DependencyItem> symbols = new ArrayList<>();
for (Expression expression : expressions) {
if (expression instanceof ClosureExpression) {
symbols.addAll(getDependencies((ClosureExpression) expression));
} else if (expression instanceof GStringExpression || expression instanceof ConstantExpression) {
// GStringExp: implementation "org.gradle:gradle-tooling-api:${gradleToolingApi}"
// ConstantExp: implementation "org.gradle:gradle-tooling-api:6.8.0"
symbols.add(new DependencyItem(expression.getText(), LSPUtils.toDependencyRange(expression)));
} else if (expression instanceof MethodCallExpression) {
symbols.addAll(getDependencies((MethodCallExpression) expression));
}
}
return symbols;
}

private List<DependencyItem> getDependencies(ClosureExpression expression) {
Statement code = expression.getCode();
if (code instanceof BlockStatement) {
return getDependencies((BlockStatement) code);
}
return Collections.emptyList();
}

private List<DependencyItem> getDependencies(BlockStatement blockStatement) {
List<Statement> statements = blockStatement.getStatements();
List<DependencyItem> results = new ArrayList<>();
for (Statement statement : statements) {
if (statement instanceof ExpressionStatement) {
results.addAll(getDependencies((ExpressionStatement) statement));
}
}
return results;
}

private List<DependencyItem> getDependencies(ExpressionStatement expressionStatement) {
Expression expression = expressionStatement.getExpression();
if (expression instanceof MethodCallExpression) {
return getDependencies((MethodCallExpression) expression);
}
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*******************************************************************************
* Copyright (c) 2021 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/
package com.microsoft.gradle.handlers;

import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.gradle.compile.CompletionVisitor.DependencyItem;
import com.microsoft.gradle.utils.LSPUtils;

import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;

public class DependencyCompletionHandler {
private DependencyItem dependency;
private Position position;
private static String URL_BASIC_SEARCH = "https://search.maven.org/solrsearch/select?q=";

private enum DependencyCompletionKind {
ID, VERSION
}

public List<CompletionItem> getDependencyCompletionItems(DependencyItem dependency, Position position) {
this.dependency = dependency;
this.position = position;
String validText = LSPUtils.getStringBeforePosition(dependency.getText(), dependency.getRange(), position);
String[] validTexts = validText.split(":", -1);
switch (validTexts.length) {
case 1:
return getDependenciesForInCompleteGroup(validTexts[0]);
case 2:
return getDependenciesForInCompleteArtifact(validTexts[0]);
case 3:
return getDependenciesForVersion(validTexts[0], validTexts[1]);
default:
return Collections.emptyList();
}
}

private List<CompletionItem> getDependenciesForInCompleteGroup(String group) {
if (group.length() < 3) {
return Collections.emptyList();
}
StringBuilder builder = new StringBuilder();
builder.append(URL_BASIC_SEARCH);
builder.append(group);
builder.append("&rows=50&wt=json");
return getDependenciesFromRestAPI(builder.toString(), DependencyCompletionKind.ID);
}

private List<CompletionItem> getDependenciesForInCompleteArtifact(String group) {
if (group.length() < 3) {
return Collections.emptyList();
}
StringBuilder builder = new StringBuilder();
builder.append(URL_BASIC_SEARCH);
builder.append("g:%22");
builder.append(group);
builder.append("%22&rows=50&wt=json");
return getDependenciesFromRestAPI(builder.toString(), DependencyCompletionKind.ID);
}

private List<CompletionItem> getDependenciesForVersion(String group, String artifact) {
if (group.length() < 3 || artifact.length() < 3) {
return Collections.emptyList();
}
StringBuilder builder = new StringBuilder();
builder.append(URL_BASIC_SEARCH);
builder.append("g:%22");
builder.append(group);
builder.append("%22+AND+a:%22");
builder.append(artifact);
builder.append("%22&core=gav&rows=50&wt=json");
return getDependenciesFromRestAPI(builder.toString(), DependencyCompletionKind.VERSION);
}

private List<CompletionItem> getDependenciesFromRestAPI(String url, DependencyCompletionKind kind) {
try (InputStreamReader reader = new InputStreamReader(new URL(url).openStream())) {
JsonObject jsonResult = new Gson().fromJson(reader, JsonObject.class);
JsonObject response = jsonResult.getAsJsonObject("response");
JsonArray docs = response.getAsJsonArray("docs");
List<CompletionItem> completions = new ArrayList<>();
for (int i = 0; i < docs.size(); i++) {
JsonElement element = docs.get(i);
if (element instanceof JsonObject) {
CompletionItem completionItem = new CompletionItem();
JsonElement labelContent = ((JsonObject) element).get("id");
String label = labelContent.getAsJsonPrimitive().getAsString();
TextEdit textEdit = new TextEdit(new Range(this.dependency.getRange().getStart(), this.position), label);
completionItem.setTextEdit(Either.forLeft(textEdit));
if (kind == DependencyCompletionKind.ID) {
completionItem.setLabel(label);
completionItem.setTextEdit(Either.forLeft(textEdit));
completionItem.setKind(CompletionItemKind.Module);
completionItem.setDetail("mavenCentral");
} else {
String version = ((JsonObject) element).get("v").getAsJsonPrimitive().getAsString();
completionItem.setLabel(version);
completionItem.setFilterText(label + ":" + version);
completionItem.setKind(CompletionItemKind.Text);
completionItem.setDetail("version");
}
completionItem.setSortText(String.valueOf(i));
completions.add(completionItem);
}
}
return completions;
} catch (Exception e) {
// TODO
}
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.gradle.utils;

import org.codehaus.groovy.ast.expr.Expression;
Expand All @@ -28,4 +27,19 @@ public static Range toRange(Expression expression) {
return new Range(new Position(expression.getLineNumber() - 1, expression.getColumnNumber() - 1),
new Position(expression.getLastLineNumber() - 1, expression.getLastColumnNumber() - 1));
}

public static Range toDependencyRange(Expression expression) {
// For dependency, the string includes open/close quotes should be excluded
return new Range(new Position(expression.getLineNumber() - 1, expression.getColumnNumber()),
new Position(expression.getLastLineNumber() - 1, expression.getLastColumnNumber() - 2));
}

public static String getStringBeforePosition(String text, Range range, Position position) {
Position start = range.getStart();
// Since it's used for extract dependency info, we doesn't support multiple line
if (start.getLine() != position.getLine()) {
return text;
}
return text.substring(0, position.getCharacter() - start.getCharacter());
}
}

0 comments on commit c14fdb7

Please sign in to comment.