Your First Volar Language Server
A simple guide to creating your first Volar language server.
This page is a work in progress. Interested in contributing some documentation to it, or want to improve it? Edit this page on GitHub
In this guide, you will learn create a simple Volar language server and VS Code client. To keep things simple, the language it’ll support will look suspiciously like HTML, albeit with one twist: Only one <style>
tag will be allowed. The language will be called HTML1.
This guide assumes that you have a basic understanding of TypeScript and Node.js, and also of what a language server is. If you’re not familiar with language servers, you might want to read the “What is the Language Server Protocol?” section of the Language Server Protocol homepage before continuing.
💡 Interested in seeing the final product? Check out the starter project on GitHub.
Prerequisites
- Visual Studio Code version 1.55 or higher.
- Node.js version 14 or higher.
- Basic knowledge of JavaScript and Node.
- (Optional) Install the Volar Labs extension for VS Code.
Getting Started
First create a new project directory and initialize a new Node.js project:
In order to be able to have both the language server and the VS Code client in the same repository, a common pattern for language servers, create a new packages
directory that will contain both parts, this is effectively known as a monorepo.
Installing and configuring TypeScript
Later in this guide, TypeScript will be used to both write the language server and the client, and to compile the TypeScript code to JavaScript, so we’ll install it now.
Additionally, we’ll create a base TypeScript configuration file that will be shared between the server and the client. This file will be used to set up the TypeScript compiler options that are common to both parts of the project.
Defining VS Code tasks
Finally, create a .vscode
directory in the root of the project, and add a launch.json
file to it. This file defines a task that can be used later to easily start your extension locally.
With all of this done, your project structure should look like this:
Directory.vscode/
- launch.json
Directorypackages/
Directoryclient/
- package.json
Directoryserver/
- package.json
- package.json
- tsconfig.base.json
The client
The client will be a VS Code extension, which necessitate a package.json
with a few specific fields. In the packages/client
folder, edit the package.json
file to look like this:
Additionally, a few dependencies from Volar and VS Code are needed. Make sure you are in the proper folder (packages/client
) and install the dependencies by running the following commands:
Next, create a tsconfig.json
file in the packages/client
folder to configure TypeScript, which will be used as a build tool in this guide. This file will tell TypeScript to output the compiled JavaScript files to a dist
directory.
As previously mentioned, this file will extend the base configuration in the tsconfig.base.json
file that was created earlier at the root of the project.
Next, create a new src
directory in the packages/client
folder, and add a new extension.ts
file to it. This file will contain the code for the client extension.
The vast majority of this code is boilerplate to start the language server, so although it may look like a lot of not very interesting code, it’s crucial to the functioning of the extension.
With this done, the client is now complete. Since the vast majority of the actual logic will be in the language server, it’s quite likely that you won’t need to revisit the client again much, even as your project grows in complexity.
Your project structure should now look like this:
Directory.vscode/
- launch.json
Directorypackages/
Directoryclient/
Directorysrc/
- extension.ts
- package.json
- tsconfig.json
Directoryserver/
- package.json
- package.json
- tsconfig.base.json
If you’re curious, you can actually start the client right now by running the Launch Extension
task in VS Code.
Since the server doesn’t exist yet, the client will fail to start and immediately crash, but, hey, it’s a start!
The server
Now onto the meat of the project: the language server. Most of this section will be spent on hooking things up more so than writing actual logic. The actual logic will be quite simple, as the language we’re supporting is quite simple.
First, we’ll be installing a few dependencies, just like we did for the client. Make sure you’re in the proper folder (packages/server
) and run the following commands:
Again, just like with the client, create a tsconfig.json
file in the packages/server
folder to configure TypeScript. This file will tell TypeScript to output the compiled JavaScript files to a dist
directory.
Since language servers are executable programs, we need to create an entry point for the server. Create a new bin
directory in the packages/server
folder, and add a file called html1-language-server.js
to it.
This file will contain a very small snippet of code that will start the language server.
We’ll also update our package.json
file in the packages/server
folder to add this file as an executable the package provides.
Now, people who install your package will be able to run the server using the html1-language-server
command. As a bonus, they can also check the version of the server by running html1-language-server --version
.
However, we still have no server to actually start! Create a new src
directory in the packages/server
folder, and create the three following files:
index.ts
: The entry point for the server, this file will create, configure and start the server.html1-service.ts
: The service that will handle the HTML1 language.languagePlugin.ts
: The language definition for the HTML1 language.
At this point, your entire project structure should look like this:
Directory.vscode/
- launch.json
Directorypackages/
Directoryclient/
Directorysrc/
- extension.ts
- package.json
- tsconfig.json
Directoryserver/
Directorybin/
- html1-language-server.js
Directorysrc/
- index.ts
- html1-service.ts
- languagePlugin.ts
- package.json
- tsconfig.json
- package.json
- tsconfig.base.json
Server configuration
We’ll first jump into the index.ts
file. This file will create the server, configuring all the languages supported and the different services that will be used and then start the server.
To go over this file a little bit, we first create a connection (createConnection
), then create the server itself. We then listen to the connection, waiting for the client to connect.
Once the connection is done, we initialize the server, including its language and service plugins. In our case, we don’t have any language plugins yet, but we do have two service plugins: one for HTML and one for CSS, both languages that our HTML1 language will support.
Defining the language
As can be expected from a language server framework, we need to define the language we’re supporting. In our case, this is HTML1. As a reminder, HTML1 is HTML with the restriction that only one <style>
tag is allowed (also, it doesn’t have script
tags, but we’ll ignore that for now).
A language definition, as its core, is simply a JavaScript object with two methods: one to create a virtual code and one to update it. A virtual code (VirtualCode
) is an object that represents a file in the language server, and is used to store information about the file, such as its content, its current version and any other metadata that might be useful.
Every file that the language server will handle will have a corresponding VirtualCode
object. The language server will create these objects when a file is opened, and update them when the file is changed.
While VirtualCode
objects can be, well, just that, objects, it’s often useful to create a JavaScript class to represent them. This class can then contain methods and properties that are useful for handling the file and its associated data.
The (second-to) last piece of the puzzle is the onSnapshotUpdated
method. This method will contain all the logic that will be executed when the file is updated. In our case, we’ll use this method to parse the file and understand its structure and content.
You might have noticed that the Html1Code
class has an embeddedCodes
property. This property is used to store any embedded VirtualCode(s) for the embedded languages that the file might contain.
In our case, HTML1 will contain two embedded languages: HTML and CSS. To find where these embedded languages are, we’ll need to parse the file and look for the <style>
tag. When we find it, we’ll create a new VirtualCode
object for the HTML and CSS code and add it to the embeddedCodes
array.
We’ll be using the vscode-html-languageservice
package to parse the HTML and CSS code, this package is also used by the HTML service that we previously added to the server.