javasalesforceapexlanguage-server-protocol

How to connect apex(salesforce) language server using raw Java?


I am interested in exploring a language server from Salesforce for Apex. I am new to this concept and want to understand how to communicate with it. So, I am seeking help to write Java Code that can communicate with the JAR provided by Salesforce. The links are attached for reference.

I need a working example of a Java code that demonstrates the process.

Salesforce Apex Language Server

Download the JAR

Provided Typescript file of example


Solution

  • After doing a lot of research, I have successfully managed to communicate with the Apex Language Server. Although the code is not perfect and there are areas for optimization, it currently works for essential tasks like initializing the server, sending a document for analysis, and retrieving completions or type hints.

    package com.iishanto;
    
    import java.io.*;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    public class LanguageServerRunner {
    
        private final String serverPath;
        private final ExecutorService executorService;
        private Process languageServerProcess;
        private BufferedWriter writer;
    
        public LanguageServerRunner(String serverPath) {
            this.serverPath = serverPath;
            this.executorService = Executors.newFixedThreadPool(3);
        }
    
        public void startServer() throws IOException {
            ProcessBuilder processBuilder = new ProcessBuilder(
                    "java",
                    "-Xms256m",
                    "-Xmx512m",
                    "-Ddebug.internal.errors=true",
                    "-Ddebug.semantic.errors=true",
                    "-Ddebug.completion.statistics=true",
                    "-jar",
                    serverPath);
            languageServerProcess = processBuilder.start();
            writer = new BufferedWriter(new OutputStreamWriter(languageServerProcess.getOutputStream()));
            handleServerOutput(languageServerProcess.getInputStream(), "Server Response");
            handleServerOutput(languageServerProcess.getErrorStream(), "Error");
        }
    
        private void handleServerOutput(InputStream inputStream, String label) {
            executorService.submit(() -> {
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        System.out.println(label + ": " + line);
                        if (label.equals("Server Response") && line.startsWith("Content-Length:")) {
                            int contentLength = Integer.parseInt(line.substring("Content-Length:".length()).trim());
                            char[] content = new char[contentLength];
                            reader.read(content, 0, contentLength);
                            System.out.println("Server Response Body: " + new String(content));
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    
        public void sendMessage(String message) throws IOException {
            String formattedMessage = "Content-Length: " + message.length() + "\r\n\r\n" + message;
            writer.write(formattedMessage);
            writer.flush();
        }
    
        public void sendInitializeMessage() throws IOException {
            String initRequest = """
                {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "initialize",
                    "params": {
                        "processId": null,
                        "rootUri": "file:///C:/Users/Example/Desktop/Project",
                        "rootPath": "C:/Users/Example/Desktop/Project/",
                        "capabilities": {
                            "textDocument": {
                                "synchronization": {
                                    "dynamicRegistration": true,
                                    "willSave": true,
                                    "willSaveWaitUntil": true,
                                    "didSave": true
                                },
                                "completion": {
                                              "dynamicRegistration": true,
                                              "completionItem": {
                                                "snippetSupport": true,
                                                "commitCharactersSupport": true
                                              }
                                            }
                            },
                            "workspace": {
                                "applyEdit": true,
                                "workspaceEdit": {
                                    "documentChanges": true
                                }
                            }
                        }
                    }
                }
                """;
            sendMessage(initRequest);
        }
    
        public void sendDidOpenMessage(String fileUri, String languageId, int version, String text) throws IOException {
            String openRequest = """
                {
                    "jsonrpc": "2.0",
                    "method": "textDocument/didOpen",
                    "params": {
                        "textDocument": {
                            "uri": "%s",
                            "languageId": "%s",
                            "version": %d,
                            "text": "%s"
                        }
                    }
                }
                """.formatted(fileUri, languageId, version, text);
            sendMessage(openRequest);
        }
    
        public void sendTypeHintingMessage(String fileUri,String content,int line,int column) throws IOException {
            String message= """
                    {
                        "jsonrpc": "2.0",
                        "id": 1,
                        "method": "textDocument/completion",
                        "params": {
                            "textDocument": {
                                 "uri": "%s"
                            },
                            "position": {
                                "line": %d,
                                "character": %d
                            },
                            "context": {
                                "triggerKind": 1,
                                "triggerCharacter": "."
                            }
                        }
                    }
                    """.formatted(fileUri,line,column);
            System.out.println(message);
            sendMessage(message);
        }
    
        public void stopServer() throws InterruptedException {
            if (languageServerProcess != null) {
                int exitCode = languageServerProcess.waitFor();
                System.out.println("Language Server exited with code: " + exitCode);
            }
            executorService.shutdown();
        }
    
        public static void main(String[] args) {
            String serverPath = "C:\\Users\\Example\\Downloads\\apex-jorje-lsp.jar";
            String filePath = "C:\\Users\\Example\\Desktop\\Project\\force-app\\main\\default\\classes\\DynamicFieldFetcher.cls";
    
            try {
                LanguageServerRunner runner = new LanguageServerRunner(serverPath);
                runner.startServer();
    
                String fileContent = new String(Files.readAllBytes(Paths.get(filePath)));
    
                runner.sendInitializeMessage();
    
                runner.sendDidOpenMessage(
                        "file:///C:/Users/Example/Desktop/Project/force-app/main/default/classes/DynamicFieldFetcher.cls",
                        "apex",
                        6,
                        fileContent
                );
    
    
                TimeUnit.MILLISECONDS.sleep(5000);
    
                runner.sendTypeHintingMessage(
                        "file:///C:/Users/Example/Desktop/Project/force-app/main/default/classes/DynamicFieldFetcher.cls"
                        ,fileContent,5,34);
    
                BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));
                String inputLine;
                while ((inputLine = userInput.readLine()) != null) {
                    runner.sendMessage(inputLine);
                }
    
                runner.stopServer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }