How to Pass Data to A Webview Panel in VS Code Extension API

Ashley
8 min readFeb 15, 2024

--

Khandelwal, Pankaj. “VS Code Extension API Workflow.” Blog.Postman.Com, 17 Nov. 2023, Accessed 14 Feb. 2024.

In this article, I will explain how my team and I managed the passage of data from an invoked function to render webview panel content in the VS Code extension. The webview API enables the rendering of HTML content within its frame and communicates with extensions using message passing. Essentially, extensions give VS Code a string of HTML. VS Code renders that HTML in an editor by posting messages to the webviews and saving the state associated with each webview. It’s important to note that webviews are run in an isolated context and should not affect the rest of the VS Code context.

The Problem

The question is: How can we establish data flow from a JavaScript file, passing through our VS Code extension context, and ultimately embedding the data in HTML for rendering within our React Components in the Webview Panel?

Given the limited documentation available for the VS Code extension API, our team found it necessary to adopt creative, yet stable approaches in organizing our pathways for transferring data across different files and environments. This article aims to offer a comprehensive explanation with examples drawn from React Labyrinth’s codebase so you can easily set up your own Webview Panel with ample reference and guidance. To achieve this, the article will be divided into two sections: Basic context and Background, outlining the correlation among files, and the Process of Data Flow, detailing the steps involved in data flow.

Our Approach

We utilized three key files to facilitate the functionality of data flow: extension.ts, panel.ts, and Flow.tsx.

Abstract Overview of Steps for Data Flow

extension.ts

The extension.ts file serves as an entry file, containing two primary functions: activate and deactivate. The activate function is executed upon activation events, while the deactivate function is responsible for resource cleanup (may not always be required).

Registering commands within the activate function involves providing a unique identifier for the command, a callback function, and an optional ‘this’ context as arguments. Also, we need to subscribe to the command in our context.subscriptions array for the VS Code extension to access the command’s functionality.

package.json

In the package.json file, it’s crucial to reference the command we registered earlier in extension.ts. When invoked, it will perform the logic in the command function block. Users can invoke this command from the command palette or create a keybinding if required in your extension.

Here is an example of our command.

// package.json 

"contributes": {
"commands": [
{
"command": "myExtension.pickFile",
"title": "Pick File",
"category": "React Labyrinth"
}
],
}

panel.ts

panel.ts is where we create our webview with its skeleton content, including the logo, URI, and HTML. This file serves as our webview and allows message passing between the webview API and the VS Code extension.

To begin, we declare a variable named panel and store the value of a newly instantiated webview. The instantiation accepts arguments specifying the type of webview, its title, the column to show the new panel in, and an options object. The enableScripts property set to true in the options object allows JS scripts to be rendered in the panel.

The initial webview has no content when declared, so we have to create HTML content. This is achieved through methods such as panel.iconPath, panel.webview.html, and other methods on the panel object.

// panel.ts

import * as vscode from 'vscode';
import { getNonce } from './utils/getNonce';
import { Tree } from './types/tree';

let panel: vscode.WebviewPanel | undefined = undefined;

export function createPanel(context: vscode.ExtensionContext, data: Tree, columnToShowIn: vscode.ViewColumn) {
// Utilize method on vscode.window object to create webview
panel = vscode.window.createWebviewPanel(
'reactLabyrinth',
'React Labyrinth',
// Create one tab
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);

// Set the icon logo of extension webview
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'RL(Final).png');

// Set URI to be the path to bundle
const bundlePath: vscode.Uri = vscode.Uri.joinPath(context.extensionUri, 'build', 'bundle.js');

// Set webview URI to pass into html script
const bundleURI: vscode.Uri = panel.webview.asWebviewUri(bundlePath);

// Render html of webview here
panel.webview.html = createWebviewHTML(bundleURI, data);

// ...

return panel;
};

Flow.tsx

Flow.tsx is a React Component responsible for rendering the HTML content of our webview context. Additionally, it utilizes the passed data to render our hierarchy component tree using helper functions and a third-party library, React Flow.

Process of Data Flow

With a comprehensive understanding of the files involved and their distinct functionalities, we can dive deeper into how we passed data from the webview to the extension. I will split the process of data flow into 4 parts: Extension to Webview, Extension to Webview (cont.), Webview to React Component, and React Component and onward.

1. Data from Extension to Webview

This segment involves initiating communication channels and sending data payloads from the extension context (extension.ts) to the webview context (panel.ts), enabling the webview to receive and process the incoming data.

In extension.ts, we’ve imported our Parser class and the createPanel function. Inside the activate function, we registered a command named myExtension.pickFile and created a new instance of the Parser class. The evaluated result of the method on the Parser class will be passed in the createPanel function in panel.ts. Below is an example of how we executed the start of the data flow.

Note: Some of the examples shown below may have omitted sections of the code that are unrelated to the data flow but are utilized for other functionalities in VS Code extension development. We created the Parser class and createPanel function to assist with our functionality.

// extension.ts

import * as vscode from 'vscode';
import {createPanel} from './panel';
import { Parser } from './parser';
import { Tree } from './types/tree';
import { showNotification } from './utils/modal';

let tree: Parser | undefined = undefined;
let panel: vscode.WebviewPanel | undefined = undefined;

