This tutorial describes how to implement a language server with LSP4J and how to integrate it into the Eclipse IDE with LSP4E.
1. Language servers and editors
The Language Server Protocol (LSP) defines how an editor talks to a language server. The language server analyzes the documents and answers requests like code completion, hover, go to definition and diagnostics. The editor shows the results. One language server can therefore serve many editors, for example the Eclipse IDE, VS Code, IntelliJ, Vim and Emacs.
The protocol is based on JSON-RPC 2.0. The client sends the content of the opened documents and requests, the server sends responses and notifications, for example the problems it found in a document.
Eclipse IDE (client) language server
| initialize -------------------------> |
| <------------------------ capabilities |
| textDocument/didOpen ---------------> |
| <--------- textDocument/publishDiagnostics
| textDocument/completion ------------> |
| <-------------------- completion items |
During the initialize request, the server tells the client which features it supports, its capabilities.
The client only sends requests for these features.
1.1. LSP4J and LSP4E
The Eclipse project LSP4J provides the Java binding for the protocol. With LSP4J, you implement a server or a client with Java interfaces and objects; LSP4J handles the JSON messages.
The Eclipse project LSP4E connects language servers to the Eclipse IDE. It uses the generic editor and contributes completion, hover, outline, hyperlinks, markers, quick fixes and code minings for every language server registered for a content type. You only register the language server and tell LSP4E for which content type to use it.
Language servers usually run as a separate process, written in any programming language.
LSP4E starts such a server via its ProcessStreamConnectionProvider.
For learning purposes, the server in the following exercises is written in Java with LSP4J and runs inside the Eclipse IDE.
2. Exercise: Add LSP4J and LSP4E to your target platform
In the following exercises, you implement a language server for AsciiDoc files and connect it to the generic editor of the Eclipse IDE. The server supports code completion, an outline, hover, navigation, validation, quick fixes and code lenses for AsciiDoc section titles, anchors and cross references.
The language server and its client need LSP4J and LSP4E, which the release repository of the Eclipse IDE contains. If your target definition file already uses this repository, add the three units to its location. Otherwise, add the following location.
<location includeAllPlatforms="false" includeConfigurePhase="true" includeMode="planner" includeSource="true" type="InstallableUnit">
<repository location="https://download.eclipse.org/releases/2026-09"/>
<unit id="org.eclipse.lsp4j" version="0.0.0"/>
<unit id="org.eclipse.lsp4j.jsonrpc" version="0.0.0"/>
<unit id="org.eclipse.lsp4e" version="0.0.0"/>
</location>
Save the file and click the Reload Target Platform link in the target definition editor.
| If you develop against the running Eclipse IDE instead of a target definition, nothing needs to be done: the Eclipse IDE for RCP and RAP Developers already contains LSP4J and LSP4E. |
3. Exercise: Implement the AsciiDoc language server
In this exercise, you implement a language server which keeps track of the open AsciiDoc documents and offers code completion for a few AsciiDoc snippets.
3.1. Create the plug-in
Create a simple plug-in project named com.vogella.lsp.asciidoc.server.
Add the following plug-ins to the Required Plug-ins on the Dependencies tab of its MANIFEST.MF file.
-
org.eclipse.lsp4j
-
org.eclipse.lsp4j.jsonrpc
They provide the LanguageServer interface, the parameter and result types of the protocol and the Launcher which connects a server to its client.
On the Runtime tab, export the com.vogella.lsp.asciidoc.server package, so that the client plug-in can start the server.
The resulting MANIFEST.MF file looks similar to the following.
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: AsciiDoc Language Server
Bundle-SymbolicName: com.vogella.lsp.asciidoc.server
Bundle-Version: 1.0.0.qualifier
Automatic-Module-Name: com.vogella.lsp.asciidoc.server
Bundle-RequiredExecutionEnvironment: JavaSE-25
Require-Bundle: org.eclipse.lsp4j,
org.eclipse.lsp4j.jsonrpc
Export-Package: com.vogella.lsp.asciidoc.server
3.2. Create the document model
The server parses each document into a small model.
It knows the section titles (== Title), the anchors ([[id]] or [#id]) and the cross references (<<id>>) of the document.
Create the following class.
package com.vogella.lsp.asciidoc.server;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses the parts of an AsciiDoc document the language server works with:
* section titles, anchors and cross references. Lines in listing, literal,
* passthrough and comment blocks, and comment lines, are skipped.
*/
public class AsciidocDocument {
public record Section(int level, String title, int line) {
}
public record Reference(String id, int line, int start, int end) {
}
private static final Pattern SECTION = Pattern.compile("^(={1,6})\\s+(.+)$");
// [[id]] or [#id], where a dot after [#id starts a role as in [#id.role]
private static final Pattern ANCHOR = Pattern.compile("^\\[(?:\\[([\\w.-]+)|#([\\w-]+))");
// skips escaped \<<id>> and passthrough +<<id>>+ references, which are text
private static final Pattern REFERENCE = Pattern.compile("(?<![\\\\+])<<([\\w.-]+)(?:,[^>]*)?>>");
// listing, literal, passthrough and comment blocks, whose content is not parsed
private static final Pattern DELIMITER = Pattern.compile("^(-{4,}|\\.{4,}|\\+{4,}|/{4,})$");
private final List<String> lines;
private final List<Section> sections = new ArrayList<>();
private final Map<String, Integer> anchors = new LinkedHashMap<>();
private final List<Reference> references = new ArrayList<>();
public AsciidocDocument(String text) {
lines = text.lines().toList();
String block = null;
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (block != null) {
// the block ends with the same delimiter it started with
if (line.equals(block)) {
block = null;
}
continue;
}
if (DELIMITER.matcher(line).matches()) {
block = line;
continue;
}
if (line.startsWith("//")) {
continue;
}
Matcher section = SECTION.matcher(line);
if (section.matches()) {
sections.add(new Section(section.group(1).length() - 1, section.group(2).trim(), i));
}
Matcher anchor = ANCHOR.matcher(line);
if (anchor.find()) {
anchors.put(anchor.group(1) != null ? anchor.group(1) : anchor.group(2), i);
}
Matcher reference = REFERENCE.matcher(line);
while (reference.find()) {
references.add(new Reference(reference.group(1), i, reference.start(), reference.end()));
}
}
}
public List<String> getLines() {
return lines;
}
public String getLine(int line) {
return line >= 0 && line < lines.size() ? lines.get(line) : "";
}
public List<Section> getSections() {
return sections;
}
/** Maps each anchor id to the line which defines it. */
public Map<String, Integer> getAnchors() {
return anchors;
}
public List<Reference> getReferences() {
return references;
}
/** Returns the cross reference at the given position, or null. */
public Reference getReferenceAt(int line, int character) {
return references.stream()
.filter(r -> r.line() == line && r.start() <= character && character < r.end())
.findFirst().orElse(null);
}
/**
* Returns the title of the section the anchor belongs to: the heading right
* after a block anchor, otherwise the section which contains the anchor, or
* null if the anchor is not defined or lies before the first section.
*/
public String getTitleForAnchor(String id) {
Integer anchorLine = anchors.get(id);
if (anchorLine == null) {
return null;
}
Section owner = null;
for (Section section : sections) {
if (section.line() == anchorLine + 1) {
return section.title();
}
if (section.line() <= anchorLine) {
owner = section;
}
}
return owner != null ? owner.title() : null;
}
}
3.3. Implement the language server
The LanguageServer interface defines the lifecycle of the server.
In initialize the server returns its capabilities.
This server receives the full document text on every change and offers code completion.
The client calls connect with a proxy object, which the server uses to send notifications to the client, for example the problems found in a document.
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 AsciidocTextDocumentService textDocumentService = new AsciidocTextDocumentService();
private final AsciidocWorkspaceService 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));
}
/**
* Receives the proxy of the client, which the text document service uses to
* send notifications, for example the problems found in a document
*/
public void connect(LanguageClient client) {
textDocumentService.setClient(client);
}
@Override
public CompletableFuture<Object> shutdown() {
return CompletableFuture.completedFuture(null);
}
@Override
public void exit() {
// a server in its own process calls System.exit(0) here,
// this one runs inside the IDE, so its connection provider stops it
}
@Override
public TextDocumentService getTextDocumentService() {
return textDocumentService;
}
@Override
public WorkspaceService getWorkspaceService() {
return workspaceService;
}
}
The workspace service handles workspace wide events. This server does not need them.
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) {
}
}
The text document service receives the didOpen, didChange and didClose notifications and answers the requests for the open documents.
TextDocumentService has default implementations for all requests, so you implement only the ones your server supports.
The completion proposals are snippets.
$1 and $2 are tab stops, ${1:java} is a tab stop with a default value, and $0 is the final cursor position.
The EMPTY document and the client field are not used yet, so the IDE warns about them.
The later exercises use EMPTY for documents the client has not opened and client to publish problems.
package com.vogella.lsp.asciidoc.server;
import java.util.ArrayList;
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.CompletionItemKind;
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.InsertTextFormat;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService;
public class AsciidocTextDocumentService implements TextDocumentService {
private static final Map<String, String> SNIPPETS = Map.of(
"source", "[source,${1:java}]\n----\n$0\n----",
"image", "image::${1:file.png}[${2:description}]",
"include", "include::${1:file.adoc}[]",
"note", "NOTE: $0");
private final Map<String, AsciidocDocument> documents = new ConcurrentHashMap<>();
// stands in for a document the client did not open, or closed already
private static final AsciidocDocument EMPTY = new AsciidocDocument("");
private LanguageClient client;
public void setClient(LanguageClient client) {
this.client = client;
}
@Override
public void didOpen(DidOpenTextDocumentParams params) {
update(params.getTextDocument().getUri(), params.getTextDocument().getText());
}
@Override
public void didChange(DidChangeTextDocumentParams params) {
// full synchronization, so the last change holds the complete text
List<TextDocumentContentChangeEvent> changes = params.getContentChanges();
if (!changes.isEmpty()) {
update(params.getTextDocument().getUri(), changes.get(changes.size() - 1).getText());
}
}
@Override
public void didClose(DidCloseTextDocumentParams params) {
documents.remove(params.getTextDocument().getUri());
}
@Override
public void didSave(DidSaveTextDocumentParams params) {
}
private void update(String uri, String text) {
documents.put(uri, new AsciidocDocument(text));
}
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params) {
List<CompletionItem> items = new ArrayList<>();
SNIPPETS.forEach((label, snippet) -> {
CompletionItem item = new CompletionItem(label);
item.setKind(CompletionItemKind.Snippet);
item.setInsertText(snippet);
item.setInsertTextFormat(InsertTextFormat.Snippet);
items.add(item);
});
return CompletableFuture.completedFuture(Either.forLeft(items));
}
}
4. Exercise: Connect the language server to the Eclipse IDE
In this exercise, you register the language server with LSP4E for AsciiDoc files.
4.1. Create the plug-in
Create a simple plug-in project named com.vogella.lsp.asciidoc.client.
Add the following plug-ins to its dependencies:
-
org.eclipse.lsp4j
-
org.eclipse.lsp4j.jsonrpc
-
org.eclipse.lsp4e
-
org.eclipse.ui.genericeditor
-
com.vogella.lsp.asciidoc.server
Select the This plug-in is a singleton option on the Overview tab, as the plug-in contributes extensions.
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: AsciiDoc Language Client
Bundle-SymbolicName: com.vogella.lsp.asciidoc.client;singleton:=true
Bundle-Version: 1.0.0.qualifier
Automatic-Module-Name: com.vogella.lsp.asciidoc.client
Bundle-RequiredExecutionEnvironment: JavaSE-25
Require-Bundle: org.eclipse.lsp4j,
org.eclipse.lsp4j.jsonrpc,
org.eclipse.lsp4e,
org.eclipse.ui.genericeditor,
com.vogella.lsp.asciidoc.server
4.2. Start the server
LSP4E talks to a language server via an input and an output stream, which a StreamConnectionProvider provides.
The following connection provider starts the server inside the IDE and connects it with piped streams.
package com.vogella.lsp.asciidoc.client;
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 com.vogella.lsp.asciidoc.server.AsciidocLanguageServer;
/**
* Starts the language server inside the IDE and connects it to LSP4E with two
* pairs of piped streams.
*/
public class AsciidocConnectionProvider implements StreamConnectionProvider {
private InputStream clientInput;
private OutputStream clientOutput;
private Future<Void> listening;
@Override
public void start() throws IOException {
PipedInputStream serverInput = new PipedInputStream();
PipedOutputStream serverOutput = new PipedOutputStream();
clientInput = new PipedInputStream(serverOutput);
clientOutput = new PipedOutputStream(serverInput);
AsciidocLanguageServer server = new AsciidocLanguageServer(); (1)
Launcher<LanguageClient> launcher = LSPLauncher.createServerLauncher(server, serverInput, serverOutput); (2)
server.connect(launcher.getRemoteProxy()); (3)
listening = launcher.startListening(); (4)
}
@Override
public InputStream getInputStream() {
return clientInput;
}
@Override
public OutputStream getOutputStream() {
return clientOutput;
}
@Override
public InputStream getErrorStream() {
return null;
}
@Override
public void stop() {
// stop listening before the streams close, otherwise LSP4J logs the
// closed stream as an error
if (listening != null) {
listening.cancel(true);
}
try {
if (clientInput != null) {
clientInput.close();
}
if (clientOutput != null) {
clientOutput.close();
}
} catch (IOException e) {
// the streams are gone either way
}
}
}
| 1 | Each connection gets a new server instance. |
| 2 | The Launcher reads the requests from the input stream, calls the server and writes the responses to the output stream. |
| 3 | The remote proxy of the launcher is the client as seen from the server. |
| 4 | Start reading requests in a background thread. |
4.3. Register the server
Create the plugin.xml file with the following content.
It defines a content type for files with the adoc extension, opens these files in the generic editor and registers the language server for the content type.
<?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-extensions="adoc"
id="com.vogella.lsp.asciidoc.content"
name="AsciiDoc"
priority="normal">
</content-type>
</extension>
<extension
point="org.eclipse.ui.editors">
<editorContentTypeBinding
contentTypeId="com.vogella.lsp.asciidoc.content"
editorId="org.eclipse.ui.genericeditor.GenericEditor">
</editorContentTypeBinding>
</extension>
<extension
point="org.eclipse.lsp4e.languageServer">
<server
class="com.vogella.lsp.asciidoc.client.AsciidocConnectionProvider"
id="com.vogella.lsp.asciidoc.server"
label="AsciiDoc Language Server">
</server>
<contentTypeMapping
contentType="com.vogella.lsp.asciidoc.content"
id="com.vogella.lsp.asciidoc.server">
</contentTypeMapping>
</extension>
</plugin>
4.4. Validate
Start a runtime Eclipse IDE which includes both plug-ins, for example via the context menu of the com.vogella.lsp.asciidoc.client project and .
In the runtime IDE, create a project with a file named example.adoc and the following content.
= Eclipse plug-in development
[[installation]]
== Installation
Download the Eclipse IDE for RCP and RAP Developers.
[[first-plugin]]
== Your first plug-in
Create a plug-in as described in <<installation>>.
=== Run the plug-in
Start it as described in <<first-plugin>>.
image::plugin-wizard.png[New plug-in wizard]
[[deployment]]
== Deployment
Export the plug-in, see <<deploy>>.
The file opens in the generic editor. Press Ctrl+Space on an empty line to see the proposals of your server. Select one, for example source, and use Tab to jump between its tab stops.
| The screenshots of these exercises show the finished server, so they also contain the code lenses and the warning of the later exercises. |

