Implementing a Language Server…How Hard Can It Be?? — Part 2

Nipuna Marcus
Ballerina Swan Lake Tech Blog
5 min readOct 1, 2019

Phew…Finally, after a year I found the time to write part 2 of this series.

Let’s RECAP …

In part 1, I introduced you to what is LSP and Bit about how we did implement the Ballerina Language Server using LSP4J java library. And as I remember I promised to write the second part to explain how we implement our Ballerina LS .. but our LS implementation grew over time (well 1 year) and it is now really complex and will take more than 3 part blog series to cover it and it won’t be much help explaining how we implement Ballerina LS as all the logic are about how to iterate Ballerina AST.

Going Forward …

So in this let get bit practical and implement a simple Language server for a VSCode plugin for an imaginary language called “HelloFalks” that has the file extension “.hello”.

HelloFalks LS Demo

Let’s begin with the LS implementation …

So as I explained in *part 1* of this series LSP4J is the library that we are going to use to implement our HelloLS. So let’s create a java project with the module and class structure as below.

HelloLS project structure

So… let’s get explaining…

if you are familiar with pom.xml you know that this is a maven project. As you can see I’ve defined two java modules inside the hellols project.

language-server

Language server implementation using LSP4J. As you can see there are three classes available under the language-server module. So these three classes are added to implement the three main interfaces provided by LSP4J as to the breakdown given in the LSP specification to support general cases, language features, and workspace management.

  • HelloLanguageServer.java — This class implements the interface available in the LSP4J called LanguageServer which contains the general functionality of the language server such as initializing the language server, shutting down the language server and so on … Also if the LS needs to publish the diagnostics(compilation error and semantic errors) back to the Client (in our case VSCode plugin) LS needs to be client aware. To make the LS client aware we need to implement the LanguageClientAware interface which allows LS to get the language client instance.
  • HelloTextDocumentService.java — This class implements the interface available in the LSP4J called TextDocumentService which contains the language features and the text synchronization endpoints explained in the LSP spec.
  • HelloWorkspaceService.java — This class implements the interface available in the LSP4J called WorkspaceService which contains the workspace features such as workspace symbol and configuration changes…

launcher

This is the launcher to run the language server. This will be called by the VSCode plugin to get the LS running.

  • StdioLauncher.java — This contains the main method that will run and initialize the listener for the LS that we implement.

Let’s get implementing …

You can find the whole implementation of the HelloLS in the following GitHub repo.

https://github.com/NipunaMarcus/hellols

Moving forward I will explain important bits of code and for the rest, I’ve already added comments within the code.

Let’s look at the parent pom.xml of the hellols project.

https://gist.github.com/NipunaMarcus/2984bcbaa99d380587231a6ddfa30b6e

If you look at dependencies you can see we have added lsp4j as a dependency…

<dependency>
<groupId>org.eclipse.lsp4j</groupId>
<artifactId>org.eclipse.lsp4j</artifactId>
<version>${lsp4j.version}</version>
</dependency>

Version is added as a property in the pom.

<lsp4j.version>0.4.1</lsp4j.version>

So this is what we gonna use to implement our LS as I explained in part 1. So let go to the language-server module and start the implementation.

So first let’s implement the HelloLanguageServer.java as it is the entry point to the LS.

https://gist.github.com/NipunaMarcus/a8e56503a28fc78fa4b3f72a3b2add82

This class is implementing both “LanguageServer” and “LanguageClientAware” interfaces provided by LSP4J. From those interfaces, we get to override methods mentioned in the “General” section of the LSP. I will explain the main parts of LS implementation as other parts are mainly explained in the LSP and in HelloLS implementation using comments.

So if you have read the LSP specification you know the “initialize” is the entry method which LS and the LS Client initialize the connection and let the client know what capabilities(such as auto-completion, formatting, find all references) that the LS supports. You can find more details about this here.

So if you look in the “initialize” method implementation you can see we have added completion as the only capability supported by HelloLS.

Then let’s look at how we have set the two main services which provide language features and workspace manager functionality. We have override “getTextDocumentService()” and “getWorkspaceService()”. These two return instances of our HelloTextDocumentService class And HelloWorkspaceService class.

So let’s look into HelloTextDocumentService implementation.

https://gist.github.com/NipunaMarcus/7063183b84d15366500172fc21a58d09

If you have a look at the override methods you can see that implementing TextDocumentService class from LSP4J given us interfaces for all the language features except for workspace management. As I only implementing a sample completion I have implemented only the completion method keeping other methods returning empty.

So if you look at the implementation I’m just creating a CompletionItem and filling it with what type of completion item is this and what is the text to be inserted and description and label of the completion item.

If you have a language AST or Source processor(string processor), this is the place where you can put your logic to filter out what to be provided as completion based on the line, column and the file which are provided as a parameter to the completion() method as CompletionParams.

I implemented the WorkspaceService from LSP4J in HelloWorspaceService but did not implement any Workspace related features as we are doing a simple LS implementation here. I will implement it in a future series where I simply walk you through ANTLR and implementing our HelloFalks language parser.

Now as we reviewed the LS implementation, let’s Look at the Launcher implementation.

https://gist.github.com/NipunaMarcus/40ce9f510c74cd08511c43db27d7df9d

If you look in the dependencies you can see I have added language server core as a dependency.

<dependency>
<groupId>org.hello.ls</groupId>
<artifactId>langserver-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

That because as you can see in the build plugin section I’m creating an Uber jar (AKA Fat jar …) using Shade plugin. This will pack the language server and the launcher together into one jar and will set the jar entry point to the StdioLauncher class. So the launcher implementation is simply like this.

https://gist.github.com/NipunaMarcus/1d4e61f8cc0fb2bd83c74751b9cc5df4

I have explained what each code line do using comments. What this does is create a listener to JsonRPC to service HelloLanguageServer and connect it to System standard input-output channel. All the functionality need to start the Listener is given in JsonRPC framework available in the LSP4J lib.

So now you can build this project using

mvn clean install

and get the hellols/launcher/target/launcher.jar as this is the jar that you need to pack with the VSCode plugin that we gonna create in the next part (part 3).

See you in the next part…

--

--

Nipuna Marcus
Ballerina Swan Lake Tech Blog

Software Engineer at Zuhlke, Ex Tech Lead at WSO2, Author of “Language Server Protocol and Implementation”