Icon for gfsd IntelliJ IDEA

Getting started with Visual Studio Code extension development

Person building a dog house next to a real house, symbolic for building an extension.

Most developers have used Visual Studio Code at some point in their careers. It has been voted the most popular IDE of 2021 by respondents of the StackOverflow developer survey, and its appeal is clear as ever. While the base program provides a framework for an IDE, all of the language support and special features are delivered as extensions, which makes it easy to extend and customize your installation. So inevitably, there comes a point where you want to write your own extension. This guide will walk you through the basics of getting started with Visual Studio Code extension development.

What is a VS Code extension?

Before we jump into coding, we should clear up what a VS Code extension is on a technical level. Extensions are basically programs, written in JavaScript or TypeScript, which hook into various parts of VS Code. They provide functions for VS Code to call when certain events happen, and can programmatically interact with (some parts of) VS Code in those functions.

Extensions are distributed as ZIP files with a specific file and folder structure inside. The files contained in this structure are usually very verbose and not friendly for humans to read or write so there’s an official build tool to generate such ZIP files from source code: vsce. Its usage will be explained later on in this post.

Development is best done in VS Code itself. It supports TypeScript out of the box and comes with special tools to run and debug your extension in another instance. In principle, other editors would work as well, but you should have VS Code ready for running and testing your extension either way.

Getting started with VS Code extensions

For starters, let’s install some command-line tools for development:

npm install --global yo generator-code vsce

…and set up our project.

