This tutorial describes how to define a language server integration for the Eclipse IDE

1. Developing with a language server

The Language Server Protocol (LSP) defines the protocol used between an editor or IDE and a language server. A language server provides features like autocompletion, go-to-definition, and find-all-references. For more details on the Language Server Protocol, refer to the official documentation at: https://github.com/microsoft/language-server-protocol and https://microsoft.github.io/language-server-protocol/specification.

LSP4J (Language Server Protocol for Java) is an Eclipse project that provides Java bindings for the Language Server Protocol. LSP is based on an extended version of JSON-RPC v2.0, and LSP4J offers a Java implementation of this. With LSP4J, you can develop a language server without handling the JSON specifics of the protocol; instead, you create endpoints that receive parameters from the client and return actions in object form based on the received messages.

The Eclipse LSP4E project offers technologies to simplify integrating language servers into the Eclipse IDE.

1.1. Required dependencies

To build a language server with Java bindings, you need:

  • org.eclipse.lsp4j

  • org.eclipse.lsp4j.jsonrpc

For an Eclipse client, you need:

  • org.eclipse.lsp4e

2. Exercise: Implementing and using a language server

Learning goal: Learn how to lay the foundation of a Java language server.

The language server developed in these exercises is responsible for a test file type named languageserver_example.txt. We call it an AsciiDoc language server for demonstration purposes, but the implementation is only exemplary and simplified.

The exercises use LSP4J 1.0.0 and Java 21, the minimum Java version required by a current Eclipse IDE.

2.1. Add LSP4J to your target platform

Add the following to your target platform for the server implementation.

<location includeAllPlatforms="false" includeConfigurePhase="true"
    includeMode="planner" includeSource="true" type="InstallableUnit">
    <repository location="https://download.eclipse.org/lsp4j/updates/releases/1.0.0/" />
        <unit id="org.eclipse.lsp4j" />
        <unit id="org.eclipse.lsp4j.jsonrpc" />
</location>

For the client which you implement later, add:

<location includeAllPlatforms="false" includeConfigurePhase="true" includeMode="planner" includeSource="true" type="InstallableUnit">
    <repository location="https://download.eclipse.org/lsp4e/releases/latest/"/>
    <unit id="org.eclipse.lsp4e"/>
</location>

2.2. Creating a plug-in for the language server implementation

In this exercise, you create a language server which provides basic code completion.

2.2.1. Create plug-in

Create a new simple plug-in project named com.vogella.lsp.asciidoc.server.

2.2.2. Adding the necessary dependencies

Open the MANIFEST.MF file in the META-INF folder of your project.

Click on the Dependencies tab and add the following plug-in dependencies:

  • org.eclipse.lsp4j

  • org.eclipse.lsp4j.jsonrpc

These plug-ins provide the LanguageServer interface, the request, parameter and response types for the server-client communication and the Launcher class which connects your server implementation to the client via input and output streams.

2.2.3. Create the classes for the language server

Create the following classes.

The document model stores the text of an open document line by line.

package com.vogella.lsp.asciidoc.server;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AsciidocDocumentModel {

    // A single line of the document
    public record DocumentLine(int line, String text) {
    }

    private final List<DocumentLine> lines = new ArrayList<>();

    public AsciidocDocumentModel(String text) {
        int lineNumber = 0;
        for (String lineText : text.lines().toList()) {
            lines.add(new DocumentLine(lineNumber++, lineText));
        }
    }

    // Returns the content of the given line or null if the line does not exist
    public String getLineContent(int lineNumber) {
        if (lineNumber < 0 || lineNumber >= lines.size()) {
            return null;
        }
        return lines.get(lineNumber).text();
    }

    public List<DocumentLine> getResolvedLines() {
        return Collections.unmodifiableList(this.lines);
    }
}

The language server implementation declares its capabilities in the initialize method and implements the shutdown and exit lifecycle methods.

