Home Tutorials Training Consulting Books Company Contact us


Get more...

This tutorial describes how the define an 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 an LanguageServer

Learning goal: Learn how to lay the foundation of a Java Language Server.

The language server we will be developing through these exercises will be responsible for a test file type named languageserver_example.txt. We will call it a Asciidoc language server but the implementation will be only examplatory.

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/builds/main/" />
        <unit id="org.eclipse.lsp4j" version="0.0.0" />
        <unit id="org.eclipse.lsp4j.jsonrpc" version="0.0.0" />
</location>

For the client which we will later implement, 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" version="0.0.0"/>
</location>

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

In this exercise, we’ll complete the exercise by establishing connections and foundational steps for creating a Language Server and the necessary client configuration.

2.2.1. Create plug-in

Create a new simple plug-in project named com.vogella.lsp.asciidoc.server. Simple plug-in means no activator, no template, not an RCP application.

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

This package includes interfaces for LSP implementations, lsp4j.services.LanguageServer, and request, parameter, and response formats for server-client communication.

We’ll use one class from this package: Launcher, which is the entry point for applications that use LSP4J. Launcher handles the wiring necessary to connect your endpoint via input and output streams.

2.2.3. Create classes for language server

package com.vogella.lsp.asciidoc.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AsciidocDocumentModel {

    // Inner class representing a line of the document
    public static class DocumentLine {
        final int line;
        final String text;

        protected DocumentLine(int line, String text) {
            this.line = line;
            this.text = text;
        }
    }

    // List to store all lines from the document
    private final List<DocumentLine> lines = new ArrayList<>();

    // Constructor to read the text and store lines
    public AsciidocDocumentModel(String text) {
        try (Reader r = new StringReader(text); BufferedReader reader = new BufferedReader(r)) {

            String lineText;
            int lineNumber = 0;
            while ((lineText = reader.readLine()) != null) {
                DocumentLine line = new DocumentLine(lineNumber, lineText);
                lines.add(line);
                lineNumber++;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Method to get the content of each line by line number
    public String getLineContent(int lineNumber) {
        if (lineNumber < 0 || lineNumber >= lines.size()) {
            return null; // Return null if the line number is out of range
        }
        return lines.get(lineNumber).text;
    }

    // Method to get all lines as an unmodifiable list
    public List<DocumentLine> getResolvedLines() {
        return Collections.unmodifiableList(this.lines);
    }
}
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 TextDocumentService textService;
    private WorkspaceService workspaceService;
    LanguageClient client;

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

    /**
     * Here we tell the framework which functionality our server supports
     */
    @Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        final InitializeResult res = new InitializeResult(new ServerCapabilities());
        res.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full);
        res.getCapabilities().setCompletionProvider(new CompletionOptions());
//      res.getCapabilities().setCodeActionProvider(Boolean.TRUE);
//      res.getCapabilities().setHoverProvider(Boolean.TRUE);
//      res.getCapabilities().setReferencesProvider(Boolean.TRUE);
//      res.getCapabilities().setDefinitionProvider(Boolean.TRUE);
//      res.getCapabilities().setDocumentSymbolProvider(Boolean.TRUE);

        return CompletableFuture.supplyAsync(() -> res);
    }

    @Override
    public CompletableFuture<Object> shutdown() {
        return CompletableFuture.supplyAsync(() -> {
            return Boolean.FALSE;
        });
    }

    @Override
    public void exit() {
        System.out.println("Shutdown");
    }

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

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

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

}
package com.vogella.lsp.asciidoc.server;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensParams;
import org.eclipse.lsp4j.Command;
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.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.InsertTextFormat;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.MarkedString;
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.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.TextDocumentService;

public class AsciidocTextDocumentService implements TextDocumentService {
    private final Map<String, AsciidocDocumentModel> docs = Collections.synchronizedMap(new HashMap<>());

    private final AsciidocLanguageServer languageServer;

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

