Skip to content

Commit 1b29130

Browse files
authored
Implement apply extension to all profiles (microsoft#188093)
* Revert "remove the extension action to apply in all profiles microsoft#157492 (microsoft#187815)" This reverts commit 429d6d2. * implement microsoft#157492
1 parent bafd442 commit 1b29130

File tree

14 files changed

+162
-28
lines changed

14 files changed

+162
-28
lines changed

src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, Target
2323
import { ILogService } from 'vs/platform/log/common/log';
2424
import { IProductService } from 'vs/platform/product/common/productService';
2525
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
26+
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
2627
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
2728

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

73+
protected readonly _onDidUpdateExtensionMetadata = this._register(new Emitter<ILocalExtension>());
74+
get onDidUpdateExtensionMetadata() { return this._onDidUpdateExtensionMetadata.event; }
75+
7276
private readonly participants: IExtensionManagementParticipant[] = [];
7377

7478
constructor(
7579
@IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService,
7680
@ITelemetryService protected readonly telemetryService: ITelemetryService,
81+
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
7782
@ILogService protected readonly logService: ILogService,
7883
@IProductService protected readonly productService: IProductService,
7984
@IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService,
@@ -147,6 +152,40 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
147152
return this.uninstallExtension(extension, options);
148153
}
149154

155+
async toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension> {
156+
if (isApplicationScopedExtension(extension.manifest)) {
157+
return extension;
158+
}
159+
160+
if (extension.isApplicationScoped) {
161+
let local = await this.updateMetadata(extension, { isApplicationScoped: false }, this.userDataProfilesService.defaultProfile.extensionsResource);
162+
if (!this.uriIdentityService.extUri.isEqual(fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) {
163+
local = await this.copyExtension(extension, this.userDataProfilesService.defaultProfile.extensionsResource, fromProfileLocation);
164+
}
165+
166+
for (const profile of this.userDataProfilesService.profiles) {
167+
const existing = (await this.getInstalled(ExtensionType.User, profile.extensionsResource))
168+
.find(e => areSameExtensions(e.identifier, extension.identifier));
169+
if (existing) {
170+
this._onDidUpdateExtensionMetadata.fire(existing);
171+
} else {
172+
this._onDidUninstallExtension.fire({ identifier: extension.identifier, profileLocation: profile.extensionsResource });
173+
}
174+
}
175+
return local;
176+
}
177+
178+
else {
179+
const local = this.uriIdentityService.extUri.isEqual(fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)
180+
? await this.updateMetadata(extension, { isApplicationScoped: true }, this.userDataProfilesService.defaultProfile.extensionsResource)
181+
: await this.copyExtension(extension, fromProfileLocation, this.userDataProfilesService.defaultProfile.extensionsResource, { isApplicationScoped: true });
182+
183+
this._onDidInstallExtensions.fire([{ identifier: local.identifier, operation: InstallOperation.Install, local, profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource, applicationScoped: true }]);
184+
return local;
185+
}
186+
187+
}
188+
150189
getExtensionsControlManifest(): Promise<IExtensionsControlManifest> {
151190
const now = new Date().getTime();
152191

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

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

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

716755
export function joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error {

src/vs/platform/extensionManagement/common/extensionManagement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ export interface IExtensionManagementService {
478478
installFromLocation(location: URI, profileLocation: URI): Promise<ILocalExtension>;
479479
installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise<ILocalExtension[]>;
480480
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;
481+
toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension>;
481482
reinstallFromGallery(extension: ILocalExtension): Promise<ILocalExtension>;
482483
getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]>;
483484
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;

src/vs/platform/extensionManagement/common/extensionManagementIpc.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ export class ExtensionManagementChannel implements IServerChannel {
142142
const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer));
143143
return extensions.map(e => transformOutgoingExtension(e, uriTransformer));
144144
}
145+
case 'toggleAppliationScope': {
146+
const extension = await this.service.toggleAppliationScope(transformIncomingExtension(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer));
147+
return transformOutgoingExtension(extension, uriTransformer);
148+
}
145149
case 'copyExtensions': {
146150
return this.service.copyExtensions(transformIncomingURI(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer));
147151
}
@@ -277,6 +281,11 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
277281
.then(extension => transformIncomingExtension(extension, null));
278282
}
279283

284+
toggleAppliationScope(local: ILocalExtension, fromProfileLocation: URI): Promise<ILocalExtension> {
285+
return this.channel.call<ILocalExtension>('toggleAppliationScope', [local, fromProfileLocation])
286+
.then(extension => transformIncomingExtension(extension, null));
287+
}
288+
280289
copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
281290
return this.channel.call<void>('copyExtensions', [fromProfileLocation, toProfileLocation]);
282291
}

