Skip to main content

Introduction

Visual Studio Code’s extension system allows third-party developers to extend the editor’s functionality while maintaining stability and security. Extensions run in a separate process (the extension host) and communicate with the main VS Code process through a well-defined API.
Key files:
  • src/vs/workbench/services/extensions/common/extensions.ts - Extension service interfaces
  • src/vs/workbench/api/ - Extension API implementation
  • src/vs/workbench/services/extensions/ - Extension host management

Architecture Overview

Process Isolation

Extensions run in a separate process for several reasons:
If an extension crashes, it doesn’t take down the entire application:
// From src/vs/workbench/services/extensions/common/extensions.ts:122
export interface IExtensionHost {
  readonly pid: number | null;
  readonly onExit: Event<[number, string | null]>;
  
  start(): Promise<IMessagePassingProtocol>;
  dispose(): void;
}
The workbench can detect when an extension host exits and restart it if necessary.
Extensions can perform CPU-intensive operations without blocking the UI:
  • Heavy computations
  • File system operations
  • Network requests
The UI remains responsive because it runs in a different process.
Extensions have limited access to system resources:
  • No direct DOM access
  • Controlled file system access through APIs
  • Cannot interfere with other extensions
  • API surface is explicitly defined and versioned

Extension Manifest

Every extension has a package.json that describes its capabilities:
{
  "name": "my-extension",
  "publisher": "publisher-name",
  "version": "1.0.0",
  "engines": {
    "vscode": "^1.80.0"
  },
  "activationEvents": [
    "onLanguage:typescript",
    "onCommand:myExtension.doSomething"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [{
      "command": "myExtension.doSomething",
      "title": "Do Something"
    }],
    "languages": [{
      "id": "mylang",
      "extensions": [".mylang"]
    }],
    "menus": {
      "editor/context": [{
        "command": "myExtension.doSomething",
        "when": "editorLangId == typescript"
      }]
    }
  }
}

Activation Events

Extensions are lazily activated based on activation events:
"activationEvents": [
  "onLanguage:typescript"
]
Activates when a file of the specified language is opened.

Extension Lifecycle

1

Discovery

VS Code scans for extensions in:
  • User extensions directory (~/.vscode/extensions)
  • Built-in extensions (extensions/ in installation)
  • Workspace extensions (.vscode/extensions in workspace)
// Extension metadata is loaded
export interface IExtensionDescription {
  readonly identifier: ExtensionIdentifier;
  readonly name: string;
  readonly version: string;
  readonly publisher: string;
  readonly engines: { vscode: string };
  readonly extensionLocation: URI;
  readonly isBuiltin: boolean;
  // ... more properties
}
2

Registration

Extension contributions are registered with VS Code:
// From src/vs/workbench/services/extensions/common/extensions.ts:38
export const IExtensionService = createDecorator<IExtensionService>('extensionService');

export interface IExtensionService {
  // All registered extensions
  readonly extensions: readonly IExtensionDescription[];
  
  // Event when extensions are registered
  readonly onDidRegisterExtensions: Event<void>;
  
  // Activate an extension
  activateByEvent(activationEvent: string): Promise<void>;
}
3

Activation

When an activation event fires:
// 1. Extension host loads the extension module
const extensionModule = await import(extensionPath);

// 2. Call the activate function
const api = await extensionModule.activate(context);

// 3. Track activation
interface ActivationTimes {
  readonly codeLoadingTime: number;
  readonly activateCallTime: number;
  readonly activateResolvedTime: number;
}
The extension’s activate function is called with a context:
export function activate(context: vscode.ExtensionContext) {
  console.log('Extension activated!');
  
  // Register commands
  const disposable = vscode.commands.registerCommand(
    'myExtension.doSomething',
    () => {
      vscode.window.showInformationMessage('Hello!');
    }
  );
  
  context.subscriptions.push(disposable);
  
  // Return public API (optional)
  return {
    doSomething() {
      // Public API for other extensions
    }
  };
}
4

Deactivation

When VS Code shuts down or the extension is disabled:
export function deactivate() {
  // Clean up resources
  // Dispose of subscriptions
  // Close connections
}
Resources registered with context.subscriptions are automatically disposed.

Extension Host

Extension Host Types

VS Code can run multiple extension hosts:
// From src/vs/workbench/services/extensions/common/extensionHostKind.ts
export enum ExtensionHostKind {
  LocalProcess = 1,      // Separate Node.js process
  LocalWebWorker = 2,    // Web Worker in browser
  Remote = 3             // Remote machine
}
Default for desktop VS Code:
  • Runs in a separate Node.js process
  • Full Node.js API access
  • Best performance for compute-intensive tasks