| LSP4E can log the messages between the IDE and a language server. Enable it under for your server, the messages are written to the Console view of the runtime IDE. |
5. Exercise: Show the sections in the outline
In this exercise, your server provides the section titles as document symbols. LSP4E shows them in the Outline view.
5.1. Implement the document symbols
Add the capability to the initialize method of AsciidocLanguageServer.
capabilities.setDocumentSymbolProvider(true);
Add the following methods to AsciidocTextDocumentService.
A section becomes the child of the closest previous section with a lower level, so === Run the plug-in is a child of == Your first plug-in.
@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
DocumentSymbolParams params) {
AsciidocDocument document = documents.getOrDefault(params.getTextDocument().getUri(), EMPTY);
List<DocumentSymbol> roots = new ArrayList<>();
// the innermost open section per level, a new section becomes the child of
// the closest one with a lower level
DocumentSymbol[] parents = new DocumentSymbol[7];
List<Section> sections = document.getSections();
for (int i = 0; i < sections.size(); i++) {
Section section = sections.get(i);
// a section ends before the next section with the same or a lower level,
// so its range contains the ranges of its children
int end = document.getLines().size() - 1;
for (int next = i + 1; next < sections.size(); next++) {
if (sections.get(next).level() <= section.level()) {
end = sections.get(next).line() - 1;
break;
}
}
Range range = new Range(new Position(section.line(), 0), new Position(end, document.getLine(end).length()));
DocumentSymbol symbol = new DocumentSymbol(section.title(), SymbolKind.String, range,
lineRange(document, section.line()));
symbol.setChildren(new ArrayList<>());
DocumentSymbol parent = null;
for (int level = section.level() - 1; level >= 0 && parent == null; level--) {
parent = parents[level];
}
if (parent == null) {
roots.add(symbol);
} else {
parent.getChildren().add(symbol);
}
parents[section.level()] = symbol;
for (int level = section.level() + 1; level < parents.length; level++) {
parents[level] = null;
}
}
return CompletableFuture.completedFuture(
roots.stream().map(Either::<SymbolInformation, DocumentSymbol>forRight).toList());
}
private static Range lineRange(AsciidocDocument document, int line) {
return new Range(new Position(line, 0), new Position(line, document.getLine(line).length()));
}
Use (Ctrl+Shift+O) to add the missing imports, here and in the following exercises.
Section is the nested record com.vogella.lsp.asciidoc.server.AsciidocDocument.Section.
5.2. Validate
Restart the runtime IDE, open example.adoc and the Outline view.
Select a section in the outline to jump to it in the editor.