    @Override
    public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) {
        return CompletableFuture.supplyAsync(() -> {
            // Example: Provide completions for AsciiDoc elements

            CompletionItem item1 = new CompletionItem();
            item1.setLabel("image::");

            CompletionItem item2 = new CompletionItem();
            item2.setLabel("include::");
            List<CompletionItem> completionItems = List.of(item1, item2);

            return Either.forLeft(completionItems);
        });
    }

    @Override
    public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) {
        return null;
    }

    public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(
            DefinitionParams params) {
        return null;
    }



    @Override
    public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
        return CompletableFuture.completedFuture(Collections.emptyList());
    }

    @Override
    public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) {
        return null;
    }

    @Override
    public CompletableFuture<CodeLens> resolveCodeLens(CodeLens unresolved) {
        return null;
    }

    @Override
    public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) {
        return null;
    }

    @Override
    public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params) {
        return null;
    }

    @Override
    public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params) {
        return null;
    }

    @Override
    public CompletableFuture<WorkspaceEdit> rename(RenameParams params) {
        return null;
    }

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

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

    @Override
    public void didClose(DidCloseTextDocumentParams params) {
        this.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 packages to use our Language Server, export the com.vogella.lsp.asciidoc.server package either in the Runtime tab or using the Export-Package.

2.2.4. Main.java

Create the following Main.java class which we’ll create a server instance, launch it to receive/send messages, set the client, and start message processing.

package com.vogella.languageserver.asciidoc;

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> l = LSPLauncher.createServerLauncher(server, in, out); (2)
        Future<?> startListening = l.startListening();
        server.setRemoteProxy(l.getRemoteProxy());                      (3)
        startListening.get();                                           (4)
    }
}
1 Create an instance of the Language Server, implementing lsp4j.services.LanguageServer:
2 Use Launcher to start the server with program’s standard input and output:
3 Set the server’s client, used when publishing diagnostics to the client:
4 Keep the server process active until Launcher stops listening:

2.2.5. Review

The above finishes a very 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

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

        launcher.startListening();
    }

    @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() {
        // Clean up resources if needed
        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>

4. Server in Action

We can 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 using the Generic Editor.

  4. Use Ctrl-Space to request auto-complete suggestions and view the available sessions.

4.1. Adjust the code completion

you can position the cursor in the completion by setting the insertTextFormat and adjusting the cursor position using the insertText and insertTextFormat properties of the CompletionItem.

In your case, you want to position the cursor after the first ---- line in the source template. You can achieve this by setting the insertText to include a cursor placeholder (like $0) where you want the cursor to be positioned. Additionally, you’ll need to set the insertTextFormat to InsertTextFormat.Snippet so that the $0 syntax is recognized as a placeholder.

Here’s how you can modify your completion method to insert the cursor at the correct position in the sourceTemplate:

Create the new class named AsciidocElements.

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

    }
}
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) {
    return CompletableFuture.supplyAsync(() -> {
        // The source template with a cursor position placeholder
        String sourceTemplateWithCursor = """


        // Create a list of CompletionItem objects, including cursor position in insertText
        List<CompletionItem> completionItems = AsciidocElements.INSTANCE.suggestions.entrySet().stream()
            .map(entry -> {
                CompletionItem item = new CompletionItem();
                item.setLabel(entry.getKey());        // Use the key as the label
                item.setInsertText(entry.getValue() + "\n$0"); // Add the placeholder cursor at the appropriate position
                item.setInsertTextFormat(InsertTextFormat.Snippet); // Use snippet format to support cursor placeholders
                return item;
            })
            .collect(Collectors.toList());

        // Return the result wrapped in Either.forLeft
        return Either.forLeft(completionItems);
    });
}

4.2. Exception at shutdown

Currently if you close the running Eclipse application you receive an error from the language server. https://github.com/eclipse-lemminx/lemminx/pull/1134/files demonstrates a solution for the exception if closed

4.3. Implementing outline

In this exercise, our server implements a (fake) outline.

4.3.1. Outline support in the server

Change the initialize method in AsciidocLanguageServer so support setDocumentSymbolProvider.

@Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        final InitializeResult res = new InitializeResult(new ServerCapabilities());
        res.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full);
        res.getCapabilities().setCompletionProvider(new CompletionOptions());
        res.getCapabilities().setDocumentSymbolProvider(Boolean.TRUE);
//      res.getCapabilities().setHoverProvider(Boolean.TRUE);
//      res.getCapabilities().setDefinitionProvider(Boolean.TRUE);
//      res.getCapabilities().setReferencesProvider(Boolean.TRUE);
        // res.getCapabilities().setCodeActionProvider(Boolean.TRUE);

        return CompletableFuture.supplyAsync(() -> res);
    }

Implement this method in AsciidocTextDocumentService.

@Override
    public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
            DocumentSymbolParams params) {
        return CompletableFuture.supplyAsync(() -> {
            // Create a list to hold the symbols
            List<Either<SymbolInformation, DocumentSymbol>> symbols = new ArrayList<>();

            // 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));

            // Add the class symbol to the list of symbols
            symbols.add(Either.forRight(classSymbol));

            // Return the list of symbols
            return symbols;
        });
    }

5. Server in Action

Restart your running application and open the outline view while opening your file.

lsp outline

5.1. Providing a hover functionality

In this exercise, our server implements hover functionality.

5.1.1. Changing the initialize method in AsciidocLanguageServer

Change the initialize method in AsciidocLanguageServer so support setDocumentSymbolProvider.

@Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        final InitializeResult res = new InitializeResult(new ServerCapabilities());
        res.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full);
        res.getCapabilities().setCompletionProvider(new CompletionOptions());
        res.getCapabilities().setDocumentSymbolProvider(Boolean.TRUE);
        res.getCapabilities().setHoverProvider(Boolean.TRUE);
//      res.getCapabilities().setDefinitionProvider(Boolean.TRUE);
//      res.getCapabilities().setReferencesProvider(Boolean.TRUE);
        // res.getCapabilities().setCodeActionProvider(Boolean.TRUE);

        return CompletableFuture.supplyAsync(() -> res);
    }

Implement this 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();
            // get file if necessary
//          String uri = params.getTextDocument().getUri();

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

                String content = """
                        ![Info Icon]

                        **Important AsciiDoc Elements:**

                        * `image::` - Defines an image element in AsciiDoc files.
                        * `include::` - Includes other AsciiDoc files into 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;
            }

            // If no specific syntax is matched, return null or empty hover
            return null;
        });
    }