package com.vogella.lsp.asciidoc.server;

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.TextDocumentSyncKind;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;

public class AsciidocLanguageServer implements LanguageServer {

    private final TextDocumentService textService;
    private final WorkspaceService workspaceService;
    private volatile boolean shutdownRequested;
    LanguageClient client;

    public AsciidocLanguageServer() {
        textService = new AsciidocTextDocumentService(this);
        workspaceService = new AsciidocWorkspaceService();
    }

    /**
     * Tells the client which functionality this server supports
     */
    @Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        ServerCapabilities capabilities = new ServerCapabilities();
        capabilities.setTextDocumentSync(TextDocumentSyncKind.Full);
        capabilities.setCompletionProvider(new CompletionOptions());
        return CompletableFuture.completedFuture(new InitializeResult(capabilities));
    }

    @Override
    public CompletableFuture<Object> shutdown() {
        // The client announces that it will exit soon, free resources here
        shutdownRequested = true;
        return CompletableFuture.completedFuture(null);
    }

    @Override
    public void exit() {
        // A standalone server would terminate its process here, for example via
        // System.exit(shutdownRequested ? 0 : 1);
        // This server runs inside the IDE process, so the connection provider
        // stops the launcher instead
        if (!shutdownRequested) {
            System.err.println("exit received without a previous shutdown request");
        }
    }

    @Override
    public TextDocumentService getTextDocumentService() {
        return textService;
    }

    @Override
    public WorkspaceService getWorkspaceService() {
        return workspaceService;
    }

    public void setRemoteProxy(LanguageClient remoteProxy) {
        this.client = remoteProxy;
    }
}

The text document service keeps track of the open documents and provides two static completion proposals. TextDocumentService provides default implementations for all optional operations, so you only implement what your server supports.

package com.vogella.lsp.asciidoc.server;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.TextDocumentService;

public class AsciidocTextDocumentService implements TextDocumentService {

    private final Map<String, AsciidocDocumentModel> docs = new ConcurrentHashMap<>();

    private final AsciidocLanguageServer languageServer;

    public AsciidocTextDocumentService(AsciidocLanguageServer languageServer) {
        this.languageServer = languageServer;
    }

    @Override
    public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) {
        // Example: provide completions for AsciiDoc elements
        CompletionItem image = new CompletionItem("image::");
        CompletionItem include = new CompletionItem("include::");
        return CompletableFuture.completedFuture(Either.forLeft(List.of(image, include)));
    }

    @Override
    public void didOpen(DidOpenTextDocumentParams params) {
        AsciidocDocumentModel model = new AsciidocDocumentModel(params.getTextDocument().getText());
        docs.put(params.getTextDocument().getUri(), model);
    }

    @Override
    public void didChange(DidChangeTextDocumentParams params) {
        AsciidocDocumentModel model = new AsciidocDocumentModel(params.getContentChanges().get(0).getText());
        docs.put(params.getTextDocument().getUri(), model);
    }

    @Override
    public void didClose(DidCloseTextDocumentParams params) {
        docs.remove(params.getTextDocument().getUri());
    }

    @Override
    public void didSave(DidSaveTextDocumentParams params) {
    }
}
package com.vogella.lsp.asciidoc.server;

import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.services.WorkspaceService;

public class AsciidocWorkspaceService implements WorkspaceService {


    @Override
    public void didChangeConfiguration(DidChangeConfigurationParams params) {
    }

    @Override
    public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
    }

}

To allow other plug-ins to use the language server, export the com.vogella.lsp.asciidoc.server package either in the Runtime tab or via the Export-Package header.

2.2.4. Main.java

A language server usually runs as a separate process and communicates with the client via its standard input and output streams. The following Main class starts the server this way.

package com.vogella.lsp.asciidoc.server;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.launch.LSPLauncher;
import org.eclipse.lsp4j.services.LanguageClient;