6. Exercise: Explain cross references and macros on hover
In this exercise, your server shows the title of the target section when the mouse rests over a cross reference.
On the image:: and include:: macros, it shows a short explanation.
6.1. Implement the hover
Add the capability to the initialize method of AsciidocLanguageServer.
capabilities.setHoverProvider(true);
Add the following constant and method to AsciidocTextDocumentService.
The hover text uses Markdown, which LSP4E renders.
private static final Map<String, String> MACROS = Map.of(
"image::", "Shows the image file in its own block, for example `image::wizard.png[New wizard]`.",
"include::", "Inserts the content of another file at this place, for example `include::exercise.adoc[]`.");
@Override
public CompletableFuture<Hover> hover(HoverParams params) {
AsciidocDocument document = documents.getOrDefault(params.getTextDocument().getUri(), EMPTY);
Position position = params.getPosition();
Reference reference = document.getReferenceAt(position.getLine(), position.getCharacter());
String text = null;
if (reference != null) {
String title = document.getTitleForAnchor(reference.id());
if (!document.getAnchors().containsKey(reference.id())) {
text = "Unknown anchor `" + reference.id() + "`";
} else if (title == null) {
text = "Links to the anchor `" + reference.id() + "`";
} else {
text = "Links to the section **" + title + "**";
}
} else {
String line = document.getLine(position.getLine());
text = MACROS.entrySet().stream()
.filter(macro -> line.startsWith(macro.getKey())
&& position.getCharacter() < macro.getKey().length())
.map(Map.Entry::getValue)
.findFirst().orElse(null);
}
if (text == null) {
return CompletableFuture.completedFuture(null);
}
return CompletableFuture.completedFuture(new Hover(new MarkupContent(MarkupKind.MARKDOWN, text)));
}
6.2. Validate
Restart the runtime IDE and open example.adoc.
Rest the mouse over <<installation>> and over image::.

