DEV Community

Cover image for VsCode extension using webview and message posting
Allan Simonsen
Allan Simonsen

Posted on

VsCode extension using webview and message posting

In this article I will show how to create a simple VsCode extension that uses a webview with a html canvas to export graphics from the canvas to a PNG file.

VsCode extension example

When we have a webview panel in our extension then we cannot save data directly to the users disk from the html page or have download links inside the webview because it is contained in a sandbox for security reasons.

To show how we can get around this barrier this example extension will create a html canvas and display a rectangle. The image data from the canvas will then be saved to the root folder of the current workspace when the user clicks a button. This will illustrate how the message posting can be used to send data from a html page in a webview back to the VsCode context.

I have created a simple extension with a single command. When activated the command will create a webview panel using the createWebView panel command. Then I created an eventlistener for catching the onDidReceiveMessage events that I will use to send data from my html canvas

Below you can see the code for creating the webview panel and setup the eventlistner.

export function activate(context: vscode.ExtensionContext) {
  const vscodeTestDisposable = vscode.commands.registerCommand('vscodetest.webview', () => {
    const webviewPanel = vscode.window.createWebviewPanel(
      'vscodeTest',
      'VsCode test webview',
      vscode.ViewColumn.One,
      {
        enableScripts: true
      }
    );    
    webviewPanel.webview.onDidReceiveMessage(
      message => {
        switch (message.command) {
          case 'saveAsPng':
            saveAsPng(message.text);
            return;
        }
      },
      undefined,
      context.subscriptions
    );
    setHtmlContent(webviewPanel.webview, context);
  });
  context.subscriptions.push(vscodeTestDisposable);
}
Enter fullscreen mode Exit fullscreen mode

In the eventlistener I call the function saveToPng(...) that will decode the received data from the html page and then save the data to a PNG file. The data is base64 encoded because the message posting can only transmit text data. So the Javascript inside the webview has to convert all binary data to a text format before posting the message. This example extension will use base64 encoded data for the posted messages.

Below you can see the method that decodes the received messages.

function saveAsPng(messageText: string) {
  const dataUrl = messageText.split(',');
  if (dataUrl.length > 0) {
    const u8arr = Base64.toUint8Array(dataUrl[1]);
    const workspaceDirectory = getWorkspaceFolder();
    const newFilePath = path.join(workspaceDirectory, 'VsCodeExtensionTest.png');
    writeFile(newFilePath, u8arr, () => {
      vscode.window.showInformationMessage(`The file ${newFilePath} has been created in the root of the workspace.`);      
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The html the webview is using for showing the html canvas is shown below. There are a few details I would like to highlight here. Because of security reasons you should have Content-Security-Policy metatag in the html that restricts the code executing in the extension to avoid code injection. Below you can see I use a script nonce. This is a random string that is generated each time the page is reloaded and it prevent external injection of foreign code into you html page.
Also for security reasons all Javascript code and CSS styles should be contained in files. Inline code should be avoided. To allow the html to load files from the local drive you need to encode the uri's to the files using the asWebviewUri(...) method.


function getNonce() {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < 32; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

function setHtmlContent(webview: vscode.Webview, extensionContext: vscode.ExtensionContext) {
  let htmlContent = `<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src cspSource; script-src 'nonce-nonce';">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="vscodeTest.css" rel="stylesheet">
  </head>
  <body>
    <div id="buttons">
      <input type="button" id="saveAsPngButton" value="Save as png">
    </div>
    <div id="canvasSection"><canvas id="vscodeTestCanvas" /></div>
    <script type="text/javascript" src="vscodeTest.js"></script>
  </body>
</html>`;
  const jsFilePath = vscode.Uri.joinPath(extensionContext.extensionUri, 'javascript', 'vscodeTest.js');
  const visUri = webview.asWebviewUri(jsFilePath);
  htmlContent = htmlContent.replace('vscodeTest.js', visUri.toString());

  const cssPath = vscode.Uri.joinPath(extensionContext.extensionUri, 'stylesheet', 'vscodeTest.css');
  const cssUri = webview.asWebviewUri(cssPath);
  htmlContent = htmlContent.replace('vscodeTest.css', cssUri.toString());

  const nonce = getNonce();
  htmlContent = htmlContent.replace('nonce-nonce', `nonce-${nonce}`);
  htmlContent = htmlContent.replace(/<script /g, `<script nonce="${nonce}" `);
  htmlContent = htmlContent.replace('cspSource', webview.cspSource);

  webview.html = htmlContent;
}
Enter fullscreen mode Exit fullscreen mode

In the example webview html page I draw a simple rectangle on the canvas and then the user must click the 'Save as Png' button to get the image data posted back to the extension context to be saved.
As described in a previous section the data has to be base64 encoded, but this is fortunately built into the canvas, so when you call the toBaseURL() function it returns the image data as a PNG base64 encoded. All we need to do is create a message and then post it using the postMessage(...) function that is part of the VsCode JS API.


function saveAsPng() {
  // Call back to the extension context to save the image to the workspace folder.
  const vscode = acquireVsCodeApi();
  vscode.postMessage({
    command: 'saveAsPng',
    text: canvas.toDataURL()
  });
}

const saveAsPngButton = document.getElementById('saveAsPngButton');
saveAsPngButton.addEventListener('click', saveAsPng);
Enter fullscreen mode Exit fullscreen mode

I hope you have found the article interesting and can use some of the techniques I have illustrated.

If you have not yet tried to create a Visual Studio Code extension, there are a lot of good articles on the VsCode website. To get started have a look at this article.

The entire code for this example extension can be found on GitHub: vscode-extension-webview

The techniques used in this article can be seen in a real-world extension in the AngularTools extension. I made a short introduction to the extension in a previous article here on Dev.io

Top comments (3)

Collapse
 
scanderino profile image
Scott Anderson

In case it is interesting/useful, I automated something similar
github.com/ScottA38/.dotfiles-mage...
github.com/ScottA38/.dotfiles-mage...

Collapse
 
hello10000 profile image
a

dev.to/free_one/vs-code-aesthetics...

check out this post on aesthetic VSCode extensions

Collapse
 
jcruzm profile image
jcruz-m

Hello Allan. Any special instruction to make it run? After nmp install I cannot figure it out how to see it in action!