public class Main {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        startServer(System.in, System.out);
    }

    public static void startServer(InputStream in, OutputStream out) throws InterruptedException, ExecutionException {
        AsciidocLanguageServer server = new AsciidocLanguageServer();   (1)
        Launcher<LanguageClient> launcher = LSPLauncher.createServerLauncher(server, in, out); (2)
        Future<Void> startListening = launcher.startListening();
        server.setRemoteProxy(launcher.getRemoteProxy());               (3)
        startListening.get();                                           (4)
    }
}
1 Create an instance of the language server, implementing lsp4j.services.LanguageServer.
2 Use Launcher to connect the server to the program’s standard input and output.
3 Set the server’s client proxy, used later to publish diagnostics to the client.
4 Keep the server process active until the Launcher stops listening.

In this tutorial the server is not started as a separate process, instead the Eclipse client of the next exercise starts it inside the IDE process.

2.2.5. Review

The above finishes a simple language server. You will now build a client to test this server.

3. Creating a LSP client in Eclipse

For the client create a simple plug-in named com.vogella.lsp.asciidoc.client.

3.1. Manifest dependencies

Add the following plug-in dependencies to your manifest:

  • org.eclipse.lsp4j

  • org.eclipse.lsp4j.jsonrpc

  • org.eclipse.lsp4e

  • org.eclipse.ui

  • org.eclipse.core.runtime

  • org.eclipse.ui.genericeditor

  • com.vogella.lsp.asciidoc.server

Also mark the plug-in as singleton via the Overview tab on the manifest.

3.2. Configure your editor extension to use the generic editor

Create a new content type and bind it to the generic editor. To keep it separated from any other editor configuration we register it for the file languageserver_example.txt.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
 <extension
         point="org.eclipse.core.contenttype.contentTypes">
      <content-type
            base-type="org.eclipse.core.runtime.text"
            file-names="languageserver_example.txt"
            id="com.vogella.lsp.asciidoc"
            name="Example Content Type (languageserver_example.txt)"
            priority="normal">
      </content-type>
   </extension>
   <extension
         point="org.eclipse.ui.editors">
      <editorContentTypeBinding
            contentTypeId="com.vogella.lsp.asciidoc"
            editorId="org.eclipse.ui.genericeditor.GenericEditor">
      </editorContentTypeBinding>
   </extension>
</plugin>

3.3. Implement connection

The connection provider starts the language server inside the IDE process and connects it to LSP4E via piped streams.

package com.vogella.lsp.asciidoc.client;

import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.Future;

import org.eclipse.lsp4e.server.StreamConnectionProvider;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.launch.LSPLauncher;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;

public class AbstractConnectionProvider implements StreamConnectionProvider {

    private InputStream inputStream;
    private OutputStream outputStream;
    private LanguageServer languageServer;
    private volatile Future<Void> listening;
    protected Launcher<LanguageClient> launcher;

    public AbstractConnectionProvider(LanguageServer languageServer) {
        this.languageServer = languageServer;
    }

    @Override
    public void start() throws IOException {
        PipedInputStream in = new PipedInputStream();
        PipedOutputStream out = new PipedOutputStream();
        PipedInputStream in2 = new PipedInputStream();
        PipedOutputStream out2 = new PipedOutputStream();

        in.connect(out2);
        out.connect(in2);

        launcher = LSPLauncher.createServerLauncher(languageServer, in2, out2);
        inputStream = in;
        outputStream = out;

        listening = launcher.startListening();
    }

    // The returned streams log the protocol traffic to the console for learning
    // purposes, remove the wrappers for production use

    @Override
    public InputStream getInputStream() {
        return new FilterInputStream(inputStream) {
            @Override
            public int read(byte[] b, int off, int len) throws IOException {
                int bytesRead = super.read(b, off, len);
                if (bytesRead > 0) {
                    System.err.print(new String(b, off, bytesRead));
                }
                return bytesRead;
            }
        };
    }

