The most important tool that we use at e.GO Mobile in software development is Visual Studio Code.

The editor is fast and offers a variety of extensions that can be quickly installed if needed.

To build own extensions, an extensive API is available, which allows you to control almost all areas of the editor.

In UI development, we mainly use React and in this article, I would like to show how to develop Visual Studio Code extensions using this technology.

For this I created a GitHub repository with a demo.

Create extension

Microsoft describes this process in detail on its page Your First Extension.

To create an extension, you will need the modules Yeoman and the corresponding generator generator-code. Both must be installed globally.

npm install --global yo
npm install --global generator-code

By executing

yo code

you run Yeoman with a wizard, which guides you through the individual steps for creating the extension.

After creating the extension, open the new directory with VSCode.

Start writing code

The file src/extension.ts already contains sample code.

The activate() function is essentially the entry point where the extension is started and initialized. The deactivate() function, on the other hand, is the part that is called when the extension is terminated.

For the sake of clarity, we will remove the example code for now:

import * as vscode from 'vscode';

export async function activate(context: vscode.ExtensionContext) {
    // extension has been activated
    // and needs to be initialized
}

export async function deactivate() {
    // extension is going to be deactivated
}

In order for the extension to be launched after the editor has been initialized, we need to fill array in activationEvents with the value onStartupFinished in the package.json (a list of all events can be found here):

{
    // ...

    "activationEvents": [
        "onStartupFinished"
    ],

    // ...
}

We can now start the extension in debug mode by clicking on the corresponding icon on the left side and clicking the play button after selecting Run Extension, if necessary.

Open a WebView

It is very easy to open a WebView.

Use the createWebviewPanel() function in the window namespace of the VSCode API:

// ...

export async function activate(context: vscode.ExtensionContext) {
    // ...

    const panel = vscode.window.createWebviewPanel(
		"testHtmlView",
		`Test WebView with React`,
		vscode.ViewColumn.One,
		{
			"enableCommandUris": true,
			"enableForms": true,
			"enableScripts": true,
			"enableFindWidget": true,
			
			// to save memory, we can rerender the
			// view if its tab gets its focus back
			"retainContextWhenHidden": false,
		}
	);

    // we add the new `panel` to list of `subscriptions`
    // so the IDE is possible to dispose if when
    // extension is being terminated automatically
    context.subscriptions.push(panel);

    // ...
}

// ...

Within the object that we have stored in panel, there is the property webview, which contains the actual view.

This object allows us to establish a connection between the editor and the underlying view using onDidReceiveMessage() method:

/// ...

const {
    webview
} = panel;

webview.onDidReceiveMessage((message: any) => {
    // handle `message` from WebView ...

    if (message?.type === 'webview-has-been-loaded') {
        // this could be a message from WebView to indicate
        // that it is ready to communicate with VSCode
    }
});

/// ...

The property html within webview is, among other things, a setter that allows us to set the HTML code for it.

Required libraries

In order to parse and execute React code (also JSX) in the view, we need 3 things:

As we do not work with pre-made frameworks such as create-react-app or Next.js, we require so-called UMD versions of these libraries.

According to ChatGPT this is: “UMD (Universal Module Definition) is a design pattern used in JavaScript to create modules that can work with different module systems, including CommonJS, AMD, and global variables.”

Or in other words: Bundlers like rollup.js can create JavaScript modules in “UMD format”. These are typically bound to the window object of the browser as a namespace.

In the case of React, the functions, classes, etc. are then available as React and ReactDOM as usual.

HTML with EJS

I use ejs by Matthew Eernisse for HTML templates.

The reason is that I can easily pass data as variables through this template engine and receive finished and clean HTML.

The resource files for the view are stored within the extension in the subdirectory media.

Here ejs comes into play: In order for the paths to the JavaScript files to be correct, I first have to create them with VSCode and then pass them on during rendering:

// ...
import * as ejs from 'ejs';
import * as vscode from 'vscode';
// ...

// we should use the more generic `fs` in VSCode
// instead of `node:fs` module to be able to deal
// with other file systems as well
//
// s. https://code.visualstudio.com/api/extension-guides/virtual-documents
const {
	fs
} = vscode.workspace;

function getNonce(size: number = 32, allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"): string {
	let nonce = "";

	for (let i = 0; i < size; i++) {
		nonce += allowedChars.charAt(Math.floor(Math.random() * allowedChars.length));
	}

	return nonce;
}

export async function activate(context: vscode.ExtensionContext) {
    const {
		extensionUri
	} = context;

    // ...

    // we need this value for security reaons
    // s. https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
    const nonce = getNonce();

    // URI to `media/` subfolder of extension
    const mediaDirUri = vscode.Uri.joinPath(extensionUri, 'media');

    // read content of `media/main.ejs`
    const mainEJSFile = vscode.Uri.joinPath(mediaDirUri, "main.ejs");
	const mainEJSContent = Buffer.from(
		await fs.readFile(mainEJSFile)
	).toString('utf8');

    // this final format makes it possible to also run the extension
    // in a VSCode instance that runs in a browser
    const rootWebUri = webview.asWebviewUri(mediaDirUri);

    // render `.ejs` content and set this final
    // HTML for `webview`
    webview.html = ejs.render(mainEJSContent, {
		nonce,
		rootUri: rootWebUri.toString()
	});

    // ...
}

// ...

The main.ejs could look like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">

        <meta name="viewport" content="width=device-width, initial-scale=1.0">

        <!-- React + Babel -->
        <script nonce="<%- nonce %>" src="<%- rootUri %>/js/react.18.2.0.js"></script>
        <script nonce="<%- nonce %>" src="<%- rootUri %>/js/react-dom.18.2.0.js"></script>
        <script nonce="<%- nonce %>" src="<%- rootUri %>/js/babel.7.23.0.js"></script>

        <script>
            // make VSCode webview API available
            // in global `window` object
            window.vscode = acquireVsCodeApi();
        </script>
    </head>

    <body>
        <div id="tgf-content"></div>

        <script nonce="<%- nonce %>" src="<%- rootUri %>/components/myComponent.jsx" type="text/babel"></script>
    </body>
</html>

Writing the React component

You may have been noticed that in one of the last lines of the .ejs file, there is a special script tag.

<script nonce="<%- nonce %>" src="<%- rootUri %>/components/myComponent.jsx" type="text/babel"></script>

This tag is not processed by the browser.

This is utilized by Babel and scans, after the page has loaded, all script elements with type="text/babel".

Babel will parse all files defined in the src attributes from the library and load them into the JavaScript context of the browser.

A simple myComponent.jsx file could look like this, for example:


const MyComponent = () => {
    // s. https://react.dev/reference/react/useEffect
    React.useEffect(() =>
        // handle messages from VSCode
        const messageHandler = () => {
            // tell VSCode that we are ready
            vscode.postMessage({
                type: 'webview-has-been-loaded',
            });
        };
        window.addEventListener('message', messageHandler);
        
        return () => {
            window.removeEventListener('message', messageHandler);
        };
    }, []);

    return (
        <div>Hello, VSCode!</div>
    );
};

// render it into `#tgf-content`
// of `main.ejs`
ReactDOM.createRoot(document.querySelector("#tgf-content"))
    .render(<MyComponent />);

Conclusion

When one deals with the topic of how websites were actually built in the past and plays around with the demo example a bit, one will see that it is quite easy to develop a Visual Studio Code extension with UI support.

Once again, I wish you a lot of fun while trying it out! 🎉