Date: 9/20/20 Tags: Tools, Svelte, Data, VSCode Permalink: /game-data-editor-vscode-part-3 Summary: Getting data into Svelte from VSCode’s virtual
September 20, 2020
Date: 9/20/20 Tags: Tools, Svelte, Data, VSCode Permalink: /game-data-editor-vscode-part-3 Summary: Getting data into Svelte from VSCode’s virtual document Title: Building a Game Data Editor in Visual Studio Code Part 3: Getting Data into Svelte —
This is part of a series of posts where I discuss building a game data editor inside of Visual Studio Code.
Part 1: Why?
Part 2: Custom Editors, Webviews, and Svelte
Part 3: Getting Data into Svelte
Part 4: Editing Data in Svelte
In the last post, I talked about getting Svelte setup to build VS Code Webviews with, specifically for use with VS Code’s Custom Editor functionality. What I didn’t explain in that post was how to then actually get the data of the document you were previewing into the webview itself. I’m going to focus on that in this post, and by the end you should have a rough toolkit that allows you to preview your data in VS Code with Svelte!
To reiterate what a Custom Editor in Code does, is that it allows you to invoke a webview in place of the native text editor. However, because we’re making an editor, we obviously want some ability to edit right? When we left off the previous tutorial, this is what we saw:
Looking at the raw example.cscratch data, we see this:
What we want to be able to do is mimic the functionality of the previous Custom Editor example, where that raw data was able to be displayed in a formatted way inside of the webview. We’ll also talk about editing the displayed data in the next post, but for now lets focus on just getting the data in there at all.
So Step 1, how do we get this data into Svelte? I’ll also be frank here: I tried a lot of different ways to do this, and I still think there are a few different valid ways, but I’m just going to show you the method I’m using that seems to work well. I couldn’t full understand the lifecycle of virtual document creation in Code’s webviews, as well as how and when the HTML was being rebuilt by Svelte, so I settled on a solution that has Svelte essentially requesting the data once it’s loaded into the DOM via onMount. Trying anything like serving the data inside the HTML didn’t really work, so instead I assumed that once Svelte has loaded, it can request the data and display correctly.
Svelte has an onMount lifecycle hook that is called “after the component is first rendered to the DOM”. Which, per my statement above, sounds like a great time to request data! In the onMount
call, I’ll send a message to the extension to say that Svelte has loaded. Here’s the updated App.svelte
(I also got rid of the old “apitest” event and cleaned up the sample, getting rid of css):
<script>
//add in onMount
import { onMount } from 'svelte';
//attach a function to onMount
onMount(() => {
vscode.postMessage({
type: 'init-view',
});
});
</script>
<main>
<h1>Hello World!</h1>
</main>
Now that we’re emitting this init-view
from Svelte, we need to catch it on the extension side and do something. Our goal here is to basically grab the document’s data, and then send that data back to Svelte. Here’s the code (added in catScratchEditor.ts
):
webviewPanel.webview.onDidReceiveMessage(e => {
switch (e.type) {
case 'add':
this.addNewScratch(document);
return;
case 'delete':
this.deleteScratch(document, e.id);
return;
case 'init-view': //added this route
webviewPanel.webview.postMessage({
type: 'init',
text: document.getText(),
});
return;
}
});
So when the extension receives the message the init the view, it just grabs the current state of the virtual document through document.getText()
and then sends that back to the webview. We’ll get to message handling in webviews in a second, but two notes.
One, as part of this code I also now remove the call in catScratchEditor.ts
to updateWebview()
at the end of the resolveCustomTextEditor
callback. You’ll see a bit more in the next section about why, but basically I want to make sure I control the data flow through the program. So now we don’t try to update anything until Svelte is ready.
Two, this init-view
event has some really interesting implications on the rest of our program. Right now we’re just sending the data of the document, but we can send anything else as part of this event, like… maybe the document’s actual type? Because we are using something like Svelte for all our webview rendering vs. needing to rely on specific HTML strings, and Svelte has the ability to conditionally display elements, we can actually get rid of the need to have one editor per data type (provided the data types themselves are roughly the same base representation). We can read in the extension of our file here, pass the type to Svelte, and have Svelte handle any differences! The reason we’d want this is because there is essentially nothing that we actually need the extension itself to do anymore, as everything is now being handled in Svelte..
Because the sample is meant to represent both binary data editors (.pawDraw
) and text-based ones (.cscratch
), combining the editors in this case is a bad idea. However, imagine we wanted to make a Dog Bark file (.dbark
), which was also text based like .cscratch
, we could easily have our editor support this file type by updating the manifest. Let’s do that (in catScratchEditor.ts
)!
webviewPanel.webview.onDidReceiveMessage(e => {
switch (e.type) {
case 'add':
this.addNewScratch(document);
return;
case 'delete':
this.deleteScratch(document, e.id);
return;
case 'init-view':
//split the filename and pop the last element for the extension
let dt = document.fileName.split('.').pop();
//dt will only ever be either cscratch or dbark, because this editor (catScratchEditor.ts) is
//only opened when one of those files is selected (per the package.json)
webviewPanel.webview.postMessage({
type: 'init',
text: document.getText(),
dataType: dt
});
return;
}
});
So above, we read in the extension of the file, and pass it as another parameter to the webview message. What we also want to do here is update the package.json
to open this editor only when either filetype is opened. In the customEditors
section of our contributes, in the catCustoms.catScratch
section, we can add our new file type to the filename pattern section, making it so that both .cscratch
and .dbark
files use the catScratchEditor.
We update this:
{
"viewType": "catCustoms.catScratch",
"displayName": "Cat Scratch",
"selector": [
{
"filenamePattern": "*.cscratch"
}
]
},
To be this:
{
"viewType": "catCustoms.catScratch",
"displayName": "Cat Scratch",
"selector": [
{
"filenamePattern": "*.{cscratch,dbark}"
}
]
},
In reality, you’d want to update the names of these files and their provides to represent that they are the routes for multiple data types, but for the sake of the tutorial I’m keeping the names constant.
I’ll also add in the first ever .dbark
file into my project, and give it some data:
Let’s run the program just to make sure our .dbark
file is recognized now:
Perfect!
Things definitely start to get a bit sticky here, and I fully admit again this may not be the best way to do things. The reason things are difficult is because, inside Svelte now, you are juggling lots of different states of things that all need to be in sync. You’ve got:
The actual state of the text file. What’s in it, etc.
The current state of the “virtual document”, the representation of your .cscratch
/.dbark
file on Code’s side.
The potential edited state of the “virtual document”, holding any edits you made that deviate from the state of the document since last save
The state of the data that Svelte holds (as json
in this case) that represents the state of the virtual document at a given moment.
Messing up one of these will make your editor not really functional, so they all need to be accounted for and mirror each other where relevant. I’m sure there is a better way to do what I’m doing here, but I’ve essentially opted to try and bootstrap the data flow in a way that makes sense to me and is easy to trace. So let’s dive in!
Once we catch that init-view
event in the extension, we’re using webviewPanel.webview.postMessage
to send the current document state back to Svelte. However, our Svelte template code is not yet setup to receive messages from the webview. We can do this in Svelte by adding a this small line of code:
<svelte:window on:message={windowMessage}/>
To the markup section of our App.svelte
component:
<script>
import { onMount } from 'svelte';
onMount(() => {
vscode.postMessage({
type: 'init-view',
});
});
//window event handler
function windowMessage(event) {}
</script>
<svelte:window on:message={windowMessage}/>
<h1>Hello World!</h1>
This is functionality Svelte offers to allow us to receive any messages from the window (/webview) and route them to a handler function (in this case, windowMessage
).
Inside the function, we’ll now catch both the init
event we setup, as well as the update
event called by the extension when the document changes. Here’s the whole code first, then I’ll walk through each bit:
let dataType = "";
let jsonData = {};
function windowMessage(event) {
const message = event.data; // The json data that the extension sent
switch (message.type) {
case 'init':
//the extension is sending us an init event with the document text
//note: this is the document NOT the state, the state takes precendece, so if any state exists use that instead
const state = vscode.getState();
if (state) {
//we push this state from the vscode workspace to the JSON this component is looking at
jsonData = JSON.parse(state.text);
}
else {
//use the state data
jsonData = JSON.parse(message.text);
}
dataType = message.dataType;
return;
case 'update':
//assign data
const text = message.text;
jsonData = JSON.parse(text);
// assign state
vscode.setState({ text });
return;
}
}
First we define two variables. One to hold the dataType, used to switch what we’re showing in Svelte, and one to hold the actual parsed JSON data.
let dataType = "";
let jsonData = {};
You can see here how, if you had two different files that weren’t represented by the same general filetype (json, csv, etc.), that you may want a different editor for them.
const state = vscode.getState();
Inside the actual callback function, inside of the init
route, we first get the state of the virtual document. Remember, this editor is invoked upon opening the requisite file, but because Code can have multiple windows open for the same file, it’s possible that we’re opening up a file that current has working changing in its virtual document. This allows us to retrieve those changes before doing anything.
if (state) {
jsonData = JSON.parse(state.text);
}
else {
jsonData = JSON.parse(message.text);
}
If there is any pre-existing state, we just assign the jsonData
to be that state. Otherwise, we use the data passed to us as the state.
dataType = message.jsonType;
Lastly we update our dataType
. Note this is only called once, on init
.
const text = message.text;
jsonData = JSON.parse(text);
vscode.setState({ text });
For the update route, because the document is only calling this if it was updated, we can just accept the sent data and assign that to the state. We grab the sent data, parse it into json, and assign the message.text
as the state of our document.
Theoretically our data is now coming into Svelte, but let’s make sure. We can add a small line in the markup section to just spit out whatever json we may or may not have. Here’s the full App.svelte after that change:
<script>
//add in onMount
import { onMount } from 'svelte';
//attach a function to onMount
onMount(() => {
vscode.postMessage({
type: 'init-view',
});
});
let dataType = "";
let jsonData = {};
function windowMessage(event) {
const message = event.data; // The json data that the extension sent
switch (message.type) {
case 'init':
//the extension is sending us an init event with the document text
//note: this is the document NOT the state, the state takes precendece, so if any state exists use that instead
const state = vscode.getState();
if (state) {
//we push this state from the vscode workspace to the JSON this component is looking at
jsonData = JSON.parse(state.text);
}
else {
//use the state data
jsonData = JSON.parse(message.text);
}
dataType = message.dataType;
return;
case 'update':
//assign data
const text = message.text;
jsonData = JSON.parse(text);
// assign state
vscode.setState({ text });
return;
}
}
</script>
<svelte:window on:message={windowMessage}/>
<h1>Hello World!</h1>
<pre>{JSON.stringify({jsonData},null,2)}</pre>
If we run it, we should expect to see the Hello World text, and then, depending on the open editor, some data dumped below it. Let’s see…
Awesome! It’s working for both scratches and barks! We can also see that the .pawDraw editor is also still intact:
I want to focus on the .cscratch
file from now on, but before we say goodbye to the .dbark
file, let’s give it its own special editor. Using the #if
directive in Svelte, we can now easily change what is being displayed by switching between data types once the data is loaded:
<svelte:window on:message={windowMessage}/>
{#if dataType == ""}
<p>Loading</p>
{:else if dataType === "dbark"}
<h1>WOOF</h1>
{:else if dataType === "cscratch"}
<h1>MEOW</h1>
{/if}
<h1>Hello World!</h1>
<pre>{JSON.stringify({jsonData},null,2)}</pre>
Note above that we can also catch the case where the dataType
isn’t loaded yet, meaning the init
call hasn’t finished. This is one of those areas where I’m assuming there is a better way to do this, but for now, 🤷♂️.
Running the extension again:
Great! Now, so long .dbark
, I barely even knew you.
So we’ve now got data flowing into Svelte. From here you could easily hook up the current data from the .cscratch
file to Svelte components and so on, but you’ll quickly get into a position where you’ll probably want to now only show the current data, but also edit it! Editing the data is the focus of the next part of this tutorial, which you can find here.
This is part of a series of posts where I discuss building a game data editor inside of Visual Studio Code.
Part 1: Why?
Part 2: Custom Editors, Webviews, and Svelte
Part 3: Getting Data into Svelte
Part 4: Editing Data in Svelte
Published on September 20, 2020.