Writing Visual Studio Code Extensions

Hi! It looks like this post has since been updated or rethought in some ways, so you may want to look at this after you're done reading here.

VSCodium

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—I’ve been looking at what it takes to develop an extension for Visual Studio Code, VSCode Rat.

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

Run VSCode with the extension

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 to the right)—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.

Hello, back

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 the blog.

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, this post is getting long enough that it’s probably a better idea to save that configuration 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.


By commenting, you agree to follow the blog's Code of Conduct and that your comment is released under the same license as the rest of the blog.

Tags:   techtips   programming   vscode

Sign up for My Newsletter!

Get monthly (or thereabouts) notifications on Entropy Arbitrage posts, additional reading of interest, random thoughts, and previews of upcoming projects, delivered right to your inbox. I won’t share your information or use it for anything else.
You can view previous issues. Unless it's still June 2020, in which case you can only see test messages.
If you disable trackers (like I do), this form won’t work, so you’ll need to sign up on Mailchimp's site. Sorry!