5.2. Test the hover functionality with your language server

Restart your running application, position your mouse curser on an element and wait a little while. Do not use the first line, as we do not should hover in the first line.

lsp hover

5.3. Creating the document model

In this exercise, we implement (fake) navigation support for our document.

5.3.1. Changing the initialize method in AsciidocLanguageServer

Change the initialize method in AsciidocLanguageServer so support setDocumentSymbolProvider.

@Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        final InitializeResult res = new InitializeResult(new ServerCapabilities());
        res.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full);
        res.getCapabilities().setCompletionProvider(new CompletionOptions());
        res.getCapabilities().setDocumentSymbolProvider(Boolean.TRUE);
        res.getCapabilities().setHoverProvider(Boolean.TRUE);
        res.getCapabilities().setDefinitionProvider(Boolean.TRUE);
        // res.getCapabilities().setReferencesProvider(Boolean.TRUE);
        // res.getCapabilities().setCodeActionProvider(Boolean.TRUE);

        return CompletableFuture.supplyAsync(() -> res);
    }

Implement the definition method in AsciidocTextDocumentService. We use a few helper methods to identify the word under the cursor.

@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. You can extract a word by splitting the line
        // based on spaces or punctuation.
        String wordUnderCursor = getWordAtPosition(lineContent, character);

        // Now you can resolve this word and create locations for the definition.
        // This logic depends on your specific requirements (e.g., file-based links,
        // symbol resolution).
        List<Location> locations = findDefinitionLocations(wordUnderCursor);

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


    /**
     * Utility method to find the word under the cursor in a given line of text.
     */
    private String getWordAtPosition(String lineContent, int character) {
        // Define word boundaries (spaces or punctuation) to split the line into words.
        // This example assumes simple word boundaries.
        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++;
        }

        // Extract the word
        return lineContent.substring(start, end);
    }

    /**
     * Resolves the definition location(s) for a given word. This is a stub method,
     * and you need to implement your own resolution logic.
     */
    private List<Location> findDefinitionLocations(String word) {
        // This logic would vary based on how you resolve definitions.
        // For example, if the word corresponds to a file or symbol in your system,
        // you would look it up and return a list of Location objects.

        List<Location> locations = new ArrayList<>();
        // Example: You could create a location based on a predefined definition file or
        // symbol.
        Location location = new Location();
        location.setUri("file:///path/to/definitionFile");
        location.setRange(new Range(new Position(5, 0), new Position(5, 10))); // Example range for the definition
        locations.add(location);

        return locations;
    }