$ yo code

     _-----_     ╭──────────────────────────╮
    |       |    │   Welcome to the Visual  │
    |--(o)--|    │   Studio Code Extension  │
   `---------´   │        generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? hello-world
? What's the identifier of your extension? hello-world
? What's the description of your extension?
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm

Writing in /src/hello-world...
[...]

Choose “New Extension (TypeScript)” and enter your extension’s details. You can always change these settings later. Optionally, initialize a Git repository and accept the default of “No” for “Bundle the source code with webpack?”. Select the package manager on your system (most likely “npm”). After that, open the newly created folder in your editor of choice and open src/extension.ts.

This is the entry point of your extension. VS Code will evaluate this file when loading your extension — but make sure you don’t put your initialization code directly in the top-level scope of the script!

A special function called activate is intended for setup code, and is called by VS Code when an extension is first “needed” after being deactivated, freshly installed, or after VS Code was started. “Needed” in this case means that one of several Activation Events has been triggered. The generated example code demonstrates this with a command Activation Event, but we’ll also explore another way to start your extension later.

Running a VS Code extension in development mode

Let’s have a look at the generated demo code in action! As mentioned before, it registers a command which can be run in the command launcher (Ctrl+Shift+P by default), so let’s try that now.

If you’re already in VS Code, go to the “Run & Debug” tab in the leftmost sidebar. Select the “Run Extension” launch configuration in the dropdown next to the green “Run” button. Then press the “Run” button (or F5).

"Run" dropdown in "Run and Debug" menu of VS Code.

If you’re not working from VS Code, run

code --extensionDevelopmentPath=$PWD

…from your shell. Note that the path given to --extensionDevelopmentPath has to be absolute.

VS Code will open, either with no workspace folder at all or with a recently opened workspace. Next, just press Ctrl+Shift+P and type “hello world”. A new command called “Hello World” should show up. Select it, hit Enter and a notification should appear.

Development instance of VS Code, executing the "Hello World" command from the generated demo code.

Checking back with the code, we can clearly see how this is implemented. The call to registerCommand tells VS Code what to do when the “Hello World” command is executed. However, this just provides the implementation. The definition of our command lives in the package.json file, under the contributes section.

"contributes": {
  "commands": [
    {
      "command": "hello-world.helloWorld",
      "title": "Hello World"
    }
  ]
},

A lot of extension functionality is defined in contributes: language support, settings, commands and more. These definitions are referred to as “Contribution Points”.

Back in extension.ts, we can see that the return value from registerCommand is pushed onto context.subscriptions. What’s that all about? “Subscriptions” might be a bit misleading here. More commonly, VS Code uses the term “Disposable”. Let’s check the docs.

“Represents a type which can release resources, such as event listening or a timer.”

Okay cool. TL;DR: most of the time, Disposables represent something that can be “stopped” or canceled (for example, providing a function to call when a command is invoked, as shown in the demo code). When your extension deactivates, context.subscriptions calls dispose on the Disposables pushed onto it, which makes it a handy tool for managing lifetime-scoped Disposables (like command handlers).

Exploring the VS Code extension API

Time to add some features. Let’s display a notification when a file is saved. It’s pretty simple: We just have to register an event listener. Since the event is related to workspaces (think editors and files), we find its handle in vscode.workspaces. onDidSaveTextDocument seems appropriate, so let’s just call it from inside the activate function:

disposable = vscode.workspace.onDidSaveTextDocument((evt) => {
  vscode.window.showInformationMessage(`Saved ${evt.fileName}`);
});

context.subscriptions.push(disposable);

Since the event listener — much like a command handler — is a “continuous thing” that can be “stopped” the registration function returns a Disposable which we have to handle. Pushing it into context.subscriptions is a good fit here since we never want to stop listening for save events while our extension is active.

Alright, let’s run that. Just press F5 to launch the last configuration again, open a text document, save and… oh no. Nothing’s happening! The problem is an easy one: our extension has not been activated yet. Remember Activation Events? As mentioned before, our extension is currently only command-activated. If you run the “Hello World” command, then try saving again, a notification should appear as expected.

We can see the configuration responsible for that in the package.json file under activationEvents.

"activationEvents": [
  "onCommand:hello-world.helloWorld"
],

Currently, only one Activation Event is registered called onCommand:hello-world.helloWorld. This event fires when the “Hello World” command is executed. Since we would like to listen to all file save events without first having to run a command, let’s replace the whole onCommand[…] string with onStartupFinished, which fires right after VS Code has started.

"activationEvents": [
  "onStartupFinished"
],

In general, you should aim for more specific Activation Events. Less extensions to start at once makes VS Code start up faster.

Now, let’s restart our launch configuration, open a file in the development host and save. Our extension finally displays a notification! By the way, if you leave the “Extension Development” instance of VS Code open while making changes, you can also press Ctrl+R to reload the window and try your changes instantly.

Let’s add a status bar item. TL;DRtD (too long, didn’t read the docs) this is the code:

disposable = vscode.window.setStatusBarMessage('Never saved anything');
context.subscriptions.push(disposable);

disposable = vscode.workspace.onDidSaveTextDocument((evt) => {
  const disposable = vscode.window.setStatusBarMessage(`Saved ${evt.fileName} at ${Date.now()}`);
  context.subscriptions.push(disposable);
});

context.subscriptions.push(disposable);

Just replace what we added for onDidSaveTextDocument before.

Status bar of VS Code showing a custom message added by our extension.

The status bar is part of the window, so we find its functionality in vscode.window. Makes sense! Status bar items are Disposables. Why? If you think about it: Status bar items can disappear, so it makes sense to use the Disposable interface here. We’ll just handle them via context.subscriptions again.

One thing to note from the docs:

Note that status bar messages stack and that they must be disposed when no longer used.

They stack? Well, if we add a timeout to the “saved” status bar messages only, we can see this in action. Just pass a number as the second parameter to the call.

vscode.window.setStatusBarMessage(`Saved ${evt.fileName} at ${Date.now()}`, 1000);

“Saved” messages will disappear after one second to reveal the message below (down to “Never saved anything”). This function pushes status bar messages onto a stack.

Building and installing a VS Code extension

Okay, that was enough about development workflows and general concepts. Let’s finally build that special ZIP file mentioned in the beginning so you can actually install and use your extension. Open your extension’s source directory in a terminal and run vsce package.

vsce package
Executing prepublish script 'npm run vscode:prepublish'...

> hello-world@0.0.1 vscode:prepublish /src/hello-world
> npm run compile


> hello-world@0.0.1 compile /src/hello-world
> tsc -p ./

ERROR  Make sure to edit the README.md file before you package or publish your extension.

Okay, apparently vsce thinks we intended to publish the extension and forgot to change the default generated README. Thanks. I like to resolve this situation with a quick echo this is not useful > README.md but you’re welcome to write a more useful README.

After this, we just re-run vsce package. This command will also display some actually helpful warnings (which you can just ignore and continue anyways). Afterwards, you get a *.vsix file. That’s the special ZIP file we mentioned, which you can open with a ZIP archive browser to explore its contents.

Installing it into your main copy of VS Code is also pretty easy: On the command line, run code --install-extension ./hello-world-0.0.1.vsix. In the GUI, go to “Extensions” and click the three horizontal dots at the top of the left sidebar. Click “Install from VSIX…” and select your VSIX file.

Menu in the "Extensions" tab of VS Code with the mouse cursor hovering over "Install from VSIX…".

And that’s it! You’re now a Visual Studio Code extension author. For more in-depth information about developing extensions and API references, check out the official docs. For more VS Code guides from us, subscribe to our newsletter, and follow us on Twitter, Facebook, and LinkedIn. In the next blog posts we talk about publishing on the Marketplace, testing and our own super-advanced testing tools for VS Code extensions. Stay tuned!

Technical | 2022-01-26