#15756 - Pre-release support for extensions

This commit is contained in:
Sandeep Somavarapu 2021-11-23 00:32:43 +01:00
parent e7b11f34e1
commit bea3784159
No known key found for this signature in database
GPG key ID: 1FED25EC4646638B
26 changed files with 738 additions and 282 deletions

View file

@ -22,6 +22,8 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; isPreReleaseVersion: boolean, hadPreReleaseVersion: boolean, installedTimestamp: number }>;
export interface IInstallExtensionTask {
readonly identifier: IExtensionIdentifier;
readonly source: IGalleryExtension | URI;
@ -85,8 +87,13 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
try {
return await this.doInstallFromGallery(extension, options);
if (!this.galleryService.isEnabled()) {
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
}
const compatible = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion, !!options.installPreReleaseVersion);
return await this.installExtension(compatible.manifest, compatible.extension, options);
} catch (error) {
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
this.logService.error(`Failed to install extension.`, extension.identifier.id);
this.logService.error(error);
throw toExtensionManagementError(error);
@ -128,41 +135,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
this.participants.push(participant);
}
private async doInstallFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise<ILocalExtension> {
if (!this.galleryService.isEnabled()) {
throw new ExtensionManagementError(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"), ExtensionManagementErrorCode.Internal);
}
if (!await this.canInstall(extension)) {
const targetPlatform = await this.getTargetPlatform();
const error = new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.Incompatible);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
try {
extension = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion);
} catch (error) {
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
const manifest = await this.galleryService.getManifest(extension, CancellationToken.None);
if (manifest === null) {
const error = new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
if (manifest.version !== extension.version) {
const error = new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error);
throw error;
}
return this.installExtension(manifest, extension, options);
}
protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise<ILocalExtension> {
// only cache gallery extensions tasks
if (!URI.isUri(extension)) {
@ -190,7 +162,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
this.logService.info('Installing the extension without checking dependencies and pack', installExtensionTask.identifier.id);
} else {
try {
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack);
const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack, !!options.installPreReleaseVersion);
for (const { gallery, manifest } of allDepsAndPackExtensionsToInstall) {
installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier));
if (this.installingExtensions.has(new ExtensionIdentifierWithVersion(gallery.identifier, gallery.version).key())) {
@ -322,7 +294,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return results;
}
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean): Promise<{ gallery: IGalleryExtension, manifest: IExtensionManifest }[]> {
private async getAllDepsAndPackExtensionsToInstall(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean): Promise<{ gallery: IGalleryExtension, manifest: IExtensionManifest }[]> {
if (!this.galleryService.isEnabled()) {
return [];
}
@ -357,17 +329,19 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
continue;
}
const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier));
if (!isDependency && !await this.canInstall(galleryExtension)) {
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
continue;
let compatible;
try {
compatible = await this.checkAndGetCompatibleVersion(galleryExtension, true, installPreRelease);
} catch (error) {
if (error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.IncompatibleTargetPlatform && !isDependency) {
this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id);
continue;
} else {
throw error;
}
}
const compatibleExtension = await this.checkAndGetCompatibleVersion(galleryExtension, true);
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
if (manifest === null) {
throw new ExtensionManagementError(`Missing manifest for extension ${compatibleExtension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
}
allDependenciesAndPacks.push({ gallery: compatibleExtension, manifest });
await collectDependenciesAndPackExtensionsToInstall(compatibleExtension.identifier, manifest);
allDependenciesAndPacks.push({ gallery: compatible.extension, manifest: compatible.manifest });
await collectDependenciesAndPackExtensionsToInstall(compatible.extension.identifier, manifest);
}
}
}
@ -378,28 +352,51 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return allDependenciesAndPacks.filter(e => !installed.some(i => areSameExtensions(i.identifier, e.gallery.identifier)));
}
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension> {
private async checkAndGetCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<{ extension: IGalleryExtension, manifest: IExtensionManifest }> {
if (await this.isMalicious(extension)) {
throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious);
}
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion);
if (!compatibleExtension) {
if (!await this.canInstall(extension)) {
const targetPlatform = await this.getTargetPlatform();
throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform);
}
const compatibleExtension = await this.getCompatibleVersion(extension, fetchCompatibleVersion, includePreRelease);
if (compatibleExtension) {
if (includePreRelease && !compatibleExtension.properties.isPreReleaseVersion && extension.hasPreReleaseVersion) {
throw new ExtensionManagementError(nls.localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
} else {
throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
return compatibleExtension;
const manifest = await this.galleryService.getManifest(compatibleExtension, CancellationToken.None);
if (manifest === null) {
throw new ExtensionManagementError(`Missing manifest for extension ${extension.identifier.id}`, ExtensionManagementErrorCode.Invalid);
}
if (manifest.version !== compatibleExtension.version) {
throw new ExtensionManagementError(`Cannot install '${extension.identifier.id}' extension because of version mismatch in Marketplace`, ExtensionManagementErrorCode.Invalid);
}
return { extension: compatibleExtension, manifest };
}
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension | null> {
protected async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> {
const targetPlatform = await this.getTargetPlatform();
let compatibleExtension: IGalleryExtension | null = null;
if (await this.galleryService.isExtensionCompatible(extension, targetPlatform)) {
if (fetchCompatibleVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) {
compatibleExtension = await this.galleryService.getCompatibleExtension(extension.identifier, includePreRelease, targetPlatform);
}
if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
compatibleExtension = extension;
}
if (!compatibleExtension && fetchCompatibleVersion) {
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, targetPlatform);
compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform);
}
return compatibleExtension;

View file

@ -10,6 +10,7 @@ import { getOrDefault } from 'vs/base/common/objects';
import { IPager } from 'vs/base/common/paging';
import { isWeb, platform } from 'vs/base/common/platform';
import { arch } from 'vs/base/common/process';
import { isBoolean } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@ -135,6 +136,7 @@ const PropertyType = {
Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies',
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
Engine: 'Microsoft.VisualStudio.Code.Engine',
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension'
};
@ -314,6 +316,11 @@ function getEngine(version: IRawGalleryExtensionVersion): string {
return (values.length > 0 && values[0].value) || '';
}
function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.PreRelease) : [];
return values.length > 0 && values[0].value === 'true';
}
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
const value = (values.length > 0 && values[0].value) || '';
@ -376,14 +383,8 @@ export function sortExtensionVersions(versions: IRawGalleryExtensionVersion[], p
return versions;
}
function toExtensionWithLatestVersion(galleryExtension: IRawGalleryExtension, index: number, query: Query, querySource: string | undefined, targetPlatform: TargetPlatform): IGalleryExtension {
const allTargetPlatforms = getAllTargetPlatforms(galleryExtension);
let latestVersion = galleryExtension.versions[0];
latestVersion = galleryExtension.versions.find(version => version.version === latestVersion.version && isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(version), allTargetPlatforms, targetPlatform)) || latestVersion;
return toExtension(galleryExtension, latestVersion, allTargetPlatforms, index, query, querySource);
}
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], index: number, query: Query, querySource?: string): IGalleryExtension {
function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], telemetryData?: any): IGalleryExtension {
const latestVersion = galleryExtension.versions[0];
const assets = <IGalleryExtensionAssets>{
manifest: getVersionAsset(version, AssetType.Manifest),
readme: getVersionAsset(version, AssetType.Details),
@ -423,7 +424,9 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
engine: getEngine(version),
localizedLanguages: getLocalizedLanguages(version),
targetPlatform: getTargetPlatformForExtensionVersion(version),
isPreReleaseVersion: isPreReleaseVersion(version)
},
hasPreReleaseVersion: isPreReleaseVersion(latestVersion),
preview: getIsPreview(galleryExtension.flags),
/* __GDPR__FRAGMENT__
"GalleryExtensionTelemetryData2" : {
@ -431,10 +434,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
"querySource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetryData: {
index: ((query.pageNumber - 1) * query.pageSize) + index,
querySource
},
telemetryData,
};
}
@ -476,8 +476,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return !!this.extensionsGalleryUrl;
}
async getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]> {
const result: IGalleryExtension[] = [];
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]>
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, includePreRelease: boolean, token: CancellationToken): Promise<IGalleryExtension[]>
async getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, arg1: any, arg2?: any): Promise<IGalleryExtension[]> {
const includePreRelease = isBoolean(arg1) ? arg1 : false;
const token: CancellationToken = isBoolean(arg1) ? arg2 : arg1;
let query = new Query()
.withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, identifiers.length)
@ -489,33 +492,20 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
}
const { galleryExtensions } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, CancellationToken.None);
for (let index = 0; index < galleryExtensions.length; index++) {
const galleryExtension = galleryExtensions[index];
if (!galleryExtension.versions.length) {
continue;
}
const id = getGalleryExtensionId(galleryExtension.publisher.publisherName, galleryExtension.extensionName);
const version = (<IExtensionIdentifierWithVersion | undefined>identifiers.find(identifier => areSameExtensions(identifier, { id })))?.version;
if (version) {
const versionAsset = galleryExtension.versions.find(v => v.version === version);
if (versionAsset) {
result.push(toExtension(galleryExtension, versionAsset, getAllTargetPlatforms(galleryExtension), index, query));
}
} else {
result.push(toExtensionWithLatestVersion(galleryExtension, index, query, undefined, CURRENT_TARGET_PLATFORM));
}
}
return result;
const galleryExtensionsByVersion = galleryExtensions.map(rawGalleryExtension => {
const id = getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName);
return { rawGalleryExtension, version: (<IExtensionIdentifierWithVersion | undefined>identifiers.find(identifier => areSameExtensions(identifier, { id })))?.version };
});
return this.converToGalleryExtensions(galleryExtensionsByVersion, includePreRelease, CURRENT_TARGET_PLATFORM, () => undefined, token);
}
async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null> {
async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null> {
const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1;
if (extension) {
if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) {
return null;
}
if (await this.isExtensionCompatible(extension, targetPlatform)) {
if (await this.isExtensionCompatible(extension, includePreRelease, targetPlatform)) {
return extension;
}
}
@ -551,19 +541,23 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
properties: [...(rawVersion.properties || []), { key: PropertyType.Engine, value: engine }]
};
}
if (await this.isRawExtensionVersionCompatible(rawVersion, allTargetPlatforms, targetPlatform)) {
return toExtension(rawExtension, rawVersion, allTargetPlatforms, 0, query);
if (await this.isRawExtensionVersionCompatible(rawVersion, includePreRelease, allTargetPlatforms, targetPlatform)) {
return toExtension(rawExtension, rawVersion, allTargetPlatforms);
}
}
return null;
}
async isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean> {
async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean> {
if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) {
return false;
}
if (!includePreRelease && extension.properties.isPreReleaseVersion) {
return false;
}
let engine = extension.properties.engine;
if (!engine) {
const manifest = await this.getManifest(extension, CancellationToken.None);
@ -575,11 +569,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return isEngineValid(engine, this.productService.version, this.productService.date);
}
private async isRawExtensionVersionCompatible(rawExtensionVersion: IRawGalleryExtensionVersion, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
private async isRawExtensionVersionCompatible(rawExtensionVersion: IRawGalleryExtensionVersion, includePreRelease: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise<boolean> {
if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawExtensionVersion), allTargetPlatforms, targetPlatform)) {
return false;
}
if (!includePreRelease && isPreReleaseVersion(rawExtensionVersion)) {
return false;
}
const engine = await this.getEngine(rawExtensionVersion);
return isEngineValid(engine, this.productService.version, this.productService.date);
}
@ -641,19 +639,69 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
}
const { galleryExtensions, total } = await this.queryGallery(query, CURRENT_TARGET_PLATFORM, token);
const extensions = galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, query, options.source, CURRENT_TARGET_PLATFORM));
const telemetryData = (index: number) => ({ index: ((query.pageNumber - 1) * query.pageSize) + index, querySource: options.source });
const extensions = await this.converToGalleryExtensions(galleryExtensions.map(rawGalleryExtension => ({ rawGalleryExtension })), !!options.includePreRelease, CURRENT_TARGET_PLATFORM, telemetryData, token);
const getPage = async (pageIndex: number, ct: CancellationToken) => {
if (ct.isCancellationRequested) {
throw canceled();
}
const nextPageQuery = query.withPage(pageIndex + 1);
const { galleryExtensions } = await this.queryGallery(nextPageQuery, CURRENT_TARGET_PLATFORM, ct);
return galleryExtensions.map((e, index) => toExtensionWithLatestVersion(e, index, nextPageQuery, options.source, CURRENT_TARGET_PLATFORM));
return await this.converToGalleryExtensions(galleryExtensions.map(rawGalleryExtension => ({ rawGalleryExtension })), !!options.includePreRelease, CURRENT_TARGET_PLATFORM, telemetryData, token);
};
return { firstPage: extensions, total, pageSize: query.pageSize, getPage } as IPager<IGalleryExtension>;
}
private async converToGalleryExtensions(rawGalleryExtensions: { rawGalleryExtension: IRawGalleryExtension, version?: string }[], includePreRelease: boolean, targetPlatform: TargetPlatform, telemetryData: (index: number) => any, token: CancellationToken): Promise<IGalleryExtension[]> {
const toExtensionWithLatestVersion = (galleryExtension: IRawGalleryExtension, index: number): IGalleryExtension => {
const allTargetPlatforms = getAllTargetPlatforms(galleryExtension);
let latestVersion = galleryExtension.versions[0];
latestVersion = galleryExtension.versions.find(version => version.version === latestVersion.version && isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(version), allTargetPlatforms, targetPlatform)) || latestVersion;
if (!includePreRelease && isPreReleaseVersion(latestVersion)) {
latestVersion = galleryExtension.versions.find(version => version.version !== latestVersion.version && !isPreReleaseVersion(version)) || latestVersion;
}
return toExtension(galleryExtension, latestVersion, allTargetPlatforms, telemetryData(index));
};
const result: [number, IGalleryExtension][] = [];
const preReleaseVersions = new Map<string, number>();
for (let index = 0; index < rawGalleryExtensions.length; index++) {
const { rawGalleryExtension, version } = rawGalleryExtensions[index];
if (version) {
const versionAsset = rawGalleryExtension.versions.find(v => v.version === version);
if (versionAsset) {
result.push([index, toExtension(rawGalleryExtension, versionAsset, getAllTargetPlatforms(rawGalleryExtension))]);
}
} else {
const extension = toExtensionWithLatestVersion(rawGalleryExtension, index);
if (extension.properties.isPreReleaseVersion && !includePreRelease) {
preReleaseVersions.set(extension.identifier.uuid, index);
} else {
result.push([index, extension]);
}
}
}
if (preReleaseVersions.size) {
const query = new Query()
.withFlags(Flags.IncludeVersions, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, preReleaseVersions.size)
.withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code')
.withFilter(FilterType.ExtensionId, ...preReleaseVersions.keys());
const { galleryExtensions } = await this.queryGallery(query, targetPlatform, token);
if (galleryExtensions.length !== preReleaseVersions.size) {
throw new Error('Not all extensions with latest versions are returned');
}
for (const rawGalleryExtension of galleryExtensions) {
const index = preReleaseVersions.get(rawGalleryExtension.extensionId)!;
const extension = toExtensionWithLatestVersion(rawGalleryExtension, index);
result.push([index, extension]);
}
}
return result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension);
}
private async queryGallery(query: Query, targetPlatform: TargetPlatform, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> {
if (!this.isEnabled()) {
throw new Error('No extension gallery service configured.');
@ -811,7 +859,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
return '';
}
async getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
async getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]> {
let query = new Query()
.withFlags(Flags.IncludeVersions, Flags.IncludeCategoryAndTags, Flags.IncludeFiles, Flags.IncludeVersionProperties)
.withPage(1, 1)
@ -836,7 +884,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
const result: IGalleryExtensionVersion[] = [];
for (const version of galleryExtensions[0].versions) {
try {
if (result[result.length - 1]?.version !== version.version && await this.isRawExtensionVersionCompatible(version, allTargetPlatforms, targetPlatform)) {
if (result[result.length - 1]?.version !== version.version && await this.isRawExtensionVersionCompatible(version, includePreRelease, allTargetPlatforms, targetPlatform)) {
result.push({ version: version.version, date: version.lastUpdated });
}
} catch (error) { /* Ignore error and skip version */ }

View file

@ -184,6 +184,7 @@ export interface IGalleryExtensionProperties {
engine?: string;
localizedLanguages?: string[];
targetPlatform: TargetPlatform;
isPreReleaseVersion: boolean;
}
export interface IGalleryExtensionAsset {
@ -253,10 +254,11 @@ export interface IGalleryExtension {
releaseDate: number;
lastUpdated: number;
preview: boolean;
hasPreReleaseVersion: boolean;
allTargetPlatforms: TargetPlatform[];
assets: IGalleryExtensionAssets;
properties: IGalleryExtensionProperties;
telemetryData: any;
telemetryData?: any;
}
export interface IGalleryMetadata {
@ -270,6 +272,8 @@ export interface ILocalExtension extends IExtension {
publisherId: string | null;
publisherDisplayName: string | null;
installedTimestamp?: number;
isPreReleaseVersion: boolean;
hadPreReleaseVersion: boolean;
}
export const enum SortBy {
@ -297,6 +301,7 @@ export interface IQueryOptions {
sortBy?: SortBy;
sortOrder?: SortOrder;
source?: string;
includePreRelease?: boolean;
}
export const enum StatisticType {
@ -325,6 +330,7 @@ export interface IExtensionGalleryService {
isEnabled(): boolean;
query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>>;
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, token: CancellationToken): Promise<IGalleryExtension[]>;
getExtensions(identifiers: ReadonlyArray<IExtensionIdentifier | IExtensionIdentifierWithVersion>, includePreRelease: boolean, token: CancellationToken): Promise<IGalleryExtension[]>;
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void>;
reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void>;
getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
@ -332,10 +338,10 @@ export interface IExtensionGalleryService {
getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getCoreTranslation(extension: IGalleryExtension, languageId: string): Promise<ITranslation | null>;
getExtensionsReport(): Promise<IReportedExtension[]>;
isExtensionCompatible(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<boolean>;
getCompatibleExtension(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getCompatibleExtension(id: IExtensionIdentifier, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getAllCompatibleVersions(extension: IGalleryExtension, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<boolean>;
getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getCompatibleExtension(id: IExtensionIdentifier, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtension | null>;
getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise<IGalleryExtensionVersion[]>;
}
export interface InstallExtensionEvent {
@ -359,6 +365,7 @@ export enum ExtensionManagementErrorCode {
Unsupported = 'Unsupported',
Malicious = 'Malicious',
Incompatible = 'Incompatible',
IncompatibleTargetPlatform = 'IncompatibleTargetPlatform',
Invalid = 'Invalid',
Download = 'Download',
Extract = 'Extract',
@ -376,7 +383,7 @@ export class ExtensionManagementError extends Error {
}
}
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean, installGivenVersion?: boolean };
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean, installGivenVersion?: boolean, installPreReleaseVersion?: boolean };
export type InstallVSIXOptions = Omit<InstallOptions, 'installGivenVersion'> & { installOnlyNewlyAddedFromExtensionPack?: boolean };
export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean };

View file

@ -19,7 +19,7 @@ import { IFile, zip } from 'vs/base/node/zip';
import * as nls from 'vs/nls';
import { IDownloadService } from 'vs/platform/download/common/download';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, Metadata, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import {
ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions,
InstallVSIXOptions, TargetPlatform
@ -29,7 +29,7 @@ import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/exten
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache';
import { ExtensionsScanner, ILocalExtensionManifest, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsScanner, ILocalExtensionManifest } from 'vs/platform/extensionManagement/node/extensionsScanner';
import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
@ -43,7 +43,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'
interface InstallableExtension {
zipPath: string;
identifierWithVersion: ExtensionIdentifierWithVersion;
metadata?: IMetadata;
metadata?: Metadata;
}
export class ExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService {
@ -226,7 +226,7 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocal
try {
const local = await this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion);
if (local) {
return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, installableExtension.metadata) : local;
return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), ...installableExtension.metadata }) : local;
}
} catch (e) {
if (isMacintosh) {
@ -287,6 +287,8 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask {
const installableExtension = await this.downloadInstallableExtension(this.gallery, this._operation);
installableExtension.metadata.isMachineScoped = this.options.isMachineScoped || existingExtension?.isMachineScoped;
installableExtension.metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin;
installableExtension.metadata.isPreReleaseVersion = this.gallery.properties.isPreReleaseVersion;
installableExtension.metadata.hadPreReleaseVersion = this.gallery.properties.isPreReleaseVersion || existingExtension?.hadPreReleaseVersion;
try {
const local = await this.installExtension(installableExtension, token);
@ -383,7 +385,7 @@ class InstallVSIXTask extends AbstractInstallExtensionTask {
return this.installExtension({ zipPath: path.resolve(this.location.fsPath), identifierWithVersion, metadata }, token);
}
private async getMetadata(name: string, token: CancellationToken): Promise<IMetadata> {
private async getMetadata(name: string, token: CancellationToken): Promise<Metadata> {
try {
const galleryExtension = (await this.galleryService.query({ names: [name], pageSize: 1 }, token)).firstPage[0];
if (galleryExtension) {

View file

@ -19,7 +19,8 @@ import * as pfs from 'vs/base/node/pfs';
import { extract, ExtractError } from 'vs/base/node/zip';
import { localize } from 'vs/nls';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionManagementError, ExtensionManagementErrorCode, IGalleryMetadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Metadata } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { ExtensionManagementError, ExtensionManagementErrorCode, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { ExtensionType, IExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
@ -28,9 +29,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { CancellationToken } from 'vscode';
export type IMetadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; isBuiltin: boolean; }>;
type IStoredMetadata = IMetadata & { installedTimestamp: number | undefined };
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: IMetadata };
export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: Metadata };
type IRelaxedLocalExtension = Omit<ILocalExtension, 'isBuiltin'> & { isBuiltin: boolean };
export class ExtensionsScanner extends Disposable {
@ -94,7 +93,7 @@ export class ExtensionsScanner extends Disposable {
return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User);
}
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise<ILocalExtension> {
const folderName = identifierWithVersion.key();
const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`);
const extensionPath = path.join(this.extensionsPath, folderName);
@ -140,21 +139,21 @@ export class ExtensionsScanner extends Disposable {
throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.extensionsPath));
}
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise<ILocalExtension> {
async saveMetadataForLocalExtension(local: ILocalExtension, metadata: Metadata): Promise<ILocalExtension> {
this.setMetadata(local, metadata);
await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp });
return local;
}
private async storeMetadata(local: ILocalExtension, storedMetadata: IStoredMetadata): Promise<ILocalExtension> {
private async storeMetadata(local: ILocalExtension, metaData: Metadata): Promise<ILocalExtension> {
// unset if false
storedMetadata.isMachineScoped = storedMetadata.isMachineScoped || undefined;
storedMetadata.isBuiltin = storedMetadata.isBuiltin || undefined;
storedMetadata.installedTimestamp = storedMetadata.installedTimestamp || undefined;
metaData.isMachineScoped = metaData.isMachineScoped || undefined;
metaData.isBuiltin = metaData.isBuiltin || undefined;
metaData.installedTimestamp = metaData.installedTimestamp || undefined;
const manifestPath = path.join(local.location.fsPath, 'package.json');
const raw = await pfs.Promises.readFile(manifestPath, 'utf8');
const { manifest } = await this.parseManifest(raw);
(manifest as ILocalExtensionManifest).__metadata = storedMetadata;
(manifest as ILocalExtensionManifest).__metadata = metaData;
await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
return local;
}
@ -302,7 +301,6 @@ export class ExtensionsScanner extends Disposable {
const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System };
if (metadata) {
this.setMetadata(local, metadata);
local.installedTimestamp = metadata.installedTimestamp;
}
return local;
}
@ -331,12 +329,15 @@ export class ExtensionsScanner extends Disposable {
}
}
private setMetadata(local: IRelaxedLocalExtension, metadata: IMetadata): void {
private setMetadata(local: IRelaxedLocalExtension, metadata: Metadata): void {
local.publisherDisplayName = metadata.publisherDisplayName || null;
local.publisherId = metadata.publisherId || null;
local.identifier.uuid = metadata.id;
local.isMachineScoped = !!metadata.isMachineScoped;
local.isPreReleaseVersion = !!metadata.isPreReleaseVersion;
local.hadPreReleaseVersion = !!metadata.hadPreReleaseVersion;
local.isBuiltin = local.type === ExtensionType.System || !!metadata.isBuiltin;
local.installedTimestamp = metadata.installedTimestamp;
}
private async removeUninstalledExtensions(): Promise<void> {
@ -392,7 +393,7 @@ export class ExtensionsScanner extends Disposable {
return this._devSystemExtensionsPath;
}
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IStoredMetadata | null; }> {
private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null; }> {
const promises = [
pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8')
.then(raw => this.parseManifest(raw)),
@ -408,7 +409,7 @@ export class ExtensionsScanner extends Disposable {
};
}
private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> {
private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null; }> {
return new Promise((c, e) => {
try {
const manifest = JSON.parse(raw);

View file

@ -419,7 +419,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
}
} catch (error) {
addToSkipped.push(e);
if (error instanceof ExtensionManagementError && error.code === ExtensionManagementErrorCode.Incompatible) {
if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.Incompatible || error.code === ExtensionManagementErrorCode.IncompatibleTargetPlatform)) {
this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, extension.displayName || extension.identifier.id);
} else {
this.logService.error(error);

View file

@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { Cache, CacheResult } from 'vs/base/common/cache';
import { Action, IAction } from 'vs/base/common/actions';
import { getErrorMessage, isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { append, $, finalHandler, join, addDisposableListener, EventType, setParentFlowTo, reset, Dimension } from 'vs/base/browser/dom';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -21,23 +21,23 @@ import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsServi
import { IExtensionManifest, IKeyBinding, IView, IViewContainer } from 'vs/platform/extensions/common/extensions';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers, ExtensionEditorTab, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions';
import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget, PreReleaseIndicatorWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import {
UpdateAction, ReloadAction, EnableDropDownAction, DisableDropDownAction, ExtensionStatusLabelAction, SetFileIconThemeAction, SetColorThemeAction,
RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction,
ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction,
InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction
InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction, UsePreReleaseVersionAction, StopUsingPreReleaseVersionAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Color } from 'vs/base/common/color';
@ -67,7 +67,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { Delegate } from 'vs/workbench/contrib/extensions/browser/extensionsList';
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { errorIcon, infoIcon, starEmptyIcon, verifiedPublisherIcon as verifiedPublisherThemeIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { MarkdownString } from 'vs/base/common/htmlContent';
@ -141,6 +140,7 @@ interface IExtensionEditorTemplate {
preview: HTMLElement;
builtin: HTMLElement;
version: HTMLElement;
preRelease: HTMLElement;
publisher: HTMLElement;
publisherDisplayName: HTMLElement;
verifiedPublisherIcon: HTMLElement;
@ -161,10 +161,13 @@ const enum WebviewIndex {
Changelog
}
const CONTEXT_SHOW_PRE_RELEASE_VERSION = new RawContextKey<boolean>('showPreReleaseVersion', false);
export class ExtensionEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.extension';
private readonly _scopedContextKeyService = this._register(new MutableDisposable<IContextKeyService>());
private template: IExtensionEditorTemplate | undefined;
private extensionReadme: Cache<string> | null;
@ -184,6 +187,8 @@ export class ExtensionEditor extends EditorPane {
private editorLoadComplete: boolean = false;
private dimension: Dimension | undefined;
private showPreReleaseVersionContextKey: IContextKey<boolean> | undefined;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ -201,6 +206,7 @@ export class ExtensionEditor extends EditorPane {
@IWebviewService private readonly webviewService: IWebviewService,
@IModeService private readonly modeService: IModeService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super(ExtensionEditor.ID, telemetryService, themeService, storageService);
this.extensionReadme = null;
@ -208,8 +214,16 @@ export class ExtensionEditor extends EditorPane {
this.extensionManifest = null;
}
override get scopedContextKeyService(): IContextKeyService | undefined {
return this._scopedContextKeyService.value;
}
createEditor(parent: HTMLElement): void {
const root = append(parent, $('.extension-editor'));
this._scopedContextKeyService.value = this.contextKeyService.createScoped(root);
this._scopedContextKeyService.value.createKey('inExtensionEditor', true);
this.showPreReleaseVersionContextKey = CONTEXT_SHOW_PRE_RELEASE_VERSION.bindTo(this._scopedContextKeyService.value);
root.tabIndex = 0; // this is required for the focus tracker on the editor
root.style.outline = 'none';
root.setAttribute('role', 'document');
@ -228,6 +242,7 @@ export class ExtensionEditor extends EditorPane {
const builtin = append(title, $('span.builtin'));
builtin.textContent = localize('builtin', "Built-in");
const preRelease = append(title, $('span.pre-release'));
const subtitle = append(details, $('.subtitle'));
const publisher = append(append(subtitle, $('.subtitle-entry')), $('.publisher.clickable', { title: localize('publisher', "Publisher"), tabIndex: 0 }));
@ -277,6 +292,7 @@ export class ExtensionEditor extends EditorPane {
icon,
iconContainer,
version,
preRelease,
installCount,
name,
navbar,
@ -306,13 +322,31 @@ export class ExtensionEditor extends EditorPane {
return disposables;
}
override async setInput(input: ExtensionsInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
override async setInput(input: ExtensionsInput, options: IExtensionEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token);
this.updatePreReleaseVersionContext();
if (this.template) {
await this.updateTemplate(input, this.template, !!options?.preserveFocus);
await this.updateTemplate(input.extension, this.template, !!options?.preserveFocus);
}
}
override setOptions(options: IExtensionEditorOptions | undefined): void {
const currentOptions: IExtensionEditorOptions | undefined = this.options;
super.setOptions(options);
this.updatePreReleaseVersionContext();
if (currentOptions?.showPreReleaseVersion !== options?.showPreReleaseVersion) {
this.openTab(ExtensionEditorTab.Readme);
}
}
private updatePreReleaseVersionContext(): void {
let showPreReleaseVersion = (<IExtensionEditorOptions | undefined>this.options)?.showPreReleaseVersion;
if (isUndefined(showPreReleaseVersion)) {
showPreReleaseVersion = !!(<ExtensionsInput>this.input).extension.gallery?.properties.isPreReleaseVersion;
}
this.showPreReleaseVersionContextKey?.set(showPreReleaseVersion);
}
async openTab(tab: ExtensionEditorTab): Promise<void> {
if (!this.input || !this.template) {
return;
@ -326,10 +360,9 @@ export class ExtensionEditor extends EditorPane {
}
}
private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
private async updateTemplate(extension: IExtension, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
this.activeElement = null;
this.editorLoadComplete = false;
const extension = input.extension;
if (this.currentIdentifier !== extension.identifier.id) {
this.initialScrollProgress.clear();
@ -338,9 +371,9 @@ export class ExtensionEditor extends EditorPane {
this.transientDisposables.clear();
this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(token)));
this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(token)));
this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token)));
this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(!!this.showPreReleaseVersionContextKey?.get(), token)));
this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(!!this.showPreReleaseVersionContextKey?.get(), token)));
this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(!!this.showPreReleaseVersionContextKey?.get(), token)));
const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, template.iconContainer, true);
this.transientDisposables.add(addDisposableListener(template.icon, 'error', () => template.icon.src = extension.iconUrlFallback, { once: true }));
@ -393,6 +426,7 @@ export class ExtensionEditor extends EditorPane {
}
const widgets = [
this.instantiationService.createInstance(PreReleaseIndicatorWidget, template.preRelease),
remoteBadge,
this.instantiationService.createInstance(InstallCountWidget, template.installCount, false),
this.instantiationService.createInstance(RatingsWidget, template.rating, false)
@ -415,11 +449,15 @@ export class ExtensionEditor extends EditorPane {
combinedInstallAction,
this.instantiationService.createInstance(InstallingLabelAction),
this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(InstallAnotherVersionAction),
[
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(InstallAnotherVersionAction),
]
]),
this.instantiationService.createInstance(UsePreReleaseVersionAction),
this.instantiationService.createInstance(StopUsingPreReleaseVersionAction),
this.instantiationService.createInstance(ToggleSyncExtensionAction),
this.instantiationService.createInstance(ExtensionEditorManageExtensionAction),
new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService),
];
const extensionStatus = this.instantiationService.createInstance(ExtensionStatusAction);
const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, extensionStatus]);

