Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Eclipse JDT: how to get data model for Java content assist

When writing Java code on Eclipse IDE, press Control + Space will pop up the content assist window.
For example, the content assist window for System. will list all the available fields and methods for class System.

I need to access the "data model" for the content assist window by code.
Using the above example, it is: given the class name System, how can I retrieve all the available fields and methods?
I spent some time on the source code of these three classes on grepcode.com:

org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext
org.eclipse.jdt.internal.ui.text.java.JavaCompletionProposalComputer
org.eclipse.jdt.ui.text.java.CompletionProposalCollector

It looks like the an ICompilationUnit instance is used to provide the fields and method names.

Then I don't understand how to generate the ICompilationUnit instance for a class in jre system library or third party library? Or if I didn't read the code in a correct way, then how did the program find fields and methods name? (I don't need to worry about the offset and UI stuff, just the "data model" part).

like image 648
CMZS Avatar asked Dec 16 '15 19:12

CMZS


2 Answers

It seems that the only option is to create a (temporary) compilation unit, which in turn requires a properly set up Java project. The infrastructure is necessary for JDT to know which JRE is used, which compiler settings are used, etc.

See here how to set up a Java project, and here how to get a compilation unit.

The compilation unit would look something like

class Foo {
  void bar() {
    java.lang.System.
  }
}

and codeComplete() would have to be called with an offset that denotes the position right after System..

like image 55
Rüdiger Herrmann Avatar answered Nov 03 '22 14:11

Rüdiger Herrmann


You can use the Eclipse JDT Language Server, which is used by different editors already (e.g. Visual Studio Code, and EMACS): https://github.com/eclipse/eclipse.jdt.ls

This way, you'll be able to provide many JDT features available in the LSP definition (e.g. code completions, references, diagnostics, etc.): https://microsoft.github.io/language-server-protocol/specifications/specification-current/

There are bindings for Java available with LSP4J over Maven:

<dependency>
    <groupId>org.eclipse.lsp4j</groupId>
    <artifactId>org.eclipse.lsp4j</artifactId>
    <version>0.12.0</version>
</dependency>

A simple implementation might look like this:

ExpressionLanguageClient.java

import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.ShowMessageRequestParams;
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
import org.eclipse.lsp4j.services.LanguageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CompletableFuture;

public class ExpressionLanguageClient implements LanguageClient {

    private final static Logger logger = LoggerFactory.getLogger(ExpressionLanguageClient.class);

    final private ExpressionCodeAssistantService expressionCodeAssistantService;

    public ExpressionLanguageClient(ExpressionCodeAssistantService expressionCodeAssistantService) {
        this.expressionCodeAssistantService = expressionCodeAssistantService;
    }

    @Override
    public void telemetryEvent(Object o) {
        // TODO
        logger.info("Expression LSP telemetry: " + o);
    }

    @Override
    public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) {
        // TODO
        logger.info("Expression LSP diagnostics: " + publishDiagnosticsParams);
    }

    @Override
    public void showMessage(MessageParams messageParams) {
        // TODO
        logger.info("Expression LSP show message: " + messageParams);
    }

    @Override
    public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams showMessageRequestParams) {
        return null;
    }

    @Override
    public void logMessage(MessageParams messageParams) {
        expressionCodeAssistantService.lspLogMessage(messageParams.getMessage());
    }

    @JsonNotification("language/status")
    public void languageStatus(Object o) {
        // avoid unsupported notification warnings
    }
}

CodeAssistantService.java

// start the Eclipse JDT LS required for the code assistant features
try {
    String[] command = new String[]{
            "java",
            "-Declipse.application=org.eclipse.jdt.ls.core.id1",
            "-Dosgi.bundles.defaultStartLevel=4",
            "-Declipse.product=org.eclipse.jdt.ls.core.product",
            "-Dlog.level=ALL",
            "-noverify",
            "-Xmx1G",
            "-jar",
            ".../eclipse.jdt.ls/org.eclipse.jdt.ls.product/target/repository/plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar",
            "-configuration",
            ".../eclipse.jdt.ls/org.eclipse.jdt.ls.product/target/repository/config_linux",
            "-data",
            "...",
            "--add-modules=ALL-SYSTEM",
            "--add-opens java.base/java.util=ALL-UNNAMED",
            "--add-opens java.base/java.lang=ALL-UNNAMED"
    };

    Process process = new ProcessBuilder(command)
            .redirectErrorStream(true)
            .start();

    ExpressionLanguageClient expressionLanguageClient = new ExpressionLanguageClient(this);

    launcher = LSPLauncher.createClientLauncher(
            expressionLanguageClient,
            process.getInputStream(),
            process.getOutputStream()
    );
    launcher.startListening();
} catch (Exception e) {
    logger.error("Could not start the language server", e);
}

Be sure to customize the commands where necessary (paths and config_mac/linux/windows).

As soon as the language server is running (maybe listen for a log message), you need to call the init event (be sure to call from a separate thread, if you call it from the language client, because it would cause a deadlock otherwise):

InitializeParams initializeParams = new InitializeParams();
initializeParams.setProcessId(((int) ProcessHandle.current().pid()));

// workspace folders are read from the initialization options, not from the param
List<String> workspaceFolders = new ArrayList<>();
workspaceFolders.add("file:" + getTempDirectory());

Map<String, Object> initializationOptions = new HashMap<>();
initializationOptions.put("workspaceFolders", workspaceFolders);
initializeParams.setInitializationOptions(initializationOptions);

CompletableFuture<InitializeResult> init = launcher.getRemoteProxy().initialize(initializeParams);
try {
    init.get();
    logger.info("LSP initialized");
} catch (Exception e) {
    logger.error("Could not initialize LSP server", e);
}

Now, you're able to get code completions like this:

TextDocumentItem textDocumentItem = new TextDocumentItem();
textDocumentItem.setText(isolatedCodeResult.getCode());
textDocumentItem.setUri("file:" + dummyFilePath);
textDocumentItem.setLanguageId("java");

DidOpenTextDocumentParams didOpenTextDocumentParams = new DidOpenTextDocumentParams();
didOpenTextDocumentParams.setTextDocument(textDocumentItem);
launcher.getRemoteProxy().getTextDocumentService().didOpen(didOpenTextDocumentParams);

TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier();
textDocumentIdentifier.setUri("file:" + dummyFilePath);

CompletionParams completionParams = new CompletionParams();
completionParams.setPosition(new Position(line + lineOffset, column));
completionParams.setTextDocument(textDocumentIdentifier);

CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion =
        launcher.getRemoteProxy().getTextDocumentService().completion(completionParams);

logger.info("Found completions: " + completion.get().getRight().getItems());

Make sure, the dummyFilePath is a an existing java file. The content does not matter, but it needs to exist in order for the JDT LS to work.

I am not sure, whether it would be better to always sync the source files with the language server. Maybe this would be faster, especially for large projects. If you just need content assistant features for minor source files, the provided example should be sufficient.

like image 1
moritz.vieli Avatar answered Nov 03 '22 14:11

moritz.vieli