Skip to main content

Introduction

Visual Studio Code’s contribution model is a registry-based system that allows features to extend the application dynamically. Both internal features and external extensions use the same mechanism, creating a uniform and powerful extensibility model.
Key files:
  • src/vs/platform/registry/common/platform.ts - Core registry system
  • src/vs/workbench/common/contributions.ts - Workbench contributions
  • src/vs/platform/actions/common/actions.ts - Action/menu contributions

Core Concepts

The Registry

The Registry is a global singleton that manages all contribution points:
// src/vs/platform/registry/common/platform.ts:63
export interface IRegistry {
  // Register a contribution to an extension point
  add(id: string, data: any): void;
  
  // Check if an extension point exists
  knows(id: string): boolean;
  
  // Access contributions for an extension point
  as<T>(id: string): T;
}

export const Registry: IRegistry = new RegistryImpl();

How It Works

1

Define Extension Point

An extension point is a registry entry that accepts contributions:
// Define the extension point interface
export interface IWorkbenchContributionsRegistry {
  registerWorkbenchContribution(
    ctor: IConstructorSignature<IWorkbenchContribution>,
    phase: LifecyclePhase
  ): void;
}

// Define the extension point ID
export namespace Extensions {
  export const Workbench = 'workbench.contributions.kind';
}

// Register the extension point
Registry.add(Extensions.Workbench, workbenchContributionsRegistry);
2

Contribute to Extension Point

Features register themselves with extension points:
// Get the registry
const registry = Registry.as<IWorkbenchContributionsRegistry>(
  Extensions.Workbench
);

// Register a contribution
registry.registerWorkbenchContribution(
  MyFeature,
  LifecyclePhase.Restored
);
3

Consume Contributions

The platform activates contributions at the appropriate time:
// From src/vs/workbench/browser/workbench.ts:162
Registry.as<IWorkbenchContributionsRegistry>(
  WorkbenchExtensions.Workbench
).start(accessor);

Workbench Contributions

Workbench contributions are the primary way features extend VS Code.

Registration

// src/vs/workbench/common/contributions.ts:420
import { registerWorkbenchContribution2 } from 'vs/workbench/common/contributions';

export class MyFeature extends Disposable implements IWorkbenchContribution {
  constructor(
    @ILogService private readonly logService: ILogService,
    @IConfigurationService private readonly configService: IConfigurationService
  ) {
    super();
    this.initialize();
  }
  
  private initialize(): void {
    this.logService.info('MyFeature initialized');
    
    // Register event listeners
    this._register(this.configService.onDidChangeConfiguration(e => {
      if (e.affectsConfiguration('myFeature')) {
        this.onConfigChanged();
      }
    }));
  }
  
  private onConfigChanged(): void {
    // Handle configuration changes
  }
}

// Register the contribution
registerWorkbenchContribution2(
  'myFeature',
  MyFeature,
  WorkbenchPhase.AfterRestored
);

Instantiation Phases

Phase: WorkbenchPhase.BlockStartup (LifecyclePhase.Starting)
registerWorkbenchContribution2(
  'criticalFeature',
  CriticalFeature,
  WorkbenchPhase.BlockStartup
);
Use very sparingly - blocks the editor from showing.Only use for features that:
  • Must initialize before anything else
  • Are critical to application startup
  • Cannot work if initialized later

Contribution Timing

From src/vs/workbench/common/contributions.ts:346:
private doInstantiateWhenIdle(
  contributions: IWorkbenchContributionRegistration[],
  instantiationService: IInstantiationService,
  logService: ILogService,
  environmentService: IEnvironmentService,
  phase: LifecyclePhase
): void {
  let i = 0;
  const forcedTimeout = phase === LifecyclePhase.Eventually ? 3000 : 500;
  
  const instantiateSome = (idle: IdleDeadline) => {
    while (i < contributions.length) {
      const contribution = contributions[i++];
      this.safeCreateContribution(
        instantiationService,
        logService,
        environmentService,
        contribution,
        phase
      );
      
      if (idle.timeRemaining() < 1) {
        // Time is up -> reschedule
        runWhenGlobalIdle(instantiateSome, forcedTimeout);
        break;
      }
    }
  };
  
  runWhenGlobalIdle(instantiateSome, forcedTimeout);
}
This ensures contributions don’t block the UI thread.