src/vs/platform/extensionManagement/node/extensionManagementService.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
Metadata, InstallVSIXOptions
3333
} from 'vs/platform/extensionManagement/common/extensionManagement';
3434
import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
35-
import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
35+
import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
3636
import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService';
3737
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
3838
import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle';
@@ -71,9 +71,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
7171
private readonly manifestCache: ExtensionsManifestCache;
7272
private readonly extensionsDownloader: ExtensionsDownloader;
7373

74-
private readonly _onDidUpdateExtensionMetadata = this._register(new Emitter<ILocalExtension>());
75-
override readonly onDidUpdateExtensionMetadata = this._onDidUpdateExtensionMetadata.event;
76-
7774
private readonly installGalleryExtensionsTasks = new Map<string, InstallGalleryExtensionTask>();
7875

7976
constructor(
@@ -87,10 +84,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
8784
@IInstantiationService instantiationService: IInstantiationService,
8885
@IFileService private readonly fileService: IFileService,
8986
@IProductService productService: IProductService,
90-
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
87+
@IUriIdentityService uriIdentityService: IUriIdentityService,
9188
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
9289
) {
93-
super(galleryService, telemetryService, logService, productService, userDataProfilesService);
90+
super(galleryService, telemetryService, uriIdentityService, logService, productService, userDataProfilesService);
9491
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
9592
this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension)));
9693
this.manifestCache = this._register(new ExtensionsManifestCache(userDataProfilesService, fileService, uriIdentityService, this, this.logService));
@@ -227,6 +224,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
227224
return this.installFromGallery(galleryExtension);
228225
}
229226

227+
protected copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
228+
return this.extensionsScanner.copyExtension(extension, fromProfileLocation, toProfileLocation, metadata);
229+
}
230+
230231
copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
231232
return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation);
232233
}
@@ -531,20 +532,25 @@ export class ExtensionsScanner extends Disposable {
531532

532533
async scanMetadata(local: ILocalExtension, profileLocation?: URI): Promise<Metadata | undefined> {
533534
if (profileLocation) {
534-
const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileLocation);
535-
return extensions.find(e => areSameExtensions(e.identifier, local.identifier))?.metadata;
535+
const extension = await this.getScannedExtension(local, profileLocation);
536+
return extension?.metadata;
536537
} else {
537538
return this.extensionsScannerService.scanMetadata(local.location);
538539
}
539540
}
540541

542+
private async getScannedExtension(local: ILocalExtension, profileLocation: URI): Promise<IScannedProfileExtension | undefined> {
543+
const extensions = await this.extensionsProfileScannerService.scanProfileExtensions(profileLocation);
544+
return extensions.find(e => areSameExtensions(e.identifier, local.identifier));
545+
}
546+
541547
async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>, profileLocation?: URI): Promise<ILocalExtension> {
542548
if (profileLocation) {
543549
await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation);
544550
} else {
545551
await this.extensionsScannerService.updateMetadata(local.location, metadata);
546552
}
547-
return this.scanLocalExtension(local.location, local.type);
553+
return this.scanLocalExtension(local.location, local.type, profileLocation);
548554
}
549555

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

582+
async copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata: Partial<Metadata>): Promise<ILocalExtension> {
583+
const source = await this.getScannedExtension(extension, fromProfileLocation);
584+
const target = await this.getScannedExtension(extension, toProfileLocation);
585+
metadata = { ...source?.metadata, ...metadata };
586+
587+
if (target) {
588+
await this.extensionsProfileScannerService.updateMetadata([[extension, { ...target.metadata, ...metadata }]], toProfileLocation);
589+
} else {
590+
await this.extensionsProfileScannerService.addExtensionsToProfile([[extension, metadata]], toProfileLocation);
591+
}
592+
593+
return this.scanLocalExtension(extension.location, extension.type, toProfileLocation);
594+
}
595+
576596
async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise<void> {
577597
const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation);
578598
const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions
@@ -633,10 +653,18 @@ export class ExtensionsScanner extends Disposable {
633653
}
634654
}
635655

