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

3

6

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).

Expressway answered 16/12, 2015 at 19:50 Comment(1)
ICompilationUnit is part of the JDT Abstract Syntax TreeNordgren
O
1

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..

Ostracize answered 17/12, 2015 at 9:48 Comment(0)
A
1

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.

Abnaki answered 7/2, 2022 at 8:45 Comment(0)
B
0

You could try to use the java reflection API to get all the members of this certain class (YourClass.getMethods() or YourClass.getDeclaredMethods()).

To make it dynamic according to your input you can use Class.forName(<fullClassName>) to get the respective class (see this post for further information about that).

The problem you might run into is that you have to give the full qualified class name therefore you might have to check the imports to find out in which package you have to search for the respective class but it's the only way to go with this method.
Simple names are just not always unique and therefore not suitable for this kind of search (explained here.

Benzyl answered 17/12, 2015 at 5:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.