7. Exercise: Navigate to and complete cross references
In this exercise, you can jump from a cross reference to its anchor and get the existing anchors proposed after typing <<.
7.1. Implement go to definition
Add the capability to the initialize method of AsciidocLanguageServer.
capabilities.setDefinitionProvider(true);
Add the following method to AsciidocTextDocumentService.
It returns the location of the anchor the cross reference at the cursor points to.
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(
DefinitionParams params) {
String uri = params.getTextDocument().getUri();
AsciidocDocument document = documents.getOrDefault(uri, EMPTY);
Position position = params.getPosition();
Reference reference = document.getReferenceAt(position.getLine(), position.getCharacter());
List<Location> locations = new ArrayList<>();
if (reference != null && document.getAnchors().containsKey(reference.id())) {
int line = document.getAnchors().get(reference.id());
locations.add(new Location(uri, lineRange(document, line)));
}
return CompletableFuture.completedFuture(Either.forLeft(locations));
}
7.2. Complete the anchors
Replace the completion method of AsciidocTextDocumentService with the following.
If the text before the cursor is a cross reference which is being typed, such as << or <<ins, it proposes the anchors of the document and shows the title of their section.
Otherwise, it proposes the snippets as before.
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params) {
AsciidocDocument document = documents.getOrDefault(params.getTextDocument().getUri(), EMPTY);
Position position = params.getPosition();
String line = document.getLine(position.getLine());
String before = line.substring(0, Math.min(position.getCharacter(), line.length()));
List<CompletionItem> items = new ArrayList<>();
// a cross reference which is being typed, such as <<ins
Matcher reference = Pattern.compile("<<([\\w.-]*)$").matcher(before);
if (reference.find()) {
// replace the whole id, also inside an existing reference such as <<ins|>>
Matcher rest = Pattern.compile("^[\\w.-]*").matcher(line.substring(before.length()));
int end = before.length() + (rest.find() ? rest.end() : 0);
Range range = new Range(new Position(position.getLine(), reference.start(1)),
new Position(position.getLine(), end));
// close the reference unless it is closed already or has a label
String closing = line.startsWith(">>", end) || line.startsWith(",", end) ? "" : ">>";
for (String id : document.getAnchors().keySet()) {
CompletionItem item = new CompletionItem(id);
item.setKind(CompletionItemKind.Reference);
CompletionItemLabelDetails details = new CompletionItemLabelDetails();
details.setDescription(document.getTitleForAnchor(id));
item.setLabelDetails(details);
item.setTextEdit(Either.forLeft(new TextEdit(range, id + closing)));
items.add(item);
}
} else {
SNIPPETS.forEach((label, snippet) -> {
CompletionItem item = new CompletionItem(label);
item.setKind(CompletionItemKind.Snippet);
item.setInsertText(snippet);
item.setInsertTextFormat(InsertTextFormat.Snippet);
items.add(item);
});
}
return CompletableFuture.completedFuture(Either.forLeft(items));
}
7.3. Validate
Restart the runtime IDE and open example.adoc.
Hold Ctrl and click on <<installation>>, or place the cursor on it and press F3.
The editor selects the [[installation]] anchor.
Type << in a new line and press Ctrl+Space to see the anchors.

