LSP and Xtext Tutorial: Creating the VS Code Client

post-thumb

This post is a continuation of my LSP and Xtext Tutorial. In my latest post I created a language in Xtext to be used with a client. In this post I am going to create VS Code plugin to connect to the language server created in the previous post. The full code for this example can be found here.

The VS Code Plugin

VS Code plugins are created using yeoman. Yeoman is a scaffolding tool, it creates the necessary files to start your project. For our VS Code plugin, we can use it to generate most of the plugin and then we proceed to customize it for our language. For those coming from the Eclipse world, yeoman is a wizard that we can launch from the command line. The installation of yeoman is outside of the scope of this tutorial.

Launching Yeoman

Yeoman’s command is yo. To start the scaffoling of a VS Code extension we launch the following command:

$ yo code

     _-----_     ╭──────────────────────────╮
    |       |    │   Welcome to the Visual  │
    |--(o)--|    │   Studio Code Extension  │
   `---------´   │        generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What type of extension do you want to create?
  New Extension (TypeScript)
  New Extension (JavaScript)
  New Color Theme
❯ New Language Support
  New Code Snippets
  New Keymap
  New Extension Pack
  New Language Pack (Localization)
  New Web Extension (TypeScript)
  New Notebook Renderer (TypeScript)

We select “New Language Support” and proceed to create the extension as follows:

Block Language Creation Yeoman
Block Language Creation Yoman

Adding dependencies

The first step is to add the dependencies. We can do this by using node to modify the package.json file. We use the following commands from a terminal at the base of the repository:

$ npm install @types/node -D
$ npm install @types/vscode -D
$ npm install typescript -D
$ npm install vscode-test -D
$ npm install vscode-languageclient 

This downloads the node dependencies and modifies the package.json.

The plugin code

The plugin code is written in Typescript. To build it we need to configure the compiler, write the actual code to activate the client, and finally configure the plugin to run the code when a .block file is open.

The compiler configuration

The compiler configuration is done using the tsconfig.json. The configuration file is below:

{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": false,
        "inlineSources": false,
        "declaration": true,
        "stripInternal": true,
        "lib": [
            "es6"
        ],
        "outDir": "out"
    },
    "include": [
        "src/**/*.ts"
    ]
}

This file configures the plugin to use the src folder to contain the Typescript sources, output the js files to the out directory and also adds some compiler options.

The code to activate the client

The code to activate the client is below:

'use strict';

import * as net from 'net';

import { Trace } from 'vscode-jsonrpc';
import { workspace, ExtensionContext } from 'vscode';
import { LanguageClient, LanguageClientOptions, StreamInfo, Position as LSPosition, Location as LSLocation } from 'vscode-languageclient/node';

export function activate(context: ExtensionContext) {
    // The server is a started as a separate app and listens on port 5007
    let connectionInfo = {
        port: 5007
    };
    let serverOptions = () => {
        // Connect to language server via socket
        let socket = net.connect(connectionInfo);
        let result: StreamInfo = {
            writer: socket,
            reader: socket
        };
        return Promise.resolve(result);
    };

    let clientOptions: LanguageClientOptions = {
        documentSelector: ['block'],
        synchronize: {
            fileEvents: workspace.createFileSystemWatcher('**/*.*')
        }
    };

    // Create the language client and start the client.
    let lc = new LanguageClient('Xtext Server', serverOptions, clientOptions);

    lc.trace = Trace.Verbose;
    let disposable = lc.start();

    // Push the disposable to the context's subscriptions so that the 
    // client can be deactivated on extension deactivation
    context.subscriptions.push(disposable);
}

This code uses the VS Code API to create a LSP client that is launched when the activate function is run. It basically configures the client to use our server on port 5007 and on localhost, and the starts the client.

The wiring

Now that we have the code to be run and the configuration to build it. We use the package.json file to tell VS Code to activate the plugin when the block language is loaded and to use the extension file as entry point. The final package.json is presented below:

{
    "name": "block-language",
    "displayName": "block-language",
    "description": "Block Language support",
    "version": "0.0.1",
    "engines": {
        "vscode": "^1.66.0"
    },
    "categories": [
        "Programming Languages"
    ],
    // this is the activation configuration
    "activationEvents": [
        "onLanguage:block"
    ],
    // this is the extension config
    "main": "out/extension",
    "devDependencies": {
        "@types/node": "^17.0.18",
        "@types/vscode": "^1.64.0",
        "typescript": "^4.5.5",
        "vscode-test": "^1.5.2"
    },
    "dependencies": {
        "vscode-languageclient": "^7.0.0"
    },
    "contributes": {
        "languages": [
            {
                "id": "block",
                "aliases": [
                    "Block Lang",
                    "block"
                ],
                "extensions": [
                    ".block"
                ],
                "configuration": "./language-configuration.json"
            }
        ],
        "grammars": [
            {
                "language": "block",
                "scopeName": "source.block",
                "path": "./syntaxes/block.tmLanguage.json"
            }
        ]
    }
}

Testing the whole

We can now launch the server from the previous post in the console and launch a VS Code instance with our plugin running. This is done by pressing F5. Below is an image of the new editor with a validation on the code.

Block Language Running  on VSCode
Block Language Running on VSCode

The previous chapter of this tutorial is here: The next chapter of this tutorial is here: LSP and Xtext Tutorial: Creating the language

comments powered by Disqus