Services form the backbone of VS Code’s architecture. They provide reusable functionality through a dependency injection system, enabling loose coupling and testability.
What are Services?
Services are singleton instances that:
Provide specific functionality (editor management, lifecycle, configuration, etc.)
Use dependency injection for loose coupling
Follow a clear interface-based design
Are instantiated on-demand or at specific lifecycle phases
All services in VS Code follow the pattern: Define an interface starting with I, then implement it in a concrete class.
Creating a Service
1. Define the Service Interface
First, create a service decorator and interface:
import { createDecorator } from 'vs/platform/instantiation/common/instantiation' ;
export const IMyService = createDecorator < IMyService >( 'myService' );
export interface IMyService {
readonly _serviceBrand : undefined ;
// Service methods
doSomething () : void ;
getData () : Promise < string >;
}
The _serviceBrand property is a type-only brand that ensures type safety in TypeScript. It’s never actually implemented.
2. Implement the Service
Create the concrete implementation:
import { Disposable } from 'vs/base/common/lifecycle' ;
import { IMyService } from './myService' ;
export class MyService extends Disposable implements IMyService {
declare readonly _serviceBrand : undefined ;
constructor (
@ IOtherService private readonly otherService : IOtherService ,
@ ILogService private readonly logService : ILogService
) {
super ();
this . logService . info ( 'MyService created' );
}
doSomething () : void {
this . logService . info ( 'Doing something...' );
this . otherService . callMethod ();
}
async getData () : Promise < string > {
return 'data' ;
}
}
3. Register the Service
Register the service as a singleton:
import { registerSingleton , InstantiationType } from 'vs/platform/instantiation/common/extensions' ;
import { IMyService } from './common/myService' ;
import { MyService } from './browser/myService' ;
registerSingleton (
IMyService ,
MyService ,
InstantiationType . Delayed // or InstantiationType.Eager
);
InstantiationType.Delayed (Recommended)
Service is created only when first requested
Reduces startup time
Most services should use this
InstantiationType.Eager
Service is created immediately at startup
Only use for critical services that must exist early
Examples: lifecycle service, log service
Common Workbench Services
VS Code provides many built-in services. Here are the most commonly used:
Editor Service
Manages editor instances and their lifecycle:
export class EditorService extends Disposable implements EditorServiceImpl {
// Events
private readonly _onDidActiveEditorChange = this._register(new Emitter<void>());
readonly onDidActiveEditorChange = this._onDidActiveEditorChange.event;
private readonly _onDidVisibleEditorsChange = this._register(new Emitter<void>());
readonly onDidVisibleEditorsChange = this._onDidVisibleEditorsChange.event;
constructor(
editorGroupsContainer: IEditorGroupsContainer | undefined,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this.registerListeners();
}
}
Usage example:
import { IEditorService } from 'vs/workbench/services/editor/common/editorService' ;
export class MyExtension {
constructor (
@ IEditorService private readonly editorService : IEditorService
) {
// Listen to active editor changes
this . editorService . onDidActiveEditorChange (() => {
const activeEditor = this . editorService . activeEditor ;
console . log ( 'Active editor changed:' , activeEditor );
});
// Open a file
this . editorService . openEditor ({
resource: URI . file ( '/path/to/file.ts' ),
options: { pinned: true }
});
}
}
Lifecycle Service
Manages application lifecycle phases and shutdown:
export interface WillShutdownEvent {
/**
* The reason why the application is shutting down.
*/
readonly reason: ShutdownReason;
/**
* A token that will signal cancellation when the
* shutdown was forced by the user.
*/
readonly token: CancellationToken;
/**
* Allows to join the shutdown. The promise can be a long running operation.
*/
join(promise: Promise<void>, joiner: IWillShutdownEventDefaultJoiner): void;
/**
* Allows to enforce the shutdown, even when there are
* pending join operations to complete.
*/
force(): void;
}
Usage example:
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle' ;
export class MyComponent {
constructor (
@ ILifecycleService private readonly lifecycleService : ILifecycleService
) {
// Wait for a specific phase
this . lifecycleService . when ( LifecyclePhase . Restored ). then (() => {
console . log ( 'Workbench fully restored!' );
});
// Handle shutdown
this . lifecycleService . onWillShutdown ( event => {
event . join (
this . saveState (),
{ id: 'myComponent' , label: 'Saving my component state' }
);
});
}
private async saveState () : Promise < void > {
// Save important state before shutdown
}
}
View Descriptor Service
Manages views and view containers:
export class ViewDescriptorService extends Disposable implements IViewDescriptorService {
private readonly _onDidChangeContainer: Emitter<{
views: IViewDescriptor[];
from: ViewContainer;
to: ViewContainer
}> = this._register(new Emitter());
readonly onDidChangeContainer = this._onDidChangeContainer.event;
private readonly _onDidChangeLocation: Emitter<{
views: IViewDescriptor[];
from: ViewContainerLocation;
to: ViewContainerLocation
}> = this._register(new Emitter());
readonly onDidChangeLocation = this._onDidChangeLocation.event;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionService private readonly extensionService: IExtensionService
) {
super();
}
}
Configuration Service
Accesses and modifies user/workspace settings:
import { IConfigurationService } from 'vs/platform/configuration/common/configuration' ;
export class MyFeature {
constructor (
@ IConfigurationService private readonly configurationService : IConfigurationService
) {
// Read configuration
const value = this . configurationService . getValue < boolean >( 'myExtension.enabled' );
// Listen to changes
this . configurationService . onDidChangeConfiguration ( e => {
if ( e . affectsConfiguration ( 'myExtension.enabled' )) {
const newValue = this . configurationService . getValue < boolean >( 'myExtension.enabled' );
this . updateFeature ( newValue );
}
});
}
}
Storage Service
Persists data across sessions:
import { IStorageService , StorageScope , StorageTarget } from 'vs/platform/storage/common/storage' ;
export class MyExtension {
constructor (
@ IStorageService private readonly storageService : IStorageService
) {
// Store data
this . storageService . store (
'myExtension.lastUsed' ,
Date . now (). toString (),
StorageScope . PROFILE , // or StorageScope.WORKSPACE
StorageTarget . USER
);
// Retrieve data
const lastUsed = this . storageService . get ( 'myExtension.lastUsed' , StorageScope . PROFILE );
// Listen to changes
this . storageService . onDidChangeValue ( StorageScope . PROFILE , 'myExtension.lastUsed' , this . _store )(
() => {
const newValue = this . storageService . get ( 'myExtension.lastUsed' , StorageScope . PROFILE );
}
);
}
}
Service Location
Services are organized by scope:
Dependency Injection Patterns
Constructor Injection
The most common pattern - inject dependencies through the constructor:
export class MyService extends Disposable implements IMyService {
constructor (
@ IEditorService private readonly editorService : IEditorService ,
@ IConfigurationService private readonly configurationService : IConfigurationService ,
@ ILogService private readonly logService : ILogService
) {
super ();
}
}
Accessor Injection
For command handlers and contributions:
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation' ;
registerAction2 ( class extends Action2 {
async run ( accessor : ServicesAccessor ) {
const editorService = accessor . get ( IEditorService );
const configService = accessor . get ( IConfigurationService );
// Use services
await editorService . openEditor ( ... );
}
});
Manual Instantiation
For creating instances manually:
export class MyService {
constructor (
@ IInstantiationService private readonly instantiationService : IInstantiationService
) {}
createComponent () : MyComponent {
// The instantiation service will inject all dependencies
return this . instantiationService . createInstance ( MyComponent );
}
}
Best Practices
Service Design Guidelines
Single Responsibility : Each service should have one clear purpose
Interface First : Always define the interface before implementation
Immutable APIs : Service methods should not modify internal state unexpectedly
Event-Driven : Use events (Emitter) for state changes
Lazy Initialization : Use InstantiationType.Delayed unless eager loading is required
Proper Disposal : Extend Disposable and register disposables with _register()
Avoid Circular Dependencies : Design services to avoid circular references
Testing Services
Create mock services for testing:
import { mock } from 'vs/base/test/common/mock' ;
class MockEditorService implements IEditorService {
_serviceBrand : undefined ;
onDidActiveEditorChange = Event . None ;
get activeEditor () {
return undefined ;
}
openEditor () {
return Promise . resolve ( undefined );
}
}
suite ( 'MyComponent' , () => {
test ( 'uses editor service' , () => {
const editorService = new MockEditorService ();
const component = new MyComponent ( editorService );
// Test component behavior
});
});
Next Steps
Workbench Contributions Learn how to register contributions that use services
Extension API Understand how services relate to the extension API