8. Exercise: Report unknown anchors
In this exercise, your server reports cross references to anchors which do not exist in the document.
8.1. Publish diagnostics
The server sends problems, called diagnostics, to the client with the textDocument/publishDiagnostics notification.
This notification does not need a capability, the server sends it whenever a document changes.
Add the following constant and method to AsciidocTextDocumentService.
The code identifies the problem type, the quick fix of the next exercise uses it.
private static final String UNKNOWN_ANCHOR = "unknown-anchor";
private List<Diagnostic> validate(AsciidocDocument document) {
List<Diagnostic> diagnostics = new ArrayList<>();
for (Reference reference : document.getReferences()) {
if (!document.getAnchors().containsKey(reference.id())) {
Range range = new Range(new Position(reference.line(), reference.start()),
new Position(reference.line(), reference.end()));
Diagnostic diagnostic = new Diagnostic(range, "Unknown anchor '" + reference.id() + "'",
DiagnosticSeverity.Warning, "asciidoc", UNKNOWN_ANCHOR);
diagnostics.add(diagnostic);
}
}
return diagnostics;
}
Change the update method so that it publishes the diagnostics after each change.
An empty list removes the previous problems of the document.
private void update(String uri, String text) {
AsciidocDocument document = new AsciidocDocument(text);
documents.put(uri, document);
client.publishDiagnostics(new PublishDiagnosticsParams(uri, validate(document)));
}
Clear the problems of a document when the client closes it.
@Override
public void didClose(DidCloseTextDocumentParams params) {
String uri = params.getTextDocument().getUri();
documents.remove(uri);
// the problems of a closed document go away with it
client.publishDiagnostics(new PublishDiagnosticsParams(uri, List.of()));
}
8.2. Validate
Restart the runtime IDE and open example.adoc.
The cross reference <<deploy>> is marked as warning in the editor and listed in the Problems view.
The Outline view marks the sections which contain it, because each section symbol covers the lines up to the next section.
Change it to <<deployment>> and the warning disappears.
Change it back to <<deploy>>, because the next exercise fixes it with a quick fix.