    @Override
    public OutputStream getOutputStream() {
        return new FilterOutputStream(outputStream) {
            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                System.err.print(new String(b, off, len));
                super.write(b, off, len);
            }
        };
    }

    @Override
    public void stop() {
        // Stop the launcher before closing the streams, otherwise the server
        // logs an InterruptedIOException when the IDE shuts down
        if (listening != null) {
            listening.cancel(true);
        }
        try {
            inputStream.close();
            outputStream.close();
        } catch (IOException e) {
            System.err.println("Error closing streams: " + e.getMessage());
        }
    }

    @Override
    public InputStream getErrorStream() {
        return null;
    }
}
package com.vogella.lsp.asciidoc.client;

import java.io.IOException;


import com.vogella.lsp.asciidoc.server.AsciidocLanguageServer;

public class ConnectionProviderSolution extends AbstractConnectionProvider {
    private static final AsciidocLanguageServer LANGUAGE_SERVER = new AsciidocLanguageServer();
    public ConnectionProviderSolution() {
        super(LANGUAGE_SERVER);
    }
    
    @Override
    public void start() throws IOException {
        super.start();
        LANGUAGE_SERVER.setRemoteProxy(launcher.getRemoteProxy());
    }
}

3.4. Configure the connection

<extension
      point="org.eclipse.lsp4e.languageServer">
    <server
          class="com.vogella.lsp.asciidoc.client.ConnectionProviderSolution"
          id="org.vogella.lsp.asciidoc.server"
          label="Solution Server">
    </server>
    <contentTypeMapping
          contentType="com.vogella.lsp.asciidoc"
          id="org.vogella.lsp.asciidoc.server">
    </contentTypeMapping>
</extension>

3.5. Server in action

Run it to test its capabilities.

  1. Right-click on the com.vogella.lsp.asciidoc.client project and select Run as  Eclipse Application.

  2. Create a new file named languageserver_example.txt.

  3. Open this file with the generic editor.

  4. Use Ctrl+Space to see the completion suggestions of your server.

3.6. Adjust the code completion

You can position the cursor inside an inserted completion by using the snippet syntax. Set the insertTextFormat property of the CompletionItem to InsertTextFormat.Snippet and place the $0 placeholder in the insertText where the cursor should end up.

Create the new class named AsciidocElements in the server plug-in. The $0 placeholder in the source template positions the cursor between the two ---- delimiter lines.

package com.vogella.lsp.asciidoc.server;

import java.util.HashMap;
import java.util.Map;

public class AsciidocElements {

    public static final AsciidocElements INSTANCE = new AsciidocElements();

    Map<String, String> suggestions = new HashMap<>();

    public AsciidocElements() {
        String sourceTemplate = """
                [source,java]
                ----
                $0
                ----
                """;
        suggestions.put("source", sourceTemplate);

    }
}

Change the completion method in AsciidocTextDocumentService to build the completion items from these suggestions.

@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) {
    List<CompletionItem> completionItems = AsciidocElements.INSTANCE.suggestions.entrySet().stream()
            .map(entry -> {
                CompletionItem item = new CompletionItem(entry.getKey()); (1)
                item.setInsertText(entry.getValue());                     (2)
                item.setInsertTextFormat(InsertTextFormat.Snippet);       (3)
                return item;
            })
            .toList();
    return CompletableFuture.completedFuture(Either.forLeft(completionItems));
}
1 Use the key of the suggestion as the label shown in the completion popup.
2 Insert the template text when the user selects the completion.
3 Use the snippet format so that the $0 placeholder positions the cursor.

Restart your application and trigger the completion again to see the template with the cursor placed inside the source block.

3.7. Avoiding an exception on IDE shutdown

When the runtime IDE closes, LSP4E sends the shutdown request and the exit notification to the server and afterwards closes the connection streams. If the launcher of the server is still listening on the closed stream at that point, LSP4J logs an exception similar to the following:

