Icon for gfsd IntelliJ IDEA

Integrating a language server (LSP) in a VS Code extension

Circles with logos of programming languages and text editors all connected to one central circle the labeled "LSP". The circle representing Visual Studio Code and the LSP circle appear slightly larger then the other circles.

If you’re working on developer assistance tooling such as auto-completion or error highlighting for a particular programming language, you probably want the functionality with more than one code editor. In the bad old days this used to be quite labor-intensive: developers had to come up with ways to share common logic across different editor platforms and technologies and write repetitive glue code for every integration. The Language Server Protocol (LSP) was invented to solve this problem.

LSP standardizes interactions between developer tooling (referred to as “servers” in the protocol) and text editors (“clients”) over a technology-neutral base layer. This completely decouples clients from servers meaning that you, as a tooling author, don’t have to write tons of glue code when targeting an editor that supports LSP. Plus, you can write your server in any language you like without having to worry about inter-process communication. LSP was first introduced in Visual Studio Code but it’s supported by a decent number of editors by now.

How does LSP work?

LSP runs over remote procedure calls using the JSON-RPC protocol (more specifically, version two of JSON-RPC). In JSON-RPC one client and one server exchange messages that either describe a request, response, or notification. Requests can be thought of as function calls with a given set of arguments that require a response. Responses either carry a return value or an error. Notifications are similar to requests, except that they don’t require a response from the other party, so they’re completely asynchronous. An example JSON-RPC request to add two numbers could look like this:

{ "jsonrpc": "2.0", "id": 1, "method": "add", "params": { "a": 2, "b": 3 } }

Followed by a response:

{ "jsonrpc": "2.0", "id": 1, "result": 5 }

Despite using the terms “client” and “server”, the protocol is entirely bidirectional. It requires a bidirectional communication channel to transmit messages, which could be anything from WebSockets to raw TCP connections, or Unix domain sockets. The LSP specification includes recommendations for handling communication channels. In practice, many language servers use standard input and output. The specification also describes the RPC layer in more detail.

On top of JSON-RPC, LSP defines a set of standard requests and notifications for servers to expose certain features to clients. The list of requests is quite long but the specification does a good job categorizing and explaining all of them. Here are some requests that we implement in the Symflower language server:

  • textDocument/didSave for triggering an analysis followed by unit test generation when a source file is saved in the editor.
  • workspace/didChangeWorkspaceFolders for keeping track of directories that should be included in future analyses.
  • textDocument/codeLens and workspace/executeCommand for letting users interactively add generated unit tests to their test files in our “test review” mode.
    • The codeLens request returns definitions for code lenses with commands to invoke when a user interacts with them in the client (usually by clicking).
    • executeCommand is called when a command is then invoked (i.e. when a user actually clicks on a code lens).

Starting a language server from a VS Code extension

Visual Studio Code provides first-class support for integrating a language server into an extension. Assuming we already have a VS Code extension (and if you don’t, you can get started in part one of this series), it’s as simple as installing the official client library and starting the server.

npm install vscode-languageclient

Where we start our language server doesn’t matter, although it usually makes sense to do this right in the activation function since that will start our server whenever the extension starts. The following example starts a language server with the command symflower language-server (Which happens to be the Symflower language server! Feel free to play around with it.) on Go and Java source code.

const serverOptions: ServerOptions = {
  command: 'symflower', // Replace with your own command.
  args: ['language-server'],
};

const clientOptions: LanguageClientOptions = {
  documentSelector: [
    // Active functionality on files of these languages.
    {
      language: 'go',
    },
    {
      language: 'java',
    },
  ],
};

const client = new LanguageClient('my-awesome-language-server', serverOptions, clientOptions);
await client.start();

We need to make sure the extension is actually running when starting our server, for example by including language Activation Events in the Extension Manifest.

And that’s it! Assuming our language server implements the LSP initialization procedure correctly, that’s all that’s required to start using it in VS Code.

Extending LSP

The standard messages in LSP are quite extensive for providing programming language support to clients but sometimes we might want to add a feature to our tool that’s not covered by the LSP standard. Fortunately, LSP doesn’t impose any restrictions on the requests we can use in our server and editor extensions.

The vscode-languageclient library lets us send non-standard requests and notifications to the server.

// Send a "custom-stuff/add" request.
const response: number = await client.sendRequest('custom-stuff/add', { a: 2, b: 3 });

And it’s also possible to define custom handlers for requests and notifications sent from the server to the client.

context.subscriptions.push(
  // Handle the "custom-stuff/add" request on the client.
  client.onRequest('custom-stuff/add', (args: { a: number; b: number }) => {
    return args.a + args.b;
  }),

  // The handler can be async.
  client.onRequest('custom-stuff/add-to-magic-number', async (args: number) => {
    return args + (await getMagicNumber());
  }),
);

Using non-standard messages, while useful, can make a server less compatible with clients that aren’t aware of our protocol extensions. We can use client capabilities to communicate support for non-standard features to a server and fall back to standards-compliant behavior on the server in case the client does not send the right capabilities.

Intercepting LSP messages in VS Code

When working on a language server it can be tremendously useful to see what messages are being passed around between the client and the server as we’re interacting with the client. The vscode-languageclient library provides a rudimentary traffic log for debugging and troubleshooting our server.

To use the log, we need to add this special configuration option to our Extension Manifest (package.json):

"my-awesome-language-server.trace.server": {
	"scope": "window",
	"type": "string",
	"enum": [
		"off",
		"messages",
		"verbose"
	],
	"default": "off",
	"description": "Echoes communication between Visual Studio Code and the language server onto the \"my-awesome-language-server\" output channel."
}

The scope, type and enum properties must be the same as shown in the example. The name (key) must match the format <name>.trace.server where <name> is the name used to start the language server.

The log will appear on the server’s output channel which by default is named the same as the language server. We can open the output view using the Output: Focus on Output View command (Ctrl+Shift+P to open the command launcher) and select the server’s channel in the top right dropdown.

Depending on the configured trace level, the output can be more or less verbose:

  • off only displays what the server outputs to standard error (when using standard I/O transport).
  • messages displays the server’s STDERR and a log of sent and received messages from the client’s point of view with types, IDs, and timestamps.
  • verbose enhances messages with a dump of parameters, return values, and errors in JSON.

And that should be everything to get you started with the language server protocol aka the LSP! Let us know if you found this guide helpful and follow us on Twitter, LinkedIn or Facebook. Subscribe to our newsletter to get notified about future blog posts on coding, testing and new features of Symflower.

Technical | 2022-11-08