Action Contributions

Actions (commands, menus, keybindings) use a specialized contribution system.

Command Registration

import { CommandsRegistry } from 'vs/platform/commands/common/commands';

CommandsRegistry.registerCommand({
  id: 'myExtension.myCommand',
  handler: (accessor: ServicesAccessor, ...args: any[]) => {
    const logService = accessor.get(ILogService);
    logService.info('Command executed');
  }
});
// src/vs/platform/actions/common/actions.ts
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';

MenuRegistry.appendMenuItem(MenuId.EditorContext, {
  command: {
    id: 'myExtension.myCommand',
    title: 'My Command',
    icon: ThemeIcon.asThemeIcon(Codicon.run)
  },
  group: 'navigation',
  order: 1,
  when: ContextKeyExpr.equals('resourceLangId', 'typescript')
});
VS Code defines numerous menu locations. From src/vs/platform/actions/common/actions.ts:65:
export class MenuId {
  static readonly CommandPalette = new MenuId('CommandPalette');
  static readonly EditorContext = new MenuId('EditorContext');
  static readonly EditorTitle = new MenuId('EditorTitle');
  static readonly EditorTitleContext = new MenuId('EditorTitleContext');
  static readonly ExplorerContext = new MenuId('ExplorerContext');
  static readonly SCMTitle = new MenuId('SCMTitle');
  static readonly DebugToolBar = new MenuId('DebugToolBar');
  static readonly ViewTitle = new MenuId('ViewTitle');
  // ... many more
}

Keybinding Registration

import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';

KeybindingsRegistry.registerCommandAndKeybindingRule({
  id: 'myExtension.myCommand',
  weight: KeybindingWeight.WorkbenchContrib,
  when: ContextKeyExpr.equals('editorTextFocus', true),
  primary: KeyMod.CtrlCmd | KeyCode.KeyK,
  handler: (accessor: ServicesAccessor) => {
    // Command implementation
  }
});

Context Keys

Context keys enable conditional contributions based on application state:
import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';

// Define context key
const MyFeatureEnabled = new RawContextKey<boolean>(
  'myFeature.enabled',
  false,
  'Whether my feature is enabled'
);

// Set context key value
export class MyFeature {
  private readonly contextKey: IContextKey<boolean>;
  
  constructor(
    @IContextKeyService contextKeyService: IContextKeyService
  ) {
    this.contextKey = MyFeatureEnabled.bindTo(contextKeyService);
    this.contextKey.set(true);
  }
}

// Use in menu contributions
MenuRegistry.appendMenuItem(MenuId.EditorContext, {
  command: { id: 'myCommand', title: 'My Command' },
  when: ContextKeyExpr.equals('myFeature.enabled', true)
});

Context Key Expressions

import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';

// Simple equality
ContextKeyExpr.equals('editorLangId', 'typescript');

// Boolean check
ContextKeyExpr.has('editorTextFocus');

// Negation
ContextKeyExpr.not('editorReadonly');

// Complex expressions
ContextKeyExpr.and(
  ContextKeyExpr.equals('editorLangId', 'typescript'),
  ContextKeyExpr.has('editorTextFocus'),
  ContextKeyExpr.not('editorReadonly')
);

// OR expressions
ContextKeyExpr.or(
  ContextKeyExpr.equals('editorLangId', 'javascript'),
  ContextKeyExpr.equals('editorLangId', 'typescript')
);

Extension Points

Common Extension Points

Workbench Contributions

Main application featuresExtensions.Workbench

Editor Contributions

Editor-specific featuresEditorExtensions.EditorCommonContributions

Configuration

Settings and preferencesExtensions.Configuration

JSON Validation

JSON schema validatorsExtensions.JSONValidation

Language Features

Language supportExtensions.LanguageFeatures

