Writing Visual Studio Code Extensions
John Colagioia (he/him)
Posted on July 8, 2020
As a quick note, I released this post on my blog, this morning, so it can get to be (as I tend to be) a bit rambling. Oh, and the original text is on GitHub (licensed CC-BY-SA), so if anything seems muddy, by all means:
- Leave a comment here,
- Leave a comment on the blog,
- File an issue on GitHub, or
- Add a pull request!
jcolag / entropy-arbitrage
John Colagioia's Blog Posts
Similar to the work on creating a browser extension such as URL Rat—part 1 and part 2 for what it took to make those work...
Writing Browser Extensions
John Colagioia (he/him) ・ Jun 18 '20 ・ 5 min read
Writing Browser Extensions with Configuration
John Colagioia (he/him) ・ Jun 26 '20 ・ 5 min read
I’ve been looking at what it takes to develop an extension for Visual Studio Code, VSCode Rat.
jcolag / vscode-rat
An extension (that nobody should use) to Visual Studio Code to "rat out" what the user is working on
Setup
The easy way to get moving is to use the Yeoman scaffolding tool to generate the base code for you.
$ npm install --global yo generator-code
$ yo code
The yeoman (that’s the ASCII art fellow) will then ask for some basic information:
- The kind of extension: A normal extension, color theme, language, snippet, key map, or pack of extensions;
- The extension’s name;
- The extension’s ID;
- The extension’s description;
- Whether you want type-checking in the configuration file;
- Whether you need a git repository; and
- Your preferred package manager.
This list of questions might change by the time you read this, but it should give you an idea of what to expect, regardless.
The file that matters most, of course, is extension.js
. There’s also jsconfig.json
, which you can look at and—at least for now—confirm that it’s mostly just the answers you gave to the questions.
Packaging
If you’re not familiar with Node.js projects, you’ll also want to get familiar with package.json
. You rarely need to update it manually, but it’s good to know what’s lurking in there. Whether you know this file or not, the new aspects are:
-
activationEvents
: The term is not subtle, referring to the list of events that activate the extension’s code. Visual Studio Code won’t load the extension until one of those events occurs. The default is a custom “Hello, World” event. -
commands
: Here, you’ll find a list of commands the user can run to operate the extension. The default—probably no surprise, given the custom event’s name—is a “Hello, World!” command.
You can probably guess that those are going to be important.
Try It
The easy way to test the extension is to open the folder in Visual Studio Code for editing and use the existing scripts to launch a new instance of Code with your extension loaded. To do that, click the “Run” button on the left (#1 on the picture below)—you can also type Ctrl+Shift+D—to get to the run/debug commands, then click the green Start Debugging arrow, marked #2 in the image.
This will start a second Visual Studio Code window. From here, you can open the Command Palette (View/Command Palette… or Ctrl+Shift+P) and, as you start typing Hello
, you’ll see the new “Hello World” command listed. Select it and you’ll get a little toast notification, fulfilling the extension’s registered action.
So, that’s what we get without doing any work.
Monitoring Files
So, now we need to fill in real code.
The first step is going to be to give our extension a reason to run other than a manual command. Microsoft has provided a list of activation events, but as mentioned, that tells Visual Studio Code when to load the extension, not when to run the extension.
In our case, since we want to monitor what our user is doing—just like with URL Rat, please don’t try to use this in production, because it’s a security nightmare—we want to load the extension when the editor starts, so that just looks like this:
"activationEvents": [
"*"
],
As in, change the activationEvents
list in package.json
to that.
Then, take a look at extension.js
. The only code that looks important is activate()
, which registers and subscribes to the command. So, we need to make our changes, there, subscribing to events that are relevant. Try something like the following in that activate()
function:
vscode.workspace.onDidSaveTextDocument((document) => {
const file = document.fileName;
vscode.window.showInformationMessage(`Saved ${file}`);
});
vscode.workspace.onDidOpenTextDocument((document) => {
const file = document.fileName;
vscode.window.showInformationMessage(`Opened ${file}`);
});
vscode.workspace.onDidChangeTextDocument((changes) => {
const file = changes.document.fileName;
vscode.window.showInformationMessage(`Modified ${file}`);
});
This is extremely primitive, but it points us in the right direction. Rather than pop up the static message when the user asks for it, we now pop up messages when files are opened, saved, and modified.
Sending to a Server
Unfortunately, the version of JavaScript supported by Visual Studio Code doesn’t appear to support fetch()
, so we can’t pull off the same trick we did with URL Rat. Instead, we need a function that looks something like this one, conveniently licensed CC-BY-SA 4.0, like the rest of my blog.
Simple and dependency-free. Uses a Promise so that you can await the result. It returns the response body and does not check the response status code.
const https = require('https')
function httpsPost({body, ...options}) {
return new Promise((resolve,reject) => {
const req = https.request({
method: 'POST',
...options,
}, res => {
…
function httpPost({body, ...options}) {
return new Promise((resolve,reject) => {
const req = http.request({
method: 'POST',
...options,
}, res => {
const chunks = [];
res.on('data', data => chunks.push(data))
res.on('end', () => {
let body = Buffer.concat(chunks);
switch(res.headers['content-type']) {
case 'application/json':
body = JSON.parse(body.toString());
break;
}
resolve(body)
})
})
req.on('error',reject);
if(body) {
req.write(body);
}
req.end();
})
}
Then, we can replace our vscode.window.showInformationMessage()
function calls with something like the following:
httpPost({
body: `Opened ${document.fileName}`,
headers: {
'Content-Type': 'text/plain',
},
hostname: 'localhost',
path: '/'
});
The verb used in the string depends on which action we’re talking about, of course. In addition, we might want to do something more with the change actions. When the files change, the object contains the same filename information as the other actions, but also includes enough information to describe the change, including the replacement text, the file offset where the change occurs, and the start and end line and column of the change.
With that information, we can collect the incremental changes made in Visual Studio Code over time, which would make it easy to reconstruct ideas that were either deleted and forgotten or lost in a crash.
What’s Left?
Like with the “first draft” of URL Rat , you probably noticed that VSCode Rat uses a hard-coded URL, whereas we would obviously want to be able to configure that information, just like we would with any other extension. And just like with the browser extension posts, this post is getting long enough that it’s probably a better idea to save that configuration discussion for next week.
Otherwise, it seems surprisingly difficult to know what events we can subscribe to and what they do (it took a long time to track down vscode.workspace.onDidSaveTextDocument()
, probably longer than it should have), but once you know them, the rest of the development is remarkably fast.
Credits : The header image is the sample image for VSCodium and made available (like the application and their entire site) under the terms of the MIT License.
Posted on July 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.