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.

Block Language Creation Wizard: Dialog 1
Block Language Creation Wizard: Dialog 1

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:

Block Language Creation Wizard: Dialog 2
Block Language Creation Wizard: Dialog 2

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.

comments powered by Disqus