// Extensions run in this host by default
{
  "main": "./out/extension.js"
}

Extension Host Communication

Extensions communicate with VS Code through RPC (Remote Procedure Call):
// Extension makes an API call
const document = await vscode.workspace.openTextDocument(uri);

// This translates to:
// 1. Serialize the request
// 2. Send message to main thread via IPC
// 3. Main thread performs operation
// 4. Send result back to extension host
// 5. Deserialize and return to extension

VS Code API

The VS Code API is the only interface extensions have to VS Code:

Core Namespaces

commands

Register and execute commands
vscode.commands.registerCommand(
  'myExt.cmd',
  () => { /* ... */ }
);

window

Interact with the editor window
vscode.window.showInformationMessage('Hi');
vscode.window.createOutputChannel('My Output');

workspace

Access workspace files and settings
const config = vscode.workspace.getConfiguration();
const files = await vscode.workspace.findFiles('**/*.ts');

languages

Register language features
vscode.languages.registerCompletionItemProvider(
  'typescript',
  completionProvider
);

debug

Debugging support
vscode.debug.startDebugging(
  workspace,
  debugConfig
);

extensions

Access other extensions
const ext = vscode.extensions.getExtension('pub.ext');
const api = ext?.exports;

API Implementation

The API is implemented in src/vs/workbench/api/:
// From src/vs/workbench/api/common/extHostCommands.ts
export class ExtHostCommands {
  private readonly _commands = new Map<string, Function>();
  
  registerCommand(
    id: string,
    callback: <T>(...args: any[]) => T | Thenable<T>
  ): vscode.Disposable {
    this._commands.set(id, callback);
    
    // Tell main thread about the command
    this._proxy.$registerCommand(id);
    
    return {
      dispose: () => {
        this._commands.delete(id);
        this._proxy.$unregisterCommand(id);
      }
    };
  }
  
  executeCommand<T>(id: string, ...args: any[]): Thenable<T> {
    // Check if it's an extension command
    const command = this._commands.get(id);
    if (command) {
      return Promise.resolve(command(...args));
    }
    
    // Ask main thread to execute
    return this._proxy.$executeCommand(id, args);
  }
}

Extension Contributions

Extensions extend VS Code through contribution points defined in package.json:

Commands

{
  "contributes": {
    "commands": [{
      "command": "myExtension.action",
      "title": "My Action",
      "category": "My Extension",
      "icon": "$(rocket)"
    }]
  }
}
// In extension code
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('myExtension.action', () => {
      vscode.window.showInformationMessage('Action executed!');
    })
  );
}
{
  "contributes": {
    "menus": {
      "editor/context": [{
        "command": "myExtension.action",
        "when": "editorLangId == typescript",
        "group": "navigation"
      }],
      "explorer/context": [{
        "command": "myExtension.action",
        "when": "resourceExtname == .ts"
      }]
    }
  }
}

Languages

{
  "contributes": {
    "languages": [{
      "id": "mylang",
      "aliases": ["MyLang", "mylang"],
      "extensions": [".mylang"],
      "configuration": "./language-configuration.json"
    }],
    "grammars": [{
      "language": "mylang",
      "scopeName": "source.mylang",
      "path": "./syntaxes/mylang.tmLanguage.json"
    }]
  }
}

Views

{
  "contributes": {
    "views": {
      "explorer": [{
        "id": "myView",
        "name": "My View"
      }]
    },
    "viewsContainers": {
      "activitybar": [{
        "id": "myContainer",
        "title": "My Container",
        "icon": "resources/icon.svg"
      }]
    }
  }
}
// Register tree view provider
const treeDataProvider = new MyTreeDataProvider();
vscode.window.registerTreeDataProvider('myView', treeDataProvider);

Extension Dependencies

Extensions can depend on other extensions:
{
  "extensionDependencies": [
    "publisher.extension-id"
  ]
}
Dependencies are activated before the dependent extension:
// From src/vs/workbench/services/extensions/common/extensions.ts:56
export class MissingExtensionDependency {
  constructor(readonly dependency: string) { }
}
If a dependency is missing, the extension won’t activate.

Extension API Export

Extensions can expose APIs to other extensions:
// In providing extension
export function activate(context: vscode.ExtensionContext) {
  // Return API
  return {
    doSomething() {
      return 'result';
    }
  };
}

// In consuming extension
export async function activate(context: vscode.ExtensionContext) {
  const ext = vscode.extensions.getExtension('publisher.provider');
  if (!ext) {
    throw new Error('Required extension not found');
  }
  
  await ext.activate();
  const api = ext.exports;
  const result = api.doSomething();
}

Extension Performance

