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.
-
Right-click on the
com.vogella.lsp.asciidoc.clientproject and select . -
Create a new file named
languageserver_example.txt. -
Open this file with the generic editor.
-
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.

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.

3.10. Implementing navigation
In this exercise, your server implements (fake) navigation support for the document.
3.10.1. Navigation support in the server
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.

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.

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.

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.

4. Eclipse Language Server resources
4.1. vogella Java example code
If you need more assistance we offer Online Training and Onsite training as well as consulting