1902 lines
75 KiB
Text
1902 lines
75 KiB
Text
diff --git a/src/vs/workbench/contrib/extensions/browser/extension.ts b/src/vs/workbench/contrib/extensions/browser/extension.ts
|
|
new file mode 100644
|
|
index 00000000000..4cbe9dbe202
|
|
--- /dev/null
|
|
+++ b/src/vs/workbench/contrib/extensions/browser/extension.ts
|
|
@@ -0,0 +1,524 @@
|
|
+/*---------------------------------------------------------------------------------------------
|
|
+ * Copyright (c) Microsoft Corporation. All rights reserved.
|
|
+ * Licensed under the MIT License. See License.txt in the project root for license information.
|
|
+ *--------------------------------------------------------------------------------------------*/
|
|
+
|
|
+import * as nls from '../../../../nls.js';
|
|
+import * as semver from '../../../../base/common/semver/semver.js';
|
|
+import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
|
+import {
|
|
+ IExtensionGalleryService, ILocalExtension, IGalleryExtension,
|
|
+ IExtensionsControlManifest, IDeprecationInfo,
|
|
+ MaliciousExtensionInfo
|
|
+} from '../../../../platform/extensionManagement/common/extensionManagement.js';
|
|
+import { EnablementState, IExtensionManagementServer, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js';
|
|
+import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, findMatchingMaliciousEntry } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
|
|
+import { URI } from '../../../../base/common/uri.js';
|
|
+import { IExtension, ExtensionState, ExtensionRuntimeState } from '../common/extensions.js';
|
|
+import { ILogService } from '../../../../platform/log/common/log.js';
|
|
+import { IFileService } from '../../../../platform/files/common/files.js';
|
|
+import { IExtensionManifest, ExtensionType, TargetPlatform, IExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
|
|
+import { IProductService } from '../../../../platform/product/common/productService.js';
|
|
+import { FileAccess } from '../../../../base/common/network.js';
|
|
+import * as resources from '../../../../base/common/resources.js';
|
|
+import { CancellationToken } from '../../../../base/common/cancellation.js';
|
|
+import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js';
|
|
+
|
|
+export interface IExtensionStateProvider<T> {
|
|
+ (extension: Extension): T;
|
|
+}
|
|
+
|
|
+export class Extension implements IExtension {
|
|
+
|
|
+ public enablementState: EnablementState = EnablementState.EnabledGlobally;
|
|
+
|
|
+ private galleryResourcesCache = new Map<string, any>();
|
|
+
|
|
+ private _missingFromGallery: boolean | undefined;
|
|
+
|
|
+ constructor(
|
|
+ private stateProvider: IExtensionStateProvider<ExtensionState>,
|
|
+ private runtimeStateProvider: IExtensionStateProvider<ExtensionRuntimeState | undefined>,
|
|
+ public readonly server: IExtensionManagementServer | undefined,
|
|
+ public local: ILocalExtension | undefined,
|
|
+ private _gallery: IGalleryExtension | undefined,
|
|
+ private readonly resourceExtensionInfo: { resourceExtension: IResourceExtension; isWorkspaceScoped: boolean } | undefined,
|
|
+ @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
|
|
+ @ITelemetryService private readonly telemetryService: ITelemetryService,
|
|
+ @ILogService private readonly logService: ILogService,
|
|
+ @IFileService private readonly fileService: IFileService,
|
|
+ @IProductService private readonly productService: IProductService
|
|
+ ) {
|
|
+ }
|
|
+
|
|
+ get resourceExtension(): IResourceExtension | undefined {
|
|
+ if (this.resourceExtensionInfo) {
|
|
+ return this.resourceExtensionInfo.resourceExtension;
|
|
+ }
|
|
+ if (this.local?.isWorkspaceScoped) {
|
|
+ return {
|
|
+ type: 'resource',
|
|
+ identifier: this.local.identifier,
|
|
+ location: this.local.location,
|
|
+ manifest: this.local.manifest,
|
|
+ changelogUri: this.local.changelogUrl,
|
|
+ readmeUri: this.local.readmeUrl,
|
|
+ };
|
|
+ }
|
|
+ return undefined;
|
|
+ }
|
|
+
|
|
+ get gallery(): IGalleryExtension | undefined {
|
|
+ return this._gallery;
|
|
+ }
|
|
+
|
|
+ set gallery(gallery: IGalleryExtension | undefined) {
|
|
+ this._gallery = gallery;
|
|
+ this.galleryResourcesCache.clear();
|
|
+ }
|
|
+
|
|
+ get missingFromGallery(): boolean {
|
|
+ return !!this._missingFromGallery;
|
|
+ }
|
|
+
|
|
+ set missingFromGallery(missing: boolean) {
|
|
+ this._missingFromGallery = missing;
|
|
+ }
|
|
+
|
|
+ get type(): ExtensionType {
|
|
+ return this.local ? this.local.type : ExtensionType.User;
|
|
+ }
|
|
+
|
|
+ get isBuiltin(): boolean {
|
|
+ return this.local ? this.local.isBuiltin : false;
|
|
+ }
|
|
+
|
|
+ get isWorkspaceScoped(): boolean {
|
|
+ if (this.local) {
|
|
+ return this.local.isWorkspaceScoped;
|
|
+ }
|
|
+ if (this.resourceExtensionInfo) {
|
|
+ return this.resourceExtensionInfo.isWorkspaceScoped;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ get name(): string {
|
|
+ if (this.gallery) {
|
|
+ return this.gallery.name;
|
|
+ }
|
|
+ return this.getManifestFromLocalOrResource()?.name ?? '';
|
|
+ }
|
|
+
|
|
+ get displayName(): string {
|
|
+ if (this.gallery) {
|
|
+ return this.gallery.displayName || this.gallery.name;
|
|
+ }
|
|
+
|
|
+ return this.getManifestFromLocalOrResource()?.displayName ?? this.name;
|
|
+ }
|
|
+
|
|
+ get identifier(): IExtensionIdentifier {
|
|
+ if (this.gallery) {
|
|
+ return this.gallery.identifier;
|
|
+ }
|
|
+ if (this.resourceExtension) {
|
|
+ return this.resourceExtension.identifier;
|
|
+ }
|
|
+ return this.local?.identifier ?? { id: '' };
|
|
+ }
|
|
+
|
|
+ get uuid(): string | undefined {
|
|
+ return this.gallery ? this.gallery.identifier.uuid : this.local?.identifier.uuid;
|
|
+ }
|
|
+
|
|
+ get publisher(): string {
|
|
+ if (this.gallery) {
|
|
+ return this.gallery.publisher;
|
|
+ }
|
|
+ return this.getManifestFromLocalOrResource()?.publisher ?? '';
|
|
+ }
|
|
+
|
|
+ get publisherDisplayName(): string {
|
|
+ if (this.gallery) {
|
|
+ return this.gallery.publisherDisplayName || this.gallery.publisher;
|
|
+ }
|
|
+
|
|
+ if (this.local?.publisherDisplayName) {
|
|
+ return this.local.publisherDisplayName;
|
|
+ }
|
|
+
|
|
+ return this.publisher;
|
|
+ }
|
|
+
|
|
+ get publisherUrl(): URI | undefined {
|
|
+ return this.gallery?.publisherLink ? URI.parse(this.gallery.publisherLink) : undefined;
|
|
+ }
|
|
+
|
|
+ get publisherDomain(): { link: string; verified: boolean } | undefined {
|
|
+ return this.gallery?.publisherDomain;
|
|
+ }
|
|
+
|
|
+ get publisherSponsorLink(): URI | undefined {
|
|
+ return this.gallery?.publisherSponsorLink ? URI.parse(this.gallery.publisherSponsorLink) : undefined;
|
|
+ }
|
|
+
|
|
+ get version(): string {
|
|
+ return this.local ? this.local.manifest.version : this.latestVersion;
|
|
+ }
|
|
+
|
|
+ get private(): boolean {
|
|
+ return this.gallery ? this.gallery.private : this.local ? this.local.private : false;
|
|
+ }
|
|
+
|
|
+ get pinned(): boolean {
|
|
+ return !!this.local?.pinned;
|
|
+ }
|
|
+
|
|
+ get latestVersion(): string {
|
|
+ return this.gallery ? this.gallery.version : this.getManifestFromLocalOrResource()?.version ?? '';
|
|
+ }
|
|
+
|
|
+ get description(): string {
|
|
+ return this.gallery ? this.gallery.description : this.getManifestFromLocalOrResource()?.description ?? '';
|
|
+ }
|
|
+
|
|
+ get url(): string | undefined {
|
|
+ return this.gallery?.detailsLink;
|
|
+ }
|
|
+
|
|
+ get iconUrl(): string | undefined {
|
|
+ return this.galleryIconUrl || this.resourceExtensionIconUrl || this.localIconUrl || this.defaultIconUrl;
|
|
+ }
|
|
+
|
|
+ get iconUrlFallback(): string | undefined {
|
|
+ return this.gallery?.assets.icon?.fallbackUri;
|
|
+ }
|
|
+
|
|
+ private get localIconUrl(): string | undefined {
|
|
+ if (this.local && this.local.manifest.icon) {
|
|
+ return FileAccess.uriToBrowserUri(resources.joinPath(this.local.location, this.local.manifest.icon)).toString(true);
|
|
+ }
|
|
+ return undefined;
|
|
+ }
|
|
+
|
|
+ private get resourceExtensionIconUrl(): string | undefined {
|
|
+ if (this.resourceExtension?.manifest.icon) {
|
|
+ return FileAccess.uriToBrowserUri(resources.joinPath(this.resourceExtension.location, this.resourceExtension.manifest.icon)).toString(true);
|
|
+ }
|
|
+ return undefined;
|
|
+ }
|
|
+
|
|
+ private get galleryIconUrl(): string | undefined {
|
|
+ return this.gallery?.assets.icon?.uri;
|
|
+ }
|
|
+
|
|
+ private get defaultIconUrl(): string | undefined {
|
|
+ if (this.type === ExtensionType.System && this.local) {
|
|
+ if (this.local.manifest && this.local.manifest.contributes) {
|
|
+ if (Array.isArray(this.local.manifest.contributes.themes) && this.local.manifest.contributes.themes.length) {
|
|
+ return FileAccess.asBrowserUri('vs/workbench/contrib/extensions/browser/media/theme-icon.png').toString(true);
|
|
+ }
|
|
+ if (Array.isArray(this.local.manifest.contributes.grammars) && this.local.manifest.contributes.grammars.length) {
|
|
+ return FileAccess.asBrowserUri('vs/workbench/contrib/extensions/browser/media/language-icon.svg').toString(true);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return undefined;
|
|
+ }
|
|
+
|
|
+ get repository(): string | undefined {
|
|
+ return this.gallery && this.gallery.assets.repository ? this.gallery.assets.repository.uri : undefined;
|
|
+ }
|
|
+
|
|
+ get licenseUrl(): string | undefined {
|
|
+ return this.gallery && this.gallery.assets.license ? this.gallery.assets.license.uri : undefined;
|
|
+ }
|
|
+
|
|
+ get supportUrl(): string | undefined {
|
|
+ return this.gallery && this.gallery.supportLink ? this.gallery.supportLink : undefined;
|
|
+ }
|
|
+
|
|
+ get state(): ExtensionState {
|
|
+ return this.stateProvider(this);
|
|
+ }
|
|
+
|
|
+ private malicious: MaliciousExtensionInfo | undefined;
|
|
+ public get isMalicious(): boolean | undefined {
|
|
+ return !!this.malicious || this.enablementState === EnablementState.DisabledByMalicious;
|
|
+ }
|
|
+
|
|
+ public get maliciousInfoLink(): string | undefined {
|
|
+ return this.malicious?.learnMoreLink;
|
|
+ }
|
|
+
|
|
+ public deprecationInfo: IDeprecationInfo | undefined;
|
|
+
|
|
+ get installCount(): number | undefined {
|
|
+ return this.gallery ? this.gallery.installCount : undefined;
|
|
+ }
|
|
+
|
|
+ get rating(): number | undefined {
|
|
+ return this.gallery ? this.gallery.rating : undefined;
|
|
+ }
|
|
+
|
|
+ get ratingCount(): number | undefined {
|
|
+ return this.gallery ? this.gallery.ratingCount : undefined;
|
|
+ }
|
|
+
|
|
+ get ratingUrl(): string | undefined {
|
|
+ return this.gallery?.ratingLink;
|
|
+ }
|
|
+
|
|
+ get outdated(): boolean {
|
|
+ try {
|
|
+ if (!this.gallery || !this.local) {
|
|
+ return false;
|
|
+ }
|
|
+ // Do not allow updating system extensions in stable
|
|
+ if (this.type === ExtensionType.System && this.productService.quality === 'stable') {
|
|
+ return false;
|
|
+ }
|
|
+ if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) {
|
|
+ return false;
|
|
+ }
|
|
+ if (semver.gt(this.latestVersion, this.version)) {
|
|
+ return true;
|
|
+ }
|
|
+ if (this.outdatedTargetPlatform) {
|
|
+ return true;
|
|
+ }
|
|
+ } catch (error) {
|
|
+ /* Ignore */
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ get outdatedTargetPlatform(): boolean {
|
|
+ return !!this.local && !!this.gallery
|
|
+ && ![TargetPlatform.UNDEFINED, TargetPlatform.WEB].includes(this.local.targetPlatform)
|
|
+ && this.gallery.properties.targetPlatform !== TargetPlatform.WEB
|
|
+ && this.local.targetPlatform !== this.gallery.properties.targetPlatform
|
|
+ && semver.eq(this.latestVersion, this.version);
|
|
+ }
|
|
+
|
|
+ get runtimeState(): ExtensionRuntimeState | undefined {
|
|
+ return this.runtimeStateProvider(this);
|
|
+ }
|
|
+
|
|
+ get telemetryData(): any {
|
|
+ const { local, gallery } = this;
|
|
+
|
|
+ if (gallery) {
|
|
+ return getGalleryExtensionTelemetryData(gallery);
|
|
+ } else if (local) {
|
|
+ return getLocalExtensionTelemetryData(local);
|
|
+ } else {
|
|
+ return {};
|
|
+ }
|
|
+ }
|
|
+
|
|
+ get preview(): boolean {
|
|
+ return this.local?.manifest.preview ?? this.gallery?.preview ?? false;
|
|
+ }
|
|
+
|
|
+ get preRelease(): boolean {
|
|
+ return !!this.local?.preRelease;
|
|
+ }
|
|
+
|
|
+ get isPreReleaseVersion(): boolean {
|
|
+ if (this.local) {
|
|
+ return this.local.isPreReleaseVersion;
|
|
+ }
|
|
+ return !!this.gallery?.properties.isPreReleaseVersion;
|
|
+ }
|
|
+
|
|
+ get hasPreReleaseVersion(): boolean {
|
|
+ return this.gallery ? this.gallery.hasPreReleaseVersion : !!this.local?.hasPreReleaseVersion;
|
|
+ }
|
|
+
|
|
+ get hasReleaseVersion(): boolean {
|
|
+ return !!this.resourceExtension || !!this.gallery?.hasReleaseVersion;
|
|
+ }
|
|
+
|
|
+ private getLocal(): ILocalExtension | undefined {
|
|
+ return this.local && !this.outdated ? this.local : undefined;
|
|
+ }
|
|
+
|
|
+ async getManifest(token: CancellationToken): Promise<IExtensionManifest | null> {
|
|
+ const local = this.getLocal();
|
|
+ if (local) {
|
|
+ return local.manifest;
|
|
+ }
|
|
+
|
|
+ if (this.gallery) {
|
|
+ return this.getGalleryManifest(token);
|
|
+ }
|
|
+
|
|
+ if (this.resourceExtension) {
|
|
+ return this.resourceExtension.manifest;
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ async getGalleryManifest(token: CancellationToken = CancellationToken.None): Promise<IExtensionManifest | null> {
|
|
+ if (this.gallery) {
|
|
+ let cache = this.galleryResourcesCache.get('manifest');
|
|
+ if (!cache) {
|
|
+ if (this.gallery.assets.manifest) {
|
|
+ this.galleryResourcesCache.set('manifest', cache = this.galleryService.getManifest(this.gallery, token)
|
|
+ .catch(e => {
|
|
+ this.galleryResourcesCache.delete('manifest');
|
|
+ throw e;
|
|
+ }));
|
|
+ } else {
|
|
+ this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.identifier.id);
|
|
+ }
|
|
+ }
|
|
+ return cache;
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ hasReadme(): boolean {
|
|
+ if (this.local && this.local.readmeUrl) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (this.gallery && this.gallery.assets.readme) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (this.resourceExtension?.readmeUri) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return this.type === ExtensionType.System;
|
|
+ }
|
|
+
|
|
+ async getReadme(token: CancellationToken): Promise<string> {
|
|
+ const local = this.getLocal();
|
|
+ if (local?.readmeUrl) {
|
|
+ const content = await this.fileService.readFile(local.readmeUrl);
|
|
+ return content.value.toString();
|
|
+ }
|
|
+
|
|
+ if (this.gallery) {
|
|
+ if (this.gallery.assets.readme) {
|
|
+ return this.galleryService.getReadme(this.gallery, token);
|
|
+ }
|
|
+ this.telemetryService.publicLog('extensions:NotFoundReadMe', this.telemetryData);
|
|
+ }
|
|
+
|
|
+ if (this.type === ExtensionType.System) {
|
|
+ return Promise.resolve(`# ${this.displayName || this.name}
|
|
+**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
|
|
+## Features
|
|
+${this.description}
|
|
+`);
|
|
+ }
|
|
+
|
|
+ if (this.resourceExtension?.readmeUri) {
|
|
+ const content = await this.fileService.readFile(this.resourceExtension?.readmeUri);
|
|
+ return content.value.toString();
|
|
+ }
|
|
+
|
|
+ return Promise.reject(new Error('not available'));
|
|
+ }
|
|
+
|
|
+ hasChangelog(): boolean {
|
|
+ if (this.local && this.local.changelogUrl) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (this.gallery && this.gallery.assets.changelog) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return this.type === ExtensionType.System;
|
|
+ }
|
|
+
|
|
+ async getChangelog(token: CancellationToken): Promise<string> {
|
|
+ const local = this.getLocal();
|
|
+ if (local?.changelogUrl) {
|
|
+ const content = await this.fileService.readFile(local.changelogUrl);
|
|
+ return content.value.toString();
|
|
+ }
|
|
+
|
|
+ if (this.gallery?.assets.changelog) {
|
|
+ return this.galleryService.getChangelog(this.gallery, token);
|
|
+ }
|
|
+
|
|
+ if (this.type === ExtensionType.System) {
|
|
+ return Promise.resolve(`Please check the [VS Code Release Notes](command:${ShowCurrentReleaseNotesActionId}) for changes to the built-in extensions.`);
|
|
+ }
|
|
+
|
|
+ return Promise.reject(new Error('not available'));
|
|
+ }
|
|
+
|
|
+ get categories(): readonly string[] {
|
|
+ const { local, gallery, resourceExtension } = this;
|
|
+ if (local && local.manifest.categories && !this.outdated) {
|
|
+ return local.manifest.categories;
|
|
+ }
|
|
+ if (gallery) {
|
|
+ return gallery.categories;
|
|
+ }
|
|
+ if (resourceExtension) {
|
|
+ return resourceExtension.manifest.categories ?? [];
|
|
+ }
|
|
+ return [];
|
|
+ }
|
|
+
|
|
+ get tags(): readonly string[] {
|
|
+ const { gallery } = this;
|
|
+ if (gallery) {
|
|
+ return gallery.tags.filter(tag => !tag.startsWith('_'));
|
|
+ }
|
|
+ return [];
|
|
+ }
|
|
+
|
|
+ get dependencies(): string[] {
|
|
+ const { local, gallery, resourceExtension } = this;
|
|
+ if (local && local.manifest.extensionDependencies && !this.outdated) {
|
|
+ return local.manifest.extensionDependencies;
|
|
+ }
|
|
+ if (gallery) {
|
|
+ return gallery.properties.dependencies || [];
|
|
+ }
|
|
+ if (resourceExtension) {
|
|
+ return resourceExtension.manifest.extensionDependencies || [];
|
|
+ }
|
|
+ return [];
|
|
+ }
|
|
+
|
|
+ get extensionPack(): string[] {
|
|
+ const { local, gallery, resourceExtension } = this;
|
|
+ if (local && local.manifest.extensionPack && !this.outdated) {
|
|
+ return local.manifest.extensionPack;
|
|
+ }
|
|
+ if (gallery) {
|
|
+ return gallery.properties.extensionPack || [];
|
|
+ }
|
|
+ if (resourceExtension) {
|
|
+ return resourceExtension.manifest.extensionPack || [];
|
|
+ }
|
|
+ return [];
|
|
+ }
|
|
+
|
|
+ setExtensionsControlManifest(extensionsControlManifest: IExtensionsControlManifest): void {
|
|
+ this.malicious = findMatchingMaliciousEntry(this.identifier, extensionsControlManifest.malicious);
|
|
+ this.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[this.identifier.id.toLowerCase()] : undefined;
|
|
+ }
|
|
+
|
|
+ private getManifestFromLocalOrResource(): IExtensionManifest | null {
|
|
+ if (this.local) {
|
|
+ return this.local.manifest;
|
|
+ }
|
|
+ if (this.resourceExtension) {
|
|
+ return this.resourceExtension.manifest;
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+}
|
|
diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.ts b/src/vs/workbench/contrib/extensions/browser/extensions.ts
|
|
new file mode 100644
|
|
index 00000000000..457317cda4b
|
|
--- /dev/null
|
|
+++ b/src/vs/workbench/contrib/extensions/browser/extensions.ts
|
|
@@ -0,0 +1,401 @@
|
|
+/*---------------------------------------------------------------------------------------------
|
|
+ * Copyright (c) Microsoft Corporation. All rights reserved.
|
|
+ * Licensed under the MIT License. See License.txt in the project root for license information.
|
|
+ *--------------------------------------------------------------------------------------------*/
|
|
+
|
|
+import { index } from '../../../../base/common/arrays.js';
|
|
+import { CancellationToken } from '../../../../base/common/cancellation.js';
|
|
+import { Event, Emitter } from '../../../../base/common/event.js';
|
|
+import { IMarkdownString } from '../../../../base/common/htmlContent.js';
|
|
+import { Disposable } from '../../../../base/common/lifecycle.js';
|
|
+import { URI } from '../../../../base/common/uri.js';
|
|
+import { IExtensionGalleryService, IGalleryExtension, IProductVersion, IExtensionInfo, InstallExtensionResult, InstallExtensionEvent, InstallOperation, DidUninstallExtensionEvent, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
|
|
+import { areSameExtensions, groupByExtension } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
|
|
+import { IExtension as IPlatformExtension, ExtensionType, IExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
|
|
+import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
|
+import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
|
+import { TelemetryTrustedValue } from '../../../../platform/telemetry/common/telemetryUtils.js';
|
|
+import { IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, IExtensionManagementServer } from '../../../services/extensionManagement/common/extensionManagement.js';
|
|
+import { Extension, IExtensionStateProvider } from './extension.js';
|
|
+import { IExtension, ExtensionState, ExtensionRuntimeState } from '../common/extensions.js';
|
|
+
|
|
+export class Extensions extends Disposable {
|
|
+
|
|
+ private readonly _onChange = this._register(new Emitter<{ extension: Extension; operation?: InstallOperation } | undefined>());
|
|
+ get onChange() { return this._onChange.event; }
|
|
+
|
|
+ private readonly _onReset = this._register(new Emitter<void>());
|
|
+ get onReset() { return this._onReset.event; }
|
|
+
|
|
+ private installing: Extension[] = [];
|
|
+ private uninstalling: Extension[] = [];
|
|
+ private installed: Extension[] = [];
|
|
+
|
|
+ constructor(
|
|
+ readonly server: IExtensionManagementServer,
|
|
+ private readonly stateProvider: IExtensionStateProvider<ExtensionState>,
|
|
+ private readonly runtimeStateProvider: IExtensionStateProvider<ExtensionRuntimeState | undefined>,
|
|
+ private readonly isWorkspaceServer: boolean,
|
|
+ @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
|
|
+ @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
|
|
+ @IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService,
|
|
+ @ITelemetryService private readonly telemetryService: ITelemetryService,
|
|
+ @IInstantiationService private readonly instantiationService: IInstantiationService
|
|
+ ) {
|
|
+ super();
|
|
+ this._register(server.extensionManagementService.onInstallExtension(e => this.onInstallExtension(e)));
|
|
+ this._register(server.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e)));
|
|
+ this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e.identifier)));
|
|
+ this._register(server.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
|
|
+ this._register(server.extensionManagementService.onDidUpdateExtensionMetadata(e => this.onDidUpdateExtensionMetadata(e.local)));
|
|
+ this._register(server.extensionManagementService.onDidChangeProfile(() => this.reset()));
|
|
+ this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e)));
|
|
+ this._register(Event.any(this.onChange, this.onReset)(() => this._local = undefined));
|
|
+ if (this.isWorkspaceServer) {
|
|
+ this._register(this.workbenchExtensionManagementService.onInstallExtension(e => {
|
|
+ if (e.workspaceScoped) {
|
|
+ this.onInstallExtension(e);
|
|
+ }
|
|
+ }));
|
|
+ this._register(this.workbenchExtensionManagementService.onDidInstallExtensions(e => {
|
|
+ const result = e.filter(e => e.workspaceScoped);
|
|
+ if (result.length) {
|
|
+ this.onDidInstallExtensions(result);
|
|
+ }
|
|
+ }));
|
|
+ this._register(this.workbenchExtensionManagementService.onUninstallExtension(e => {
|
|
+ if (e.workspaceScoped) {
|
|
+ this.onUninstallExtension(e.identifier);
|
|
+ }
|
|
+ }));
|
|
+ this._register(this.workbenchExtensionManagementService.onDidUninstallExtension(e => {
|
|
+ if (e.workspaceScoped) {
|
|
+ this.onDidUninstallExtension(e);
|
|
+ }
|
|
+ }));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private _local: Extension[] | undefined;
|
|
+ get local(): Extension[] {
|
|
+ if (!this._local) {
|
|
+ this._local = [];
|
|
+ for (const extension of this.installed) {
|
|
+ this._local.push(extension);
|
|
+ }
|
|
+ for (const extension of this.installing) {
|
|
+ if (!this.installed.some(installed => areSameExtensions(installed.identifier, extension.identifier))) {
|
|
+ this._local.push(extension);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return this._local;
|
|
+ }
|
|
+
|
|
+ async queryInstalled(productVersion: IProductVersion): Promise<IExtension[]> {
|
|
+ await this.fetchInstalledExtensions(productVersion);
|
|
+ this._onChange.fire(undefined);
|
|
+ return this.local;
|
|
+ }
|
|
+
|
|
+ async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion, flagExtensionsMissingFromGallery?: IExtensionInfo[]): Promise<void> {
|
|
+ const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion);
|
|
+ for (const [extension, gallery] of extensions) {
|
|
+ // update metadata of the extension if it does not exist
|
|
+ if (extension.local && !extension.local.identifier.uuid) {
|
|
+ extension.local = await this.updateMetadata(extension.local, gallery);
|
|
+ }
|
|
+ if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) {
|
|
+ extension.gallery = gallery;
|
|
+ this._onChange.fire({ extension });
|
|
+ }
|
|
+ }
|
|
+ // Detect extensions that do not have a corresponding gallery entry.
|
|
+ if (flagExtensionsMissingFromGallery) {
|
|
+ const extensionsToQuery = [];
|
|
+ for (const extension of this.local) {
|
|
+ // Extension is already paired with a gallery object
|
|
+ if (extension.gallery) {
|
|
+ continue;
|
|
+ }
|
|
+ // Already flagged as missing from gallery
|
|
+ if (extension.missingFromGallery) {
|
|
+ continue;
|
|
+ }
|
|
+ // A UUID indicates extension originated from gallery
|
|
+ if (!extension.identifier.uuid) {
|
|
+ continue;
|
|
+ }
|
|
+ // Extension is not present in the set we are concerned about
|
|
+ if (!flagExtensionsMissingFromGallery.some(f => areSameExtensions(f, extension.identifier))) {
|
|
+ continue;
|
|
+ }
|
|
+ extensionsToQuery.push(extension);
|
|
+ }
|
|
+ if (extensionsToQuery.length) {
|
|
+ const queryResult = await this.galleryService.getExtensions(extensionsToQuery.map(e => ({ ...e.identifier, version: e.version })), CancellationToken.None);
|
|
+ const queriedIds: string[] = [];
|
|
+ const missingIds: string[] = [];
|
|
+ for (const extension of extensionsToQuery) {
|
|
+ queriedIds.push(extension.identifier.id);
|
|
+ const gallery = queryResult.find(g => areSameExtensions(g.identifier, extension.identifier));
|
|
+ if (gallery) {
|
|
+ extension.gallery = gallery;
|
|
+ } else {
|
|
+ extension.missingFromGallery = true;
|
|
+ missingIds.push(extension.identifier.id);
|
|
+ }
|
|
+ this._onChange.fire({ extension });
|
|
+ }
|
|
+ type MissingFromGalleryClassification = {
|
|
+ owner: 'joshspicer';
|
|
+ comment: 'Report when installed extensions are no longer available in the gallery';
|
|
+ queriedIds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extensions queried as potentially missing from gallery' };
|
|
+ missingIds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extensions determined missing from gallery' };
|
|
+ };
|
|
+ type MissingFromGalleryEvent = {
|
|
+ readonly queriedIds: TelemetryTrustedValue<string>;
|
|
+ readonly missingIds: TelemetryTrustedValue<string>;
|
|
+ };
|
|
+ this.telemetryService.publicLog2<MissingFromGalleryEvent, MissingFromGalleryClassification>('extensions:missingFromGallery', {
|
|
+ queriedIds: new TelemetryTrustedValue(queriedIds.join(';')),
|
|
+ missingIds: new TelemetryTrustedValue(missingIds.join(';'))
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise<[Extension, IGalleryExtension][]> {
|
|
+ const mappedExtensions = this.mapInstalledExtensionWithGalleryExtension(galleryExtensions);
|
|
+ const targetPlatform = await this.server.extensionManagementService.getTargetPlatform();
|
|
+ const compatibleGalleryExtensions: IGalleryExtension[] = [];
|
|
+ const compatibleGalleryExtensionsToFetch: IExtensionInfo[] = [];
|
|
+ await Promise.allSettled(mappedExtensions.map(async ([extension, gallery]) => {
|
|
+ if (extension.local) {
|
|
+ if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform, productVersion)) {
|
|
+ compatibleGalleryExtensions.push(gallery);
|
|
+ } else {
|
|
+ compatibleGalleryExtensionsToFetch.push({ ...extension.local.identifier, preRelease: extension.local.preRelease });
|
|
+ }
|
|
+ }
|
|
+ }));
|
|
+ if (compatibleGalleryExtensionsToFetch.length) {
|
|
+ const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true, productVersion }, CancellationToken.None);
|
|
+ compatibleGalleryExtensions.push(...result);
|
|
+ }
|
|
+ return this.mapInstalledExtensionWithGalleryExtension(compatibleGalleryExtensions);
|
|
+ }
|
|
+
|
|
+ private mapInstalledExtensionWithGalleryExtension(galleryExtensions: IGalleryExtension[]): [Extension, IGalleryExtension][] {
|
|
+ const mappedExtensions: [Extension, IGalleryExtension][] = [];
|
|
+ const byUUID = new Map<string, IGalleryExtension>(), byID = new Map<string, IGalleryExtension>();
|
|
+ for (const gallery of galleryExtensions) {
|
|
+ byUUID.set(gallery.identifier.uuid, gallery);
|
|
+ byID.set(gallery.identifier.id.toLowerCase(), gallery);
|
|
+ }
|
|
+ for (const installed of this.installed) {
|
|
+ if (installed.uuid) {
|
|
+ const gallery = byUUID.get(installed.uuid);
|
|
+ if (gallery) {
|
|
+ mappedExtensions.push([installed, gallery]);
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ if (installed.local?.source !== 'resource') {
|
|
+ const gallery = byID.get(installed.identifier.id.toLowerCase());
|
|
+ if (gallery) {
|
|
+ mappedExtensions.push([installed, gallery]);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return mappedExtensions;
|
|
+ }
|
|
+
|
|
+ private async updateMetadata(localExtension: ILocalExtension, gallery: IGalleryExtension): Promise<ILocalExtension> {
|
|
+ let isPreReleaseVersion = false;
|
|
+ if (localExtension.manifest.version !== gallery.version) {
|
|
+ type GalleryServiceMatchInstalledExtensionClassification = {
|
|
+ owner: 'sandy081';
|
|
+ comment: 'Report when a request is made to update metadata of an installed extension';
|
|
+ };
|
|
+ this.telemetryService.publicLog2<{}, GalleryServiceMatchInstalledExtensionClassification>('galleryService:updateMetadata');
|
|
+ const galleryWithLocalVersion: IGalleryExtension | undefined = (await this.galleryService.getExtensions([{ ...localExtension.identifier, version: localExtension.manifest.version }], CancellationToken.None))[0];
|
|
+ isPreReleaseVersion = !!galleryWithLocalVersion?.properties?.isPreReleaseVersion;
|
|
+ }
|
|
+ return this.workbenchExtensionManagementService.updateMetadata(localExtension, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId, isPreReleaseVersion });
|
|
+ }
|
|
+
|
|
+ canInstall(galleryExtension: IGalleryExtension): Promise<true | IMarkdownString> {
|
|
+ return this.server.extensionManagementService.canInstall(galleryExtension);
|
|
+ }
|
|
+
|
|
+ private onInstallExtension(event: InstallExtensionEvent): void {
|
|
+ const { source } = event;
|
|
+ if (source && !URI.isUri(source)) {
|
|
+ const extension = this.installed.find(e => areSameExtensions(e.identifier, source.identifier))
|
|
+ ?? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, undefined, source, undefined);
|
|
+ this.installing.push(extension);
|
|
+ this._onChange.fire({ extension });
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private async fetchInstalledExtensions(productVersion?: IProductVersion): Promise<void> {
|
|
+ const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest();
|
|
+ const all = await this.server.extensionManagementService.getInstalled(undefined, undefined, productVersion);
|
|
+ if (this.isWorkspaceServer) {
|
|
+ all.push(...await this.workbenchExtensionManagementService.getInstalledWorkspaceExtensions(true));
|
|
+ }
|
|
+
|
|
+ // dedup workspace, user and system extensions by giving priority to workspace first and then to user extension.
|
|
+ const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => {
|
|
+ if (extensions.length === 1) {
|
|
+ result.push(extensions[0]);
|
|
+ } else {
|
|
+ let workspaceExtension: ILocalExtension | undefined,
|
|
+ userExtension: ILocalExtension | undefined,
|
|
+ systemExtension: ILocalExtension | undefined;
|
|
+ for (const extension of extensions) {
|
|
+ if (extension.isWorkspaceScoped) {
|
|
+ workspaceExtension = extension;
|
|
+ } else if (extension.type === ExtensionType.User) {
|
|
+ userExtension = extension;
|
|
+ } else {
|
|
+ systemExtension = extension;
|
|
+ }
|
|
+ }
|
|
+ const extension = workspaceExtension ?? userExtension ?? systemExtension;
|
|
+ if (extension) {
|
|
+ result.push(extension);
|
|
+ }
|
|
+ }
|
|
+ return result;
|
|
+ }, [] as ILocalExtension[]);
|
|
+
|
|
+ const byId = index(this.installed, e => e.local ? e.local.identifier.id : e.identifier.id);
|
|
+ this.installed = installed.map(local => {
|
|
+ const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined);
|
|
+ extension.local = local;
|
|
+ extension.enablementState = this.extensionEnablementService.getEnablementState(local);
|
|
+ extension.setExtensionsControlManifest(extensionsControlManifest);
|
|
+ return extension;
|
|
+ });
|
|
+ }
|
|
+
|
|
+ private async reset(): Promise<void> {
|
|
+ this.installed = [];
|
|
+ this.installing = [];
|
|
+ this.uninstalling = [];
|
|
+ await this.fetchInstalledExtensions();
|
|
+ this._onReset.fire();
|
|
+ }
|
|
+
|
|
+ private async onDidInstallExtensions(results: readonly InstallExtensionResult[]): Promise<void> {
|
|
+ const extensions: Extension[] = [];
|
|
+ for (const event of results) {
|
|
+ const { local, source } = event;
|
|
+ const gallery = source && !URI.isUri(source) ? source : undefined;
|
|
+ const location = source && URI.isUri(source) ? source : undefined;
|
|
+ const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0] : null;
|
|
+ this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing;
|
|
+
|
|
+ let extension: Extension | undefined = installingExtension ? installingExtension
|
|
+ : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined)
|
|
+ : undefined;
|
|
+ if (extension) {
|
|
+ if (local) {
|
|
+ const installed = this.installed.filter(e => areSameExtensions(e.identifier, extension!.identifier))[0];
|
|
+ if (installed) {
|
|
+ extension = installed;
|
|
+ } else {
|
|
+ this.installed.push(extension);
|
|
+ }
|
|
+ extension.local = local;
|
|
+ if (!extension.gallery) {
|
|
+ extension.gallery = gallery;
|
|
+ }
|
|
+ extension.enablementState = this.extensionEnablementService.getEnablementState(local);
|
|
+ }
|
|
+ extensions.push(extension);
|
|
+ }
|
|
+ this._onChange.fire(!local || !extension ? undefined : { extension, operation: event.operation });
|
|
+ }
|
|
+
|
|
+ if (extensions.length) {
|
|
+ const manifest = await this.server.extensionManagementService.getExtensionsControlManifest();
|
|
+ for (const extension of extensions) {
|
|
+ extension.setExtensionsControlManifest(manifest);
|
|
+ }
|
|
+ this.matchInstalledExtensionsWithGallery(extensions);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private async onDidUpdateExtensionMetadata(local: ILocalExtension): Promise<void> {
|
|
+ const extension = this.installed.find(e => areSameExtensions(e.identifier, local.identifier));
|
|
+ if (extension?.local) {
|
|
+ extension.local = local;
|
|
+ this._onChange.fire({ extension });
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private async matchInstalledExtensionsWithGallery(extensions: Extension[]): Promise<void> {
|
|
+ const toMatch = extensions.filter(e => e.local && !e.gallery && e.local.source !== 'resource');
|
|
+ if (!toMatch.length) {
|
|
+ return;
|
|
+ }
|
|
+ if (!this.galleryService.isEnabled()) {
|
|
+ return;
|
|
+ }
|
|
+ const galleryExtensions = await this.galleryService.getExtensions(toMatch.map(e => ({ ...e.identifier, preRelease: e.local?.preRelease })), { compatible: true, targetPlatform: await this.server.extensionManagementService.getTargetPlatform() }, CancellationToken.None);
|
|
+ for (const extension of extensions) {
|
|
+ const compatible = galleryExtensions.find(e => areSameExtensions(e.identifier, extension.identifier));
|
|
+ if (compatible) {
|
|
+ extension.gallery = compatible;
|
|
+ this._onChange.fire({ extension });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private onUninstallExtension(identifier: IExtensionIdentifier): void {
|
|
+ const extension = this.installed.filter(e => areSameExtensions(e.identifier, identifier))[0];
|
|
+ if (extension) {
|
|
+ const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0] || extension;
|
|
+ this.uninstalling = [uninstalling, ...this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier))];
|
|
+ this._onChange.fire(uninstalling ? { extension: uninstalling } : undefined);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private onDidUninstallExtension({ identifier, error }: DidUninstallExtensionEvent): void {
|
|
+ const uninstalled = this.uninstalling.find(e => areSameExtensions(e.identifier, identifier)) || this.installed.find(e => areSameExtensions(e.identifier, identifier));
|
|
+ this.uninstalling = this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier));
|
|
+ if (!error) {
|
|
+ this.installed = this.installed.filter(e => !areSameExtensions(e.identifier, identifier));
|
|
+ }
|
|
+ if (uninstalled) {
|
|
+ this._onChange.fire({ extension: uninstalled });
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private onEnablementChanged(platformExtensions: readonly IPlatformExtension[]) {
|
|
+ const extensions = this.local.filter(e => platformExtensions.some(p => areSameExtensions(e.identifier, p.identifier)));
|
|
+ for (const extension of extensions) {
|
|
+ if (extension.local) {
|
|
+ const enablementState = this.extensionEnablementService.getEnablementState(extension.local);
|
|
+ if (enablementState !== extension.enablementState) {
|
|
+ extension.enablementState = enablementState;
|
|
+ this._onChange.fire({ extension });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ getExtensionState(extension: Extension): ExtensionState {
|
|
+ if (extension.gallery && this.installing.some(e => !!e.gallery && areSameExtensions(e.gallery.identifier, extension.gallery!.identifier))) {
|
|
+ return ExtensionState.Installing;
|
|
+ }
|
|
+ if (this.uninstalling.some(e => areSameExtensions(e.identifier, extension.identifier))) {
|
|
+ return ExtensionState.Uninstalling;
|
|
+ }
|
|
+ const local = this.installed.filter(e => e === extension || (e.gallery && extension.gallery && areSameExtensions(e.gallery.identifier, extension.gallery.identifier)))[0];
|
|
+ return local ? ExtensionState.Installed : ExtensionState.Uninstalled;
|
|
+ }
|
|
+}
|
|
diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
|
|
index de710641876..7bf681e7a9b 100644
|
|
--- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
|
|
+++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
|
|
@@ -6,7 +6,6 @@
|
|
import * as nls from '../../../../nls.js';
|
|
import * as semver from '../../../../base/common/semver/semver.js';
|
|
import { Event, Emitter } from '../../../../base/common/event.js';
|
|
-import { index } from '../../../../base/common/arrays.js';
|
|
import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise } from '../../../../base/common/async.js';
|
|
import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js';
|
|
import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
|
@@ -14,8 +13,8 @@ import { IPager, singlePagePager } from '../../../../base/common/paging.js';
|
|
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
|
import {
|
|
IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions,
|
|
- InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, WEB_EXTENSION_TAG, InstallExtensionResult,
|
|
- IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX,
|
|
+ InstallOperation, WEB_EXTENSION_TAG, InstallExtensionResult,
|
|
+ IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX,
|
|
InstallOptions, IProductVersion,
|
|
UninstallExtensionInfo,
|
|
TargetPlatformToString,
|
|
@@ -24,12 +23,11 @@ import {
|
|
EXTENSION_INSTALL_SKIP_PUBLISHER_TRUST_CONTEXT,
|
|
ExtensionManagementError,
|
|
ExtensionManagementErrorCode,
|
|
- MaliciousExtensionInfo,
|
|
shouldRequireRepositorySignatureFor,
|
|
IGalleryExtensionVersion
|
|
} from '../../../../platform/extensionManagement/common/extensionManagement.js';
|
|
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js';
|
|
-import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId, findMatchingMaliciousEntry } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
|
|
+import { areSameExtensions, groupByExtension, getGalleryExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
|
|
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
|
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
|
import { IHostService } from '../../../services/host/browser/host.js';
|
|
@@ -38,17 +36,17 @@ import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConf
|
|
import { ACTIVE_GROUP, IEditorService, MODAL_GROUP, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
|
|
import { IURLService, IURLHandler, IOpenURLOptions } from '../../../../platform/url/common/url.js';
|
|
import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js';
|
|
+import { Extension } from './extension.js';
|
|
+import { Extensions } from './extensions.js';
|
|
import { ILogService } from '../../../../platform/log/common/log.js';
|
|
import { IProgressOptions, IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
|
|
import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
|
|
-import * as resources from '../../../../base/common/resources.js';
|
|
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
|
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
|
import { IFileService } from '../../../../platform/files/common/files.js';
|
|
-import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform, ExtensionIdentifier, IExtensionIdentifier, IExtensionDescription, isApplicationScopedExtension } from '../../../../platform/extensions/common/extensions.js';
|
|
+import { ExtensionType, TargetPlatform, ExtensionIdentifier, IExtensionIdentifier, IExtensionDescription, isApplicationScopedExtension } from '../../../../platform/extensions/common/extensions.js';
|
|
import { ILanguageService } from '../../../../editor/common/languages/language.js';
|
|
import { IProductService } from '../../../../platform/product/common/productService.js';
|
|
-import { FileAccess } from '../../../../base/common/network.js';
|
|
import { IIgnoredExtensionsManagementService } from '../../../../platform/userDataSync/common/ignoredExtensions.js';
|
|
import { IUserDataAutoSyncService, IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js';
|
|
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
|
@@ -67,7 +65,6 @@ import { IUpdateService, StateType } from '../../../../platform/update/common/up
|
|
import { areApiProposalsCompatible, isEngineValid } from '../../../../platform/extensions/common/extensionValidator.js';
|
|
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
|
|
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
|
-import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js';
|
|
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
|
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
|
|
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
|
|
@@ -76,10 +73,6 @@ import { fromNow } from '../../../../base/common/date.js';
|
|
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
|
|
import { IMeteredConnectionService } from '../../../../platform/meteredConnection/common/meteredConnection.js';
|
|
|
|
-interface IExtensionStateProvider<T> {
|
|
- (extension: Extension): T;
|
|
-}
|
|
-
|
|
interface InstalledExtensionsEvent {
|
|
readonly extensionIds: TelemetryTrustedValue<string>;
|
|
readonly count: number;
|
|
@@ -90,887 +83,9 @@ type ExtensionsLoadClassification = {
|
|
readonly extensionIds: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The list of extension ids that are installed.' };
|
|
readonly count: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The number of extensions that are installed.' };
|
|
};
|
|
-
|
|
-export class Extension implements IExtension {
|
|
-
|
|
- public enablementState: EnablementState = EnablementState.EnabledGlobally;
|
|
-
|
|
- private galleryResourcesCache = new Map<string, any>();
|
|
-
|
|
- private _missingFromGallery: boolean | undefined;
|
|
-
|
|
- constructor(
|
|
- private stateProvider: IExtensionStateProvider<ExtensionState>,
|
|
- private runtimeStateProvider: IExtensionStateProvider<ExtensionRuntimeState | undefined>,
|
|
- public readonly server: IExtensionManagementServer | undefined,
|
|
- public local: ILocalExtension | undefined,
|
|
- private _gallery: IGalleryExtension | undefined,
|
|
- private readonly resourceExtensionInfo: { resourceExtension: IResourceExtension; isWorkspaceScoped: boolean } | undefined,
|
|
- @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
|
|
- @ITelemetryService private readonly telemetryService: ITelemetryService,
|
|
- @ILogService private readonly logService: ILogService,
|
|
- @IFileService private readonly fileService: IFileService,
|
|
- @IProductService private readonly productService: IProductService
|
|
- ) {
|
|
- }
|
|
-
|
|
- get resourceExtension(): IResourceExtension | undefined {
|
|
- if (this.resourceExtensionInfo) {
|
|
- return this.resourceExtensionInfo.resourceExtension;
|
|
- }
|
|
- if (this.local?.isWorkspaceScoped) {
|
|
- return {
|
|
- type: 'resource',
|
|
- identifier: this.local.identifier,
|
|
- location: this.local.location,
|
|
- manifest: this.local.manifest,
|
|
- changelogUri: this.local.changelogUrl,
|
|
- readmeUri: this.local.readmeUrl,
|
|
- };
|
|
- }
|
|
- return undefined;
|
|
- }
|
|
-
|
|
- get gallery(): IGalleryExtension | undefined {
|
|
- return this._gallery;
|
|
- }
|
|
-
|
|
- set gallery(gallery: IGalleryExtension | undefined) {
|
|
- this._gallery = gallery;
|
|
- this.galleryResourcesCache.clear();
|
|
- }
|
|
-
|
|
- get missingFromGallery(): boolean {
|
|
- return !!this._missingFromGallery;
|
|
- }
|
|
-
|
|
- set missingFromGallery(missing: boolean) {
|
|
- this._missingFromGallery = missing;
|
|
- }
|
|
-
|
|
- get type(): ExtensionType {
|
|
- return this.local ? this.local.type : ExtensionType.User;
|
|
- }
|
|
-
|
|
- get isBuiltin(): boolean {
|
|
- return this.local ? this.local.isBuiltin : false;
|
|
- }
|
|
-
|
|
- get isWorkspaceScoped(): boolean {
|
|
- if (this.local) {
|
|
- return this.local.isWorkspaceScoped;
|
|
- }
|
|
- if (this.resourceExtensionInfo) {
|
|
- return this.resourceExtensionInfo.isWorkspaceScoped;
|
|
- }
|
|
- return false;
|
|
- }
|
|
-
|
|
- get name(): string {
|
|
- if (this.gallery) {
|
|
- return this.gallery.name;
|
|
- }
|
|
- return this.getManifestFromLocalOrResource()?.name ?? '';
|
|
- }
|
|
-
|
|
- get displayName(): string {
|
|
- if (this.gallery) {
|
|
- return this.gallery.displayName || this.gallery.name;
|
|
- }
|
|
-
|
|
- return this.getManifestFromLocalOrResource()?.displayName ?? this.name;
|
|
- }
|
|
-
|
|
- get identifier(): IExtensionIdentifier {
|
|
- if (this.gallery) {
|
|
- return this.gallery.identifier;
|
|
- }
|
|
- if (this.resourceExtension) {
|
|
- return this.resourceExtension.identifier;
|
|
- }
|
|
- return this.local?.identifier ?? { id: '' };
|
|
- }
|
|
-
|
|
- get uuid(): string | undefined {
|
|
- return this.gallery ? this.gallery.identifier.uuid : this.local?.identifier.uuid;
|
|
- }
|
|
-
|
|
- get publisher(): string {
|
|
- if (this.gallery) {
|
|
- return this.gallery.publisher;
|
|
- }
|
|
- return this.getManifestFromLocalOrResource()?.publisher ?? '';
|
|
- }
|
|
-
|
|
- get publisherDisplayName(): string {
|
|
- if (this.gallery) {
|
|
- return this.gallery.publisherDisplayName || this.gallery.publisher;
|
|
- }
|
|
-
|
|
- if (this.local?.publisherDisplayName) {
|
|
- return this.local.publisherDisplayName;
|
|
- }
|
|
-
|
|
- return this.publisher;
|
|
- }
|
|
-
|
|
- get publisherUrl(): URI | undefined {
|
|
- return this.gallery?.publisherLink ? URI.parse(this.gallery.publisherLink) : undefined;
|
|
- }
|
|
-
|
|
- get publisherDomain(): { link: string; verified: boolean } | undefined {
|
|
- return this.gallery?.publisherDomain;
|
|
- }
|
|
-
|
|
- get publisherSponsorLink(): URI | undefined {
|
|
- return this.gallery?.publisherSponsorLink ? URI.parse(this.gallery.publisherSponsorLink) : undefined;
|
|
- }
|
|
-
|
|
- get version(): string {
|
|
- return this.local ? this.local.manifest.version : this.latestVersion;
|
|
- }
|
|
-
|
|
- get private(): boolean {
|
|
- return this.gallery ? this.gallery.private : this.local ? this.local.private : false;
|
|
- }
|
|
-
|
|
- get pinned(): boolean {
|
|
- return !!this.local?.pinned;
|
|
- }
|
|
-
|
|
- get latestVersion(): string {
|
|
- return this.gallery ? this.gallery.version : this.getManifestFromLocalOrResource()?.version ?? '';
|
|
- }
|
|
-
|
|
- get description(): string {
|
|
- return this.gallery ? this.gallery.description : this.getManifestFromLocalOrResource()?.description ?? '';
|
|
- }
|
|
-
|
|
- get url(): string | undefined {
|
|
- return this.gallery?.detailsLink;
|
|
- }
|
|
-
|
|
- get iconUrl(): string | undefined {
|
|
- return this.galleryIconUrl || this.resourceExtensionIconUrl || this.localIconUrl || this.defaultIconUrl;
|
|
- }
|
|
-
|
|
- get iconUrlFallback(): string | undefined {
|
|
- return this.gallery?.assets.icon?.fallbackUri;
|
|
- }
|
|
-
|
|
- private get localIconUrl(): string | undefined {
|
|
- if (this.local && this.local.manifest.icon) {
|
|
- return FileAccess.uriToBrowserUri(resources.joinPath(this.local.location, this.local.manifest.icon)).toString(true);
|
|
- }
|
|
- return undefined;
|
|
- }
|
|
-
|
|
- private get resourceExtensionIconUrl(): string | undefined {
|
|
- if (this.resourceExtension?.manifest.icon) {
|
|
- return FileAccess.uriToBrowserUri(resources.joinPath(this.resourceExtension.location, this.resourceExtension.manifest.icon)).toString(true);
|
|
- }
|
|
- return undefined;
|
|
- }
|
|
-
|
|
- private get galleryIconUrl(): string | undefined {
|
|
- return this.gallery?.assets.icon?.uri;
|
|
- }
|
|
-
|
|
- private get defaultIconUrl(): string | undefined {
|
|
- if (this.type === ExtensionType.System && this.local) {
|
|
- if (this.local.manifest && this.local.manifest.contributes) {
|
|
- if (Array.isArray(this.local.manifest.contributes.themes) && this.local.manifest.contributes.themes.length) {
|
|
- return FileAccess.asBrowserUri('vs/workbench/contrib/extensions/browser/media/theme-icon.png').toString(true);
|
|
- }
|
|
- if (Array.isArray(this.local.manifest.contributes.grammars) && this.local.manifest.contributes.grammars.length) {
|
|
- return FileAccess.asBrowserUri('vs/workbench/contrib/extensions/browser/media/language-icon.svg').toString(true);
|
|
- }
|
|
- }
|
|
- }
|
|
- return undefined;
|
|
- }
|
|
-
|
|
- get repository(): string | undefined {
|
|
- return this.gallery && this.gallery.assets.repository ? this.gallery.assets.repository.uri : undefined;
|
|
- }
|
|
-
|
|
- get licenseUrl(): string | undefined {
|
|
- return this.gallery && this.gallery.assets.license ? this.gallery.assets.license.uri : undefined;
|
|
- }
|
|
-
|
|
- get supportUrl(): string | undefined {
|
|
- return this.gallery && this.gallery.supportLink ? this.gallery.supportLink : undefined;
|
|
- }
|
|
-
|
|
- get state(): ExtensionState {
|
|
- return this.stateProvider(this);
|
|
- }
|
|
-
|
|
- private malicious: MaliciousExtensionInfo | undefined;
|
|
- public get isMalicious(): boolean | undefined {
|
|
- return !!this.malicious || this.enablementState === EnablementState.DisabledByMalicious;
|
|
- }
|
|
-
|
|
- public get maliciousInfoLink(): string | undefined {
|
|
- return this.malicious?.learnMoreLink;
|
|
- }
|
|
-
|
|
- public deprecationInfo: IDeprecationInfo | undefined;
|
|
-
|
|
- get installCount(): number | undefined {
|
|
- return this.gallery ? this.gallery.installCount : undefined;
|
|
- }
|
|
-
|
|
- get rating(): number | undefined {
|
|
- return this.gallery ? this.gallery.rating : undefined;
|
|
- }
|
|
-
|
|
- get ratingCount(): number | undefined {
|
|
- return this.gallery ? this.gallery.ratingCount : undefined;
|
|
- }
|
|
-
|
|
- get ratingUrl(): string | undefined {
|
|
- return this.gallery?.ratingLink;
|
|
- }
|
|
-
|
|
- get outdated(): boolean {
|
|
- try {
|
|
- if (!this.gallery || !this.local) {
|
|
- return false;
|
|
- }
|
|
- // Do not allow updating system extensions in stable
|
|
- if (this.type === ExtensionType.System && this.productService.quality === 'stable') {
|
|
- return false;
|
|
- }
|
|
- if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) {
|
|
- return false;
|
|
- }
|
|
- if (semver.gt(this.latestVersion, this.version)) {
|
|
- return true;
|
|
- }
|
|
- if (this.outdatedTargetPlatform) {
|
|
- return true;
|
|
- }
|
|
- } catch (error) {
|
|
- /* Ignore */
|
|
- }
|
|
- return false;
|
|
- }
|
|
-
|
|
- get outdatedTargetPlatform(): boolean {
|
|
- return !!this.local && !!this.gallery
|
|
- && ![TargetPlatform.UNDEFINED, TargetPlatform.WEB].includes(this.local.targetPlatform)
|
|
- && this.gallery.properties.targetPlatform !== TargetPlatform.WEB
|
|
- && this.local.targetPlatform !== this.gallery.properties.targetPlatform
|
|
- && semver.eq(this.latestVersion, this.version);
|
|
- }
|
|
-
|
|
- get runtimeState(): ExtensionRuntimeState | undefined {
|
|
- return this.runtimeStateProvider(this);
|
|
- }
|
|
-
|
|
- get telemetryData(): any {
|
|
- const { local, gallery } = this;
|
|
-
|
|
- if (gallery) {
|
|
- return getGalleryExtensionTelemetryData(gallery);
|
|
- } else if (local) {
|
|
- return getLocalExtensionTelemetryData(local);
|
|
- } else {
|
|
- return {};
|
|
- }
|
|
- }
|
|
-
|
|
- get preview(): boolean {
|
|
- return this.local?.manifest.preview ?? this.gallery?.preview ?? false;
|
|
- }
|
|
-
|
|
- get preRelease(): boolean {
|
|
- return !!this.local?.preRelease;
|
|
- }
|
|
-
|
|
- get isPreReleaseVersion(): boolean {
|
|
- if (this.local) {
|
|
- return this.local.isPreReleaseVersion;
|
|
- }
|
|
- return !!this.gallery?.properties.isPreReleaseVersion;
|
|
- }
|
|
-
|
|
- get hasPreReleaseVersion(): boolean {
|
|
- return this.gallery ? this.gallery.hasPreReleaseVersion : !!this.local?.hasPreReleaseVersion;
|
|
- }
|
|
-
|
|
- get hasReleaseVersion(): boolean {
|
|
- return !!this.resourceExtension || !!this.gallery?.hasReleaseVersion;
|
|
- }
|
|
-
|
|
- private getLocal(): ILocalExtension | undefined {
|
|
- return this.local && !this.outdated ? this.local : undefined;
|
|
- }
|
|
-
|
|
- async getManifest(token: CancellationToken): Promise<IExtensionManifest | null> {
|
|
- const local = this.getLocal();
|
|
- if (local) {
|
|
- return local.manifest;
|
|
- }
|
|
-
|
|
- if (this.gallery) {
|
|
- return this.getGalleryManifest(token);
|
|
- }
|
|
-
|
|
- if (this.resourceExtension) {
|
|
- return this.resourceExtension.manifest;
|
|
- }
|
|
-
|
|
- return null;
|
|
- }
|
|
-
|
|
- async getGalleryManifest(token: CancellationToken = CancellationToken.None): Promise<IExtensionManifest | null> {
|
|
- if (this.gallery) {
|
|
- let cache = this.galleryResourcesCache.get('manifest');
|
|
- if (!cache) {
|
|
- if (this.gallery.assets.manifest) {
|
|
- this.galleryResourcesCache.set('manifest', cache = this.galleryService.getManifest(this.gallery, token)
|
|
- .catch(e => {
|
|
- this.galleryResourcesCache.delete('manifest');
|
|
- throw e;
|
|
- }));
|
|
- } else {
|
|
- this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.identifier.id);
|
|
- }
|
|
- }
|
|
- return cache;
|
|
- }
|
|
- return null;
|
|
- }
|
|
-
|
|
- hasReadme(): boolean {
|
|
- if (this.local && this.local.readmeUrl) {
|
|
- return true;
|
|
- }
|
|
-
|
|
- if (this.gallery && this.gallery.assets.readme) {
|
|
- return true;
|
|
- }
|
|
-
|
|
- if (this.resourceExtension?.readmeUri) {
|
|
- return true;
|
|
- }
|
|
-
|
|
- return this.type === ExtensionType.System;
|
|
- }
|
|
-
|
|
- async getReadme(token: CancellationToken): Promise<string> {
|
|
- const local = this.getLocal();
|
|
- if (local?.readmeUrl) {
|
|
- const content = await this.fileService.readFile(local.readmeUrl);
|
|
- return content.value.toString();
|
|
- }
|
|
-
|
|
- if (this.gallery) {
|
|
- if (this.gallery.assets.readme) {
|
|
- return this.galleryService.getReadme(this.gallery, token);
|
|
- }
|
|
- this.telemetryService.publicLog('extensions:NotFoundReadMe', this.telemetryData);
|
|
- }
|
|
-
|
|
- if (this.type === ExtensionType.System) {
|
|
- return Promise.resolve(`# ${this.displayName || this.name}
|
|
-**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
|
|
-## Features
|
|
-${this.description}
|
|
-`);
|
|
- }
|
|
-
|
|
- if (this.resourceExtension?.readmeUri) {
|
|
- const content = await this.fileService.readFile(this.resourceExtension?.readmeUri);
|
|
- return content.value.toString();
|
|
- }
|
|
-
|
|
- return Promise.reject(new Error('not available'));
|
|
- }
|
|
-
|
|
- hasChangelog(): boolean {
|
|
- if (this.local && this.local.changelogUrl) {
|
|
- return true;
|
|
- }
|
|
-
|
|
- if (this.gallery && this.gallery.assets.changelog) {
|
|
- return true;
|
|
- }
|
|
-
|
|
- return this.type === ExtensionType.System;
|
|
- }
|
|
-
|
|
- async getChangelog(token: CancellationToken): Promise<string> {
|
|
- const local = this.getLocal();
|
|
- if (local?.changelogUrl) {
|
|
- const content = await this.fileService.readFile(local.changelogUrl);
|
|
- return content.value.toString();
|
|
- }
|
|
-
|
|
- if (this.gallery?.assets.changelog) {
|
|
- return this.galleryService.getChangelog(this.gallery, token);
|
|
- }
|
|
-
|
|
- if (this.type === ExtensionType.System) {
|
|
- return Promise.resolve(`Please check the [VS Code Release Notes](command:${ShowCurrentReleaseNotesActionId}) for changes to the built-in extensions.`);
|
|
- }
|
|
-
|
|
- return Promise.reject(new Error('not available'));
|
|
- }
|
|
-
|
|
- get categories(): readonly string[] {
|
|
- const { local, gallery, resourceExtension } = this;
|
|
- if (local && local.manifest.categories && !this.outdated) {
|
|
- return local.manifest.categories;
|
|
- }
|
|
- if (gallery) {
|
|
- return gallery.categories;
|
|
- }
|
|
- if (resourceExtension) {
|
|
- return resourceExtension.manifest.categories ?? [];
|
|
- }
|
|
- return [];
|
|
- }
|
|
-
|
|
- get tags(): readonly string[] {
|
|
- const { gallery } = this;
|
|
- if (gallery) {
|
|
- return gallery.tags.filter(tag => !tag.startsWith('_'));
|
|
- }
|
|
- return [];
|
|
- }
|
|
-
|
|
- get dependencies(): string[] {
|
|
- const { local, gallery, resourceExtension } = this;
|
|
- if (local && local.manifest.extensionDependencies && !this.outdated) {
|
|
- return local.manifest.extensionDependencies;
|
|
- }
|
|
- if (gallery) {
|
|
- return gallery.properties.dependencies || [];
|
|
- }
|
|
- if (resourceExtension) {
|
|
- return resourceExtension.manifest.extensionDependencies || [];
|
|
- }
|
|
- return [];
|
|
- }
|
|
-
|
|
- get extensionPack(): string[] {
|
|
- const { local, gallery, resourceExtension } = this;
|
|
- if (local && local.manifest.extensionPack && !this.outdated) {
|
|
- return local.manifest.extensionPack;
|
|
- }
|
|
- if (gallery) {
|
|
- return gallery.properties.extensionPack || [];
|
|
- }
|
|
- if (resourceExtension) {
|
|
- return resourceExtension.manifest.extensionPack || [];
|
|
- }
|
|
- return [];
|
|
- }
|
|
-
|
|
- setExtensionsControlManifest(extensionsControlManifest: IExtensionsControlManifest): void {
|
|
- this.malicious = findMatchingMaliciousEntry(this.identifier, extensionsControlManifest.malicious);
|
|
- this.deprecationInfo = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[this.identifier.id.toLowerCase()] : undefined;
|
|
- }
|
|
-
|
|
- private getManifestFromLocalOrResource(): IExtensionManifest | null {
|
|
- if (this.local) {
|
|
- return this.local.manifest;
|
|
- }
|
|
- if (this.resourceExtension) {
|
|
- return this.resourceExtension.manifest;
|
|
- }
|
|
- return null;
|
|
- }
|
|
-}
|
|
-
|
|
const EXTENSIONS_AUTO_UPDATE_KEY = 'extensions.autoUpdate';
|
|
const EXTENSIONS_DONOT_AUTO_UPDATE_KEY = 'extensions.donotAutoUpdate';
|
|
const EXTENSIONS_DISMISSED_NOTIFICATIONS_KEY = 'extensions.dismissedNotifications';
|
|
-
|
|
-class Extensions extends Disposable {
|
|
-
|
|
- private readonly _onChange = this._register(new Emitter<{ extension: Extension; operation?: InstallOperation } | undefined>());
|
|
- get onChange() { return this._onChange.event; }
|
|
-
|
|
- private readonly _onReset = this._register(new Emitter<void>());
|
|
- get onReset() { return this._onReset.event; }
|
|
-
|
|
- private installing: Extension[] = [];
|
|
- private uninstalling: Extension[] = [];
|
|
- private installed: Extension[] = [];
|
|
-
|
|
- constructor(
|
|
- readonly server: IExtensionManagementServer,
|
|
- private readonly stateProvider: IExtensionStateProvider<ExtensionState>,
|
|
- private readonly runtimeStateProvider: IExtensionStateProvider<ExtensionRuntimeState | undefined>,
|
|
- private readonly isWorkspaceServer: boolean,
|
|
- @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
|
|
- @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
|
|
- @IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService,
|
|
- @ITelemetryService private readonly telemetryService: ITelemetryService,
|
|
- @IInstantiationService private readonly instantiationService: IInstantiationService
|
|
- ) {
|
|
- super();
|
|
- this._register(server.extensionManagementService.onInstallExtension(e => this.onInstallExtension(e)));
|
|
- this._register(server.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e)));
|
|
- this._register(server.extensionManagementService.onUninstallExtension(e => this.onUninstallExtension(e.identifier)));
|
|
- this._register(server.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));
|
|
- this._register(server.extensionManagementService.onDidUpdateExtensionMetadata(e => this.onDidUpdateExtensionMetadata(e.local)));
|
|
- this._register(server.extensionManagementService.onDidChangeProfile(() => this.reset()));
|
|
- this._register(extensionEnablementService.onEnablementChanged(e => this.onEnablementChanged(e)));
|
|
- this._register(Event.any(this.onChange, this.onReset)(() => this._local = undefined));
|
|
- if (this.isWorkspaceServer) {
|
|
- this._register(this.workbenchExtensionManagementService.onInstallExtension(e => {
|
|
- if (e.workspaceScoped) {
|
|
- this.onInstallExtension(e);
|
|
- }
|
|
- }));
|
|
- this._register(this.workbenchExtensionManagementService.onDidInstallExtensions(e => {
|
|
- const result = e.filter(e => e.workspaceScoped);
|
|
- if (result.length) {
|
|
- this.onDidInstallExtensions(result);
|
|
- }
|
|
- }));
|
|
- this._register(this.workbenchExtensionManagementService.onUninstallExtension(e => {
|
|
- if (e.workspaceScoped) {
|
|
- this.onUninstallExtension(e.identifier);
|
|
- }
|
|
- }));
|
|
- this._register(this.workbenchExtensionManagementService.onDidUninstallExtension(e => {
|
|
- if (e.workspaceScoped) {
|
|
- this.onDidUninstallExtension(e);
|
|
- }
|
|
- }));
|
|
- }
|
|
- }
|
|
-
|
|
- private _local: Extension[] | undefined;
|
|
- get local(): Extension[] {
|
|
- if (!this._local) {
|
|
- this._local = [];
|
|
- for (const extension of this.installed) {
|
|
- this._local.push(extension);
|
|
- }
|
|
- for (const extension of this.installing) {
|
|
- if (!this.installed.some(installed => areSameExtensions(installed.identifier, extension.identifier))) {
|
|
- this._local.push(extension);
|
|
- }
|
|
- }
|
|
- }
|
|
- return this._local;
|
|
- }
|
|
-
|
|
- async queryInstalled(productVersion: IProductVersion): Promise<IExtension[]> {
|
|
- await this.fetchInstalledExtensions(productVersion);
|
|
- this._onChange.fire(undefined);
|
|
- return this.local;
|
|
- }
|
|
-
|
|
- async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion, flagExtensionsMissingFromGallery?: IExtensionInfo[]): Promise<void> {
|
|
- const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion);
|
|
- for (const [extension, gallery] of extensions) {
|
|
- // update metadata of the extension if it does not exist
|
|
- if (extension.local && !extension.local.identifier.uuid) {
|
|
- extension.local = await this.updateMetadata(extension.local, gallery);
|
|
- }
|
|
- if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) {
|
|
- extension.gallery = gallery;
|
|
- this._onChange.fire({ extension });
|
|
- }
|
|
- }
|
|
- // Detect extensions that do not have a corresponding gallery entry.
|
|
- if (flagExtensionsMissingFromGallery) {
|
|
- const extensionsToQuery = [];
|
|
- for (const extension of this.local) {
|
|
- // Extension is already paired with a gallery object
|
|
- if (extension.gallery) {
|
|
- continue;
|
|
- }
|
|
- // Already flagged as missing from gallery
|
|
- if (extension.missingFromGallery) {
|
|
- continue;
|
|
- }
|
|
- // A UUID indicates extension originated from gallery
|
|
- if (!extension.identifier.uuid) {
|
|
- continue;
|
|
- }
|
|
- // Extension is not present in the set we are concerned about
|
|
- if (!flagExtensionsMissingFromGallery.some(f => areSameExtensions(f, extension.identifier))) {
|
|
- continue;
|
|
- }
|
|
- extensionsToQuery.push(extension);
|
|
- }
|
|
- if (extensionsToQuery.length) {
|
|
- const queryResult = await this.galleryService.getExtensions(extensionsToQuery.map(e => ({ ...e.identifier, version: e.version })), CancellationToken.None);
|
|
- const queriedIds: string[] = [];
|
|
- const missingIds: string[] = [];
|
|
- for (const extension of extensionsToQuery) {
|
|
- queriedIds.push(extension.identifier.id);
|
|
- const gallery = queryResult.find(g => areSameExtensions(g.identifier, extension.identifier));
|
|
- if (gallery) {
|
|
- extension.gallery = gallery;
|
|
- } else {
|
|
- extension.missingFromGallery = true;
|
|
- missingIds.push(extension.identifier.id);
|
|
- }
|
|
- this._onChange.fire({ extension });
|
|
- }
|
|
- type MissingFromGalleryClassification = {
|
|
- owner: 'joshspicer';
|
|
- comment: 'Report when installed extensions are no longer available in the gallery';
|
|
- queriedIds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extensions queried as potentially missing from gallery' };
|
|
- missingIds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extensions determined missing from gallery' };
|
|
- };
|
|
- type MissingFromGalleryEvent = {
|
|
- readonly queriedIds: TelemetryTrustedValue<string>;
|
|
- readonly missingIds: TelemetryTrustedValue<string>;
|
|
- };
|
|
- this.telemetryService.publicLog2<MissingFromGalleryEvent, MissingFromGalleryClassification>('extensions:missingFromGallery', {
|
|
- queriedIds: new TelemetryTrustedValue(queriedIds.join(';')),
|
|
- missingIds: new TelemetryTrustedValue(missingIds.join(';'))
|
|
- });
|
|
- }
|
|
- }
|
|
- }
|
|
-
|
|
- private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise<[Extension, IGalleryExtension][]> {
|
|
- const mappedExtensions = this.mapInstalledExtensionWithGalleryExtension(galleryExtensions);
|
|
- const targetPlatform = await this.server.extensionManagementService.getTargetPlatform();
|
|
- const compatibleGalleryExtensions: IGalleryExtension[] = [];
|
|
- const compatibleGalleryExtensionsToFetch: IExtensionInfo[] = [];
|
|
- await Promise.allSettled(mappedExtensions.map(async ([extension, gallery]) => {
|
|
- if (extension.local) {
|
|
- if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform, productVersion)) {
|
|
- compatibleGalleryExtensions.push(gallery);
|
|
- } else {
|
|
- compatibleGalleryExtensionsToFetch.push({ ...extension.local.identifier, preRelease: extension.local.preRelease });
|
|
- }
|
|
- }
|
|
- }));
|
|
- if (compatibleGalleryExtensionsToFetch.length) {
|
|
- const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true, productVersion }, CancellationToken.None);
|
|
- compatibleGalleryExtensions.push(...result);
|
|
- }
|
|
- return this.mapInstalledExtensionWithGalleryExtension(compatibleGalleryExtensions);
|
|
- }
|
|
-
|
|
- private mapInstalledExtensionWithGalleryExtension(galleryExtensions: IGalleryExtension[]): [Extension, IGalleryExtension][] {
|
|
- const mappedExtensions: [Extension, IGalleryExtension][] = [];
|
|
- const byUUID = new Map<string, IGalleryExtension>(), byID = new Map<string, IGalleryExtension>();
|
|
- for (const gallery of galleryExtensions) {
|
|
- byUUID.set(gallery.identifier.uuid, gallery);
|
|
- byID.set(gallery.identifier.id.toLowerCase(), gallery);
|
|
- }
|
|
- for (const installed of this.installed) {
|
|
- if (installed.uuid) {
|
|
- const gallery = byUUID.get(installed.uuid);
|
|
- if (gallery) {
|
|
- mappedExtensions.push([installed, gallery]);
|
|
- continue;
|
|
- }
|
|
- }
|
|
- if (installed.local?.source !== 'resource') {
|
|
- const gallery = byID.get(installed.identifier.id.toLowerCase());
|
|
- if (gallery) {
|
|
- mappedExtensions.push([installed, gallery]);
|
|
- }
|
|
- }
|
|
- }
|
|
- return mappedExtensions;
|
|
- }
|
|
-
|
|
- private async updateMetadata(localExtension: ILocalExtension, gallery: IGalleryExtension): Promise<ILocalExtension> {
|
|
- let isPreReleaseVersion = false;
|
|
- if (localExtension.manifest.version !== gallery.version) {
|
|
- type GalleryServiceMatchInstalledExtensionClassification = {
|
|
- owner: 'sandy081';
|
|
- comment: 'Report when a request is made to update metadata of an installed extension';
|
|
- };
|
|
- this.telemetryService.publicLog2<{}, GalleryServiceMatchInstalledExtensionClassification>('galleryService:updateMetadata');
|
|
- const galleryWithLocalVersion: IGalleryExtension | undefined = (await this.galleryService.getExtensions([{ ...localExtension.identifier, version: localExtension.manifest.version }], CancellationToken.None))[0];
|
|
- isPreReleaseVersion = !!galleryWithLocalVersion?.properties?.isPreReleaseVersion;
|
|
- }
|
|
- return this.workbenchExtensionManagementService.updateMetadata(localExtension, { id: gallery.identifier.uuid, publisherDisplayName: gallery.publisherDisplayName, publisherId: gallery.publisherId, isPreReleaseVersion });
|
|
- }
|
|
-
|
|
- canInstall(galleryExtension: IGalleryExtension): Promise<true | IMarkdownString> {
|
|
- return this.server.extensionManagementService.canInstall(galleryExtension);
|
|
- }
|
|
-
|
|
- private onInstallExtension(event: InstallExtensionEvent): void {
|
|
- const { source } = event;
|
|
- if (source && !URI.isUri(source)) {
|
|
- const extension = this.installed.find(e => areSameExtensions(e.identifier, source.identifier))
|
|
- ?? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, undefined, source, undefined);
|
|
- this.installing.push(extension);
|
|
- this._onChange.fire({ extension });
|
|
- }
|
|
- }
|
|
-
|
|
- private async fetchInstalledExtensions(productVersion?: IProductVersion): Promise<void> {
|
|
- const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest();
|
|
- const all = await this.server.extensionManagementService.getInstalled(undefined, undefined, productVersion);
|
|
- if (this.isWorkspaceServer) {
|
|
- all.push(...await this.workbenchExtensionManagementService.getInstalledWorkspaceExtensions(true));
|
|
- }
|
|
-
|
|
- // dedup workspace, user and system extensions by giving priority to workspace first and then to user extension.
|
|
- const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => {
|
|
- if (extensions.length === 1) {
|
|
- result.push(extensions[0]);
|
|
- } else {
|
|
- let workspaceExtension: ILocalExtension | undefined,
|
|
- userExtension: ILocalExtension | undefined,
|
|
- systemExtension: ILocalExtension | undefined;
|
|
- for (const extension of extensions) {
|
|
- if (extension.isWorkspaceScoped) {
|
|
- workspaceExtension = extension;
|
|
- } else if (extension.type === ExtensionType.User) {
|
|
- userExtension = extension;
|
|
- } else {
|
|
- systemExtension = extension;
|
|
- }
|
|
- }
|
|
- const extension = workspaceExtension ?? userExtension ?? systemExtension;
|
|
- if (extension) {
|
|
- result.push(extension);
|
|
- }
|
|
- }
|
|
- return result;
|
|
- }, []);
|
|
-
|
|
- const byId = index(this.installed, e => e.local ? e.local.identifier.id : e.identifier.id);
|
|
- this.installed = installed.map(local => {
|
|
- const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined);
|
|
- extension.local = local;
|
|
- extension.enablementState = this.extensionEnablementService.getEnablementState(local);
|
|
- extension.setExtensionsControlManifest(extensionsControlManifest);
|
|
- return extension;
|
|
- });
|
|
- }
|
|
-
|
|
- private async reset(): Promise<void> {
|
|
- this.installed = [];
|
|
- this.installing = [];
|
|
- this.uninstalling = [];
|
|
- await this.fetchInstalledExtensions();
|
|
- this._onReset.fire();
|
|
- }
|
|
-
|
|
- private async onDidInstallExtensions(results: readonly InstallExtensionResult[]): Promise<void> {
|
|
- const extensions: Extension[] = [];
|
|
- for (const event of results) {
|
|
- const { local, source } = event;
|
|
- const gallery = source && !URI.isUri(source) ? source : undefined;
|
|
- const location = source && URI.isUri(source) ? source : undefined;
|
|
- const installingExtension = gallery ? this.installing.filter(e => areSameExtensions(e.identifier, gallery.identifier))[0] : null;
|
|
- this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing;
|
|
-
|
|
- let extension: Extension | undefined = installingExtension ? installingExtension
|
|
- : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined, undefined)
|
|
- : undefined;
|
|
- if (extension) {
|
|
- if (local) {
|
|
- const installed = this.installed.filter(e => areSameExtensions(e.identifier, extension!.identifier))[0];
|
|
- if (installed) {
|
|
- extension = installed;
|
|
- } else {
|
|
- this.installed.push(extension);
|
|
- }
|
|
- extension.local = local;
|
|
- if (!extension.gallery) {
|
|
- extension.gallery = gallery;
|
|
- }
|
|
- extension.enablementState = this.extensionEnablementService.getEnablementState(local);
|
|
- }
|
|
- extensions.push(extension);
|
|
- }
|
|
- this._onChange.fire(!local || !extension ? undefined : { extension, operation: event.operation });
|
|
- }
|
|
-
|
|
- if (extensions.length) {
|
|
- const manifest = await this.server.extensionManagementService.getExtensionsControlManifest();
|
|
- for (const extension of extensions) {
|
|
- extension.setExtensionsControlManifest(manifest);
|
|
- }
|
|
- this.matchInstalledExtensionsWithGallery(extensions);
|
|
- }
|
|
- }
|
|
-
|
|
- private async onDidUpdateExtensionMetadata(local: ILocalExtension): Promise<void> {
|
|
- const extension = this.installed.find(e => areSameExtensions(e.identifier, local.identifier));
|
|
- if (extension?.local) {
|
|
- extension.local = local;
|
|
- this._onChange.fire({ extension });
|
|
- }
|
|
- }
|
|
-
|
|
- private async matchInstalledExtensionsWithGallery(extensions: Extension[]): Promise<void> {
|
|
- const toMatch = extensions.filter(e => e.local && !e.gallery && e.local.source !== 'resource');
|
|
- if (!toMatch.length) {
|
|
- return;
|
|
- }
|
|
- if (!this.galleryService.isEnabled()) {
|
|
- return;
|
|
- }
|
|
- const galleryExtensions = await this.galleryService.getExtensions(toMatch.map(e => ({ ...e.identifier, preRelease: e.local?.preRelease })), { compatible: true, targetPlatform: await this.server.extensionManagementService.getTargetPlatform() }, CancellationToken.None);
|
|
- for (const extension of extensions) {
|
|
- const compatible = galleryExtensions.find(e => areSameExtensions(e.identifier, extension.identifier));
|
|
- if (compatible) {
|
|
- extension.gallery = compatible;
|
|
- this._onChange.fire({ extension });
|
|
- }
|
|
- }
|
|
- }
|
|
-
|
|
- private onUninstallExtension(identifier: IExtensionIdentifier): void {
|
|
- const extension = this.installed.filter(e => areSameExtensions(e.identifier, identifier))[0];
|
|
- if (extension) {
|
|
- const uninstalling = this.uninstalling.filter(e => areSameExtensions(e.identifier, identifier))[0] || extension;
|
|
- this.uninstalling = [uninstalling, ...this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier))];
|
|
- this._onChange.fire(uninstalling ? { extension: uninstalling } : undefined);
|
|
- }
|
|
- }
|
|
-
|
|
- private onDidUninstallExtension({ identifier, error }: DidUninstallExtensionEvent): void {
|
|
- const uninstalled = this.uninstalling.find(e => areSameExtensions(e.identifier, identifier)) || this.installed.find(e => areSameExtensions(e.identifier, identifier));
|
|
- this.uninstalling = this.uninstalling.filter(e => !areSameExtensions(e.identifier, identifier));
|
|
- if (!error) {
|
|
- this.installed = this.installed.filter(e => !areSameExtensions(e.identifier, identifier));
|
|
- }
|
|
- if (uninstalled) {
|
|
- this._onChange.fire({ extension: uninstalled });
|
|
- }
|
|
- }
|
|
-
|
|
- private onEnablementChanged(platformExtensions: readonly IPlatformExtension[]) {
|
|
- const extensions = this.local.filter(e => platformExtensions.some(p => areSameExtensions(e.identifier, p.identifier)));
|
|
- for (const extension of extensions) {
|
|
- if (extension.local) {
|
|
- const enablementState = this.extensionEnablementService.getEnablementState(extension.local);
|
|
- if (enablementState !== extension.enablementState) {
|
|
- extension.enablementState = enablementState;
|
|
- this._onChange.fire({ extension });
|
|
- }
|
|
- }
|
|
- }
|
|
- }
|
|
-
|
|
- getExtensionState(extension: Extension): ExtensionState {
|
|
- if (extension.gallery && this.installing.some(e => !!e.gallery && areSameExtensions(e.gallery.identifier, extension.gallery!.identifier))) {
|
|
- return ExtensionState.Installing;
|
|
- }
|
|
- if (this.uninstalling.some(e => areSameExtensions(e.identifier, extension.identifier))) {
|
|
- return ExtensionState.Uninstalling;
|
|
- }
|
|
- const local = this.installed.filter(e => e === extension || (e.gallery && extension.gallery && areSameExtensions(e.gallery.identifier, extension.gallery.identifier)))[0];
|
|
- return local ? ExtensionState.Installed : ExtensionState.Uninstalled;
|
|
- }
|
|
-}
|
|
-
|
|
export class ExtensionsWorkbenchService extends Disposable implements IExtensionsWorkbenchService, IURLHandler {
|
|
|
|
private static readonly UpdatesCheckInterval = 1000 * 60 * 60 * 12; // 12 hours
|