Activation Time Tracking

// From src/vs/workbench/services/extensions/common/extensions.ts:348
export class ActivationTimes {
  constructor(
    public readonly codeLoadingTime: number,
    public readonly activateCallTime: number,
    public readonly activateResolvedTime: number,
    public readonly activationReason: ExtensionActivationReason
  ) {}
}
View extension performance with the Developer: Show Running Extensions command.

Best Practices for Performance

Use specific activation events, not *:
// Good
"activationEvents": [
  "onLanguage:typescript",
  "onCommand:myExtension.action"
]

// Bad
"activationEvents": ["*"]
Defer expensive operations:
// Good: Lazy load
let expensiveModule: any;

async function useExpensiveModule() {
  if (!expensiveModule) {
    expensiveModule = await import('./expensive');
  }
  return expensiveModule.doSomething();
}

// Bad: Load everything at activation
import * as expensive from './expensive';

export function activate(context: vscode.ExtensionContext) {
  expensive.initialize(); // Blocks activation
}
Don’t block activation with async work:
// Good
export function activate(context: vscode.ExtensionContext) {
  // Register quickly
  context.subscriptions.push(
    vscode.commands.registerCommand('myCmd', async () => {
      // Async work happens when command is executed
      await doExpensiveWork();
    })
  );
  
  // Start background work without awaiting
  initializeAsync().catch(console.error);
}

// Bad
export async function activate(context: vscode.ExtensionContext) {
  await loadEverything(); // Blocks extension host
  await connectToServer();
  await downloadData();
}
Clean up properly:
export function activate(context: vscode.ExtensionContext) {
  const watcher = vscode.workspace.createFileSystemWatcher('**/*.ts');
  
  // Register for disposal
  context.subscriptions.push(watcher);
  
  // Or manually manage
  const disposable = vscode.window.onDidChangeActiveTextEditor(editor => {
    // ...
  });
  
  context.subscriptions.push(disposable);
}

Extension Testing

Extensions can be tested using VS Code’s extension test runner:
// src/test/suite/extension.test.ts
import * as assert from 'assert';
import * as vscode from 'vscode';

suite('Extension Test Suite', () => {
  test('Sample test', async () => {
    const ext = vscode.extensions.getExtension('publisher.extension-id');
    assert.ok(ext);
    
    await ext.activate();
    
    await vscode.commands.executeCommand('myExtension.action');
    
    // Assert results
  });
});

Built-in Extensions

VS Code ships with built-in extensions in the extensions/ directory:
extensions/
├── typescript-language-features/   # TypeScript/JavaScript support
├── html-language-features/         # HTML support
├── css-language-features/          # CSS support
├── git/                            # Git integration
├── markdown-language-features/     # Markdown support
├── emmet/                          # Emmet abbreviations
└── theme-*/                        # Built-in themes
Built-in extensions use the same extension API as third-party extensions:
// From src/vs/workbench/services/extensions/common/extensions.ts:21
export const nullExtensionDescription = Object.freeze<IExtensionDescription>({
  identifier: new ExtensionIdentifier('nullExtensionDescription'),
  name: 'Null Extension Description',
  version: '0.0.0',
  publisher: 'vscode',
  engines: { vscode: '' },
  extensionLocation: URI.parse('void:location'),
  isBuiltin: false,
  // ...
});

Extension API Versioning

Extensions declare compatible VS Code versions:
{
  "engines": {
    "vscode": "^1.80.0"
  }
}
The ^1.80.0 means compatible with VS Code 1.80.0 and newer.
The extension API is backwards compatible. New APIs are added, but existing APIs are never removed or changed in breaking ways.

Proposed APIs

New APIs start as proposed and require explicit opt-in:
{
  "enabledApiProposals": [
    "myProposalName"
  ]
}
// From src/vs/workbench/services/extensions/common/extensions.ts:323
export function isProposedApiEnabled(
  extension: IExtensionDescription,
  proposal: ApiProposalName
): boolean {
  if (!extension.enabledApiProposals) {
    return false;
  }
  return extension.enabledApiProposals.includes(proposal);
}
Proposed APIs can change or be removed. They should only be used for experimentation and feedback.

Next Steps

Extension API Docs

Official VS Code Extension API documentation

Contribution Model

How internal features use the same contribution system

Dependency Injection

How services are managed in VS Code

Architecture Overview

High-level architecture overview

Key Takeaways

  • Extensions run in a separate process for stability and security
  • Extensions are lazily activated based on events
  • The VS Code API is the only interface between extensions and the core
  • RPC communication bridges the extension host and main process
  • Built-in and third-party extensions use the same API and contribution points
  • The API is backwards compatible and versioned