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.
-
Right-click on the
com.vogella.lsp.asciidoc.client
project and select Run as > Eclipse Application. -
Create a new file named
languageserver_example.txt
. -
Open this file using the Generic Editor.
-
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.

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.

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.

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.

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.

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.

7. Eclipse Language Server
https://github.com/mickaelistria/eclipse-languageserver-demo/blob/master/README.md https://github.com/LucasBullen/LSP4J_Tutorial/blob/master/Exercises/1/1-README.md Le LanguageServer de Chamrousse
7.1. vogella Java example code
endifdef::printrun[]
If you need more assistance we offer Online Training and Onsite training as well as consulting