Visual Studio Code Extension Development Kit
October 1, 2024
A few months ago, I started working on a Visual Studio Code extension, and the experience has been really interesting. The extension API, much like the editor itself, is remarkably well-designed. It’s flexible yet rigid1, well-documented and effectively highlights what makes one of the most extensible pieces of software out there.
In addition to excellent documentation, the official microsoft/vscode repository also includes built-in extensions for things like Git, Jupyter Notebooks, and TypeScript. Delving into these will give one an idea of how polished extensions should be crafted. I found them incredibly helpful.
After finishing the first extension, I started to work on another one and realized I had to implement a lot of the same functionalities in both. This prompted me to put together a little library called VEDK, which stands for VS Code Extension Development Kit.
With only around 500 lines of code, the library primarily simplifies constructor-based dependency injection using Reflection
. It also features wrappers upon existing extension APIs to make life easier when using them through DI. For instance, ContextKeys
class lets you save and persist context keys and restore them when the editor starts.
The idea is simple: encapsulate features and contribution points into singleton classes. The following is a simple extension command implementation.
import * as vscode from "vscode";
import { Injectable, Extension, GlobalState } from "vedk";
@Injectable()
export class SayHelloCommand implements vscode.Disposable {
private readonly disposable: vscode.Disposable;
constructor() {
this.disposable = vscode.commands.registerCommand(
"extension.sayHello",
this.run,
this
);
}
async run() {
await vscode.window.showInformationMessage("Hello");
}
dispose() {
this.disposable.dispose();
}
}
Then, pass these entries through the Extension
class, which registers them into an IoC container.
const extension = new Extension({
entries: [SayHelloCommand],
});
export async function activate(context: vscode.ExtensionContext) {
await extension.activate(context);
}
This lets you acquire @Injectable()
dependencies via the constructor. In another command, you could do:
@Injectable()
export class GreetAndRememberCommand {
private readonly disposable: vscode.Disposable;
constructor(
private readonly sayHello: SayHelloCommand,
private readonly globalState: GlobalState
) {
this.disposable = vscode.commands.registerCommand(
"extension.greet",
this.run,
this
);
}
async run() {
await this.sayHello.run();
await this.globalState.update("greeted", true);
}
dispose() {
this.disposable.dispose();
}
}
If you’ve worked with Angular or NestJS, it should feel familiar. It’s basically a lightweight DI solution specifically for VS Code, which makes sense because of how features in VS Code are expected to be encapsulated and dispose resources.
If you dig into VS Code’s source code or its built-in extensions, you’ll see this pattern being used everywhere. Decorators are all over the place! It’s a practical approach and VEDK tries to make it easier to adopt.
Removing VEDK is as easy as removing Injectable
decorators and replacing the IoC logic with manual DI or another framework. This is different from libraries like reactive-vscode, which try to do too much and end up making your extension overly dependent on them.
VEDK only requires you to have reflect-metadata installed, and has zero dependencies. It should work in any VS Code like environment on all platforms including the web2.
Footnotes
For example, you can add a custom FileDecoration Provider to customize file explorer, but decoration text must be “very short” to keep the UI sane. ↩
Including but not limited to GitHub Codespaces, VSCodium, and Cursor. ↩