Output Channels

Output panel channelsExtensions.OutputChannels

Example: Full Feature Contribution

Here’s a complete example of a feature using the contribution model:
// 1. Define interfaces and identifiers
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';

export const IMyFeatureService = createDecorator<IMyFeatureService>('myFeatureService');

export interface IMyFeatureService {
  readonly _serviceBrand: undefined;
  performAction(): Promise<void>;
}

// 2. Implement the service
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';

export class MyFeatureService extends Disposable implements IMyFeatureService {
  declare readonly _serviceBrand: undefined;
  
  constructor(
    @ILogService private readonly logService: ILogService
  ) {
    super();
  }
  
  async performAction(): Promise<void> {
    this.logService.info('Action performed');
  }
}

// 3. Register the service
import { registerSingleton, InstantiationType } from 'vs/platform/instantiation/common/extensions';

registerSingleton(
  IMyFeatureService,
  MyFeatureService,
  InstantiationType.Delayed
);

// 4. Create workbench contribution
import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';

class MyFeatureContribution extends Disposable implements IWorkbenchContribution {
  constructor(
    @IMyFeatureService private readonly myFeatureService: IMyFeatureService,
    @IConfigurationService private readonly configService: IConfigurationService
  ) {
    super();
    this.initialize();
  }
  
  private initialize(): void {
    // Set up event listeners
    this._register(this.configService.onDidChangeConfiguration(e => {
      if (e.affectsConfiguration('myFeature')) {
        this.myFeatureService.performAction();
      }
    }));
  }
}

registerWorkbenchContribution2(
  'myFeatureContribution',
  MyFeatureContribution,
  WorkbenchPhase.AfterRestored
);

// 5. Register commands
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';

CommandsRegistry.registerCommand({
  id: 'myFeature.performAction',
  handler: async (accessor: ServicesAccessor) => {
    const myFeatureService = accessor.get(IMyFeatureService);
    await myFeatureService.performAction();
  }
});

// 6. Register menu items
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';

MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
  command: {
    id: 'myFeature.performAction',
    title: { value: 'My Feature: Perform Action', original: 'My Feature: Perform Action' }
  },
  when: ContextKeyExpr.true()
});

// 7. Register keybindings
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';

KeybindingsRegistry.registerKeybindingRule({
  id: 'myFeature.performAction',
  weight: KeybindingWeight.WorkbenchContrib,
  when: ContextKeyExpr.true(),
  primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyP
});

Best Practices

  • Use AfterRestored or Eventually for most features
  • Only use blocking phases when absolutely necessary
  • Consider lazy loading for rarely-used features
  • Use editor-specific loading when possible
// Good: Light initialization
constructor(
  @IContextKeyService contextKeyService: IContextKeyService
) {
  super();
  // Just set up basic state
  this.initializeContextKeys(contextKeyService);
}

// Bad: Heavy work in constructor
constructor() {
  super();
  this.loadLargeDataset(); // Don't do this!
  this.performExpensiveOperation(); // Or this!
}
export class MyContribution extends Disposable {
  constructor(
    @IFileService private readonly fileService: IFileService
  ) {
    super();
    
    // Always register disposables
    this._register(this.fileService.onDidFilesChange(() => {
      this.handleFileChange();
    }));
  }
}
  • Define clear, semantic context key names
  • Update context keys when state changes
  • Use context keys to show/hide UI elements conditionally

Performance Monitoring

// VS Code tracks contribution timing
// From src/vs/workbench/common/contributions.ts:397
const now = Date.now();
const instance = instantiationService.createInstance(contribution.ctor);
const time = Date.now() - now;

if (time > WARN_THRESHOLD) {
  logService.warn(
    `Creation of workbench contribution '${contribution.id}' took ${time}ms.`
  );
}
Monitor contribution performance using:
  • Developer: Startup Performance command
  • Chrome DevTools Performance profiler
  • VS Code’s built-in performance marks

Next Steps

Extension System

Learn how external extensions contribute

Layered Architecture

Understand where contributions fit