src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts
423 LOC · 262 covered · 161 uncovered · 63 ranges · 71 concepts · 11 introducers · 43 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
extensionDescriptionRegistry.ts ×37
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
import { Emitter } from '../../../../base/common/event.js';
import * as path from '../../../../base/common/path.js';
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { promiseWithResolvers } from '../../../../base/common/async.js';
export class DeltaExtensionsResult {
constructor(
public readonly removedDueToLooping: IExtensionDescription[]
) { }
export interface IReadOnlyExtensionDescriptionRegistry {
containsActivationEvent(activationEvent: string): boolean;
containsExtension(extensionId: ExtensionIdentifier): boolean;
getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[];
getAllExtensionDescriptions(): IExtensionDescription[];
getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined;
getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined;
getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined;
}
export class ExtensionDescriptionRegistry extends Disposable implements IReadOnlyExtensionDescriptionRegistry {
public static isHostExtension(extensionId: ExtensionIdentifier | string, myRegistry: ExtensionDescriptionRegistry, globalRegistry: ExtensionDescriptionRegistry): boolean {
if (myRegistry.getExtensionDescription(extensionId)) {
// I have this extension
return false;
}
const extensionDescription = globalRegistry.getExtensionDescription(extensionId);
extensionDescriptionRegistry.ts ×1
if (!extensionDescription) {
return false;
}
if ((extensionDescription.main || extensionDescription.browser) && extensionDescription.api === 'none') {
extensionDescriptionRegistry.ts ×37
}
private readonly _onDidChange = this._register(new Emitter<void>());
public readonly onDidChange = this._onDidChange.event;
private _versionId: number = 0;
private _extensionDescriptions: IExtensionDescription[];
private _extensionsMap!: ExtensionIdentifierMap<IExtensionDescription>;
private _extensionsArr!: IExtensionDescription[];
private _activationMap!: Map<string, IExtensionDescription[]>;
constructor(
private readonly _activationEventsReader: IActivationEventsReader,
extensionDescriptionRegistry.ts ×3
extensionDescriptions: IExtensionDescription[]
) {
super();
this._extensionDescriptions = extensionDescriptions;
this._initialize();
}
private _initialize(): void {
// Ensure extensions are stored in the order: builtin, user, under development
extensionDescriptionRegistry.ts ×3
this._extensionDescriptions.sort(extensionCmp);
this._extensionsMap = new ExtensionIdentifierMap<IExtensionDescription>();
this._extensionsArr = [];
this._activationMap = new Map<string, IExtensionDescription[]>();
for (const extensionDescription of this._extensionDescriptions) {
if (this._extensionsMap.has(extensionDescription.identifier)) {
extensionDescriptionRegistry.ts ×2
// No overwriting allowed!
console.error('Extension `' + extensionDescription.identifier.value + '` is already registered');
continue;
}
this._extensionsMap.set(extensionDescription.identifier, extensionDescription);
this._extensionsArr.push(extensionDescription);
const activationEvents = this._activationEventsReader.readActivationEvents(extensionDescription);
for (const activationEvent of activationEvents) {
if (!this._activationMap.has(activationEvent)) {
this._activationMap.set(activationEvent, []);
}
this._activationMap.get(activationEvent)!.push(extensionDescription);
}
}
public set(extensionDescriptions: IExtensionDescription[]): { versionId: number } {
this._extensionDescriptions = extensionDescriptions;
this._initialize();
this._versionId++;
this._onDidChange.fire(undefined);
return {
versionId: this._versionId
};
}
public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult {
// It is possible that an extension is removed, only to be added again at a different version
extensionDescriptionRegistry.ts ×11
// so we will first handle removals
this._extensionDescriptions = removeExtensions(this._extensionDescriptions, toRemove);
// Then, handle the extensions to add
this._extensionDescriptions = this._extensionDescriptions.concat(toAdd);
// Immediately remove looping extensions!
const looping = ExtensionDescriptionRegistry._findLoopingExtensions(this._extensionDescriptions);
this._extensionDescriptions = removeExtensions(this._extensionDescriptions, looping.map(ext => ext.identifier));
this._initialize();
this._versionId++;
this._onDidChange.fire(undefined);
return new DeltaExtensionsResult(this._versionId, looping);
}
private static _findLoopingExtensions(extensionDescriptions: IExtensionDescription[]): IExtensionDescription[] {
private _arcs = new Map<string, string[]>();
private _nodesSet = new Set<string>();
private _nodesArr: string[] = [];
addNode(id: string): void {
if (!this._nodesSet.has(id)) {
this._nodesSet.add(id);
this._nodesArr.push(id);
}
}
addArc(from: string, to: string): void {
this.addNode(from);
this.addNode(to);
if (this._arcs.has(from)) {
this._arcs.get(from)!.push(to);
} else {
this._arcs.set(from, [to]);
}
}
getArcs(id: string): string[] {
if (this._arcs.has(id)) {
return this._arcs.get(id)!;
}
return [];
}
hasOnlyGoodArcs(id: string, good: Set<string>): boolean {
const dependencies = G.getArcs(id);
for (let i = 0; i < dependencies.length; i++) {
if (!good.has(dependencies[i])) {
return false;
}
}
return true;
}
getNodes(): string[] {
return this._nodesArr;
}
};
const descs = new ExtensionIdentifierMap<IExtensionDescription>();
for (const extensionDescription of extensionDescriptions) {
descs.set(extensionDescription.identifier, extensionDescription);
if (extensionDescription.extensionDependencies) {
for (const depId of extensionDescription.extensionDependencies) {
G.addArc(ExtensionIdentifier.toKey(extensionDescription.identifier), ExtensionIdentifier.toKey(depId));
}
}
// initialize with all extensions with no dependencies.
const good = new Set<string>();
G.getNodes().filter(id => G.getArcs(id).length === 0).forEach(id => good.add(id));
// all other extensions will be processed below.
const nodes = G.getNodes().filter(id => !good.has(id));
let madeProgress: boolean;
do {
madeProgress = false;
// find one extension which has only good deps
for (let i = 0; i < nodes.length; i++) {
const id = nodes[i];
if (G.hasOnlyGoodArcs(id, good)) {
nodes.splice(i, 1);
i--;
good.add(id);
madeProgress = true;
}
}
// The remaining nodes are bad and have loops
return nodes.map(id => descs.get(id)!);
}
public containsActivationEvent(activationEvent: string): boolean {
return this._activationMap.has(activationEvent);
}
public containsExtension(extensionId: ExtensionIdentifier): boolean {
return this._extensionsMap.has(extensionId);
}
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
return extensions ? extensions.slice(0) : [];
}
public getAllExtensionDescriptions(): IExtensionDescription[] {
}
public getSnapshot(): ExtensionDescriptionRegistrySnapshot {
return new ExtensionDescriptionRegistrySnapshot(
this._versionId,
this.getAllExtensionDescriptions()
);
}
public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined {
return extension ? extension : undefined;
}
public getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined {
for (const extensionDescription of this._extensionsArr) {
if (extensionDescription.uuid === uuid) {
return extensionDescription;
}
}
return undefined;
}
public getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined {
return (
this.getExtensionDescription(extensionId)
?? (uuid ? this.getExtensionDescriptionByUUID(uuid) : undefined)
);
}
export class ExtensionDescriptionRegistrySnapshot {
constructor(
public readonly versionId: number,
public readonly extensions: readonly IExtensionDescription[]
) { }
export interface IActivationEventsReader {
readActivationEvents(extensionDescription: IExtensionDescription): string[];
}
export class LockableExtensionDescriptionRegistry implements IReadOnlyExtensionDescriptionRegistry {
private readonly _actual: ExtensionDescriptionRegistry;
private readonly _lock = new Lock();
constructor(activationEventsReader: IActivationEventsReader) {
this._actual = new ExtensionDescriptionRegistry(activationEventsReader, []);
}
public async acquireLock(customerName: string): Promise<ExtensionDescriptionRegistryLock> {
const lock = await this._lock.acquire(customerName);
return new ExtensionDescriptionRegistryLock(this, lock);
}
public deltaExtensions(acquiredLock: ExtensionDescriptionRegistryLock, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): DeltaExtensionsResult {
if (!acquiredLock.isAcquiredFor(this)) {
throw new Error('Lock is not held');
}
return this._actual.deltaExtensions(toAdd, toRemove);
}
public containsActivationEvent(activationEvent: string): boolean {
return this._actual.containsActivationEvent(activationEvent);
}
public containsExtension(extensionId: ExtensionIdentifier): boolean {
extensionDescriptionRegistry.ts ×37
return this._actual.containsExtension(extensionId);
}
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
extensionDescriptionRegistry.ts ×37
return this._actual.getExtensionDescriptionsForActivationEvent(activationEvent);
}
public getAllExtensionDescriptions(): IExtensionDescription[] {
extensionDescriptionRegistry.ts ×37
return this._actual.getAllExtensionDescriptions();
}
public getSnapshot(): ExtensionDescriptionRegistrySnapshot {
extensionDescriptionRegistry.ts ×37
return this._actual.getSnapshot();
}
public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | undefined {
extensionDescriptionRegistry.ts ×37
return this._actual.getExtensionDescription(extensionId);
}
public getExtensionDescriptionByUUID(uuid: string): IExtensionDescription | undefined {
extensionDescriptionRegistry.ts ×37
return this._actual.getExtensionDescriptionByUUID(uuid);
}
public getExtensionDescriptionByIdOrUUID(extensionId: ExtensionIdentifier | string, uuid: string | undefined): IExtensionDescription | undefined {
extensionDescriptionRegistry.ts ×37
return this._actual.getExtensionDescriptionByIdOrUUID(extensionId, uuid);
}
export class ExtensionDescriptionRegistryLock extends Disposable {
private _isDisposed = false;
constructor(
private readonly _registry: LockableExtensionDescriptionRegistry,
lock: IDisposable
) {
super();
this._register(lock);
}
public isAcquiredFor(registry: LockableExtensionDescriptionRegistry): boolean {
return !this._isDisposed && this._registry === registry;
}
class LockCustomer {
public readonly promise: Promise<IDisposable>;
private readonly _resolve: (value: IDisposable) => void;
constructor(
public readonly name: string
) {
const withResolvers = promiseWithResolvers<IDisposable>();
this.promise = withResolvers.promise;
this._resolve = withResolvers.resolve;
}
resolve(value: IDisposable): void {
this._resolve(value);
}
class Lock {
private readonly _pendingCustomers: LockCustomer[] = [];
private _isLocked = false;
public async acquire(customerName: string): Promise<IDisposable> {
const customer = new LockCustomer(customerName);
this._pendingCustomers.push(customer);
this._advance();
return customer.promise;
}
private _advance(): void {
if (this._isLocked) {
// cannot advance yet
return;
}
if (this._pendingCustomers.length === 0) {
// no more waiting customers
return;
}
const customer = this._pendingCustomers.shift()!;
this._isLocked = true;
let customerHoldsLock = true;
const logLongRunningCustomerTimeout = setTimeout(() => {
if (customerHoldsLock) {
console.warn(`The customer named ${customer.name} has been holding on to the lock for 30s. This might be a problem.`);
}
}, 30 * 1000 /* 30 seconds */);
const releaseLock = () => {
if (!customerHoldsLock) {
return;
}
clearTimeout(logLongRunningCustomerTimeout);
customerHoldsLock = false;
this._isLocked = false;
this._advance();
};
customer.resolve(toDisposable(releaseLock));
}
const enum SortBucket {
Builtin = 0,
User = 1,
Dev = 2
}
/**
* Ensure that:
* - first are builtin extensions
* - second are user extensions
* - third are extensions under development
*
* In each bucket, extensions must be sorted alphabetically by their folder name.
*/
function extensionCmp(a: IExtensionDescription, b: IExtensionDescription): number {
extensionDescriptionRegistry.ts ×4
const aSortBucket = (a.isBuiltin ? SortBucket.Builtin : a.isUnderDevelopment ? SortBucket.Dev : SortBucket.User);
const bSortBucket = (b.isBuiltin ? SortBucket.Builtin : b.isUnderDevelopment ? SortBucket.Dev : SortBucket.User);
if (aSortBucket !== bSortBucket) {
return aSortBucket - bSortBucket;
}
const aLastSegment = path.posix.basename(a.extensionLocation.path);
extensionDescriptionRegistry.ts ×4
const bLastSegment = path.posix.basename(b.extensionLocation.path);
if (aLastSegment < bLastSegment) {
return -1;
}
return 1;
}
}
function removeExtensions(arr: IExtensionDescription[], toRemove: ExtensionIdentifier[]): IExtensionDescription[] {
extensionDescriptionRegistry.ts ×11
const toRemoveSet = new ExtensionIdentifierSet(toRemove);
return arr.filter(extension => !toRemoveSet.has(extension.identifier));
}