INFO: The input stream was closed.
java.io.InterruptedIOException
    at java.base/java.io.PipedInputStream.read(PipedInputStream.java:334)
    at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.listen(StreamMessageProducer.java:82)
    at org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor.run(ConcurrentMessageProcessor.java:114)

To prevent this, cancel the Future returned by Launcher.startListening() before closing the streams, as done in the stop() method of AbstractConnectionProvider above. Cancelling this future closes the underlying LSP4J message producer, so LSP4J stops reading and treats the closed stream as the expected end of the communication instead of reporting it.

A language server which runs as a separate process typically terminates its JVM in the exit() implementation instead, for example via System.exit(0). Do not call System.exit in the server of this tutorial, it runs inside the IDE process and would terminate the whole IDE.

3.8. Implementing outline

In this exercise, your server provides a (fake) outline.

3.8.1. Outline support in the server

Enable document symbol support by adding the following line to the initialize method of AsciidocLanguageServer.

capabilities.setDocumentSymbolProvider(Boolean.TRUE);

Implement the documentSymbol method in AsciidocTextDocumentService.

@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
        DocumentSymbolParams params) {
    return CompletableFuture.supplyAsync(() -> {
        // Create a symbol for a class
        DocumentSymbol classSymbol = new DocumentSymbol();
        classSymbol.setName("MyClass");
        classSymbol.setKind(SymbolKind.Class);
        classSymbol.setRange(new Range(new Position(0, 0), new Position(0, 10)));
        classSymbol.setSelectionRange(new Range(new Position(0, 0), new Position(0, 10)));

        // Create a symbol for a method inside the class
        DocumentSymbol methodSymbol = new DocumentSymbol();
        methodSymbol.setName("myMethod");
        methodSymbol.setKind(SymbolKind.Method);
        methodSymbol.setRange(new Range(new Position(1, 0), new Position(1, 10)));
        methodSymbol.setSelectionRange(new Range(new Position(1, 0), new Position(1, 10)));

        // Add the method symbol as a child of the class symbol
        classSymbol.setChildren(List.of(methodSymbol));

        List<Either<SymbolInformation, DocumentSymbol>> symbols = new ArrayList<>();
        symbols.add(Either.forRight(classSymbol));
        return symbols;
    });
}

3.8.2. Test the outline with your language server

Restart your running application, open your file and open the Outline view.

lsp outline

3.9. Providing a hover functionality

In this exercise, your server implements hover functionality.

3.9.1. Hover support in the server

Enable hover support by adding the following line to the initialize method of AsciidocLanguageServer.

capabilities.setHoverProvider(Boolean.TRUE);

Implement the hover method in AsciidocTextDocumentService.

@Override
public CompletableFuture<Hover> hover(HoverParams params) {
    return CompletableFuture.supplyAsync(() -> {
        // Get the position where the hover request was made
        Position position = params.getPosition();

        // We hover only after the first line
        if (position.getLine() > 0) {

            String content = """
                    **Important AsciiDoc elements:**

                    * `image::` - Defines an image element in AsciiDoc files.
                    * `include::` - Includes other AsciiDoc files in the current one.

                    **Usage example:**
                    ```asciidoc
                    image::path/to/image.png[]
                    include::example.adoc[]
                    ```
                    """;

            // Create the Hover object with content in Markdown format
            Hover hover = new Hover();
            hover.setContents(new MarkupContent(MarkupKind.MARKDOWN, content));
            return hover;
        }

        // No hover for the first line
        return null;
    });
}

3.9.2. Test the hover functionality with your language server

Restart your running application, position your mouse cursor on an element and wait a little while. Do not use the first line, as the server does not provide hover for the first line.

lsp hover

3.10. Implementing navigation

In this exercise, your server implements (fake) navigation support for the document.

Enable definition support by adding the following line to the initialize method of AsciidocLanguageServer.

capabilities.setDefinitionProvider(Boolean.TRUE);