// This method is called when your extension is activated
function activate(context: vscode.ExtensionContext) {

// This is the column where Webview will be revealed to
let columnToShowIn : vscode.ViewColumn | undefined = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;

// Command that allows for User to select the root file of their React application.
const pickFile: vscode.Disposable = vscode.commands.registerCommand('myExtension.pickFile', async () => {

const fileArray: vscode.Uri[] = await vscode.window.showOpenDialog({ canSelectFolders: false, canSelectFiles: true, canSelectMany: false });

if (!fileArray || fileArray.length === 0) {
showNotification({message: 'No file selected'});
return;
}

// Create Tree to be inserted into returned HTML
tree = new Parser(fileArray[0].path);
tree.parse();
const data: Tree = tree.getTree();

// Create a new webview to display root file selected with passed-in data.
if (!panel) {
panel = createPanel(context, data, columnToShowIn);

// ...

context.subscriptions.push(pickFile, showPanel);
}

2. Data from Extension to Webview (cont.) in parser.ts

This segment focuses on establishing bidirectional communication channels between the webview and the extension for the exchange of data, enabling the webview to receive data emitted from the extension.

In panel.ts, we begin by invoking createWebviewHTML(), passing in the data we got from extension.ts and the URI from panel.ts. Our createWebviewHTML function looks like this:

// panel.ts

// Creates the HTML page for webview
function createWebviewHTML(URI: vscode.Uri, initialData: Tree) : string {
return (
`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Labyrinth</title>
</head>
<body>
<div id="root"></div>
<script>
const vscode = acquireVsCodeApi();
window.onload = () => {
vscode.postMessage({
type: 'onData',
value: ${JSON.stringify(initialData)}
});
}
</script>
<script nonce=${nonce} src=${URI}></script>
</body>
</html>
`
);
}

To facilitate message passing from the extension to the webview, we use a method provided by the VS Code API object called acquireVsCodeApi().

Note: The acquireVsCodeApi() function can only be invoked once per session.

Additionally, our other script tag contains getNonce() and our URI, which helps with creating new IDs of scripts for security reasons and sanitization.

Upon instantiating the vscode variable, both the script and webview have access to the vscode global object. We then call window.onload(), where the vscode global object posts a message with a key-value pair of type: string and the value: JSON.stringify(data). The postMessage() method will send any JSON serializable data to the webview and will be received in a message event. The value of the type property can be any string you declare, but it must match the responding type for the message to be received.

3. Data from Webview to React Component

This segment involves transmitting data from the webview to the React Component.

Previously, the postMessage() method, with the type of ‘onData’ and its value, was sent from the extension and is now anticipating its message to be received. In panel.ts, this message event will be intercepted by accessing a method called onDidReceiveMessage() on our panel.webview object to handle incoming messages.

The onDidReceiveMessage() method tries finding a context to receive based on the matching message.type within the switch/case statement. When the message.type is ‘onData’, indicating a match, the code block associated with this condition is executed.

In our implementation, we’ve accounted for edge cases and invoked another postMessage() method on the panel.webview object. This message contains the value of the data intended for the React Component with the type key-value pair. This will continue the data flow chain—instead of moving from the extension to the webview, we are now passing the data from the webview to a React Component.

// panel.ts

// ...

// Sends data to Flow.tsx to be displayed after parsed data is received
panel.webview.onDidReceiveMessage(
async (msg) => {
switch (msg.type) {
case 'onData':
if (!msg.value) break;
context.workspaceState.update('reactLabyrinth', msg.value);
panel.webview.postMessage(
{
type: 'parsed-data',
value: msg.value, // tree object
settings: vscode.workspace.getConfiguration('reactLabyrinth')
});
break;
}
},
undefined,
context.subscriptions
);

// ...

4. Data Received in React Component

In Flow.tsx, we are listening for a message event with the type ‘parsed-data’. We accomplish this using a useEffect hook, ensuring that the effect only runs once by specifying an empty dependency array. Within this effect, we attach an event listener to the window object.

Using a switch/case statement, we retrieve the value on event.data.type for matches with ‘parsed-data’. Upon recognizing a message originating from the webview, we invoke a helper function to dynamically render our nodes and edges.

// Flow.tsx
const OverviewFlow = () => {

// Required to have different initial states to render through D3
const initialNodes: Node[] = [];
const initialEdges: Edge[] = [];

const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);

useEffect(() => {
window.addEventListener('message', (e: MessageEvent) => {
// Object containing type prop and value prop
const msg: MessageEvent = e;
const flowBuilder = new FlowBuilder;

switch (msg.data.type) {
case 'parsed-data': {
let data: Tree | undefined = msg.data.value;

// Creates our Tree structure
flowBuilder.mappedData(data, initialNodes, initialEdges);

setEdges(initialEdges);
setNodes(initialNodes);
break;
}
}
});
}, []);

return (
// All the code here ...
);
}

With this setup, we completed the journey of data flow from the extension to the webview, and finally to the React Component. This cohesive flow ensures that data is transmitted seamlessly across different components of our application and the implementation of data in our necessitated components.

Conclusion

In conclusion, navigating the process of data flow from the VS Code extension environment to a Webview Panel, and onward to a React Component can present challenges, especially for those new to VS Code extension development. We hope the information outlined and our shared experience when building out React Labyrinth can help you create a fully functional Webview Panel to provide a robust VS Code extension with a great user experience. It’s important to highlight that while this guide provides a solid foundation of data flow in webviews, there is still much more to explore and experiment with in the VS Code extension development environment. We encourage you to continue learning with this powerful toolset to unlock its full potential.

If you have any questions, please feel free to reach out to us on our React Labyrinth LinkedIn page.

--

--

Ashley

A Software Engineer building software to make life easier and more meaningful