Path: blob/main/src/vs/workbench/common/contributions.ts
3291 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IInstantiationService, IConstructorSignature, ServicesAccessor, BrandedService } from '../../platform/instantiation/common/instantiation.js';6import { ILifecycleService, LifecyclePhase } from '../services/lifecycle/common/lifecycle.js';7import { Registry } from '../../platform/registry/common/platform.js';8import { IdleDeadline, DeferredPromise, runWhenGlobalIdle } from '../../base/common/async.js';9import { mark } from '../../base/common/performance.js';10import { ILogService } from '../../platform/log/common/log.js';11import { IEnvironmentService } from '../../platform/environment/common/environment.js';12import { getOrSet } from '../../base/common/map.js';13import { Disposable, DisposableStore, isDisposable } from '../../base/common/lifecycle.js';14import { IEditorPaneService } from '../services/editor/common/editorPaneService.js';1516/**17* A workbench contribution that will be loaded when the workbench starts and disposed when the workbench shuts down.18*/19export interface IWorkbenchContribution {20// Marker Interface21}2223export namespace Extensions {24/**25* @deprecated use `registerWorkbenchContribution2` instead.26*/27export const Workbench = 'workbench.contributions.kind';28}2930export const enum WorkbenchPhase {3132/**33* The first phase signals that we are about to startup getting ready.34*35* Note: doing work in this phase blocks an editor from showing to36* the user, so please rather consider to use the other types, preferable37* `Lazy` to only instantiate the contribution when really needed.38*/39BlockStartup = LifecyclePhase.Starting,4041/**42* Services are ready and the window is about to restore its UI state.43*44* Note: doing work in this phase blocks an editor from showing to45* the user, so please rather consider to use the other types, preferable46* `Lazy` to only instantiate the contribution when really needed.47*/48BlockRestore = LifecyclePhase.Ready,4950/**51* Views, panels and editors have restored. Editors are given a bit of52* time to restore their contents.53*/54AfterRestored = LifecyclePhase.Restored,5556/**57* The last phase after views, panels and editors have restored and58* some time has passed (2-5 seconds).59*/60Eventually = LifecyclePhase.Eventually61}6263/**64* A workbenchch contribution that will only be instantiated65* when calling `getWorkbenchContribution`.66*/67export interface ILazyWorkbenchContributionInstantiation {68readonly lazy: true;69}7071/**72* A workbench contribution that will be instantiated when the73* corresponding editor is being created.74*/75export interface IOnEditorWorkbenchContributionInstantiation {76readonly editorTypeId: string;77}7879function isOnEditorWorkbenchContributionInstantiation(obj: unknown): obj is IOnEditorWorkbenchContributionInstantiation {80const candidate = obj as IOnEditorWorkbenchContributionInstantiation | undefined;81return !!candidate && typeof candidate.editorTypeId === 'string';82}8384export type WorkbenchContributionInstantiation = WorkbenchPhase | ILazyWorkbenchContributionInstantiation | IOnEditorWorkbenchContributionInstantiation;8586function toWorkbenchPhase(phase: LifecyclePhase.Restored | LifecyclePhase.Eventually): WorkbenchPhase.AfterRestored | WorkbenchPhase.Eventually {87switch (phase) {88case LifecyclePhase.Restored:89return WorkbenchPhase.AfterRestored;90case LifecyclePhase.Eventually:91return WorkbenchPhase.Eventually;92}93}9495function toLifecyclePhase(instantiation: WorkbenchPhase): LifecyclePhase {96switch (instantiation) {97case WorkbenchPhase.BlockStartup:98return LifecyclePhase.Starting;99case WorkbenchPhase.BlockRestore:100return LifecyclePhase.Ready;101case WorkbenchPhase.AfterRestored:102return LifecyclePhase.Restored;103case WorkbenchPhase.Eventually:104return LifecyclePhase.Eventually;105}106}107108type IWorkbenchContributionSignature<Service extends BrandedService[]> = new (...services: Service) => IWorkbenchContribution;109110export interface IWorkbenchContributionsRegistry {111112/**113* @deprecated use `registerWorkbenchContribution2` instead.114*/115registerWorkbenchContribution<Services extends BrandedService[]>(contribution: IWorkbenchContributionSignature<Services>, phase: LifecyclePhase.Restored | LifecyclePhase.Eventually): void;116117/**118* Starts the registry by providing the required services.119*/120start(accessor: ServicesAccessor): void;121122/**123* A promise that resolves when all contributions up to the `Restored`124* phase have been instantiated.125*/126readonly whenRestored: Promise<void>;127128/**129* Provides access to the instantiation times of all contributions by130* lifecycle phase.131*/132readonly timings: Map<LifecyclePhase, Array<[string /* ID */, number /* Creation Time */]>>;133}134135interface IWorkbenchContributionRegistration {136readonly id: string | undefined;137readonly ctor: IConstructorSignature<IWorkbenchContribution>;138}139140export class WorkbenchContributionsRegistry extends Disposable implements IWorkbenchContributionsRegistry {141142static readonly INSTANCE = new WorkbenchContributionsRegistry();143144private static readonly BLOCK_BEFORE_RESTORE_WARN_THRESHOLD = 20;145private static readonly BLOCK_AFTER_RESTORE_WARN_THRESHOLD = 100;146147private instantiationService: IInstantiationService | undefined;148private lifecycleService: ILifecycleService | undefined;149private logService: ILogService | undefined;150private environmentService: IEnvironmentService | undefined;151private editorPaneService: IEditorPaneService | undefined;152153private readonly contributionsByPhase = new Map<LifecyclePhase, IWorkbenchContributionRegistration[]>();154private readonly contributionsByEditor = new Map<string, IWorkbenchContributionRegistration[]>();155private readonly contributionsById = new Map<string, IWorkbenchContributionRegistration>();156157private readonly instancesById = new Map<string, IWorkbenchContribution>();158private readonly instanceDisposables = this._register(new DisposableStore());159160private readonly timingsByPhase = new Map<LifecyclePhase, Array<[string /* ID */, number /* Creation Time */]>>();161get timings() { return this.timingsByPhase; }162163private readonly pendingRestoredContributions = new DeferredPromise<void>();164readonly whenRestored = this.pendingRestoredContributions.p;165166registerWorkbenchContribution2(id: string, ctor: IConstructorSignature<IWorkbenchContribution>, phase: WorkbenchPhase.BlockStartup | WorkbenchPhase.BlockRestore): void;167registerWorkbenchContribution2(id: string | undefined, ctor: IConstructorSignature<IWorkbenchContribution>, phase: WorkbenchPhase.AfterRestored | WorkbenchPhase.Eventually): void;168registerWorkbenchContribution2(id: string, ctor: IConstructorSignature<IWorkbenchContribution>, lazy: ILazyWorkbenchContributionInstantiation): void;169registerWorkbenchContribution2(id: string, ctor: IConstructorSignature<IWorkbenchContribution>, onEditor: IOnEditorWorkbenchContributionInstantiation): void;170registerWorkbenchContribution2(id: string | undefined, ctor: IConstructorSignature<IWorkbenchContribution>, instantiation: WorkbenchContributionInstantiation): void {171const contribution: IWorkbenchContributionRegistration = { id, ctor };172173// Instantiate directly if we already have a matching instantiation condition174if (175this.instantiationService && this.lifecycleService && this.logService && this.environmentService && this.editorPaneService &&176(177(typeof instantiation === 'number' && this.lifecycleService.phase >= instantiation) ||178(typeof id === 'string' && isOnEditorWorkbenchContributionInstantiation(instantiation) && this.editorPaneService.didInstantiateEditorPane(instantiation.editorTypeId))179)180) {181this.safeCreateContribution(this.instantiationService, this.logService, this.environmentService, contribution, typeof instantiation === 'number' ? toLifecyclePhase(instantiation) : this.lifecycleService.phase);182}183184// Otherwise keep contributions by instantiation kind for later instantiation185else {186187// by phase188if (typeof instantiation === 'number') {189getOrSet(this.contributionsByPhase, toLifecyclePhase(instantiation), []).push(contribution);190}191192if (typeof id === 'string') {193194// by id195if (!this.contributionsById.has(id)) {196this.contributionsById.set(id, contribution);197} else {198console.error(`IWorkbenchContributionsRegistry#registerWorkbenchContribution(): Can't register multiple contributions with same id '${id}'`);199}200201// by editor202if (isOnEditorWorkbenchContributionInstantiation(instantiation)) {203getOrSet(this.contributionsByEditor, instantiation.editorTypeId, []).push(contribution);204}205}206}207}208209registerWorkbenchContribution(ctor: IConstructorSignature<IWorkbenchContribution>, phase: LifecyclePhase.Restored | LifecyclePhase.Eventually): void {210this.registerWorkbenchContribution2(undefined, ctor, toWorkbenchPhase(phase));211}212213getWorkbenchContribution<T extends IWorkbenchContribution>(id: string): T {214if (this.instancesById.has(id)) {215return this.instancesById.get(id) as T;216}217218const instantiationService = this.instantiationService;219const lifecycleService = this.lifecycleService;220const logService = this.logService;221const environmentService = this.environmentService;222if (!instantiationService || !lifecycleService || !logService || !environmentService) {223throw new Error(`IWorkbenchContributionsRegistry#getContribution('${id}'): cannot be called before registry started`);224}225226const contribution = this.contributionsById.get(id);227if (!contribution) {228throw new Error(`IWorkbenchContributionsRegistry#getContribution('${id}'): contribution with that identifier is unknown.`);229}230231if (lifecycleService.phase < LifecyclePhase.Restored) {232logService.warn(`IWorkbenchContributionsRegistry#getContribution('${id}'): contribution instantiated before LifecyclePhase.Restored!`);233}234235this.safeCreateContribution(instantiationService, logService, environmentService, contribution, lifecycleService.phase);236237const instance = this.instancesById.get(id);238if (!instance) {239throw new Error(`IWorkbenchContributionsRegistry#getContribution('${id}'): failed to create contribution.`);240}241242return instance as T;243}244245start(accessor: ServicesAccessor): void {246const instantiationService = this.instantiationService = accessor.get(IInstantiationService);247const lifecycleService = this.lifecycleService = accessor.get(ILifecycleService);248const logService = this.logService = accessor.get(ILogService);249const environmentService = this.environmentService = accessor.get(IEnvironmentService);250const editorPaneService = this.editorPaneService = accessor.get(IEditorPaneService);251252// Dispose contributions on shutdown253this._register(lifecycleService.onDidShutdown(() => {254this.instanceDisposables.clear();255}));256257// Instantiate contributions by phase when they are ready258for (const phase of [LifecyclePhase.Starting, LifecyclePhase.Ready, LifecyclePhase.Restored, LifecyclePhase.Eventually]) {259this.instantiateByPhase(instantiationService, lifecycleService, logService, environmentService, phase);260}261262// Instantiate contributions by editor when they are created or have been263for (const editorTypeId of this.contributionsByEditor.keys()) {264if (editorPaneService.didInstantiateEditorPane(editorTypeId)) {265this.onEditor(editorTypeId, instantiationService, lifecycleService, logService, environmentService);266}267}268this._register(editorPaneService.onWillInstantiateEditorPane(e => this.onEditor(e.typeId, instantiationService, lifecycleService, logService, environmentService)));269}270271private onEditor(editorTypeId: string, instantiationService: IInstantiationService, lifecycleService: ILifecycleService, logService: ILogService, environmentService: IEnvironmentService): void {272const contributions = this.contributionsByEditor.get(editorTypeId);273if (contributions) {274this.contributionsByEditor.delete(editorTypeId);275276for (const contribution of contributions) {277this.safeCreateContribution(instantiationService, logService, environmentService, contribution, lifecycleService.phase);278}279}280}281282private instantiateByPhase(instantiationService: IInstantiationService, lifecycleService: ILifecycleService, logService: ILogService, environmentService: IEnvironmentService, phase: LifecyclePhase): void {283284// Instantiate contributions directly when phase is already reached285if (lifecycleService.phase >= phase) {286this.doInstantiateByPhase(instantiationService, logService, environmentService, phase);287}288289// Otherwise wait for phase to be reached290else {291lifecycleService.when(phase).then(() => this.doInstantiateByPhase(instantiationService, logService, environmentService, phase));292}293}294295private async doInstantiateByPhase(instantiationService: IInstantiationService, logService: ILogService, environmentService: IEnvironmentService, phase: LifecyclePhase): Promise<void> {296const contributions = this.contributionsByPhase.get(phase);297if (contributions) {298this.contributionsByPhase.delete(phase);299300switch (phase) {301case LifecyclePhase.Starting:302case LifecyclePhase.Ready: {303304// instantiate everything synchronously and blocking305// measure the time it takes as perf marks for diagnosis306307mark(`code/willCreateWorkbenchContributions/${phase}`);308309for (const contribution of contributions) {310this.safeCreateContribution(instantiationService, logService, environmentService, contribution, phase);311}312313mark(`code/didCreateWorkbenchContributions/${phase}`);314315break;316}317318case LifecyclePhase.Restored:319case LifecyclePhase.Eventually: {320321// for the Restored/Eventually-phase we instantiate contributions322// only when idle. this might take a few idle-busy-cycles but will323// finish within the timeouts324// given that, we must ensure to await the contributions from the325// Restored-phase before we instantiate the Eventually-phase326327if (phase === LifecyclePhase.Eventually) {328await this.pendingRestoredContributions.p;329}330331this.doInstantiateWhenIdle(contributions, instantiationService, logService, environmentService, phase);332333break;334}335}336}337}338339private doInstantiateWhenIdle(contributions: IWorkbenchContributionRegistration[], instantiationService: IInstantiationService, logService: ILogService, environmentService: IEnvironmentService, phase: LifecyclePhase): void {340mark(`code/willCreateWorkbenchContributions/${phase}`);341342let i = 0;343const forcedTimeout = phase === LifecyclePhase.Eventually ? 3000 : 500;344345const instantiateSome = (idle: IdleDeadline) => {346while (i < contributions.length) {347const contribution = contributions[i++];348this.safeCreateContribution(instantiationService, logService, environmentService, contribution, phase);349if (idle.timeRemaining() < 1) {350// time is up -> reschedule351runWhenGlobalIdle(instantiateSome, forcedTimeout);352break;353}354}355356if (i === contributions.length) {357mark(`code/didCreateWorkbenchContributions/${phase}`);358359if (phase === LifecyclePhase.Restored) {360this.pendingRestoredContributions.complete();361}362}363};364365runWhenGlobalIdle(instantiateSome, forcedTimeout);366}367368private safeCreateContribution(instantiationService: IInstantiationService, logService: ILogService, environmentService: IEnvironmentService, contribution: IWorkbenchContributionRegistration, phase: LifecyclePhase): void {369if (typeof contribution.id === 'string' && this.instancesById.has(contribution.id)) {370return;371}372373const now = Date.now();374375try {376if (typeof contribution.id === 'string') {377mark(`code/willCreateWorkbenchContribution/${phase}/${contribution.id}`);378}379380const instance = instantiationService.createInstance(contribution.ctor);381if (typeof contribution.id === 'string') {382this.instancesById.set(contribution.id, instance);383this.contributionsById.delete(contribution.id);384}385if (isDisposable(instance)) {386this.instanceDisposables.add(instance);387}388} catch (error) {389logService.error(`Unable to create workbench contribution '${contribution.id ?? contribution.ctor.name}'.`, error);390} finally {391if (typeof contribution.id === 'string') {392mark(`code/didCreateWorkbenchContribution/${phase}/${contribution.id}`);393}394}395396if (typeof contribution.id === 'string' || !environmentService.isBuilt /* only log out of sources where we have good ctor names */) {397const time = Date.now() - now;398if (time > (phase < LifecyclePhase.Restored ? WorkbenchContributionsRegistry.BLOCK_BEFORE_RESTORE_WARN_THRESHOLD : WorkbenchContributionsRegistry.BLOCK_AFTER_RESTORE_WARN_THRESHOLD)) {399logService.warn(`Creation of workbench contribution '${contribution.id ?? contribution.ctor.name}' took ${time}ms.`);400}401402if (typeof contribution.id === 'string') {403let timingsForPhase = this.timingsByPhase.get(phase);404if (!timingsForPhase) {405timingsForPhase = [];406this.timingsByPhase.set(phase, timingsForPhase);407}408409timingsForPhase.push([contribution.id, time]);410}411}412}413}414415/**416* Register a workbench contribution that will be instantiated417* based on the `instantiation` property.418*/419export const registerWorkbenchContribution2 = WorkbenchContributionsRegistry.INSTANCE.registerWorkbenchContribution2.bind(WorkbenchContributionsRegistry.INSTANCE) as {420<Services extends BrandedService[]>(id: string, ctor: IWorkbenchContributionSignature<Services>, instantiation: WorkbenchContributionInstantiation): void;421};422423/**424* Provides access to a workbench contribution with a specific identifier.425* The contribution is created if not yet done.426*427* Note: will throw an error if428* - called too early before the registry has started429* - no contribution is known for the given identifier430*/431export const getWorkbenchContribution = WorkbenchContributionsRegistry.INSTANCE.getWorkbenchContribution.bind(WorkbenchContributionsRegistry.INSTANCE);432433Registry.add(Extensions.Workbench, WorkbenchContributionsRegistry.INSTANCE);434435436