Implement the definition method in AsciidocTextDocumentService. Two helper methods identify the word under the cursor and resolve its definition.

@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(
        DefinitionParams params) {

    // Get the document URI and retrieve the model
    AsciidocDocumentModel model = this.docs.get(params.getTextDocument().getUri());
    if (model == null) {
        return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList()));
    }

    // Get the line where the cursor is located
    int line = params.getPosition().getLine();
    int character = params.getPosition().getCharacter();

    // Retrieve the content of the line
    String lineContent = model.getLineContent(line);
    if (lineContent == null) {
        return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList()));
    }

    // Find the word under the cursor
    String wordUnderCursor = getWordAtPosition(lineContent, character);

    // Resolve this word to locations, the resolution logic depends on your
    // language, for example file-based links or symbol lookup
    List<Location> locations = findDefinitionLocations(wordUnderCursor);

    return CompletableFuture.completedFuture(Either.forLeft(locations));
}

/**
 * Finds the word under the cursor in a given line of text using simple word
 * boundaries.
 */
private String getWordAtPosition(String lineContent, int character) {
    int start = character;
    int end = character;

    // Find the start of the word (left of the cursor)
    while (start > 0 && Character.isLetterOrDigit(lineContent.charAt(start - 1))) {
        start--;
    }

    // Find the end of the word (right of the cursor)
    while (end < lineContent.length() && Character.isLetterOrDigit(lineContent.charAt(end))) {
        end++;
    }

    return lineContent.substring(start, end);
}

/**
 * Resolves the definition location(s) for a given word. This is a stub which
 * always returns the same location, implement your own resolution logic here.
 */
private List<Location> findDefinitionLocations(String word) {
    List<Location> locations = new ArrayList<>();
    Location location = new Location();
    location.setUri("file:///path/to/definitionFile");
    location.setRange(new Range(new Position(5, 0), new Position(5, 10)));
    locations.add(location);
    return locations;
}

3.10.2. Test the navigation with your language server

Restart your running application, open your document, press Ctrl and click on a word.

lsp navigation

3.11. Setting up document validations

You will now add checks to your document. Whenever the document is opened or changed, the server analyzes it and pushes diagnostics to the client. Publishing diagnostics does not require an additional server capability, so the initialize method stays unchanged.

3.11.1. Validating the document

Create the following method in AsciidocTextDocumentService to check the document.

private List<Diagnostic> validate(AsciidocDocumentModel model) {
    List<Diagnostic> diagnostics = new ArrayList<>();

    // Simulate finding a placeholder issue
    for (int i = 0; i < model.getResolvedLines().size(); i++) {
        String line = model.getResolvedLines().get(i).text();
        int index = line.indexOf("PLACEHOLDER_TEXT");
        if (index != -1) {
            // Create a diagnostic for the placeholder text issue
            Diagnostic diagnostic = new Diagnostic();
            diagnostic.setSeverity(DiagnosticSeverity.Warning);
            diagnostic.setMessage("Found placeholder text that should be replaced.");
            diagnostic.setCode("placeholder.text.issue");
            diagnostic.setRange(
                    new Range(new Position(i, index), new Position(i, index + "PLACEHOLDER_TEXT".length())));
            diagnostics.add(diagnostic);
        }
    }

    return diagnostics;
}

Adjust the didOpen and didChange methods to publish the diagnostics to the client.

@Override
public void didOpen(DidOpenTextDocumentParams params) {
    AsciidocDocumentModel model = new AsciidocDocumentModel(params.getTextDocument().getText());
    docs.put(params.getTextDocument().getUri(), model);
    CompletableFuture.runAsync(() -> languageServer.client
            .publishDiagnostics(new PublishDiagnosticsParams(params.getTextDocument().getUri(), validate(model))));
}