9. Exercise: Offer quick fixes for unknown anchors
In this exercise, your server offers a quick fix, called code action, which replaces an unknown anchor with an existing one.
9.1. Implement the code actions
Add the capability to the initialize method of AsciidocLanguageServer.
capabilities.setCodeActionProvider(true);
Add the following method to AsciidocTextDocumentService.
The client sends the diagnostics at the cursor position.
For each unknown anchor, the server offers one code action per existing anchor.
The code action contains the text edit, so the client applies it without asking the server again.
@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
String uri = params.getTextDocument().getUri();
AsciidocDocument document = documents.getOrDefault(uri, EMPTY);
List<Either<Command, CodeAction>> actions = new ArrayList<>();
for (Diagnostic diagnostic : params.getContext().getDiagnostics()) {
if (diagnostic.getCode() == null || !UNKNOWN_ANCHOR.equals(diagnostic.getCode().getLeft())) {
continue;
}
// the document may have changed since it was validated, so look the
// reference up in the current text
Position start = diagnostic.getRange().getStart();
Reference reference = document.getReferenceAt(start.getLine(), start.getCharacter());
if (reference == null || document.getAnchors().containsKey(reference.id())) {
continue;
}
Range range = new Range(new Position(reference.line(), reference.start()),
new Position(reference.line(), reference.end()));
// keep the text of a labelled reference such as <<deploy,Deployment>>
String text = document.getLine(reference.line()).substring(reference.start(), reference.end());
for (String id : document.getAnchors().keySet()) {
TextEdit edit = new TextEdit(range, text.replaceFirst("<<[\\w.-]+", "<<" + id));
String title = document.getTitleForAnchor(id);
CodeAction action = new CodeAction("Link to '" + (title != null ? title : id) + "'");
action.setKind(CodeActionKind.QuickFix);
action.setDiagnostics(List.of(diagnostic));
action.setEdit(new WorkspaceEdit(Map.of(uri, List.of(edit))));
actions.add(Either.forRight(action));
}
}
return CompletableFuture.completedFuture(actions);
}
9.2. Validate
Restart the runtime IDE and open example.adoc.
Place the cursor on <<deploy>> and press Ctrl+1.
Select Link to 'Deployment' to fix the cross reference.

