From 7f0f98fe382f90559e945beab450ad8b18ef9af8 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Mon, 6 Sep 2021 10:36:05 +0800 Subject: [PATCH] feat: Support Basic Auto Completion (#971) --- .../src/languageServer/languageServer.ts | 36 +++- gradle-language-server/build.gradle | 1 + .../gradle/GradleLanguageServer.java | 16 ++ .../com/microsoft/gradle/GradleServices.java | 74 ++++++- .../gradle/compile/CompletionVisitor.java | 36 +++- .../gradle/delegate/GradleDelegate.java | 56 +++++ .../gradle/handlers/CompletionHandler.java | 114 +++++++++++ .../resolver/GradleLibraryResolver.java | 193 ++++++++++++++++++ .../com/microsoft/gradle/utils/LSPUtils.java | 7 + 9 files changed, 525 insertions(+), 8 deletions(-) create mode 100644 gradle-language-server/src/main/java/com/microsoft/gradle/delegate/GradleDelegate.java create mode 100644 gradle-language-server/src/main/java/com/microsoft/gradle/handlers/CompletionHandler.java create mode 100644 gradle-language-server/src/main/java/com/microsoft/gradle/resolver/GradleLibraryResolver.java diff --git a/extension/src/languageServer/languageServer.ts b/extension/src/languageServer/languageServer.ts index 29fe1fe3a..bc89b3803 100644 --- a/extension/src/languageServer/languageServer.ts +++ b/extension/src/languageServer/languageServer.ts @@ -6,9 +6,18 @@ import * as net from 'net'; import * as path from 'path'; import * as vscode from 'vscode'; -import { LanguageClientOptions } from 'vscode-languageclient'; +import { + DidChangeConfigurationNotification, + LanguageClientOptions, +} from 'vscode-languageclient'; import { LanguageClient, StreamInfo } from 'vscode-languageclient/node'; -import { getConfigGradleJavaHome } from '../util/config'; +import { + getConfigGradleJavaHome, + getConfigJavaImportGradleHome, + getConfigJavaImportGradleUserHome, + getConfigJavaImportGradleVersion, + getConfigJavaImportGradleWrapperEnabled, +} from '../util/config'; const CHANNEL_NAME = 'Gradle Language Server'; export async function startLanguageServer( @@ -16,6 +25,7 @@ export async function startLanguageServer( ): Promise { void vscode.window.withProgress( { location: vscode.ProgressLocation.Window }, + // eslint-disable-next-line sonarjs/cognitive-complexity (progress) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars return new Promise(async (resolve, _reject) => { @@ -26,6 +36,9 @@ export async function startLanguageServer( documentSelector: [{ scheme: 'file', language: 'gradle' }], outputChannel: vscode.window.createOutputChannel(CHANNEL_NAME), outputChannelName: CHANNEL_NAME, + initializationOptions: { + settings: getGradleSettings(), + }, }; let serverOptions; if (process.env.VSCODE_DEBUG_LANGUAGE_SERVER === 'true') { @@ -82,6 +95,16 @@ export async function startLanguageServer( }); const disposable = languageClient.start(); context.subscriptions.push(disposable); + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('java.import.gradle')) { + languageClient.sendNotification( + DidChangeConfigurationNotification.type, + { settings: getGradleSettings() } + ); + } + }) + ); }); } ); @@ -101,3 +124,12 @@ async function awaitServerConnection(port: string): Promise { return server; }); } + +function getGradleSettings(): unknown { + return { + gradleHome: getConfigJavaImportGradleHome(), + gradleVersion: getConfigJavaImportGradleVersion(), + gradleWrapperEnabled: getConfigJavaImportGradleWrapperEnabled(), + gradleUserHome: getConfigJavaImportGradleUserHome(), + }; +} diff --git a/gradle-language-server/build.gradle b/gradle-language-server/build.gradle index 2b2fdecba..1f5829cb2 100644 --- a/gradle-language-server/build.gradle +++ b/gradle-language-server/build.gradle @@ -16,6 +16,7 @@ dependencies { 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" + implementation "org.apache.bcel:bcel:6.5.0" } ext.mainClass = "com.microsoft.gradle.GradleLanguageServer" diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java index d202d5487..995f48020 100644 --- a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java @@ -13,11 +13,16 @@ import java.io.IOException; import java.net.Socket; +import java.net.URI; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.microsoft.gradle.semantictokens.TokenModifier; import com.microsoft.gradle.semantictokens.TokenType; @@ -32,6 +37,7 @@ import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.eclipse.lsp4j.TextDocumentSyncOptions; +import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; @@ -71,6 +77,16 @@ public GradleLanguageServer() { @Override public CompletableFuture initialize(InitializeParams params) { + Map initOptions = new Gson().fromJson((JsonElement) params.getInitializationOptions(), Map.class); + // TODO: support multiple workspace folders + List workspaceFolders = params.getWorkspaceFolders(); + for (WorkspaceFolder folder : workspaceFolders) { + URI uri = URI.create(folder.getUri()); + this.gradleServices.getLibraryResolver().setWorkspacePath(Paths.get(uri)); + break; + } + Object settings = initOptions.get("settings"); + this.gradleServices.applySetting(settings); ServerCapabilities serverCapabilities = new ServerCapabilities(); SemanticTokensWithRegistrationOptions semanticOptions = new SemanticTokensWithRegistrationOptions(); semanticOptions.setFull(new SemanticTokensServerFull(false)); diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java index 55f3dd972..89e5722d8 100644 --- a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java @@ -21,16 +21,23 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; -import com.microsoft.gradle.compile.SemanticTokenVisitor; -import com.microsoft.gradle.compile.DocumentSymbolVisitor; +import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.microsoft.gradle.compile.CompletionVisitor; -import com.microsoft.gradle.compile.GradleCompilationUnit; import com.microsoft.gradle.compile.CompletionVisitor.DependencyItem; +import com.microsoft.gradle.compile.DocumentSymbolVisitor; +import com.microsoft.gradle.compile.GradleCompilationUnit; +import com.microsoft.gradle.compile.SemanticTokenVisitor; +import com.microsoft.gradle.handlers.CompletionHandler; import com.microsoft.gradle.handlers.DependencyCompletionHandler; import com.microsoft.gradle.manager.GradleFilesManager; +import com.microsoft.gradle.resolver.GradleLibraryResolver; import com.microsoft.gradle.semantictokens.SemanticToken; import com.microsoft.gradle.utils.LSPUtils; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.ErrorCollector; import org.codehaus.groovy.control.Phases; @@ -50,6 +57,7 @@ import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SemanticTokens; @@ -69,12 +77,18 @@ public class GradleServices implements TextDocumentService, WorkspaceService, La private SemanticTokenVisitor semanticTokenVisitor; private DocumentSymbolVisitor documentSymbolVisitor; private CompletionVisitor completionVisitor; + private GradleLibraryResolver libraryResolver; public GradleServices() { this.gradleFilesManager = new GradleFilesManager(); this.semanticTokenVisitor = new SemanticTokenVisitor(); this.documentSymbolVisitor = new DocumentSymbolVisitor(); this.completionVisitor = new CompletionVisitor(); + this.libraryResolver = new GradleLibraryResolver(); + } + + public GradleLibraryResolver getLibraryResolver() { + return this.libraryResolver; } @Override @@ -116,7 +130,18 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { @Override public void didChangeConfiguration(DidChangeConfigurationParams params) { - // TODO + Map settings = new Gson().fromJson((JsonElement) params.getSettings(), Map.class); + this.applySetting(settings); + } + + public void applySetting(Object settings) { + if (settings instanceof Map) { + this.getLibraryResolver().setGradleHome((String)((Map)settings).get("gradleHome")); + this.getLibraryResolver().setGradleVersion((String)((Map)settings).get("gradleVersion")); + this.getLibraryResolver().setGradleWrapperEnabled((Boolean)((Map)settings).get("gradleWrapperEnabled")); + this.getLibraryResolver().setGradleUserHome((String)((Map)settings).get("gradleUserHome")); + this.getLibraryResolver().resolve(); + } } private void compile(URI uri, GradleCompilationUnit unit) { @@ -201,6 +226,9 @@ public CompletableFuture, CompletionList>> completio } this.completionVisitor.visitCompilationUnit(uri, unit); List dependencies = this.completionVisitor.getDependencies(uri); + if (dependencies == null) { + return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); + } for (DependencyItem dependency : dependencies) { if (Ranges.containsPosition(dependency.getRange(), params.getPosition())) { DependencyCompletionHandler handler = new DependencyCompletionHandler(); @@ -208,6 +236,42 @@ public CompletableFuture, CompletionList>> completio .completedFuture(Either.forLeft(handler.getDependencyCompletionItems(dependency, params.getPosition()))); } } - return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); + // should return empty if in constants + List constants = this.completionVisitor.getConstants(uri); + for (Expression constant : constants) { + Range range = LSPUtils.toRange(constant); + if (Ranges.containsPosition(range, params.getPosition())) { + return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList())); + } + } + Set methodCalls = this.completionVisitor.getMethodCalls(uri); + MethodCallExpression containingCall = null; + for (MethodCallExpression call : methodCalls) { + Expression expression = call.getArguments(); + Range range = LSPUtils.toRange(expression); + if (Ranges.containsPosition(range, params.getPosition()) && (containingCall == null || Ranges + .containsRange(LSPUtils.toRange(containingCall.getArguments()), LSPUtils.toRange(call.getArguments())))) { + // find inner containing call + containingCall = call; + } + } + CompletionHandler handler = new CompletionHandler(); + // check again + if (containingCall == null && isGradleRoot(uri, params.getPosition())) { + return CompletableFuture.completedFuture(Either.forLeft(handler.getCompletionItems(null, this.libraryResolver))); + } + return CompletableFuture + .completedFuture(Either.forLeft(handler.getCompletionItems(containingCall, this.libraryResolver))); + } + + private boolean isGradleRoot(URI uri, Position position) { + List statements = this.completionVisitor.getStatements(uri); + for (Statement statement : statements) { + Range range = LSPUtils.toRange(statement); + if (Ranges.containsPosition(range, position)) { + return false; + } + } + return true; } } diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/compile/CompletionVisitor.java b/gradle-language-server/src/main/java/com/microsoft/gradle/compile/CompletionVisitor.java index c158948b5..828463f40 100644 --- a/gradle-language-server/src/main/java/com/microsoft/gradle/compile/CompletionVisitor.java +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/compile/CompletionVisitor.java @@ -12,11 +12,12 @@ import java.net.URI; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import com.microsoft.gradle.utils.LSPUtils; @@ -56,11 +57,26 @@ public Range getRange() { private URI currentUri; private Map> dependencies = new HashMap<>(); + private Map> methodCalls = new HashMap<>(); + private Map> statements = new HashMap<>(); + private Map> constants = new HashMap<>(); public List getDependencies(URI uri) { return this.dependencies.get(uri); } + public Set getMethodCalls(URI uri) { + return this.methodCalls.get(uri); + } + + public List getStatements(URI uri) { + return this.statements.get(uri); + } + + public List getConstants(URI uri) { + return this.constants.get(uri); + } + public void visitCompilationUnit(URI uri, GradleCompilationUnit compilationUnit) { this.currentUri = uri; compilationUnit.iterator().forEachRemaining(unit -> visitSourceUnit(uri, unit)); @@ -70,11 +86,16 @@ public void visitSourceUnit(URI uri, SourceUnit unit) { ModuleNode moduleNode = unit.getAST(); if (moduleNode != null) { this.dependencies.put(uri, new ArrayList<>()); + this.methodCalls.put(uri, new HashSet<>()); + this.statements.put(uri, new ArrayList<>()); + this.constants.put(uri, new ArrayList<>()); visitModule(moduleNode); } } public void visitModule(ModuleNode node) { + BlockStatement blockStatement = node.getStatementBlock(); + this.statements.put(currentUri, blockStatement.getStatements()); node.getClasses().forEach(classNode -> { super.visitClass(classNode); }); @@ -82,6 +103,7 @@ public void visitModule(ModuleNode node) { @Override public void visitMethodCallExpression(MethodCallExpression node) { + this.methodCalls.get(this.currentUri).add(node); if (node.getMethodAsString().equals("dependencies")) { this.dependencies.put(currentUri, getDependencies(node)); } @@ -139,4 +161,16 @@ private List getDependencies(ExpressionStatement expressionState } return Collections.emptyList(); } + + @Override + public void visitConstantExpression(ConstantExpression expression) { + this.constants.get(currentUri).add(expression); + super.visitConstantExpression(expression); + } + + @Override + public void visitGStringExpression(GStringExpression expression) { + this.constants.get(currentUri).add(expression); + super.visitGStringExpression(expression); + } } diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/delegate/GradleDelegate.java b/gradle-language-server/src/main/java/com/microsoft/gradle/delegate/GradleDelegate.java new file mode 100644 index 000000000..4b2065870 --- /dev/null +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/delegate/GradleDelegate.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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.delegate; + +import java.util.HashMap; +import java.util.Map; + +public class GradleDelegate { + private static String PROJECT = "org.gradle.api.Project"; + private static Map delegateMap; + + static { + delegateMap = new HashMap<>(); + // plugins + delegateMap.put("application", "org.gradle.api.plugins.ApplicationPluginConvention"); + delegateMap.put("base", "org.gradle.api.plugins.BasePluginConvention"); + delegateMap.put("java", "org.gradle.api.plugins.JavaPluginConvention"); + delegateMap.put("war", "org.gradle.api.plugins.WarPluginConvention"); + // basic closures + delegateMap.put("plugins", "org.gradle.plugin.use.PluginDependenciesSpec"); + delegateMap.put("configurations", "org.gradle.api.artifacts.Configuration"); + delegateMap.put("dependencySubstitution", "org.gradle.api.artifacts.DependencySubstitutions"); + delegateMap.put("resolutionStrategy", "org.gradle.api.artifacts.ResolutionStrategy"); + delegateMap.put("artifacts", "org.gradle.api.artifacts.dsl.ArtifactHandler"); + delegateMap.put("components", "org.gradle.api.artifacts.dsl.ComponentMetadataHandler"); + delegateMap.put("modules", "org.gradle.api.artifacts.dsl.ComponentModuleMetadataHandler"); + delegateMap.put("dependencies", "org.gradle.api.artifacts.dsl.DependencyHandler"); + delegateMap.put("repositories", "org.gradle.api.artifacts.dsl.RepositoryHandler"); + delegateMap.put("publishing", "org.gradle.api.publish.PublishingExtension"); + delegateMap.put("publications", "org.gradle.api.publish.PublicationContainer"); + delegateMap.put("sourceSets", "org.gradle.api.tasks.SourceSet"); + delegateMap.put("distributions", "org.gradle.api.distribution.Distribution"); + delegateMap.put("fileTree", "org.gradle.api.file.ConfigurableFileTree"); + delegateMap.put("copySpec", "org.gradle.api.file.CopySpec"); + delegateMap.put("exec", "org.gradle.process.ExecSpec"); + delegateMap.put("files", "org.gradle.api.file.ConfigurableFileCollection"); + delegateMap.put("task", "org.gradle.api.Task"); + } + + public static Map getDelegateMap() { + return delegateMap; + } + + public static String getDefault() { + return PROJECT; + } +} diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/handlers/CompletionHandler.java b/gradle-language-server/src/main/java/com/microsoft/gradle/handlers/CompletionHandler.java new file mode 100644 index 000000000..c7ed965a7 --- /dev/null +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/handlers/CompletionHandler.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * 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.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.microsoft.gradle.delegate.GradleDelegate; +import com.microsoft.gradle.resolver.GradleLibraryResolver; + +import org.apache.bcel.classfile.JavaClass; +import org.apache.bcel.classfile.Method; +import org.apache.bcel.generic.ObjectType; +import org.apache.bcel.generic.Type; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.InsertTextFormat; + +public class CompletionHandler { + + public List getCompletionItems(MethodCallExpression containingCall, GradleLibraryResolver resolver) { + String delegateClassName = (containingCall == null) ? GradleDelegate.getDefault() + : GradleDelegate.getDelegateMap().get(containingCall.getMethodAsString()); + if (delegateClassName == null) { + return Collections.emptyList(); + } + JavaClass delegateClass = resolver.getGradleLibraries().get(delegateClassName); + if (delegateClass == null) { + return Collections.emptyList(); + } + return getCompletionItemsFromClass(delegateClass); + } + + private List getCompletionItemsFromClass(JavaClass javaClass) { + if (javaClass == null) { + return Collections.emptyList(); + } + List results = new ArrayList<>(); + List methodNames = new ArrayList<>(); + Set resultSet = new HashSet<>(); + Method[] methods = javaClass.getMethods(); + for (Method method : methods) { + StringBuilder labelBuilder = new StringBuilder(); + String methodName = method.getName(); + // When parsing a abstract class, we'll get a "" method which can't be called directly, + // So we filter it here. + if (methodName.equals("")) { + continue; + } + methodNames.add(methodName); + labelBuilder.append(methodName); + labelBuilder.append("("); + for (Type type : method.getArgumentTypes()) { + if (type instanceof ObjectType) { + String[] classNameSplits = ((ObjectType) type).getClassName().split("\\."); + String className = classNameSplits[classNameSplits.length - 1]; + String variableName = className.substring(0, 1).toLowerCase(); + labelBuilder.append(className); + labelBuilder.append(" "); + labelBuilder.append(variableName); + labelBuilder.append(","); + } + } + if (labelBuilder.charAt(labelBuilder.length() - 1) == ',') { + labelBuilder.deleteCharAt(labelBuilder.length() - 1); + } + labelBuilder.append(")"); + String label = labelBuilder.toString(); + CompletionItem item = new CompletionItem(label); + item.setKind(CompletionItemKind.Function); + item.setInsertTextFormat(InsertTextFormat.Snippet); + StringBuilder builder = new StringBuilder(); + builder.append(methodName); + if (label.endsWith("(Closure c)")) { + // for single closure, we offer curly brackets + builder.append(" {$0}"); + } else { + builder.append("($0)"); + } + item.setInsertText(builder.toString()); + if (resultSet.add(label)) { + results.add(item); + } + } + for (String methodName : methodNames) { + if (methodName.startsWith("set") && methodName.length() > 3) { + // for cases like setVersion() and getVersion(), + // we offer version as a property + String getMethod = "get" + methodName.substring(3); + if (methodNames.contains(getMethod)) { + String property = methodName.substring(3, 4).toLowerCase() + methodName.substring(4); + CompletionItem item = new CompletionItem(property); + item.setKind(CompletionItemKind.Property); + if (resultSet.add(property)) { + results.add(item); + } + } + } + } + return results; + } +} diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/resolver/GradleLibraryResolver.java b/gradle-language-server/src/main/java/com/microsoft/gradle/resolver/GradleLibraryResolver.java new file mode 100644 index 000000000..2d3fcb8a2 --- /dev/null +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/resolver/GradleLibraryResolver.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * 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.resolver; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.bcel.classfile.ClassFormatException; +import org.apache.bcel.classfile.ClassParser; +import org.apache.bcel.classfile.JavaClass; + +public class GradleLibraryResolver { + private Map gradleLibraries = new HashMap<>(); + private String gradleHome; + private String gradleVersion; + private boolean gradleWrapperEnabled; + private String gradleUserHome; + private Path workspacePath; + + public void setGradleHome(String gradleHome) { + this.gradleHome = gradleHome; + } + + public void setGradleVersion(String gradleVersion) { + this.gradleVersion = gradleVersion; + } + + public void setGradleWrapperEnabled(boolean gradleWrapperEnabled) { + this.gradleWrapperEnabled = gradleWrapperEnabled; + } + + public void setGradleUserHome(String gradleUserHome) { + this.gradleUserHome = gradleUserHome; + } + + public void setWorkspacePath(Path workspacePath) { + this.workspacePath = workspacePath; + } + + public Map getGradleLibraries() { + return this.gradleLibraries; + } + + public void resolve() { + Path gradleUserHomePath = (this.gradleUserHome == null) ? Path.of(System.getProperty("user.home"), ".gradle") + : Path.of(this.gradleUserHome); + File libFile = null; + if (this.gradleWrapperEnabled) { + libFile = findLibWithWrapper(gradleUserHomePath); + } else if (this.gradleVersion != null) { + libFile = findLibWithDist(gradleUserHomePath, "gradle-" + this.gradleVersion); + } else if (this.gradleHome != null) { + Path libPath = Path.of(this.gradleHome).resolve("lib"); + libFile = findLibFile(libPath.toFile()); + } else { + return; + } + if (libFile == null || !libFile.exists()) { + return; + } + try { + JarFile libJar = new JarFile(libFile); + getGradleLibraries(libFile.toPath(), libJar); + File pluginLibFile = findPluginLibFile(libFile.toPath().getParent().resolve(Path.of("plugins")).toFile()); + if (pluginLibFile == null) { + return; + } + JarFile pluginLibJar = new JarFile(pluginLibFile); + getGradleLibraries(pluginLibFile.toPath(), pluginLibJar); + } catch (Exception e) { + // Do Nothing + } + } + + private File findLibWithWrapper(Path gradleUserHomePath) { + if (this.workspacePath == null) { + return null; + } + Path propertiesRelativePath = Path.of("gradle", "wrapper", "gradle-wrapper.properties"); + Path propertiesPath = this.workspacePath.resolve(propertiesRelativePath); + File propertiesFile = propertiesPath.toFile(); + if (!propertiesFile.exists()) { + return null; + } + try (FileInputStream stream = new FileInputStream(propertiesFile)) { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String content = null; + while ((content = reader.readLine()) != null) { + if (content.startsWith("distributionUrl")) { + Pattern p = Pattern.compile("(gradle-(?s)(.*))-bin"); + Matcher matcher = p.matcher(content); + if (matcher.find()) { + String gradleDist = matcher.group(1); + return findLibWithDist(gradleUserHomePath, gradleDist); + } + } + } + } catch (IOException e) { + // Do Nothing + } + return null; + } + + private File findLibWithDist(Path gradleUserHomePath, String gradleDist) { + Path distPath = gradleUserHomePath.resolve(Path.of("wrapper", "dists")); + File distFolder = searchInFolder(gradleDist, distPath.toFile()); + if (distFolder != null && distFolder.exists()) { + Path libPath = distFolder.toPath().resolve("lib"); + return findLibFile(libPath.toFile()); + } + return null; + } + + private File searchInFolder(String gradleDist, File folder) { + for (File file : folder.listFiles()) { + if (file.isDirectory()) { + if (file.getName().equals(gradleDist)) { + return file; + } else { + File searchResult = searchInFolder(gradleDist, file); + if (searchResult != null) { + return searchResult; + } + } + } + } + return null; + } + + private File findLibFile(File folder) { + for (File file : folder.listFiles()) { + String name = file.getName(); + if (name.startsWith("gradle-core-api") && name.endsWith(".jar")) { + return file; + } + } + // For Gradle version under 5.6, the name of library file is like gradle-core-${version}.jar + for (File file : folder.listFiles()) { + String name = file.getName(); + if (name.startsWith("gradle-core") && name.endsWith(".jar")) { + return file; + } + } + return null; + } + + private File findPluginLibFile(File folder) { + for (File file : folder.listFiles()) { + String name = file.getName(); + if (name.startsWith("gradle-plugins") && name.endsWith(".jar")) { + return file; + } + } + return null; + } + + private void getGradleLibraries(Path jarPath, JarFile jarFile) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (!entry.getName().endsWith(".class")) { + continue; + } + ClassParser parser = new ClassParser(jarPath.toString(), entry.getName()); + try { + JavaClass javaClass = parser.parse(); + String className = javaClass.getClassName(); + this.gradleLibraries.put(className, javaClass); + } catch (IOException | ClassFormatException e) { + // Do Nothing + } + } + } +} diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/utils/LSPUtils.java b/gradle-language-server/src/main/java/com/microsoft/gradle/utils/LSPUtils.java index 2dbdac510..2bb97924e 100644 --- a/gradle-language-server/src/main/java/com/microsoft/gradle/utils/LSPUtils.java +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/utils/LSPUtils.java @@ -11,6 +11,7 @@ package com.microsoft.gradle.utils; import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.syntax.SyntaxException; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; @@ -28,6 +29,12 @@ public static Range toRange(Expression expression) { new Position(expression.getLastLineNumber() - 1, expression.getLastColumnNumber() - 1)); } + public static Range toRange(Statement statement) { + // LSP Range start from 0, while groovy expressions start from 1 + return new Range(new Position(statement.getLineNumber() - 1, statement.getColumnNumber() - 1), + new Position(statement.getLastLineNumber() - 1, statement.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()),