@Override
public void didChange(DidChangeTextDocumentParams params) {
    AsciidocDocumentModel model = new AsciidocDocumentModel(params.getContentChanges().get(0).getText());
    docs.put(params.getTextDocument().getUri(), model);
    CompletableFuture.runAsync(() -> languageServer.client
            .publishDiagnostics(new PublishDiagnosticsParams(params.getTextDocument().getUri(), validate(model))));
}

3.11.2. Test the validation with your language server

Restart your running application, open your document and enter the PLACEHOLDER_TEXT text a few times. You should see the warnings from your validation.

lsp validation10

3.12. Implementing code actions

In this exercise, your server implements code actions for quick fixes in the document.

3.12.1. Code action support in the server

Enable code action support by adding the following line to the initialize method of AsciidocLanguageServer.

capabilities.setCodeActionProvider(Boolean.TRUE);

Implement the codeAction method in AsciidocTextDocumentService. It offers a quick fix for the diagnostics created in the validation exercise.

@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
    List<Either<Command, CodeAction>> actions = new ArrayList<>();

    // Check the diagnostics for the current document
    for (Diagnostic diagnostic : params.getContext().getDiagnostics()) {
        if ("placeholder.text.issue".equals(diagnostic.getCode().getLeft())) {
            // Create a text edit for replacing the placeholder
            TextEdit edit = new TextEdit();
            edit.setRange(diagnostic.getRange());
            edit.setNewText("replacement_text");

            // Create a workspace edit
            WorkspaceEdit workspaceEdit = new WorkspaceEdit();
            workspaceEdit.setChanges(Collections.singletonMap(params.getTextDocument().getUri(), List.of(edit)));

            // Create the code action
            CodeAction codeAction = new CodeAction("Replace placeholder with 'replacement_text'");
            codeAction.setKind(CodeActionKind.QuickFix);
            codeAction.setEdit(workspaceEdit);

            actions.add(Either.forRight(codeAction));
        }
    }

    return CompletableFuture.completedFuture(actions);
}

3.12.2. Test the code action with your language server

Restart your running application, open your document, place the cursor on a validation warning and press Ctrl+1. Use the code action to replace the text.

lsp code actions

3.13. Implementing code lenses

In this exercise, your server implements code lens support for TODO text in the document.

3.13.1. Code lens support in the server

Enable code lens support by adding the following line to the initialize method of AsciidocLanguageServer.

capabilities.setCodeLensProvider(new CodeLensOptions(false));

3.13.2. Change the data model

Add the following method to AsciidocDocumentModel.

// Returns all lines of the document as strings
public List<String> getLines() {
    List<String> result = new ArrayList<>();
    for (DocumentLine line : lines) {
        result.add(line.text());
    }
    return Collections.unmodifiableList(result);
}

Implement the codeLens method in AsciidocTextDocumentService.

@Override
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) {
    return CompletableFuture.supplyAsync(() -> {
        // Retrieve the document from the model
        String uri = params.getTextDocument().getUri();
        AsciidocDocumentModel model = docs.get(uri);
        if (model == null) {
            return Collections.emptyList();
        }

        List<CodeLens> codeLenses = new ArrayList<>();
        List<String> lines = model.getLines();

        // Scan for "TODO" comments
        for (int i = 0; i < lines.size(); i++) {
            String line = lines.get(i);
            int todoIndex = line.indexOf("TODO");
            if (todoIndex != -1) {
                // Define the range for the TODO
                Range range = new Range(new Position(i, todoIndex), new Position(i, todoIndex + "TODO".length()));

                // Create a CodeLens with a command
                Command command = new Command("Resolve TODO", "example.resolveTodo",
                        Collections.singletonList("Resolve the TODO at line " + (i + 1)));

                codeLenses.add(new CodeLens(range, command, null));
            }
        }

        return codeLenses;
    });
}

3.13.3. Test the code lenses with your language server

Restart your running application, open your document and type TODO in a line. You should see your code lenses.

lsp code lenses

4. Eclipse Language Server resources

Home Tutorials Training Consulting Books Company Contact us


Get more...