636-
private async scanLocalExtension(location: URI, type: ExtensionType): Promise<ILocalExtension> {
637-
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true });
638-
if (scannedExtension) {
639-
return this.toLocalExtension(scannedExtension);
656+
private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise<ILocalExtension> {
657+
if (profileLocation) {
658+
const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation });
659+
const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location));
660+
if (scannedExtension) {
661+
return this.toLocalExtension(scannedExtension);
662+
}
663+
} else {
664+
const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true });
665+
if (scannedExtension) {
666+
return this.toLocalExtension(scannedExtension);
667+
}
640668
}
641669
throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path));
642670
}

src/vs/platform/userDataSync/common/extensionsMerge.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ function areSame(fromExtension: ISyncExtension, toExtension: ISyncExtension, che
278278
return false;
279279
}
280280

281+
if (fromExtension.isApplicationScoped !== toExtension.isApplicationScoped) {
282+
/* extension application scope has changed */
283+
return false;
284+
}
285+
281286
if (checkInstalledProperty && fromExtension.installed !== toExtension.installed) {
282287
/* extension installed property changed */
283288
return false;

src/vs/platform/userDataSync/common/extensionsSync.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagemen
1818
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';
1919
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
2020
import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage';
21-
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
21+
import { ExtensionType, IExtensionIdentifier, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions';
2222
import { IFileService } from 'vs/platform/files/common/files';
2323
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
2424
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
@@ -375,8 +375,11 @@ export class LocalExtensionsProvider {
375375
const disabledExtensions = extensionEnablementService.getDisabledExtensions();
376376
return installedExtensions
377377
.map(extension => {
378-
const { identifier, isBuiltin, manifest, preRelease, pinned } = extension;
378+
const { identifier, isBuiltin, manifest, preRelease, pinned, isApplicationScoped } = extension;
379379
const syncExntesion: ILocalSyncExtension = { identifier, preRelease, version: manifest.version, pinned: !!pinned };
380+
if (!isApplicationScopedExtension(manifest)) {
381+
syncExntesion.isApplicationScoped = isApplicationScoped;
382+
}
380383
if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) {
381384
syncExntesion.disabled = true;
382385
}
@@ -481,6 +484,7 @@ export class LocalExtensionsProvider {
481484
installGivenVersion: e.pinned && !!e.version,
482485
installPreReleaseVersion: e.preRelease,
483486
profileLocation: profile.extensionsResource,
487+
isApplicationScoped: e.isApplicationScoped,
484488
context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SYNC_CONTEXT]: true }
485489
}
486490
});

src/vs/platform/userDataSync/common/userDataSync.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ export interface ILocalSyncExtension {
337337
preRelease: boolean;
338338
disabled?: boolean;
339339
installed?: boolean;
340+
isApplicationScoped?: boolean;
340341
state?: IStringDictionary<any>;
341342
}
342343

@@ -347,6 +348,7 @@ export interface IRemoteSyncExtension {
347348
preRelease?: boolean;
348349
disabled?: boolean;
349350
installed?: boolean;
351+
isApplicationScoped?: boolean;
350352
state?: IStringDictionary<any>;
351353
}
352354

src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,24 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
14311431
}
14321432
});
14331433

1434+
this.registerExtensionAction({
1435+
id: 'workbench.extensions.action.toggleApplyToAllProfiles',
1436+
title: { value: localize('workbench.extensions.action.toggleApplyToAllProfiles', "Apply Extension to all Profiles"), original: `Apply Extension to all Profiles` },
1437+
toggled: ContextKeyExpr.has('isApplicationScopedExtension'),
1438+
menu: {
1439+
id: MenuId.ExtensionContext,
1440+
group: '2_configure',
1441+
when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate()),
1442+
order: 4
1443+
},
1444+
run: async (accessor: ServicesAccessor, id: string) => {
1445+
const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier));
1446+
if (extension) {
1447+
return this.extensionsWorkbenchService.toggleApplyExtensionToAllProfiles(extension);
1448+
}
1449+
}
1450+
});
1451+
14341452
this.registerExtensionAction({
14351453
id: 'workbench.extensions.action.ignoreRecommendation',
14361454
title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` },

0 commit comments

Comments
 (0)