Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/common/contributions.ts
3291 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IInstantiationService, IConstructorSignature, ServicesAccessor, BrandedService } from '../../platform/instantiation/common/instantiation.js';
7
import { ILifecycleService, LifecyclePhase } from '../services/lifecycle/common/lifecycle.js';
8
import { Registry } from '../../platform/registry/common/platform.js';
9
import { IdleDeadline, DeferredPromise, runWhenGlobalIdle } from '../../base/common/async.js';
10
import { mark } from '../../base/common/performance.js';
11
import { ILogService } from '../../platform/log/common/log.js';
12
import { IEnvironmentService } from '../../platform/environment/common/environment.js';
13
import { getOrSet } from '../../base/common/map.js';
14
import { Disposable, DisposableStore, isDisposable } from '../../base/common/lifecycle.js';
15
import { IEditorPaneService } from '../services/editor/common/editorPaneService.js';
16
17
/**
18
* A workbench contribution that will be loaded when the workbench starts and disposed when the workbench shuts down.
19
*/
20
export interface IWorkbenchContribution {
21
// Marker Interface
22
}
23
24
export namespace Extensions {
25
/**
26
* @deprecated use `registerWorkbenchContribution2` instead.
27
*/
28
export const Workbench = 'workbench.contributions.kind';
29
}
30
31
export const enum WorkbenchPhase {
32
33
/**
34
* The first phase signals that we are about to startup getting ready.
35
*
36
* Note: doing work in this phase blocks an editor from showing to
37
* the user, so please rather consider to use the other types, preferable
38
* `Lazy` to only instantiate the contribution when really needed.
39
*/
40
BlockStartup = LifecyclePhase.Starting,
41
42
/**
43
* Services are ready and the window is about to restore its UI state.
44
*
45
* Note: doing work in this phase blocks an editor from showing to
46
* the user, so please rather consider to use the other types, preferable
47
* `Lazy` to only instantiate the contribution when really needed.
48
*/
49
BlockRestore = LifecyclePhase.Ready,
50
51
/**
52
* Views, panels and editors have restored. Editors are given a bit of
53
* time to restore their contents.
54
*/
55
AfterRestored = LifecyclePhase.Restored,
56
57
/**
58
* The last phase after views, panels and editors have restored and
59
* some time has passed (2-5 seconds).
60
*/
61
Eventually = LifecyclePhase.Eventually
62
}
63
64
/**
65
* A workbenchch contribution that will only be instantiated
66
* when calling `getWorkbenchContribution`.
67
*/
68
export interface ILazyWorkbenchContributionInstantiation {
69
readonly lazy: true;
70
}
71
72
/**
73
* A workbench contribution that will be instantiated when the
74
* corresponding editor is being created.
75
*/
76
export interface IOnEditorWorkbenchContributionInstantiation {
77
readonly editorTypeId: string;
78
}
79
80
function isOnEditorWorkbenchContributionInstantiation(obj: unknown): obj is IOnEditorWorkbenchContributionInstantiation {
81
const candidate = obj as IOnEditorWorkbenchContributionInstantiation | undefined;
82
return !!candidate && typeof candidate.editorTypeId === 'string';
83
}
84
85
export type WorkbenchContributionInstantiation = WorkbenchPhase | ILazyWorkbenchContributionInstantiation | IOnEditorWorkbenchContributionInstantiation;
86
87
function toWorkbenchPhase(phase: LifecyclePhase.Restored | LifecyclePhase.Eventually): WorkbenchPhase.AfterRestored | WorkbenchPhase.Eventually {
88
switch (phase) {
89
case LifecyclePhase.Restored:
90
return WorkbenchPhase.AfterRestored;
91
case LifecyclePhase.Eventually:
92
return WorkbenchPhase.Eventually;
93
}
94
}
95
96
function toLifecyclePhase(instantiation: WorkbenchPhase): LifecyclePhase {
97
switch (instantiation) {
98
case WorkbenchPhase.BlockStartup:
99
return LifecyclePhase.Starting;
100
case WorkbenchPhase.BlockRestore:
101
return LifecyclePhase.Ready;
102
case WorkbenchPhase.AfterRestored:
103
return LifecyclePhase.Restored;
104
case WorkbenchPhase.Eventually:
105
return LifecyclePhase.Eventually;
106
}
107
}
108
109
type IWorkbenchContributionSignature<Service extends BrandedService[]> = new (...services: Service) => IWorkbenchContribution;
110
111
export interface IWorkbenchContributionsRegistry {
112
113
/**
114
* @deprecated use `registerWorkbenchContribution2` instead.
115
*/
116
registerWorkbenchContribution<Services extends BrandedService[]>(contribution: IWorkbenchContributionSignature<Services>, phase: LifecyclePhase.Restored | LifecyclePhase.Eventually): void;
117
118
/**
119
* Starts the registry by providing the required services.
120
*/
121
start(accessor: ServicesAccessor): void;
122
123
/**
124
* A promise that resolves when all contributions up to the `Restored`
125
* phase have been instantiated.
126
*/
127
readonly whenRestored: Promise<void>;
128
129
/**
130
* Provides access to the instantiation times of all contributions by
131
* lifecycle phase.
132
*/
133
readonly timings: Map<LifecyclePhase, Array<[string /* ID */, number /* Creation Time */]>>;
134
}
135
136
interface IWorkbenchContributionRegistration {
137
readonly id: string | undefined;
138
readonly ctor: IConstructorSignature<IWorkbenchContribution>;
139
}
140
141
export class WorkbenchContributionsRegistry extends Disposable implements IWorkbenchContributionsRegistry {
142
143
static readonly INSTANCE = new WorkbenchContributionsRegistry();
144
145
private static readonly BLOCK_BEFORE_RESTORE_WARN_THRESHOLD = 20;
146
private static readonly BLOCK_AFTER_RESTORE_WARN_THRESHOLD = 100;
147
148
private instantiationService: IInstantiationService | undefined;
149
private lifecycleService: ILifecycleService | undefined;
150
private logService: ILogService | undefined;
151
private environmentService: IEnvironmentService | undefined;
152
private editorPaneService: IEditorPaneService | undefined;
153
154
private readonly contributionsByPhase = new Map<LifecyclePhase, IWorkbenchContributionRegistration[]>();
155
private readonly contributionsByEditor = new Map<string, IWorkbenchContributionRegistration[]>();
156
private readonly contributionsById = new Map<string, IWorkbenchContributionRegistration>();
157
158
private readonly instancesById = new Map<string, IWorkbenchContribution>();
159
private readonly instanceDisposables = this._register(new DisposableStore());
160
161
private readonly timingsByPhase = new Map<LifecyclePhase, Array<[string /* ID */, number /* Creation Time */]>>();
162
get timings() { return this.timingsByPhase; }
163
164
private readonly pendingRestoredContributions = new DeferredPromise<void>();
165
readonly whenRestored = this.pendingRestoredContributions.p;
166
167
registerWorkbenchContribution2(id: string, ctor: IConstructorSignature<IWorkbenchContribution>, phase: WorkbenchPhase.BlockStartup | WorkbenchPhase.BlockRestore): void;
168
registerWorkbenchContribution2(id: string | undefined, ctor: IConstructorSignature<IWorkbenchContribution>, phase: WorkbenchPhase.AfterRestored | WorkbenchPhase.Eventually): void;
169
registerWorkbenchContribution2(id: string, ctor: IConstructorSignature<IWorkbenchContribution>, lazy: ILazyWorkbenchContributionInstantiation): void;
170
registerWorkbenchContribution2(id: string, ctor: IConstructorSignature<IWorkbenchContribution>, onEditor: IOnEditorWorkbenchContributionInstantiation): void;
171
registerWorkbenchContribution2(id: string | undefined, ctor: IConstructorSignature<IWorkbenchContribution>, instantiation: WorkbenchContributionInstantiation): void {
172
const contribution: IWorkbenchContributionRegistration = { id, ctor };
173
174
// Instantiate directly if we already have a matching instantiation condition
175
if (
176
this.instantiationService && this.lifecycleService && this.logService && this.environmentService && this.editorPaneService &&
177
(
178
(typeof instantiation === 'number' && this.lifecycleService.phase >= instantiation) ||
179
(typeof id === 'string' && isOnEditorWorkbenchContributionInstantiation(instantiation) && this.editorPaneService.didInstantiateEditorPane(instantiation.editorTypeId))
180
)
181
) {
182
this.safeCreateContribution(this.instantiationService, this.logService, this.environmentService, contribution, typeof instantiation === 'number' ? toLifecyclePhase(instantiation) : this.lifecycleService.phase);
183
}
184
185
// Otherwise keep contributions by instantiation kind for later instantiation
186
else {
187
188
// by phase
189
if (typeof instantiation === 'number') {
190
getOrSet(this.contributionsByPhase, toLifecyclePhase(instantiation), []).push(contribution);
191
}
192
193
if (typeof id === 'string') {
194
195
// by id
196
if (!this.contributionsById.has(id)) {
197
this.contributionsById.set(id, contribution);
198
} else {
199
console.error(`IWorkbenchContributionsRegistry#registerWorkbenchContribution(): Can't register multiple contributions with same id '${id}'`);
200
}
201
202
// by editor
203
if (isOnEditorWorkbenchContributionInstantiation(instantiation)) {
204
getOrSet(this.contributionsByEditor, instantiation.editorTypeId, []).push(contribution);
205
}
206
}
207
}
208
}
209
210
registerWorkbenchContribution(ctor: IConstructorSignature<IWorkbenchContribution>, phase: LifecyclePhase.Restored | LifecyclePhase.Eventually): void {
211
this.registerWorkbenchContribution2(undefined, ctor, toWorkbenchPhase(phase));
212
}
213
214
getWorkbenchContribution<T extends IWorkbenchContribution>(id: string): T {
215
if (this.instancesById.has(id)) {
216
return this.instancesById.get(id) as T;
217
}
218
219
const instantiationService = this.instantiationService;
220
const lifecycleService = this.lifecycleService;
221
const logService = this.logService;
222
const environmentService = this.environmentService;
223
if (!instantiationService || !lifecycleService || !logService || !environmentService) {
224
throw new Error(`IWorkbenchContributionsRegistry#getContribution('${id}'): cannot be called before registry started`);
225
}
226
227
const contribution = this.contributionsById.get(id);
228
if (!contribution) {
229
throw new Error(`IWorkbenchContributionsRegistry#getContribution('${id}'): contribution with that identifier is unknown.`);
230
}
231
232
if (lifecycleService.phase < LifecyclePhase.Restored) {
233
logService.warn(`IWorkbenchContributionsRegistry#getContribution('${id}'): contribution instantiated before LifecyclePhase.Restored!`);
234
}
235
236
this.safeCreateContribution(instantiationService, logService, environmentService, contribution, lifecycleService.phase);
237
238
const instance = this.instancesById.get(id);
239
if (!instance) {
240
throw new Error(`IWorkbenchContributionsRegistry#getContribution('${id}'): failed to create contribution.`);
241
}
242
243
return instance as T;
244
}
245
246
start(accessor: ServicesAccessor): void {
247
const instantiationService = this.instantiationService = accessor.get(IInstantiationService);
248
const lifecycleService = this.lifecycleService = accessor.get(ILifecycleService);
249
const logService = this.logService = accessor.get(ILogService);
250
const environmentService = this.environmentService = accessor.get(IEnvironmentService);
251
const editorPaneService = this.editorPaneService = accessor.get(IEditorPaneService);
252
253
// Dispose contributions on shutdown
254
this._register(lifecycleService.onDidShutdown(() => {
255
this.instanceDisposables.clear();
256
}));
257
258
// Instantiate contributions by phase when they are ready
259
for (const phase of [LifecyclePhase.Starting, LifecyclePhase.Ready, LifecyclePhase.Restored, LifecyclePhase.Eventually]) {
260
this.instantiateByPhase(instantiationService, lifecycleService, logService, environmentService, phase);
261
}
262
263
// Instantiate contributions by editor when they are created or have been
264
for (const editorTypeId of this.contributionsByEditor.keys()) {
265
if (editorPaneService.didInstantiateEditorPane(editorTypeId)) {
266
this.onEditor(editorTypeId, instantiationService, lifecycleService, logService, environmentService);
267
}
268
}
269
this._register(editorPaneService.onWillInstantiateEditorPane(e => this.onEditor(e.typeId, instantiationService, lifecycleService, logService, environmentService)));
270
}
271
272
private onEditor(editorTypeId: string, instantiationService: IInstantiationService, lifecycleService: ILifecycleService, logService: ILogService, environmentService: IEnvironmentService): void {
273
const contributions = this.contributionsByEditor.get(editorTypeId);
274
if (contributions) {
275
this.contributionsByEditor.delete(editorTypeId);
276
277
for (const contribution of contributions) {
278
this.safeCreateContribution(instantiationService, logService, environmentService, contribution, lifecycleService.phase);
279
}
280
}
281
}
282
283
private instantiateByPhase(instantiationService: IInstantiationService, lifecycleService: ILifecycleService, logService: ILogService, environmentService: IEnvironmentService, phase: LifecyclePhase): void {
284
285
// Instantiate contributions directly when phase is already reached
286
if (lifecycleService.phase >= phase) {
287
this.doInstantiateByPhase(instantiationService, logService, environmentService, phase);
288
}
289
290
// Otherwise wait for phase to be reached
291
else {
292
lifecycleService.when(phase).then(() => this.doInstantiateByPhase(instantiationService, logService, environmentService, phase));
293
}
294
}
295
296
private async doInstantiateByPhase(instantiationService: IInstantiationService, logService: ILogService, environmentService: IEnvironmentService, phase: LifecyclePhase): Promise<void> {
297
const contributions = this.contributionsByPhase.get(phase);
298
if (contributions) {
299
this.contributionsByPhase.delete(phase);
300
301
switch (phase) {
302
case LifecyclePhase.Starting:
303
case LifecyclePhase.Ready: {
304
305
// instantiate everything synchronously and blocking
306
// measure the time it takes as perf marks for diagnosis
307
308
mark(`code/willCreateWorkbenchContributions/${phase}`);
309
310
for (const contribution of contributions) {
311
this.safeCreateContribution(instantiationService, logService, environmentService, contribution, phase);
312
}
313
314
mark(`code/didCreateWorkbenchContributions/${phase}`);
315
316
break;
317
}
318
319
case LifecyclePhase.Restored:
320
case LifecyclePhase.Eventually: {
321
322
// for the Restored/Eventually-phase we instantiate contributions
323
// only when idle. this might take a few idle-busy-cycles but will
324
// finish within the timeouts
325
// given that, we must ensure to await the contributions from the
326
// Restored-phase before we instantiate the Eventually-phase
327
328
if (phase === LifecyclePhase.Eventually) {
329
await this.pendingRestoredContributions.p;
330
}
331
332
this.doInstantiateWhenIdle(contributions, instantiationService, logService, environmentService, phase);
333
334
break;
335
}
336
}
337
}
338
}
339
340
private doInstantiateWhenIdle(contributions: IWorkbenchContributionRegistration[], instantiationService: IInstantiationService, logService: ILogService, environmentService: IEnvironmentService, phase: LifecyclePhase): void {
341
mark(`code/willCreateWorkbenchContributions/${phase}`);
342
343
let i = 0;
344
const forcedTimeout = phase === LifecyclePhase.Eventually ? 3000 : 500;
345
346
const instantiateSome = (idle: IdleDeadline) => {
347
while (i < contributions.length) {
348
const contribution = contributions[i++];
349
this.safeCreateContribution(instantiationService, logService, environmentService, contribution, phase);
350
if (idle.timeRemaining() < 1) {
351
// time is up -> reschedule
352
runWhenGlobalIdle(instantiateSome, forcedTimeout);
353
break;
354
}
355
}
356
357
if (i === contributions.length) {
358
mark(`code/didCreateWorkbenchContributions/${phase}`);
359
360
if (phase === LifecyclePhase.Restored) {
361
this.pendingRestoredContributions.complete();
362
}
363
}
364
};
365
366
runWhenGlobalIdle(instantiateSome, forcedTimeout);
367
}
368
369
private safeCreateContribution(instantiationService: IInstantiationService, logService: ILogService, environmentService: IEnvironmentService, contribution: IWorkbenchContributionRegistration, phase: LifecyclePhase): void {
370
if (typeof contribution.id === 'string' && this.instancesById.has(contribution.id)) {
371
return;
372
}
373
374
const now = Date.now();
375
376
try {
377
if (typeof contribution.id === 'string') {
378
mark(`code/willCreateWorkbenchContribution/${phase}/${contribution.id}`);
379
}
380
381
const instance = instantiationService.createInstance(contribution.ctor);
382
if (typeof contribution.id === 'string') {
383
this.instancesById.set(contribution.id, instance);
384
this.contributionsById.delete(contribution.id);
385
}
386
if (isDisposable(instance)) {
387
this.instanceDisposables.add(instance);
388
}
389
} catch (error) {
390
logService.error(`Unable to create workbench contribution '${contribution.id ?? contribution.ctor.name}'.`, error);
391
} finally {
392
if (typeof contribution.id === 'string') {
393
mark(`code/didCreateWorkbenchContribution/${phase}/${contribution.id}`);
394
}
395
}
396
397
if (typeof contribution.id === 'string' || !environmentService.isBuilt /* only log out of sources where we have good ctor names */) {
398
const time = Date.now() - now;
399
if (time > (phase < LifecyclePhase.Restored ? WorkbenchContributionsRegistry.BLOCK_BEFORE_RESTORE_WARN_THRESHOLD : WorkbenchContributionsRegistry.BLOCK_AFTER_RESTORE_WARN_THRESHOLD)) {
400
logService.warn(`Creation of workbench contribution '${contribution.id ?? contribution.ctor.name}' took ${time}ms.`);
401
}
402
403
if (typeof contribution.id === 'string') {
404
let timingsForPhase = this.timingsByPhase.get(phase);
405
if (!timingsForPhase) {
406
timingsForPhase = [];
407
this.timingsByPhase.set(phase, timingsForPhase);
408
}
409
410
timingsForPhase.push([contribution.id, time]);
411
}
412
}
413
}
414
}
415
416
/**
417
* Register a workbench contribution that will be instantiated
418
* based on the `instantiation` property.
419
*/
420
export const registerWorkbenchContribution2 = WorkbenchContributionsRegistry.INSTANCE.registerWorkbenchContribution2.bind(WorkbenchContributionsRegistry.INSTANCE) as {
421
<Services extends BrandedService[]>(id: string, ctor: IWorkbenchContributionSignature<Services>, instantiation: WorkbenchContributionInstantiation): void;
422
};
423
424
/**
425
* Provides access to a workbench contribution with a specific identifier.
426
* The contribution is created if not yet done.
427
*
428
* Note: will throw an error if
429
* - called too early before the registry has started
430
* - no contribution is known for the given identifier
431
*/
432
export const getWorkbenchContribution = WorkbenchContributionsRegistry.INSTANCE.getWorkbenchContribution.bind(WorkbenchContributionsRegistry.INSTANCE);
433
434
Registry.add(Extensions.Workbench, WorkbenchContributionsRegistry.INSTANCE);
435
436