Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement apply extension to all profiles #188093

Merged
merged 2 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, Target
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';

export type ExtensionVerificationStatus = boolean | string;
Expand Down Expand Up @@ -69,11 +70,15 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
protected _onDidUninstallExtension = this._register(new Emitter<DidUninstallExtensionEvent>());
get onDidUninstallExtension() { return this._onDidUninstallExtension.event; }

protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter<ILocalExtension>());
get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; }

private readonly participants: IExtensionManagementParticipant[] = [];

constructor(
@IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService,
@ITelemetryService protected readonly telemetryService: ITelemetryService,
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
@ILogService protected readonly logService: ILogService,
@IProductService protected readonly productService: IProductService,
@IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService,
Expand Down Expand Up @@ -147,6 +152,40 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
return this.uninstallExtension(extension, options);
}

async toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension> {
if (isApplicationScopedExtension(extension.manifest)) {
return extension;
}

if (extension.isApplicationScoped) {
let local = await this.updateMetadata(extension, { isApplicationScoped: false }, this.userDataProfilesService.defaultProfile.extensionsResource);
if (!this.uriIdentityService.extUri.isEqual(fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) {
local = await this.copyExtension(extension, this.userDataProfilesService.defaultProfile.extensionsResource, fromProfileLocation);
}

for (const profile of this.userDataProfilesService.profiles) {
const existing = (await this.getInstalled(ExtensionType.User, profile.extensionsResource))
.find(e => areSameExtensions(e.identifier, extension.identifier));
if (existing) {
this._onDidUpdateExtensionMetadata.fire(existing);
} else {
this._onDidUninstallExtension.fire({ identifier: extension.identifier, profileLocation: profile.extensionsResource });
}
}
return local;
}

else {
const local = this.uriIdentityService.extUri.isEqual(fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)
? await this.updateMetadata(extension, { isApplicationScoped: true }, this.userDataProfilesService.defaultProfile.extensionsResource)
: await this.copyExtension(extension, fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource, { isApplicationScoped: true });

this._onDidInstallExtensions.fire([{ identifier: local.identifier, operation: InstallOperation.Install, local, profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource, applicationScoped: true }]);
return local;
}

}

getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
const now = new Date().getTime();

Expand Down Expand Up @@ -705,12 +744,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
abstract reinstallFromGallery(extension: ILocalExtension): Promise<ILocalExtension>;
abstract cleanUp(): Promise<void>;

abstract onDidUpdateExtensionMetadata: Event<ILocalExtension>;
abstract updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation?: URI): Promise<ILocalExtension>;

protected abstract getCurrentExtensionsManifestLocation(): URI;
protected abstract createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask;
protected abstract createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask;
protected abstract copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial<Metadata>): Promise<ILocalExtension>;
}

export function joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ export interface IExtensionManagementService {
installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension>;
installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise<ILocalExtension[]>;
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;
toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension>;
reinstallFromGallery(extension: ILocalExtension): Promise<ILocalExtension>;
getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]>;
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ export class ExtensionManagementChannel implements IServerChannel {
const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer));
return extensions.map(e => transformOutgoingExtension(e, uriTransformer));
}
case 'toggleAppliationScope': {
const extension = await this.service.toggleAppliationScope(transformIncomingExtension(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer));
return transformOutgoingExtension(extension, uriTransformer);
}
case 'copyExtensions': {
return this.service.copyExtensions(transformIncomingURI(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer));
}
Expand Down Expand Up @@ -277,6 +281,11 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
.then(extension => transformIncomingExtension(extension, null));
}

toggleAppliationScope(local: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension> {
return this.channel.call<ILocalExtension>('toggleAppliationScope', [local, fromProfileLocation])
.then(extension => transformIncomingExtension(extension, null));
}

copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
return this.channel.call<void>('copyExtensions', [fromProfileLocation, toProfileLocation]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
Metadata, InstallVSIXOptions
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
Expand Down Expand Up @@ -71,9 +71,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
private readonly manifestCache: ExtensionsManifestCache;
private readonly extensionsDownloader: ExtensionsDownloader;

private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter<ILocalExtension>());
override readonly onDidUpdateExtensionMetadata = this._onDidUpdateExtensionMetadata.event;

private readonly installGalleryExtensionsTasks = new Map<string, InstallGalleryExtensionTask>();

constructor(
Expand All @@ -87,10 +84,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
@IInstantiationService instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService,
@IProductService productService: IProductService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
) {
super(galleryService, telemetryService, logService, productService, userDataProfilesService);
super(galleryService, telemetryService, uriIdentityService, logService, productService, userDataProfilesService);
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
this.manifestCache = this._register(new ExtensionsManifestCache(userDataProfilesService, fileService, uriIdentityService, this, this.logService));
Expand Down Expand Up @@ -227,6 +224,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
return this.installFromGallery(galleryExtension);
}

protected copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
return this.extensionsScanner.copyExtension(extension, fromProfileLocation, toProfileLocation, metadata);
}

copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation);
}
Expand Down Expand Up @@ -531,20 +532,25 @@ export class ExtensionsScanner extends Disposable {

async scanMetadata(local: ILocalExtension, profileLocation?: URI): Promise<Metadata | undefined> {
if (profileLocation) {
const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileLocation);
return extensions.find(e => areSameExtensions(e.identifier, local.identifier))?.metadata;
const extension = await this.getScannedExtension(local, profileLocation);
return extension?.metadata;
} else {
return this.extensionsScannerService.scanMetadata(local.location);
}
}

private async getScannedExtension(local: ILocalExtension, profileLocation: URI): Promise<IScannedProfileExtension | undefined> {
const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileLocation);
return extensions.find(e => areSameExtensions(e.identifier, local.identifier));
}

async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation?: URI): Promise<ILocalExtension> {
if (profileLocation) {
await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation);
} else {
await this.extensionsScannerService.updateMetadata(local.location, metadata);
}
return this.scanLocalExtension(local.location, local.type);
return this.scanLocalExtension(local.location, local.type, profileLocation);
}

getUninstalledExtensions(): Promise<IStringDictionary<boolean>> {
Expand Down Expand Up @@ -573,6 +579,20 @@ export class ExtensionsScanner extends Disposable {
await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]);
}

async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
const source = await this.getScannedExtension(extension, fromProfileLocation);
const target = await this.getScannedExtension(extension, toProfileLocation);
metadata = { ...source?.metadata, ...metadata };

if (target) {
await this.extensionsProfileScannerService.updateMetadata([[extension, { ...target.metadata, ...metadata }]], toProfileLocation);
} else {
await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, metadata]], toProfileLocation);
}

return this.scanLocalExtension(extension.location, extension.type, toProfileLocation);
}

async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation);
const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions
Expand Down Expand Up @@ -633,10 +653,18 @@ export class ExtensionsScanner extends Disposable {
}
}

private async scanLocalExtension(location: URI, type: ExtensionType): Promise<ILocalExtension> {
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true });
if (scannedExtension) {
return this.toLocalExtension(scannedExtension);
private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise<ILocalExtension> {
if (profileLocation) {
const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation });
const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location));
if (scannedExtension) {
return this.toLocalExtension(scannedExtension);
}
} else {
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true });
if (scannedExtension) {
return this.toLocalExtension(scannedExtension);
}
}
throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path));
}
Expand Down
5 changes: 5 additions & 0 deletions src/vs/platform/userDataSync/common/extensionsMerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ function areSame(fromExtension: ISyncExtension, toExtension: ISyncExtension, che
return false;
}

if (fromExtension.isApplicationScoped !== toExtension.isApplicationScoped) {
/* extension application scope has changed */
return false;
}

if (checkInstalledProperty && fromExtension.installed !== toExtension.installed) {
/* extension installed property changed */
return false;
Expand Down
8 changes: 6 additions & 2 deletions src/vs/platform/userDataSync/common/extensionsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagemen
import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ExtensionType, IExtensionIdentifier, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
Expand Down Expand Up @@ -375,8 +375,11 @@ export class LocalExtensionsProvider {
const disabledExtensions = extensionEnablementService.getDisabledExtensions();
return installedExtensions
.map(extension => {
const { identifier, isBuiltin, manifest, preRelease, pinned } = extension;
const { identifier, isBuiltin, manifest, preRelease, pinned, isApplicationScoped } = extension;
const syncExntesion: ILocalSyncExtension = { identifier, preRelease, version: manifest.version, pinned: !!pinned };
if (!isApplicationScopedExtension(manifest)) {
syncExntesion.isApplicationScoped = isApplicationScoped;
}
if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
syncExntesion.disabled = true;
}
Expand Down Expand Up @@ -481,6 +484,7 @@ export class LocalExtensionsProvider {
installGivenVersion: e.pinned && !!e.version,
installPreReleaseVersion: e.preRelease,
profileLocation: profile.extensionsResource,
isApplicationScoped: e.isApplicationScoped,
context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SYNC_CONTEXT]: true }
}
});
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/userDataSync/common/userDataSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export interface ILocalSyncExtension {
preRelease: boolean;
disabled?: boolean;
installed?: boolean;
isApplicationScoped?: boolean;
state?: IStringDictionary<any>;
}

Expand All @@ -347,6 +348,7 @@ export interface IRemoteSyncExtension {
preRelease?: boolean;
disabled?: boolean;
installed?: boolean;
isApplicationScoped?: boolean;
state?: IStringDictionary<any>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,24 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
}
});

this.registerExtensionAction({
id: 'workbench.extensions.action.toggleApplyToAllProfiles',
title: { value: localize('workbench.extensions.action.toggleApplyToAllProfiles', "Apply Extension to all Profiles"), original: `Apply Extension to all Profiles` },
toggled: ContextKeyExpr.has('isApplicationScopedExtension'),
menu: {
id: MenuId.ExtensionContext,
group: '2_configure',
when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate()),
order: 4
},
run: async (accessor: ServicesAccessor, id: string) => {
const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier));
if (extension) {
return this.extensionsWorkbenchService.toggleApplyExtensionToAllProfiles(extension);
}
}
});

this.registerExtensionAction({
id: 'workbench.extensions.action.ignoreRecommendation',
title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` },
Expand Down