10. Exercise: Count the references with code lenses
In this exercise, your server shows above each anchor how often the document references it. A code lens is a text which the editor shows above a line. LSP4E shows code lenses as code minings.
10.1. Implement the code lenses
Add the capability to the initialize method of AsciidocLanguageServer.
false tells the client that the server sends complete code lenses, so the client does not need to resolve them later.
capabilities.setCodeLensProvider(new CodeLensOptions(false));
Add the following method to AsciidocTextDocumentService.
@Override
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) {
AsciidocDocument document = documents.getOrDefault(params.getTextDocument().getUri(), EMPTY);
List<CodeLens> lenses = new ArrayList<>();
document.getAnchors().forEach((id, line) -> {
long count = document.getReferences().stream().filter(r -> r.id().equals(id)).count();
Command command = new Command(count == 1 ? "1 reference" : count + " references", "");
lenses.add(new CodeLens(lineRange(document, line), command, null));
});
return CompletableFuture.completedFuture(lenses);
}
The title of the command is the text of the code lens.
The command id is empty, so a click on the code lens does nothing.
A server which lists its command ids in the ExecuteCommandOptions capability runs the command in WorkspaceService.executeCommand when the user clicks on the code lens.
10.2. Validate
Restart the runtime IDE and open example.adoc.
The number of references is shown above each anchor and updates while you type.

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