LSP and Xtext Tutorial: Creating the language
This post is a continuation of my LSP and Xtext Tutorial. In this post I present how to create the language to have all the necessary configurations to work. The full code for this example can be found here.
Create the project
The project is created as a standard Xtext project. The first part of the wizard is configured as usual. We use as an example the block project the I have used in a previous tip. Below is the first dialog for the wizard.
The second dialog deviates a little bit from the first one.
- I unchecked the “Eclipse plug-in” checkbox
- Checked the “Generic IDE support”
- Set Preferred Build System to Maven
- Build Language Server to “Fat Jar”
- Source Layout to Plain
The resulting configuration appears below:
Define the Grammar
I use the same grammar as was used in a previous tip. I include it below to make the tutorial easier to follow:
grammar com.idiomaticsoft.dsl.block.Block with org.eclipse.xtext.common.Terminals
generate block "http://www.idiomaticsoft.com/dsl/block/Block"
Model:
blocks+=Block*;
Block:
'block' name=ID '{' (members+=Member)* '}';
Member:
Block | Field | Alias;
Field:
'field' name=ID;
Alias:
'alias' name=ID 'aliases' alias=[Member|MemberFQN];
MemberFQN:
ID ("." ID)*;
Add a Simple Validator
To showcase that the language actually works I included a simple validation in the language.The validator checks that the name of the blocks starts with an uppercase letter.
package com.idiomaticsoft.dsl.block.validation;
import org.eclipse.xtext.validation.Check;
import com.idiomaticsoft.dsl.block.block.BlockPackage;
import com.idiomaticsoft.dsl.block.block.Member;
/**
* This class contains custom validation rules.
*
* See
* https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#validation
*/
public class BlockValidator extends AbstractBlockValidator {
public static final String INVALID_NAME = "invalidName";
@Check
public void checkField(Member member) {
if (!Character.isUpperCase(member.getName().charAt(0))) {
warning("Name should start with a capital", BlockPackage.Literals.MEMBER__NAME, INVALID_NAME);
}
}
}
Implement an LSP server launcher
The final step is to implement an LSP server launcher. The code of the server was taken from one of the examples in the documentation. This code goes into the com.idiomaticsoft.dsl.block.ide
project, which contains the necessary dependencies to launch a language server. The code just connects the input and output of the server to a socket listening on port 5007 of localhost.
package com.idiomaticsoft.dsl.block.ide;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.Channels;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Function;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.xtext.ide.server.LanguageServerImpl;
import org.eclipse.xtext.ide.server.ServerModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
/**
* This code was taken from
* https://github.com/itemis/xtext-languageserver-example/blob/master/org.xtext.example.mydsl.ide/src/org/xtext/example/mydsl/ide/RunServer.java
*
*/
public class ServerLauncher {
public static void main(String[] args) throws InterruptedException, IOException {
Injector injector = Guice.createInjector(new ServerModule());
LanguageServerImpl languageServer = injector.getInstance(LanguageServerImpl.class);
Function<MessageConsumer, MessageConsumer> wrapper = consumer -> {
MessageConsumer result = consumer;
return result;
};
Launcher<LanguageClient> launcher = createSocketLauncher(languageServer, LanguageClient.class,
new InetSocketAddress("localhost", 5007), Executors.newCachedThreadPool(), wrapper);
languageServer.connect(launcher.getRemoteProxy());
Future<?> future = launcher.startListening();
while (!future.isDone()) {
Thread.sleep(10_000l);
}
}
static <T> Launcher<T> createSocketLauncher(Object localService, Class<T> remoteInterface,
SocketAddress socketAddress, ExecutorService executorService,
Function<MessageConsumer, MessageConsumer> wrapper) throws IOException {
AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open().bind(socketAddress);
AsynchronousSocketChannel socketChannel;
try {
socketChannel = serverSocket.accept().get();
return Launcher.createIoLauncher(localService, remoteInterface, Channels.newInputStream(socketChannel),
Channels.newOutputStream(socketChannel), executorService, wrapper);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
}
Configure the IDE project to launch the server
The final step is to configure the build to run the JAR that was created in the previous step. To do this, I configure the maven-shade-plugin
to use it as the main class. The resulting XML appears below.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.idiomaticsoft.dsl.block</groupId>
<artifactId>com.idiomaticsoft.dsl.block.parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>com.idiomaticsoft.dsl.block.ide</artifactId>
<packaging>jar</packaging>
<build>
<sourceDirectory>src</sourceDirectory>
<resources>
<resource>
<directory>src</directory>
<excludes>
<exclude>**/*.java</exclude>
<exclude>**/*.xtend</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.eclipse.xtend</groupId>
<artifactId>xtend-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>initialize</phase>
<goals>
<goal>add-source</goal>
<goal>add-resource</goal>
</goals>
<configuration>
<sources>
<source>src-gen</source>
</sources>
<resources>
<resource>
<directory>src-gen</directory>
<excludes>
<exclude>**/*.java</exclude>
<exclude>**/*.g</exclude>
</excludes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.idiomaticsoft.dsl.block.ide.ServerLauncher</mainClass>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>plugin.properties</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer">
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/INDEX.LIST</exclude>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>.options</exclude>
<exclude>.api_description</exclude>
<exclude>*.profile</exclude>
<exclude>*.html</exclude>
<exclude>about.*</exclude>
<exclude>about_files/*</exclude>
<exclude>plugin.xml</exclude>
<exclude>systembundle.properties</exclude>
<exclude>profile.list</exclude>
<exclude>**/*._trace</exclude>
<exclude>**/*.g</exclude>
<exclude>**/*.mwe2</exclude>
<exclude>**/*.xtext</exclude>
</excludes>
</filter>
</filters>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>ls</shadedClassifierName>
<minimizeJar>false</minimizeJar>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>com.idiomaticsoft.dsl.block</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.ide</artifactId>
<version>${xtextVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.xtext</groupId>
<artifactId>org.eclipse.xtext.xbase.ide</artifactId>
<version>${xtextVersion}</version>
</dependency>
</dependencies>
</project>
Conclusion
When mvn clean install
is run, this project creates a fat jar in the target
folder of the com.idiomaticsoft.dsl.block.ide
project. The jar is named com.idiomaticsoft.dsl.block.ide-1.0.0-SNAPSHOT-ls.jar
. The jar is executable and can be run using the command java -jar com.idiomaticsoft.dsl.block.ide-1.0.0-SNAPSHOT-ls.jar
from the target directory.
On launch, the jar opens port 5007 on localhost and any language server client can connect to it. In the next part of this tutorial I present how to do this for a VS Code plugin.
- Previous chapter in the tutorial: LSP and Xtext Tutorial: Introduction
- The next chapter of this tutorial is here: LSP and Xtext Tutorial: Creating the VS Code Client