Visual Studio Code extension with UI in React.js
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! 🎉