View file

@ -15,7 +15,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo
import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, DefaultViewsContext, ExtensionsSortByContext, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions';
import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, UsePreReleaseVersionAction, StopUsingPreReleaseVersionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor';
import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet';
@ -1156,6 +1156,75 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
// Extension Context Menu
private registerContextMenuActions(): void {
this.registerExtensionAction({
id: 'workbench.extensions.action.showPreReleaseVersion',
title: { value: localize('show pre-release version', "Show Pre-release Version"), original: 'Show Pre-release Version' },
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 0,
when: ContextKeyExpr.or(
ContextKeyExpr.and(ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('galleryExtensionIsPreReleaseVersion')),
ContextKeyExpr.and(ContextKeyExpr.has('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('showPreReleaseVersion')))
},
run: async (accessor: ServicesAccessor, extensionId: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = (await extensionWorkbenchService.queryGallery({ names: [extensionId] }, CancellationToken.None)).firstPage[0];
extensionWorkbenchService.open(extension, { showPreReleaseVersion: true });
}
});
this.registerExtensionAction({
id: 'workbench.extensions.action.showReleasedVersion',
title: { value: localize('show released version', "Show Released Version"), original: 'Show Released Version' },
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 1,
when: ContextKeyExpr.or(
ContextKeyExpr.and(ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.has('galleryExtensionIsPreReleaseVersion'), ContextKeyExpr.equals('extensionStatus', 'installed')),
ContextKeyExpr.and(ContextKeyExpr.has('inExtensionEditor'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.has('showPreReleaseVersion')))
},
run: async (accessor: ServicesAccessor, extensionId: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = (await extensionWorkbenchService.queryGallery({ names: [extensionId] }, CancellationToken.None)).firstPage[0];
extensionWorkbenchService.open(extension, { showPreReleaseVersion: false });
}
});
this.registerExtensionAction({
id: UsePreReleaseVersionAction.ID,
title: UsePreReleaseVersionAction.TITLE,
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 2,
when: ContextKeyExpr.and(ContextKeyExpr.not('installedExtensionIsPreReleaseVersion'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'))
},
run: async (accessor: ServicesAccessor, id: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = extensionWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id }));
if (extension) {
await extensionWorkbenchService.install(extension, { installPreReleaseVersion: true });
}
}
});
this.registerExtensionAction({
id: StopUsingPreReleaseVersionAction.ID,
title: StopUsingPreReleaseVersionAction.TITLE,
menu: {
id: MenuId.ExtensionContext,
group: '0_install',
order: 3,
when: ContextKeyExpr.and(ContextKeyExpr.has('installedExtensionIsPreReleaseVersion'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'))
},
run: async (accessor: ServicesAccessor, id: string) => {
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
const extension = extensionWorkbenchService.local.find(e => areSameExtensions(e.identifier, { id }));
if (extension) {
await extensionWorkbenchService.install(extension, { installPreReleaseVersion: false });
}
}
});
this.registerExtensionAction({
id: 'workbench.extensions.action.copyExtension',
title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' },

View file

@ -33,7 +33,7 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MenuId, IMenuService } from 'vs/platform/actions/common/actions';
import { MenuId, IMenuService, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
@ -63,6 +63,7 @@ import { isVirtualWorkspace } from 'vs/platform/remote/common/remoteHosts';
import { escapeMarkdownSyntaxTokens, IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { ViewContainerLocation } from 'vs/workbench/common/views';
import { flatten } from 'vs/base/common/arrays';
function getRelativeDateLabel(date: Date): string {
const delta = new Date().getTime() - date.getTime();
@ -133,7 +134,7 @@ export class PromptExtensionInstallFailureAction extends Action {
return;
}
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.Malicious].includes(<ExtensionManagementErrorCode>this.error.name)) {
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious].includes(<ExtensionManagementErrorCode>this.error.name)) {
await this.dialogService.show(Severity.Info, getErrorMessage(this.error));
return;
}
@ -185,28 +186,40 @@ export class ActionWithDropDownAction extends ExtensionAction {
}
override set extension(extension: IExtension | null) {
this.actions.forEach(a => a.extension = extension);
this.extensionActions.forEach(a => a.extension = extension);
super.extension = extension;
}
protected readonly extensionActions: ExtensionAction[];
constructor(
id: string, label: string,
protected readonly actions: ExtensionAction[],
private readonly actionsGroups: ExtensionAction[][],
) {
super(id, label);
this.extensionActions = flatten(actionsGroups);
this.update();
this._register(Event.any(...actions.map(a => a.onDidChange))(() => this.update(true)));
actions.forEach(a => this._register(a));
this._register(Event.any(...this.extensionActions.map(a => a.onDidChange))(() => this.update(true)));
this.extensionActions.forEach(a => this._register(a));
}
update(donotUpdateActions?: boolean): void {
if (!donotUpdateActions) {
this.actions.forEach(a => a.update());
this.extensionActions.forEach(a => a.update());
}
const enabledActions = this.actions.filter(a => a.enabled);
this.action = enabledActions[0];
this._menuActions = enabledActions.slice(1);
const enabledActionsGroups = this.actionsGroups.map(actionsGroup => actionsGroup.filter(a => a.enabled));
let actions: IAction[] = [];
for (const enabledActions of enabledActionsGroups) {
if (enabledActions.length) {
actions = [...actions, ...enabledActions, new Separator()];
}
}
actions = actions.length ? actions.slice(0, actions.length - 1) : actions;
this.action = actions[0];
this._menuActions = actions.slice(1);
this.enabled = !!this.action;
if (this.action) {
@ -214,7 +227,7 @@ export class ActionWithDropDownAction extends ExtensionAction {
this.tooltip = this.action.tooltip;
}
let clazz = (this.action || this.actions[0])?.class || '';
let clazz = (this.action || this.extensionActions[0])?.class || '';
clazz = clazz ? `${clazz} action-dropdown` : 'action-dropdown';
if (this._menuActions.length === 0) {
clazz += ' action-dropdown';
@ -223,7 +236,7 @@ export class ActionWithDropDownAction extends ExtensionAction {
}
override run(): Promise<void> {
const enabledActions = this.actions.filter(a => a.enabled);
const enabledActions = this.extensionActions.filter(a => a.enabled);
return enabledActions[0].run();
}
}
@ -241,14 +254,14 @@ export abstract class AbstractInstallAction extends ExtensionAction {
private readonly updateThrottler = new Throttler();
constructor(
id: string, label: string, cssClass: string,
id: string, private readonly installPreReleaseVersion: boolean, cssClass: string,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionService private readonly runtimeExtensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@ILabelService private readonly labelService: ILabelService,
) {
super(id, label, cssClass, false);
super(id, localize('install', "Install"), cssClass, false);
this.update();
this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this));
}
@ -261,7 +274,7 @@ export abstract class AbstractInstallAction extends ExtensionAction {
this.enabled = false;
if (this.extension && !this.extension.isBuiltin) {
if (this.extension.state === ExtensionState.Uninstalled && await this.extensionsWorkbenchService.canInstall(this.extension)) {
this.enabled = true;
this.enabled = !this.installPreReleaseVersion || this.extension.hasPreReleaseVersion;
this.updateLabel();
}
}
@ -271,7 +284,7 @@ export abstract class AbstractInstallAction extends ExtensionAction {
if (!this.extension) {
return;
}
this.extensionsWorkbenchService.open(this.extension);
this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: this.installPreReleaseVersion });
alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName));
@ -324,13 +337,23 @@ export abstract class AbstractInstallAction extends ExtensionAction {
return null;
}
protected abstract updateLabel(): void;
protected abstract getInstallOptions(): InstallOptions;
protected updateLabel(): void {
this.label = this.getLabel();
}
protected getLabel(): string {
return this.installPreReleaseVersion && this.extension?.hasPreReleaseVersion ? localize('install pre-release', "Install Pre-release Version") : localize('install', "Install");
}
protected getInstallOptions(): InstallOptions {
return { installPreReleaseVersion: this.installPreReleaseVersion };
}
}
export class InstallAction extends AbstractInstallAction {
constructor(
installPreReleaseVersion: boolean,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService runtimeExtensionService: IExtensionService,
@ -341,7 +364,7 @@ export class InstallAction extends AbstractInstallAction {
@IUserDataAutoSyncEnablementService protected readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IUserDataSyncResourceEnablementService protected readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
) {
super(`extensions.installAndSync`, localize('install', "Install"), InstallAction.Class,
super(`extensions.install`, installPreReleaseVersion, InstallAction.Class,
extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService);
this.updateLabel();
this._register(labelService.onDidChangeFormatters(() => this.updateLabel(), this));
@ -349,13 +372,11 @@ export class InstallAction extends AbstractInstallAction {
Event.filter(userDataSyncResourceEnablementService.onDidChangeResourceEnablement, e => e[0] === SyncResource.Extensions))(() => this.update()));
}
protected updateLabel(): void {
if (!this.extension) {
return;
}
protected override getLabel(): string {
const baseLabel = super.getLabel();
const donotSyncLabel = localize('do no sync', "Do not sync");
const isMachineScoped = this.getInstallOptions().isMachineScoped;
this.label = isMachineScoped ? localize('install and do no sync', "Install (Do not sync)") : localize('install', "Install");
// When remote connection exists
if (this._manifest && this.extensionManagementServerService.remoteExtensionManagementServer) {
@ -364,19 +385,33 @@ export class InstallAction extends AbstractInstallAction {
if (server === this.extensionManagementServerService.remoteExtensionManagementServer) {
const host = this.extensionManagementServerService.remoteExtensionManagementServer.label;
this.label = isMachineScoped
? localize({ key: 'install in remote and do not sync', comment: ['This is the name of the action to install an extension in remote server and do not sync it. Placeholder is for the name of remote server.'] }, "Install in {0} (Do not sync)", host)
: localize({ key: 'install in remote', comment: ['This is the name of the action to install an extension in remote server. Placeholder is for the name of remote server.'] }, "Install in {0}", host);
return;
return isMachineScoped
? localize({
key: 'install extension in remote and do not sync',
comment: [
'First placeholder is install action label.',
'Second placeholder is the name of the action to install an extension in remote server and do not sync it. Placeholder is for the name of remote server.',
'Third placeholder is do not sync label.',
]
}, "{0} in {1} ({2})", baseLabel, host, donotSyncLabel)
: localize({
key: 'install extension in remote',
comment: [
'First placeholder is install action label.',
'Second placeholder is the name of the action to install an extension in remote server and do not sync it. Placeholder is for the name of remote server.',
]
}, "{0} in {1}", baseLabel, host);
}
this.label = isMachineScoped ? localize('install locally and do not sync', "Install Locally (Do not sync)") : localize('install locally', "Install Locally");
return;
return isMachineScoped ?
localize('install extension locally and do not sync', "{0} Locally ({1})", baseLabel, donotSyncLabel) : localize('install extension locally', "{0} Locally", baseLabel);
}
return isMachineScoped ? `${baseLabel} (${donotSyncLabel})` : baseLabel;
}
protected getInstallOptions(): InstallOptions {
return { isMachineScoped: this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions) };
protected override getInstallOptions(): InstallOptions {
return { ...super.getInstallOptions(), isMachineScoped: this.userDataAutoSyncEnablementService.isEnabled() && this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions) };
}
}
@ -384,6 +419,7 @@ export class InstallAction extends AbstractInstallAction {
export class InstallAndSyncAction extends AbstractInstallAction {
constructor(
installPreReleaseVersion: boolean,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService runtimeExtensionService: IExtensionService,
@ -393,7 +429,7 @@ export class InstallAndSyncAction extends AbstractInstallAction {
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
) {
super(`extensions.installAndSync`, localize('install', "Install"), InstallAndSyncAction.Class,
super('extensions.installAndSync', installPreReleaseVersion, AbstractInstallAction.Class,
extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService);
this.tooltip = localize({ key: 'install everywhere tooltip', comment: ['Placeholder is the name of the product. Eg: Visual Studio Code or Visual Studio Code - Insiders'] }, "Install this extension in all your synced {0} instances", productService.nameLong);
this._register(Event.any(userDataAutoSyncEnablementService.onDidChangeEnablement,
@ -407,18 +443,16 @@ export class InstallAndSyncAction extends AbstractInstallAction {
}
}
protected updateLabel(): void { }
protected getInstallOptions(): InstallOptions {
return { isMachineScoped: false };
protected override getInstallOptions(): InstallOptions {
return { ...super.getInstallOptions(), isMachineScoped: false };
}
}
export class InstallDropdownAction extends ActionWithDropDownAction {
set manifest(manifest: IExtensionManifest) {
this.actions.forEach(a => (<AbstractInstallAction>a).manifest = manifest);
this.actions.forEach(a => a.update());
this.extensionActions.forEach(a => (<AbstractInstallAction>a).manifest = manifest);
this.extensionActions.forEach(a => a.update());
this.update();
}
@ -426,8 +460,14 @@ export class InstallDropdownAction extends ActionWithDropDownAction {
@IInstantiationService instantiationService: IInstantiationService,
) {
super(`extensions.installActions`, '', [
instantiationService.createInstance(InstallAndSyncAction),
instantiationService.createInstance(InstallAction),
[
instantiationService.createInstance(InstallAndSyncAction, false),
instantiationService.createInstance(InstallAndSyncAction, true),
],
[
instantiationService.createInstance(InstallAction, false),
instantiationService.createInstance(InstallAction, true),
]
]);
}
@ -812,7 +852,7 @@ export class DropDownMenuActionViewItem extends ActionViewItem {
}
}
export function getContextMenuActions(extension: IExtension | undefined | null, inExtensionEditor: boolean, instantiationService: IInstantiationService): IAction[][] {
function getContextMenuActionsGroups(extension: IExtension | undefined | null, contextKeyService: IContextKeyService, instantiationService: IInstantiationService): [string, Array<MenuItemAction | SubmenuItemAction>][] {
return instantiationService.invokeFunction(accessor => {
const menuService = accessor.get(IMenuService);
const extensionRecommendationsService = accessor.get(IExtensionRecommendationsService);
@ -826,25 +866,38 @@ export function getContextMenuActions(extension: IExtension | undefined | null,
cksOverlay.push(['isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]]);
cksOverlay.push(['isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace]);
cksOverlay.push(['isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase())]);
cksOverlay.push(['inExtensionEditor', inExtensionEditor]);
if (extension.state === ExtensionState.Installed) {
cksOverlay.push(['extensionStatus', 'installed']);
}
cksOverlay.push(['installedExtensionIsPreReleaseVersion', !!extension.local?.isPreReleaseVersion]);
cksOverlay.push(['galleryExtensionIsPreReleaseVersion', !!extension.gallery?.properties.isPreReleaseVersion]);
cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]);
}
const contextKeyService = accessor.get(IContextKeyService).createOverlay(cksOverlay);
const groups: IAction[][] = [];
const menu = menuService.createMenu(MenuId.ExtensionContext, contextKeyService);
menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => {
const menu = menuService.createMenu(MenuId.ExtensionContext, contextKeyService.createOverlay(cksOverlay));
const actionsGroups = menu.getActions({ shouldForwardArgs: true });
menu.dispose();
return actionsGroups;
});
}
function toActions(actionsGroups: [string, Array<MenuItemAction | SubmenuItemAction>][], instantiationService: IInstantiationService): IAction[][] {
const result: IAction[][] = [];
for (const [, actions] of actionsGroups) {
result.push(actions.map(action => {
if (action instanceof SubmenuAction) {
return action;
}
return instantiationService.createInstance(MenuItemExtensionAction, action);
})));
menu.dispose();
}));
}
return result;
}
return groups;
});
export function getContextMenuActions(extension: IExtension | undefined | null, contextKeyService: IContextKeyService, instantiationService: IInstantiationService): IAction[][] {
const actionsGroups = getContextMenuActionsGroups(extension, contextKeyService, instantiationService);
return toActions(actionsGroups, instantiationService);
}
export class ManageExtensionAction extends ExtensionDropDownAction {
@ -858,6 +911,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService private readonly extensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super(ManageExtensionAction.ID, '', '', true, instantiationService);
@ -886,6 +940,11 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
groups.push(themesGroup);
}
}
const contextMenuActionsGroups = getContextMenuActionsGroups(this.extension, this.contextKeyService, this.instantiationService);
const installActions = toActions(contextMenuActionsGroups.filter(([group]) => group === '0_install'), this.instantiationService);
const otherActions = toActions(contextMenuActionsGroups.filter(([group]) => group !== '0_install'), this.instantiationService);
groups.push([
this.instantiationService.createInstance(EnableGloballyAction),
this.instantiationService.createInstance(EnableForWorkspaceAction)
@ -895,11 +954,12 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
this.instantiationService.createInstance(DisableForWorkspaceAction, runningExtensions)
]);
groups.push([
...(installActions[0] || []),
this.instantiationService.createInstance(InstallAnotherVersionAction),
this.instantiationService.createInstance(UninstallAction),
this.instantiationService.createInstance(InstallAnotherVersionAction)
]);
getContextMenuActions(this.extension, false, this.instantiationService).forEach(actions => groups.push(actions));
otherActions.forEach(actions => groups.push(actions));
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
@ -930,7 +990,8 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
export class ExtensionEditorManageExtensionAction extends ExtensionDropDownAction {
constructor(
@IInstantiationService instantiationService: IInstantiationService
private readonly contextKeyService: IContextKeyService,
instantiationService: IInstantiationService
) {
super('extensionEditor.manageExtension', '', `${ExtensionAction.ICON_ACTION_CLASS} manage ${ThemeIcon.asClassName(manageExtensionIcon)}`, true, instantiationService);
this.tooltip = localize('manage', "Manage");
@ -940,7 +1001,7 @@ export class ExtensionEditorManageExtensionAction extends ExtensionDropDownActio
override run(): Promise<any> {
const actionGroups: IAction[][] = [];
getContextMenuActions(this.extension, true, this.instantiationService).forEach(actions => actionGroups.push(actions));
getContextMenuActions(this.extension, this.contextKeyService, this.instantiationService).forEach(actions => actionGroups.push(actions));
actionGroups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
extensionAction.extension = this.extension;
@ -976,6 +1037,58 @@ export class MenuItemExtensionAction extends ExtensionAction {
}
}
export class UsePreReleaseVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.usePreReleaseVersion';
static readonly TITLE = { value: localize('use pre-release version', "Use Pre-release Version"), original: 'Use Pre-release Version' };
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`;
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
super(UsePreReleaseVersionAction.ID, UsePreReleaseVersionAction.TITLE.value, UsePreReleaseVersionAction.Class);
this.update();
}
update(): void {
this.enabled = !!this.extension && !this.extension.local?.isPreReleaseVersion && this.extension.hasPreReleaseVersion && this.extension.state === ExtensionState.Installed;
}
override async run(): Promise<any> {
if (!this.enabled) {
return;
}
return this.commandService.executeCommand(UsePreReleaseVersionAction.ID, this.extension?.identifier.id);
}
}
export class StopUsingPreReleaseVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.stopUsingPreReleaseVersion';
static readonly TITLE = { value: localize('stop using pre-release version', "Stop Using Pre-release Version"), original: 'Stop Using Pre-release Version' };
private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} hide-when-disabled`;
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
super(StopUsingPreReleaseVersionAction.ID, StopUsingPreReleaseVersionAction.TITLE.value, StopUsingPreReleaseVersionAction.Class);
this.update();
}
update(): void {
this.enabled = !!this.extension && this.extension.state === ExtensionState.Installed && !!this.extension.local?.isPreReleaseVersion;
}
override async run(): Promise<any> {
if (!this.enabled) {
return;
}
return this.commandService.executeCommand(StopUsingPreReleaseVersionAction.ID, this.extension?.identifier.id);
}
}
export class InstallAnotherVersionAction extends ExtensionAction {
static readonly ID = 'workbench.extensions.action.install.anotherVersion';
@ -1019,7 +1132,7 @@ export class InstallAnotherVersionAction extends ExtensionAction {
private async getVersionEntries(): Promise<(IQuickPickItem & { latest: boolean, id: string })[]> {
const targetPlatform = await this.extension!.server!.extensionManagementService.getTargetPlatform();
const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension!.gallery!, targetPlatform);
const allVersions = await this.extensionGalleryService.getAllCompatibleVersions(this.extension!.gallery!, true, targetPlatform);
return allVersions.map((v, i) => ({ id: v.version, label: v.version, description: `${getRelativeDateLabel(new Date(Date.parse(v.date)))}${v.version === this.extension!.version ? ` (${localize('current', "Current")})` : ''}`, latest: i === 0 }));
}
}
@ -1166,8 +1279,10 @@ export class EnableDropDownAction extends ActionWithDropDownAction {
@IInstantiationService instantiationService: IInstantiationService
) {
super('extensions.enable', localize('enableAction', "Enable"), [
instantiationService.createInstance(EnableGloballyAction),
instantiationService.createInstance(EnableForWorkspaceAction)
[
instantiationService.createInstance(EnableGloballyAction),
instantiationService.createInstance(EnableForWorkspaceAction)
]
]);
}
}
@ -1182,7 +1297,7 @@ export class DisableDropDownAction extends ActionWithDropDownAction {
instantiationService.createInstance(DisableGloballyAction, []),
instantiationService.createInstance(DisableForWorkspaceAction, [])
];
super('extensions.disable', localize('disableAction', "Disable"), actions);
super('extensions.disable', localize('disableAction', "Disable"), [actions]);
const updateRunningExtensions = async () => {
const runningExtensions = await extensionService.getExtensions();
@ -2475,7 +2590,7 @@ export class InstallLocalExtensionsInRemoteAction extends AbstractInstallExtensi
const targetPlatform = await this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getTargetPlatform();
await Promises.settled(localExtensionsToInstall.map(async extension => {
if (this.extensionGalleryService.isEnabled()) {
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, targetPlatform);
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, !!extension.local?.isPreReleaseVersion, targetPlatform);
if (gallery) {
galleryExtensions.push(gallery);
return;
@ -2524,7 +2639,7 @@ export class InstallRemoteExtensionsInLocalAction extends AbstractInstallExtensi
const targetPlatform = await this.extensionManagementServerService.localExtensionManagementServer!.extensionManagementService.getTargetPlatform();
await Promises.settled(extensions.map(async extension => {
if (this.extensionGalleryService.isEnabled()) {
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, targetPlatform);
const gallery = await this.extensionGalleryService.getCompatibleExtension(extension.identifier, !!extension.local?.isPreReleaseVersion, targetPlatform);
if (gallery) {
galleryExtensions.push(gallery);
return;

View file

@ -25,6 +25,7 @@ export const remoteIcon = registerIcon('extensions-remote', Codicon.remote, loca
export const installCountIcon = registerIcon('extensions-install-count', Codicon.cloudDownload, localize('installCountIcon', 'Icon shown along with the install count in the extensions view and editor.'));
export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.'));
export const verifiedPublisherIcon = registerIcon('extensions-verified-publisher', Codicon.verifiedFilled, localize('verifiedPublisher', 'Icon used for the verified extension publisher in the extensions view and editor.'));
export const preReleaseIcon = registerIcon('extensions-pre-release', Codicon.circleFilled, localize('preReleaseIcon', 'Icon shown for extensions having pre-release versions in extensions view and editor.'));
export const starFullIcon = registerIcon('extensions-star-full', Codicon.starFull, localize('starFullIcon', 'Full star icon used for the rating in the extensions editor.'));
export const starHalfIcon = registerIcon('extensions-star-half', Codicon.starHalf, localize('starHalfIcon', 'Half star icon used for the rating in the extensions editor.'));

View file

@ -15,7 +15,7 @@ import { Event } from 'vs/base/common/event';
import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { UpdateAction, ManageExtensionAction, ReloadAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionActivationStatusWidget, PreReleaseBookmarkWidget, PreReleaseIndicatorWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { INotificationService } from 'vs/platform/notification/common/notification';
@ -78,6 +78,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
renderTemplate(root: HTMLElement): ITemplateData {
const recommendationWidget = this.instantiationService.createInstance(RecommendationWidget, append(root, $('.extension-bookmark-container')));
const preReleaseWidget = this.instantiationService.createInstance(PreReleaseBookmarkWidget, append(root, $('.extension-bookmark-container')));
const element = append(root, $('.extension-list-item'));
const iconContainer = append(element, $('.icon-container'));
const icon = append(iconContainer, $<HTMLImageElement>('img.icon'));
@ -89,6 +90,7 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
const name = append(header, $('span.name'));
const installCount = append(header, $('span.install-count'));
const ratings = append(header, $('span.ratings'));
const preRelease = append(header, $('span.pre-release'));
const syncIgnore = append(header, $('span.sync-ignored'));
const activationStatus = append(header, $('span.activation-status'));
const headerRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, header, false);
@ -131,10 +133,12 @@ export class Renderer implements IPagedRenderer<IExtension, ITemplateData> {
const widgets = [
recommendationWidget,
preReleaseWidget,
iconRemoteBadgeWidget,
extensionPackBadgeWidget,
headerRemoteBadgeWidget,
extensionHoverWidget,
this.instantiationService.createInstance(PreReleaseIndicatorWidget, preRelease),
this.instantiationService.createInstance(SyncIgnoredWidget, syncIgnore),
this.instantiationService.createInstance(ExtensionActivationStatusWidget, activationStatus, true),
this.instantiationService.createInstance(InstallCountWidget, installCount, true),

View file

@ -287,7 +287,7 @@ export class ExtensionsListView extends ViewPane {
groups = await manageExtensionAction.getActionGroups(runningExtensions);
} else if (e.element) {
groups = getContextMenuActions(e.element, false, this.instantiationService);
groups = getContextMenuActions(e.element, this.contextKeyService, this.instantiationService);
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
extensionAction.extension = e.element!;

View file

@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, ratingIcon, remoteIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, verifiedPublisherIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons';
import { registerColor, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
@ -158,6 +158,29 @@ export class RatingsWidget extends ExtensionWidget {
}
}
export class PreReleaseIndicatorWidget extends ExtensionWidget {
constructor(private container: HTMLElement) {
super();
container.classList.add('extension-pre-release');
this.render();
}
render(): void {
this.container.innerText = '';
if (!this.extension) {
return;
}
if (!this.extension.local?.isPreReleaseVersion) {
return;
}
append(this.container, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));
}
}
export class RecommendationWidget extends ExtensionWidget {
private element?: HTMLElement;
@ -183,7 +206,7 @@ export class RecommendationWidget extends ExtensionWidget {
render(): void {
this.clear();
if (!this.extension) {
if (!this.extension || this.extension.state === ExtensionState.Installed) {
return;
}
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
@ -196,6 +219,41 @@ export class RecommendationWidget extends ExtensionWidget {
}
export class PreReleaseBookmarkWidget extends ExtensionWidget {
private element?: HTMLElement;
private readonly disposables = this._register(new DisposableStore());
constructor(
private parent: HTMLElement,
) {
super();
this.render();
this._register(toDisposable(() => this.clear()));
}
private clear(): void {
if (this.element) {
this.parent.removeChild(this.element);
}
this.element = undefined;
this.disposables.clear();
}
render(): void {
this.clear();
if (!this.extension) {
return;
}
if (this.extension.hasPreReleaseVersion) {
this.element = append(this.parent, $('div.extension-bookmark'));
const preRelease = append(this.element, $('.pre-release'));
append(preRelease, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));
}
}
}
export class RemoteBadgeWidget extends ExtensionWidget {
private readonly remoteBadge = this._register(new MutableDisposable<RemoteBadge>());
@ -304,26 +362,26 @@ export class ExtensionPackCountWidget extends ExtensionWidget {
export class SyncIgnoredWidget extends ExtensionWidget {
private element: HTMLElement;
constructor(
container: HTMLElement,
private readonly container: HTMLElement,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
) {
super();
this.element = append(container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));
this.element.title = localize('syncingore.label', "This extension is ignored during sync.");
this.element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));
this.element.classList.add('hide');
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectedKeys.includes('settingsSync.ignoredExtensions'))(() => this.render()));
this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => this.update()));
this.render();
}
render(): void {
this.element.classList.toggle('hide', !(this.extension && this.extension.state === ExtensionState.Installed && this.userDataAutoSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)));
this.container.innerText = '';
if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataAutoSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) {
const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));
element.title = localize('syncingore.label', "This extension is ignored during sync.");
element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));
}
}
}
@ -416,6 +474,10 @@ export class ExtensionHoverWidget extends ExtensionWidget {
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
markdown.appendMarkdown(`**${this.extension.displayName}**&nbsp;_v${this.extension.version}_`);
if (this.extension.state === ExtensionState.Installed && this.extension.local?.isPreReleaseVersion) {
const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);
markdown.appendMarkdown(`&nbsp;<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span>`);
}
markdown.appendText(`\n`);
if (this.extension.description) {
@ -430,6 +492,12 @@ export class ExtensionHoverWidget extends ExtensionWidget {
markdown.appendText(`\n`);
}
const preReleaseMessage = this.getPreReleaseMessage(this.extension);
if (preReleaseMessage) {
markdown.appendMarkdown(preReleaseMessage);
markdown.appendText(`\n`);
}
const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension);
const extensionStatus = this.extensionStatusAction.status;
const reloadRequiredMessage = this.reloadAction.enabled ? this.reloadAction.tooltip : '';
@ -488,12 +556,28 @@ export class ExtensionHoverWidget extends ExtensionWidget {
}
private getRecommendationMessage(extension: IExtension): string | undefined {
const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()];
if (recommendation?.reasonText) {
const bgColor = this.themeService.getColorTheme().getColor(extensionButtonProminentBackground);
return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span>&nbsp;${recommendation.reasonText}`;
if (extension.state === ExtensionState.Installed) {
return undefined;
}
return undefined;
const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()];
if (!recommendation?.reasonText) {
return undefined;
}
const bgColor = this.themeService.getColorTheme().getColor(extensionButtonProminentBackground);
return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span>&nbsp;${recommendation.reasonText}`;
}
private getPreReleaseMessage(extension: IExtension): string | undefined {
if (!extension.hasPreReleaseVersion) {
return undefined;
}
if (extension.state === ExtensionState.Installed && extension.local?.isPreReleaseVersion) {
return undefined;
}
const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);
const preReleaseVersionLink = `[${localize('Show prerelease version', "Pre-release version")}](${URI.parse(`command:workbench.extensions.action.showPreReleaseVersion?${encodeURIComponent(JSON.stringify([extension.identifier.id]))}`)})`;
const message = localize('has prerelease', "There is a new {0} of this extension available in the Marketplace.", preReleaseVersionLink);
return `<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span>&nbsp;${message}.`;
}
}
@ -501,6 +585,7 @@ export class ExtensionHoverWidget extends ExtensionWidget {
// Rating icon
export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hc: '#FF8E00' }, localize('extensionIconStarForeground', "The icon color for extension ratings."), true);
export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', { dark: textLinkForeground, light: textLinkForeground, hc: textLinkForeground }, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true);
export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hc: '#1d9271' }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), true);
registerThemingParticipant((theme, collector) => {
const extensionRatingIcon = theme.getColor(extensionRatingIconColor);
@ -523,4 +608,11 @@ registerThemingParticipant((theme, collector) => {
if (extensionVerifiedPublisherIcon) {
collector.addRule(`${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${extensionVerifiedPublisherIcon}; }`);
}
const extensionPreReleaseIcon = theme.getColor(extensionPreReleaseIconColor);
if (extensionPreReleaseIcon) {
collector.addRule(`.extension-bookmark .pre-release { border-top-color: ${extensionPreReleaseIcon}; }`);
collector.addRule(`.extension-bookmark .pre-release ${ThemeIcon.asCSSSelector(preReleaseIcon)} { color: #ffffff; }`);
collector.addRule(`${ThemeIcon.asCSSSelector(preReleaseIcon)} { color: ${extensionPreReleaseIcon}; }`);
}
});

View file

@ -14,7 +14,7 @@ import { IPager, mapPager, singlePagePager } from 'vs/base/common/paging';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import {
IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions,
InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult
InstallExtensionEvent, DidUninstallExtensionEvent, IExtensionIdentifier, InstallOperation, DefaultIconPath, InstallOptions, WEB_EXTENSION_TAG, InstallExtensionResult, isIExtensionIdentifier
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
@ -22,10 +22,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { URI } from 'vs/base/common/uri';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext } from 'vs/workbench/contrib/extensions/common/extensions';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
@ -200,7 +200,7 @@ class Extension implements IExtension {
}
get outdated(): boolean {
return !!this.gallery && this.type === ExtensionType.User && semver.gt(this.latestVersion, this.version);
return !!this.gallery && this.type === ExtensionType.User && semver.gt(this.latestVersion, this.version) && this.local?.isPreReleaseVersion === this.gallery?.properties.isPreReleaseVersion;
}
get telemetryData(): any {
@ -217,20 +217,40 @@ class Extension implements IExtension {
return this.gallery ? this.gallery.preview : false;
}
getManifest(token: CancellationToken): Promise<IExtensionManifest | null> {
if (this.local && !this.outdated) {
return Promise.resolve(this.local.manifest);
get hasPreReleaseVersion(): boolean {
return !!this.gallery?.hasPreReleaseVersion;
}
private getLocal(preRelease: boolean): ILocalExtension | undefined {
return this.local && !this.outdated && this.local.isPreReleaseVersion === preRelease ? this.local : undefined;
}
private async getGallery(preRelease: boolean, token: CancellationToken): Promise<IGalleryExtension | undefined> {
if (this.gallery) {
if (preRelease === this.gallery.properties.isPreReleaseVersion) {
return this.gallery;
}
return (await this.galleryService.getExtensions([this.gallery.identifier], preRelease, token))[0];
}
return undefined;
}
async getManifest(preRelease: boolean, token: CancellationToken): Promise<IExtensionManifest | null> {
const local = this.getLocal(preRelease);
if (local) {
return local.manifest;
}
if (this.gallery) {
if (this.gallery.assets.manifest) {
return this.galleryService.getManifest(this.gallery, token);
const gallery = await this.getGallery(preRelease, token);
if (gallery) {
if (gallery.assets.manifest) {
return this.galleryService.getManifest(gallery, token);
}
this.logService.error(nls.localize('Manifest is not found', "Manifest is not found"), this.identifier.id);
return Promise.resolve(null);
return null;
}
return Promise.resolve(null);
return null;
}
hasReadme(): boolean {
@ -245,14 +265,17 @@ class Extension implements IExtension {
return this.type === ExtensionType.System;
}
getReadme(token: CancellationToken): Promise<string> {
if (this.local && this.local.readmeUrl && !this.outdated) {
return this.fileService.readFile(this.local.readmeUrl).then(content => content.value.toString());
async getReadme(preRelease: boolean, token: CancellationToken): Promise<string> {
const local = this.getLocal(preRelease);
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);
const gallery = await this.getGallery(preRelease, token);
if (gallery) {
if (gallery.assets.readme) {
return this.galleryService.getReadme(gallery, token);
}
this.telemetryService.publicLog('extensions:NotFoundReadMe', this.telemetryData);
}
@ -280,14 +303,16 @@ ${this.description}
return this.type === ExtensionType.System;
}
getChangelog(token: CancellationToken): Promise<string> {
if (this.local && this.local.changelogUrl && !this.outdated) {
return this.fileService.readFile(this.local.changelogUrl).then(content => content.value.toString());
async getChangelog(preRelease: boolean, token: CancellationToken): Promise<string> {
const local = this.getLocal(preRelease);
if (local?.changelogUrl) {
const content = await this.fileService.readFile(local.changelogUrl);
return content.value.toString();
}
if (this.gallery && this.gallery.assets.changelog) {
return this.galleryService.getChangelog(this.gallery, token);
const gallery = await this.getGallery(preRelease, token);
if (gallery?.assets.changelog) {
return this.galleryService.getChangelog(gallery, token);
}
if (this.type === ExtensionType.System) {
@ -401,9 +426,8 @@ class Extensions extends Disposable {
if (maliciousExtensionSet.has(extension.identifier.id)) {
extension.isMalicious = true;
}
// Loading the compatible version only there is an engine property
// Otherwise falling back to old way so that we will not make many roundtrips
const compatible = gallery.properties.engine ? await this.galleryService.getCompatibleExtension(gallery, await this.server.extensionManagementService.getTargetPlatform()) : gallery;
const compatible = await this.getCompatibleExtension(gallery, !!extension.local?.isPreReleaseVersion);
if (!compatible) {
return false;
}
@ -418,6 +442,17 @@ class Extensions extends Disposable {
return false;
}
private async getCompatibleExtension(extensionOrIdentifier: IGalleryExtension | IExtensionIdentifier, includePreRelease: boolean): Promise<IGalleryExtension | null> {
if (isIExtensionIdentifier(extensionOrIdentifier)) {
return this.galleryService.getCompatibleExtension(extensionOrIdentifier, includePreRelease, await this.server.extensionManagementService.getTargetPlatform());
}
const extension = extensionOrIdentifier;
if (includePreRelease && extension.hasPreReleaseVersion && !extension.properties.isPreReleaseVersion) {
return this.getCompatibleExtension(extension.identifier, includePreRelease);
}
return this.galleryService.getCompatibleExtension(extension, includePreRelease, await this.server.extensionManagementService.getTargetPlatform());
}
canInstall(galleryExtension: IGalleryExtension): Promise<boolean> {
return this.server.extensionManagementService.canInstall(galleryExtension);
}
@ -426,7 +461,7 @@ class Extensions extends Disposable {
if (!this.galleryService.isEnabled()) {
return;
}
const compatible = await this.galleryService.getCompatibleExtension(extension.identifier, await this.server.extensionManagementService.getTargetPlatform());
const compatible = await this.getCompatibleExtension(extension.identifier, !!extension.local?.isPreReleaseVersion);
if (compatible) {
extension.gallery = compatible;
this._onChange.fire({ extension });
@ -706,7 +741,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1;
const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2;
options.text = options.text ? this.resolveQueryText(options.text) : options.text;
const report = await this.extensionManagementService.getExtensionsReport();
const maliciousSet = getMaliciousExtensionsSet(report);
try {
@ -743,8 +778,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return text.substr(0, 350);
}
async open(extension: IExtension, options?: { sideByside?: boolean, preserveFocus?: boolean, pinned?: boolean, tab?: ExtensionEditorTab }): Promise<void> {
const editor = await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), { preserveFocus: options?.preserveFocus, pinned: options?.pinned }, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP);
async open(extension: IExtension, options?: IExtensionEditorOptions): Promise<void> {
const editor = await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), options, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP);
if (options?.tab && editor instanceof ExtensionEditor) {
await editor.openTab(options.tab);
}
@ -977,7 +1012,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
(this.getAutoUpdateValue() === true || (e.local && this.extensionEnablementService.isEnabled(e.local)))
);
return Promises.settled(toUpdate.map(e => this.install(e)));
return Promises.settled(toUpdate.map(e => this.install(e, e.local?.hadPreReleaseVersion ? { installPreReleaseVersion: true } : undefined)));
}
async canInstall(extension: IExtension): Promise<boolean> {

View file

@ -89,15 +89,23 @@
align-items: center;
}
.extension-list-item > .details > .header-container > .header > .activation-status:not(:empty),
.extension-list-item > .details > .header-container > .header > .install-count:not(:empty) {
font-size: 80%;
margin: 0 6px;
}
.extension-list-item > .details > .header-container > .header > .activation-status:not(:empty) {
font-size: 80%;
margin-left: 2px;
}
.extension-list-item > .details > .header-container > .header > .activation-status:not(:empty) .codicon {
margin-right: 2px;
}
.extension-list-item > .details > .header-container > .header .codicon {
font-size: 120%;
margin-right: 2px;
margin-right: 3px;
-webkit-mask: inherit;
}
@ -130,9 +138,12 @@
color: currentColor;
}
.extension-list-item > .details > .header-container > .header .pre-release {
display: flex;
}
.extension-list-item > .details > .header-container > .header .sync-ignored {
display: flex;
margin-left: 6px;
}
.extension-list-item > .details > .header-container > .header .sync-ignored > .codicon {

View file

@ -50,6 +50,7 @@
.monaco-action-bar .action-item.disabled .action-label.extension-action.undo-ignore,
.monaco-action-bar .action-item.disabled .action-label.extension-action.install:not(.installing),
.monaco-action-bar .action-item.disabled .action-label.extension-action.uninstall:not(.uninstalling),
.monaco-action-bar .action-item.disabled .action-label.extension-action.hide-when-disabled,
.monaco-action-bar .action-item.disabled .action-label.extension-action.update,
.monaco-action-bar .action-item.disabled .action-label.extension-action.theme,
.monaco-action-bar .action-item.disabled .action-label.extension-action.extension-sync,

View file

@ -82,6 +82,11 @@
white-space: nowrap;
}
.extension-editor > .header > .details > .title > .pre-release {
display: flex;
margin-left: 4px;
}
.extension-editor > .header > .details > .title > .builtin {
font-size: 10px;
font-style: italic;

View file

@ -37,14 +37,16 @@
width: 20px;
}
.extension-bookmark > .recommendation {
.extension-bookmark > .recommendation,
.extension-bookmark > .pre-release {
border-right: 20px solid transparent;
border-top: 20px solid;
box-sizing: border-box;
position: relative;
}
.extension-bookmark > .recommendation > .codicon {
.extension-bookmark > .recommendation > .codicon,
.extension-bookmark > .pre-release > .codicon {
position: absolute;
bottom: 9px;
left: 1px;

View file

@ -16,6 +16,7 @@ import { URI } from 'vs/base/common/uri';
import { IView, IViewPaneContainer } from 'vs/workbench/common/views';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput';
export const VIEWLET_ID = 'workbench.view.extensions';
@ -48,6 +49,7 @@ export interface IExtension {
readonly publisherDomain?: { link: string, verified: boolean };
readonly version: string;
readonly latestVersion: string;
readonly hasPreReleaseVersion: boolean;
readonly description: string;
readonly url?: string;
readonly repository?: string;
@ -65,11 +67,11 @@ export interface IExtension {
readonly extensionPack: string[];
readonly telemetryData: any;
readonly preview: boolean;
getManifest(token: CancellationToken): Promise<IExtensionManifest | null>;
getReadme(token: CancellationToken): Promise<string>;
getManifest(preRelease: boolean, token: CancellationToken): Promise<IExtensionManifest | null>;
hasReadme(): boolean;
getChangelog(token: CancellationToken): Promise<string>;
getReadme(preRelease: boolean, token: CancellationToken): Promise<string>;
hasChangelog(): boolean;
getChangelog(preRelease: boolean, token: CancellationToken): Promise<string>;
readonly server?: IExtensionManagementServer;
readonly local?: ILocalExtension;
gallery?: IGalleryExtension;
@ -96,7 +98,7 @@ export interface IExtensionsWorkbenchService {
installVersion(extension: IExtension, version: string): Promise<IExtension>;
reinstall(extension: IExtension): Promise<IExtension>;
setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise<void>;
open(extension: IExtension, options?: { sideByside?: boolean, preserveFocus?: boolean, pinned?: boolean, tab?: string }): Promise<void>;
open(extension: IExtension, options?: IExtensionEditorOptions): Promise<void>;
checkForUpdates(): Promise<void>;
getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined;

View file

@ -8,9 +8,16 @@ import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionEditorTab, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { join } from 'vs/base/common/path';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
export interface IExtensionEditorOptions extends IEditorOptions {
showPreReleaseVersion?: boolean;
tab?: ExtensionEditorTab;
sideByside?: boolean;
}
export class ExtensionsInput extends EditorInput {

View file

@ -219,6 +219,7 @@ suite('ExtensionRecommendationsService Test', () => {
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
async whenInstalledExtensionsRegistered() { return true; }

View file

@ -108,7 +108,8 @@ async function setupTest() {
local.publisherId = metadata.publisherId;
return local;
},
async canInstall() { return true; }
async canInstall() { return true; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
@ -153,14 +154,14 @@ suite('ExtensionsActions', () => {
teardown(() => disposables.dispose());
test('Install action is disabled when there is no extension', () => {
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
assert.ok(!testObject.enabled);
});
test('Test Install action when state is installed', () => {
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
@ -196,7 +197,7 @@ suite('ExtensionsActions', () => {
test('Test Install action when state is uninstalled', async () => {
const workbenchService = instantiationService.get(IExtensionsWorkbenchService);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const gallery = aGalleryExtension('a');
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
@ -209,7 +210,7 @@ suite('ExtensionsActions', () => {
});
test('Test Install action when extension is system action', () => {
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a', {}, { type: ExtensionType.System });
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
@ -224,7 +225,7 @@ suite('ExtensionsActions', () => {
});
test('Test Install action when extension doesnot has gallery', () => {
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction);
const testObject: ExtensionsActions.InstallAction = instantiationService.createInstance(ExtensionsActions.InstallAction, false);
instantiationService.createInstance(ExtensionContainers, [testObject]);
const local = aLocalExtension('a');
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
@ -386,7 +387,9 @@ suite('ExtensionsActions', () => {
return workbenchService.queryLocal()
.then(async extensions => {
testObject.extension = extensions[0];
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: local.identifier, version: '1.0.1' })));
const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.1' });
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery);
assert.ok(!testObject.enabled);
return new Promise<void>(c => {
testObject.onDidChange(() => {
@ -409,6 +412,7 @@ suite('ExtensionsActions', () => {
testObject.extension = extensions[0];
const gallery = aGalleryExtension('a', { identifier: local.identifier, version: '1.0.1' });
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery);
await instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None);
const promise = Event.toPromise(testObject.onDidChange);
installEvent.fire({ identifier: local.identifier, source: gallery });
@ -2408,7 +2412,8 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IE
local.publisherDisplayName = metadata.publisherDisplayName;
local.publisherId = metadata.publisherId;
return local;
}
},
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
};
}

View file

@ -97,6 +97,7 @@ suite('ExtensionsListView Tests', () => {
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
async getTargetPlatform() { return getTargetPlatform(platform, arch); }
});
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
instantiationService.stub(IContextKeyService, new MockContextKeyService());

View file

@ -101,7 +101,8 @@ suite('ExtensionsWorkbenchServiceTest', () => {
local.publisherId = metadata.publisherId;
return local;
},
async canInstall() { return true; }
async canInstall() { return true; },
getTargetPlatform: async () => getTargetPlatform(platform, arch)
});
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({
@ -306,6 +307,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
});
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local1, local2]);
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery1));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery1);
testObject = await aWorkbenchService();
await testObject.queryLocal();
@ -1511,7 +1513,8 @@ suite('ExtensionsWorkbenchServiceTest', () => {
local.publisherDisplayName = metadata.publisherDisplayName;
local.publisherId = metadata.publisherId;
return local;
}
},
getTargetPlatform: async () => getTargetPlatform(platform, arch)
};
}
});

View file

@ -10,13 +10,11 @@ import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionM
import { IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, Metadata, IUninstallExtensionTask, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService';
import { IProductService } from 'vs/platform/product/common/productService';
type Metadata = Partial<IGalleryMetadata & { isMachineScoped: boolean; }>;
export class WebExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService {
declare readonly _serviceBrand: undefined;
@ -68,8 +66,8 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe
return this.installExtension(manifest, location, options);
}
protected override async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean): Promise<IGalleryExtension | null> {
const compatibleExtension = await super.getCompatibleVersion(extension, fetchCompatibleVersion);
protected override async getCompatibleVersion(extension: IGalleryExtension, fetchCompatibleVersion: boolean, includePreRelease: boolean): Promise<IGalleryExtension | null> {
const compatibleExtension = await super.getCompatibleVersion(extension, fetchCompatibleVersion, includePreRelease);
if (compatibleExtension) {
return compatibleExtension;
}
@ -110,6 +108,9 @@ function toLocalExtension(extension: IScannedExtension): ILocalExtension {
isMachineScoped: !!metadata.isMachineScoped,
publisherId: metadata.publisherId || null,
publisherDisplayName: metadata.publisherDisplayName || null,
installedTimestamp: metadata.installedTimestamp,
isPreReleaseVersion: !!metadata.isPreReleaseVersion,
hadPreReleaseVersion: !!metadata.hadPreReleaseVersion
};
}
@ -149,6 +150,9 @@ class InstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implem
metadata.id = this.extension.identifier.uuid;
metadata.publisherDisplayName = this.extension.publisherDisplayName;
metadata.publisherId = this.extension.publisherId;
metadata.installedTimestamp = Date.now();
metadata.isPreReleaseVersion = this.extension.properties.isPreReleaseVersion;
metadata.hadPreReleaseVersion = this.extension.properties.isPreReleaseVersion || metadata.hadPreReleaseVersion;
}
const scannedExtension = URI.isUri(this.extension) ? await this.webExtensionsScannerService.addExtension(this.extension, metadata)

View file

@ -76,7 +76,7 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
private async downloadAndInstall(extension: IGalleryExtension, installOptions: InstallOptions): Promise<ILocalExtension> {
this.logService.info(`Downloading the '${extension.identifier.id}' extension locally and install`);
const compatible = await this.checkAndGetCompatible(extension);
const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion);
installOptions = { ...installOptions, donotIncludePackAndDependencies: true };
const installed = await this.getInstalled(ExtensionType.User);
const workspaceExtensions = await this.getAllWorkspaceDependenciesAndPackedExtensions(compatible, CancellationToken.None);
@ -90,7 +90,7 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
}
private async downloadCompatibleAndInstall(extension: IGalleryExtension, installed: ILocalExtension[], installOptions: InstallOptions): Promise<ILocalExtension> {
const compatible = await this.checkAndGetCompatible(extension);
const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion);
const location = joinPath(this.environmentService.tmpDir, generateUuid());
this.logService.info('Downloaded extension:', compatible.identifier.id, location.path);
await this.galleryService.download(compatible, location, installed.filter(i => areSameExtensions(i.identifier, compatible.identifier))[0] ? InstallOperation.Update : InstallOperation.Install);
@ -99,11 +99,16 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC
return local;
}
private async checkAndGetCompatible(extension: IGalleryExtension): Promise<IGalleryExtension> {
const compatible = await this.galleryService.getCompatibleExtension(extension, await this.getTargetPlatform());
if (!compatible) {
throw new ExtensionManagementError(localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of VS Code (version {1}).", extension.identifier.id, this.productService.version), ExtensionManagementErrorCode.Incompatible);
private async checkAndGetCompatible(extension: IGalleryExtension, includePreRelease: boolean): Promise<IGalleryExtension> {
const compatible = await this.galleryService.getCompatibleExtension(extension, includePreRelease, await this.getTargetPlatform());
if (compatible) {
if (includePreRelease && !compatible.properties.isPreReleaseVersion && extension.hasPreReleaseVersion) {
throw new ExtensionManagementError(localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
} else {
throw new ExtensionManagementError(localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible);
}
return compatible;
}