hckr.fyi // thoughts

How to Build a Visual Studio Code Extension

by Michael Szul on

Yesterday I needed to transform some XSLT, and wanted to be able to do it in Visual Studio Code, which is my editor and IDE of choice. I wasn't happy with the XSLT transformations that were out there, so I decided to build my own. Visual Studio Code makes it drop dead simple to build extensions in either TypeScript or JavaScript, so getting up-and-running was easy, and I took it from start to finish in just a few hours.

The easiest way to get started is to use Yeoman to generate out some scaffolding. If you don't already have Yeoman installed, you can install it via:

npm install yo -g
    

On top of that, you're going to have to install the Visual Studio Code generator:

npm install generator-code -g
    

Once you have both of these installed, you can just type yo code on the command line, and the Yeoman generator will walk you through creating a scaffold for your new extension.

By default, this generates out a simple Hello World extension. On the command line, change directories into the application you just created, and then launch Visual Studio Code for that directory. For my example, my project was called "xslt-transform":

cd ./xslt-transform
    code ./
    

Once your project is up in Visual Studio Code, the generated project contains the appropriate tasks.json and launch.json files to allow for debugging. When you click the debug indicator on the debug tab in this launched project, it will launch a new version of Visual Studio Code with the extension enabled. You can actually run this immediately to see the "Hello World" example.

Of course, I didn't want a "Hello World" extension, I wanted to transform some XSLT, so let's do that.

The problem with a lot of XSLT processors, is that anything built from a Unix or Linux machine usually relies on some native libraries or bindings, such as libxml2. With Node.JS, this can also be the case. Fortunately, Fidus Writer has a collection of open source tools for their collaborative academic publishing platform, including an "xslt-processor" package that relies solely on JavaScript code.

npm install xslt-processor --save
    

This package will do most of the heavy lifting. We just need to get the files, and pass them to the functions in this package.

Note: Since we're writing this in TypeScript and "xslt-processor" does not have a typings file, we want to create a file called schema.d.ts in the "src" folder. This will contain one line:

declare module 'xslt-processor';
    

Now let's edit the extension.ts file to add in our code.

Import the methods we need from the package:

import { xmlParse, xsltProcess } from "xslt-processor";
    

We're also going to need to use the "fs" package:

import * as fs from "fs";
    

The stubbed out Visual Studio Code extension code has two exported functions: activate() and deactivate(). The activate() function is called when the extension is activated, and you'll notice a logged message and a disposable constant. This constant is the result of the registerCommand() function, which registers your code with the application, and the command palette. The first parameter is a name key for your extension. If you change this (it probably says helloWorld right now), make sure you update your package.json file. The next parameter is a callback function to get executed when your extension is run.

const disposable = vscode.commands.registerCommand('extension.runXSLTTransform', async () => {
        ...
    });
    

Notice that the function is async. I changed this since I am awaiting functions inside of the callback.

If order to accomplish XSLT transformations, we need to have an XML and an XSLT file. We're going to assume that the XML file is the active window, and we will have to select the XSLT file. We'll get the XSLT file with the showOpenDialog() method:

const xsltFile = await vscode.window.showOpenDialog(
            {
                canSelectFiles: true,
                canSelectFolders: false,
                canSelectMany: false,
                filters: {
                    'XSLT' : ['xsl','xslt']
                }
            }
        );
    

This method accepts an object of options. We only want a single file selected, and we want to filter it to XSLT files.

Now let's make sure we have data and an active window:

if(vscode.window.activeTextEditor !== undefined && xsltFile !== undefined) {
       ...
    }
    

We get the XML from the active window with the activeTextEditor object:

const xml = vscode.window.activeTextEditor.document.getText();
    

We then read the XSLT file into a constant:

const xslt = fs.readFileSync(xsltFile[0].fsPath).toString();
    

Now we need to process the XSLT transformation. Thanks to the "xslt-processor" package, this is three lines of code:

const rXml = xmlParse(xml);
    const rXslt = xmlParse(xslt);
    const result = xsltProcess(rXml, rXslt);
    

Since we want to display the result, we use the openTextDocument method, setting the language option, and then open that document beside the XML:

const textDoc = await vscode.workspace.openTextDocument(
                {
                    content: result,
                    language: 'xml'
                }
            );
    
    vscode.window.showTextDocument(textDoc, vscode.ViewColumn.Beside);
    

In my case, most of these transformations are to transform XML to HTML, so I want to open a web preview as well:

const web = vscode.window.createWebviewPanel('transformPreview', 'XSLT Results', vscode.ViewColumn.Beside, { });
    web.webview.html = result;
    

Lastly, the most important thing is to push the disposable object onto the extension subscription array:

context.subscriptions.push(disposable);
    

That's it. You now have a fully functioning extension that actually does something other than print "Hello World" to the user. The entire extension.ts file looks like this:

'use strict';
    
    import * as vscode from 'vscode';
    import * as fs from 'fs';
    import { xmlParse, xsltProcess } from 'xslt-processor';
    
    export function activate(context: vscode.ExtensionContext) {
        console.log('Congratulations, your extension "xslt-transform" is now active!');
        const disposable = vscode.commands.registerCommand('extension.runXSLTTransform', async () => {
            const xsltFile = await vscode.window.showOpenDialog(
                {
                    canSelectFiles: true,
                    canSelectFolders: false,
                    canSelectMany: false,
                    filters: {
                        'XSLT' : ['xsl','xslt']
                    }
                }
            );
            if(vscode.window.activeTextEditor !== undefined && xsltFile !== undefined) {
                const xml = vscode.window.activeTextEditor.document.getText();
                const xslt = fs.readFileSync(xsltFile[0].fsPath).toString();
                try {
                    const rXml = xmlParse(xml);
                    const rXslt = xmlParse(xslt);
                    const result = xsltProcess(rXml, rXslt);
                    const textDoc = await vscode.workspace.openTextDocument(
                        {
                            content: result,
                            language: 'xml'
                        }
                    );
    
                    vscode.window.showTextDocument(textDoc, vscode.ViewColumn.Beside);
    
                    const web = vscode.window.createWebviewPanel('transformPreview', 'XSLT Results', vscode.ViewColumn.Beside, { });
                    web.webview.html = result;
    
                }
                catch(e) {
                    vscode.window.showErrorMessage(e);
                }
            }
            else {
                vscode.window.showErrorMessage('An error occurred while accessing the XML and/or XSLT source files. Please be sure the active window is XML, and you have selected an appropriate XSLT file.');
            }
        });
    
        context.subscriptions.push(disposable);
    }
    
    export function deactivate() {
    }
    

Now you're ready to publish it to the Visual Studio Code marketplace. When I finished this, however, I decided to open a pull request to the XML Tools extension instead, trying to contribute to a more robust all-in-one solution.