6. Server in Action

You should be able to hold the CTRL key and click on a word to navigate to a new editor.

6.1. Test the navigation with your language server

Open you document, press CTRL and click on the word.

lsp navigation

6.2. Setting up document validations

You will now add checks to your document. Whenever the document is created or changes the server will analyse this and push changes to the client.

6.2.1. Changing the initialize method in AsciidocLanguageServer

Change the initialize method in AsciidocLanguageServer so support setDocumentSymbolProvider.

@Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        final InitializeResult res = new InitializeResult(new ServerCapabilities());
        res.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full);
        res.getCapabilities().setCompletionProvider(new CompletionOptions());
        res.getCapabilities().setDocumentSymbolProvider(Boolean.TRUE);
        res.getCapabilities().setHoverProvider(Boolean.TRUE);
        res.getCapabilities().setDefinitionProvider(Boolean.TRUE);
        // res.getCapabilities().setReferencesProvider(Boolean.TRUE);
        // res.getCapabilities().setCodeActionProvider(Boolean.TRUE);

        return CompletableFuture.supplyAsync(() -> res);
    }

Create the following method 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 `didChange and `didOpen methods to use this method.

@Override
    public void didOpen(DidOpenTextDocumentParams params) {
        AsciidocDocumentModel model = new AsciidocDocumentModel(params.getTextDocument().getText());
        this.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());
        this.docs.put(params.getTextDocument().getUri(), model);
        CompletableFuture.runAsync(() -> languageServer.client
                .publishDiagnostics(new PublishDiagnosticsParams(params.getTextDocument().getUri(), validate(model))));

    }

6.3. Test the validation with your language server

Open you document, enter a few times the PLACEHOLDER_TEXT. You should see the warnings from your validation.

lsp validation10

6.4. Creating the document model

In this exercise, we implement (fake) navigation support for our document.

6.4.1. Changing the initialize method in AsciidocLanguageServer

Change the initialize method in AsciidocLanguageServer so support setDocumentSymbolProvider.

@Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        final InitializeResult res = new InitializeResult(new ServerCapabilities());
        res.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full);
        res.getCapabilities().setCompletionProvider(new CompletionOptions());
        res.getCapabilities().setDocumentSymbolProvider(Boolean.TRUE);
        res.getCapabilities().setHoverProvider(Boolean.TRUE);
        res.getCapabilities().setDefinitionProvider(Boolean.TRUE);
        // res.getCapabilities().setReferencesProvider(Boolean.TRUE);
        res.getCapabilities().setCodeActionProvider(Boolean.TRUE);

        return CompletableFuture.supplyAsync(() -> res);
    }

Implement the codeAction method in AsciidocTextDocumentService.

    @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);

                // Add to the actions list
                actions.add(Either.forRight(codeAction));
            }
        }

        // Return the actions as a CompletableFuture
        return CompletableFuture.completedFuture(actions);
    }

6.5. Test the code action with your language server

Open you document, press CTRL+1 and see the code action. Use it to replace the text.

lsp code actions

6.6. Creating code lenses model

In this exercise, we implement code lenses support for TODO text in our document.

6.6.1. Add support in the server

Change the initialize method in AsciidocLanguageServer so support setDocumentSymbolProvider.

    @Override
    public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
        final InitializeResult res = new InitializeResult(new ServerCapabilities());
        res.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full);
        res.getCapabilities().setCompletionProvider(new CompletionOptions());
        res.getCapabilities().setDocumentSymbolProvider(Boolean.TRUE);
        res.getCapabilities().setHoverProvider(Boolean.TRUE);
        res.getCapabilities().setDefinitionProvider(Boolean.TRUE);
        res.getCapabilities().setCodeActionProvider(Boolean.TRUE);
        res.getCapabilities().setCodeLensProvider(new CodeLensOptions(false));
//      res.getCapabilities().setReferencesProvider(Boolean.TRUE);
        return CompletableFuture.supplyAsync(() -> res);
    }

6.6.2. Change data model

Add the following method to AsciidocDocumentModel.

 // Method to get a list of all lines 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 text from your 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)));

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

            return codeLenses;
        });
    }

6.7. Test the code lenses with your language server

Open you document, type TODO the line You should see your code lenses.

lsp code lenses

7. Eclipse Language Server