Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,20 @@ shadowJar {

repositories {
mavenCentral()
maven { url 'https://repo.gradle.org/gradle/libs-releases' }
}

def lsp4jVersion = "0.24.0"

dependencies {
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0"
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0"
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j:$lsp4jVersion"
implementation "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:$lsp4jVersion"
implementation 'org.gradle:gradle-tooling-api:8.10.2'
implementation "org.apache.groovy:groovy:4.0.26"
implementation "com.google.code.gson:gson:2.13.1"
implementation "io.github.classgraph:classgraph:4.8.179"
implementation "org.slf4j:slf4j-api:2.0.17"
implementation "org.slf4j:slf4j-simple:2.0.17"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.11.4"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.11.4"
}
Expand Down
179 changes: 152 additions & 27 deletions src/main/java/net/prominic/groovyls/GroovyLanguageServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,79 @@
////////////////////////////////////////////////////////////////////////////////
package net.prominic.groovyls;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import net.prominic.groovyls.config.CompilationUnitFactory;
import net.prominic.groovyls.config.ICompilationUnitFactory;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.services.*;
import org.gradle.tooling.GradleConnector;
import org.gradle.tooling.ProjectConnection;
import org.gradle.tooling.model.idea.IdeaDependency;
import org.gradle.tooling.model.idea.IdeaModule;
import org.gradle.tooling.model.idea.IdeaProject;
import org.gradle.tooling.model.idea.IdeaSingleEntryLibraryDependency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.SignatureHelpOptions;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;

import net.prominic.groovyls.config.CompilationUnitFactory;
import net.prominic.groovyls.config.ICompilationUnitFactory;
import java.util.stream.Stream;

public class GroovyLanguageServer implements LanguageServer, LanguageClientAware {

public static void main(String[] args) {
InputStream systemIn = System.in;
OutputStream systemOut = System.out;
// redirect System.out to System.err because we need to prevent
// System.out from receiving anything that isn't an LSP message
private static final Logger logger = LoggerFactory.getLogger(GroovyLanguageServer.class);
private LanguageClient client;

public static void main(String[] args) throws IOException {
if (args.length > 0 && "--tcp".equals(args[0])) {
int port = 5007;
if (args.length > 1) {
try {
port = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
logger.error("Invalid port number: {}", args[1]);
System.exit(1);
}
}

try (ServerSocket serverSocket = new ServerSocket(port)) {
logger.info("Groovy Language Server listening on port {}", port);
Socket socket = serverSocket.accept();
logger.info("Client connected.");

InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
startServer(in, out);
}
} else {
logger.info("Groovy Language Server starting in stdio mode.");
InputStream in = System.in;
OutputStream out = System.out;
startServer(in, out);
}
}

private static void startServer(InputStream in, OutputStream out) {
// Redirect System.out to System.err to avoid corrupting the communication channel
System.setOut(new PrintStream(System.err));

GroovyLanguageServer server = new GroovyLanguageServer();
Launcher<LanguageClient> launcher = Launcher.createLauncher(server, LanguageClient.class, systemIn, systemOut);
Launcher<LanguageClient> launcher = Launcher.createLauncher(server, LanguageClient.class, in, out);
server.connect(launcher.getRemoteProxy());
launcher.startListening();
}

private GroovyServices groovyServices;
private final GroovyServices groovyServices;

public GroovyLanguageServer() {
this(new CompilationUnitFactory());
Expand All @@ -68,15 +101,47 @@ public GroovyLanguageServer(ICompilationUnitFactory compilationUnitFactory) {
this.groovyServices = new GroovyServices(compilationUnitFactory);
}

private List<Path> discoverGradleProjects(Path root) throws IOException {
List<Path> gradleProjects = new ArrayList<>();
try (Stream<Path> fileStream = Files.walk(root)) {
fileStream
.filter(Files::isRegularFile)
.filter(p -> Set.of("build.gradle", "build.gradle.kts").contains(p.getFileName().toString()))
.forEach(buildFile -> gradleProjects.add(buildFile.getParent()));
}
return gradleProjects;
}

@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
String rootUriString = params.getRootUri();
if (rootUriString != null) {
URI uri = URI.create(params.getRootUri());
URI uri = URI.create(rootUriString);
Path workspaceRoot = Paths.get(uri);
groovyServices.setWorkspaceRoot(workspaceRoot);
}

List<WorkspaceFolder> folders = params.getWorkspaceFolders();
if (folders != null) {
for (WorkspaceFolder folder : folders) {
Path folderPath = Paths.get(URI.create(folder.getUri()));
try {
List<Path> gradleProjects = discoverGradleProjects(folderPath);
for (Path gradleProject : gradleProjects) {
if (client != null) {
client.showMessage(new MessageParams(
MessageType.Info,
"Importing Gradle project: " + gradleProject
));
}
importGradleProject(gradleProject);
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}

CompletionOptions completionOptions = new CompletionOptions(false, Arrays.asList("."));
ServerCapabilities serverCapabilities = new ServerCapabilities();
serverCapabilities.setCompletionProvider(completionOptions);
Expand All @@ -97,6 +162,65 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
return CompletableFuture.completedFuture(initializeResult);
}

public void importGradleProject(Path folderPath) {
GradleConnector connector = GradleConnector.newConnector()
.forProjectDirectory(folderPath.toFile());

try (ProjectConnection connection = connector.connect()) {
// First run the build (blocking)
connection.newBuild()
.forTasks("classes")
.setStandardOutput(System.out)
.setStandardError(System.err)
.run();

IdeaProject project = connection.getModel(IdeaProject.class);

List<String> classpathList = new ArrayList<>();

for (IdeaModule module : project.getChildren()) {
// Compiler output dirs
if (module.getCompilerOutput() != null) {
File outputDir = module.getCompilerOutput().getOutputDir();
if (outputDir != null) {
classpathList.add(outputDir.getAbsolutePath());
}
}

classpathList.addAll(discoverClassDirs(folderPath));

for (IdeaDependency dep : module.getDependencies()) {
if (dep instanceof IdeaSingleEntryLibraryDependency) {
File file = ((IdeaSingleEntryLibraryDependency) dep).getFile();
if (file != null && file.exists()) {
classpathList.add(file.getAbsolutePath());
}
}
}
}

logger.info("classpathList: {}", classpathList);
groovyServices.updateClasspath(classpathList);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}

private List<String> discoverClassDirs(Path projectDir) throws IOException {
Path classesRoot = projectDir.resolve("build/classes");
List<String> classDirs = new ArrayList<>();

if (Files.exists(classesRoot)) {
try (Stream<Path> stream = Files.walk(classesRoot, 2)) {
stream
.filter(Files::isDirectory)
.map(Path::toString)
.forEach(classDirs::add);
}
}
return classDirs;
}

@Override
public CompletableFuture<Object> shutdown() {
return CompletableFuture.completedFuture(new Object());
Expand All @@ -119,6 +243,7 @@ public WorkspaceService getWorkspaceService() {

@Override
public void connect(LanguageClient client) {
this.client = client;
groovyServices.connect(client);
}
}
57 changes: 15 additions & 42 deletions src/main/java/net/prominic/groovyls/GroovyServices.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,38 +48,7 @@
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.DefinitionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureHelpParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TypeDefinitionParams;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
Expand Down Expand Up @@ -185,6 +154,17 @@ public void didChangeConfiguration(DidChangeConfigurationParams params) {
this.updateClasspath(settings);
}

void updateClasspath(List<String> classpathList) {
if (!classpathList.equals(compilationUnitFactory.getAdditionalClasspathList())) {
compilationUnitFactory.setAdditionalClasspathList(classpathList);

createOrUpdateCompilationUnit();
compile();
visitAST();
previousContext = null;
}
}

private void updateClasspath(JsonObject settings) {
List<String> classpathList = new ArrayList<>();

Expand All @@ -198,14 +178,7 @@ private void updateClasspath(JsonObject settings) {
}
}

if (!classpathList.equals(compilationUnitFactory.getAdditionalClasspathList())) {
compilationUnitFactory.setAdditionalClasspathList(classpathList);

createOrUpdateCompilationUnit();
compile();
visitAST();
previousContext = null;
}
updateClasspath(classpathList);
}

// --- REQUESTS
Expand Down Expand Up @@ -359,9 +332,9 @@ public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> docume
}

@Override
public CompletableFuture<List<? extends SymbolInformation>> symbol(WorkspaceSymbolParams params) {
public CompletableFuture<Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>>> symbol(WorkspaceSymbolParams params) {
WorkspaceSymbolProvider provider = new WorkspaceSymbolProvider(astVisitor);
return provider.provideWorkspaceSymbols(params.getQuery());
return provider.provideWorkspaceSymbols(params.getQuery()).thenApply(Either::forLeft);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
////////////////////////////////////////////////////////////////////////////////
/// /////////////////////////////////////////////////////////////////////////////
// Copyright 2022 Prominic.NET, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -138,24 +138,31 @@ protected void getClasspathList(List<String> result) {
mustBeDirectory = true;
}

File file = new File(entry);
if (!file.exists()) {
continue;
}
if (file.isDirectory()) {
for (File child : file.listFiles()) {
if (!child.getName().endsWith(".jar") || !child.isFile()) {
continue;
}
result.add(child.getPath());
}
} else if (!mustBeDirectory && file.isFile()) {
if (file.getName().endsWith(".jar")) {
result.add(entry);
}
}
}
}
File file = new File(entry);
if (!file.exists()) {
continue;
}

if (file.isDirectory()) {
// Always add directories (important for build/classes output)
result.add(file.getPath());

// And if user used '*', include jars inside
if (mustBeDirectory) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
if (child.isFile() && child.getName().endsWith(".jar")) {
result.add(child.getPath());
}
}
}
}
} else if (!mustBeDirectory && file.isFile() && file.getName().endsWith(".jar")) {
result.add(entry);
}
}
}

protected void addDirectoryToCompilationUnit(Path dirPath, GroovyLSCompilationUnit compilationUnit,
FileContentsTracker fileContentsTracker, Set<URI> changedUris) {
Expand Down
Loading