Enhance settings profiles management (#155966)

* fix #154178

* - Support renaming profile
- Refactor profile actions

* fix compilation

* fix label
This commit is contained in:
Sandeep Somavarapu 2022-07-22 15:42:52 +02:00 committed by GitHub
parent 6567f3da69
commit d902fec1d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 228 additions and 77 deletions

View File

@ -59,7 +59,7 @@ class ProfileExtensionsCleaner extends Disposable {
this.onDidChangeProfiles({ added: this.userDataProfilesService.profiles, removed: [], all: this.userDataProfilesService.profiles });
}
private async onDidChangeProfiles({ added, removed, all }: DidChangeProfilesEvent): Promise<void> {
private async onDidChangeProfiles({ added, removed, all }: Omit<DidChangeProfilesEvent, 'updated'>): Promise<void> {
try {
await Promise.all(removed.map(profile => profile.extensionsResource ? this.removeExtensionsFromProfile(profile.extensionsResource) : Promise.resolve()));
} catch (error) {

View File

@ -31,7 +31,12 @@ export class BrowserUserDataProfilesService extends UserDataProfilesService impl
this._register(this.changesBroadcastChannel.onDidReceiveData(changes => {
try {
this._profilesObject = undefined;
this._onDidChangeProfiles.fire({ added: changes.added.map(p => reviveProfile(p, this.profilesHome.scheme)), removed: changes.removed.map(p => reviveProfile(p, this.profilesHome.scheme)), all: this.profiles });
this._onDidChangeProfiles.fire({
added: changes.added.map(p => reviveProfile(p, this.profilesHome.scheme)),
removed: changes.removed.map(p => reviveProfile(p, this.profilesHome.scheme)),
updated: changes.updated.map(p => reviveProfile(p, this.profilesHome.scheme)),
all: this.profiles
});
} catch (error) {/* ignore */ }
}));
}
@ -54,9 +59,9 @@ export class BrowserUserDataProfilesService extends UserDataProfilesService impl
return [];
}
protected override triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[]) {
super.triggerProfilesChanges(added, removed);
this.changesBroadcastChannel.postData({ added, removed });
protected override triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]) {
super.triggerProfilesChanges(added, removed, updated);
this.changesBroadcastChannel.postData({ added, removed, updated });
}
protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void {

View File

@ -19,6 +19,7 @@ import { ResourceMap } from 'vs/base/common/map';
import { IStringDictionary } from 'vs/base/common/collections';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { Promises } from 'vs/base/common/async';
import { generateUuid } from 'vs/base/common/uuid';
/**
* Flags to indicate whether to use the default profile or not.
@ -68,7 +69,7 @@ export const PROFILES_ENABLEMENT_CONFIG = 'workbench.experimental.settingsProfil
export type EmptyWindowWorkspaceIdentifier = 'empty-window';
export type WorkspaceIdentifier = ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier | EmptyWindowWorkspaceIdentifier;
export type DidChangeProfilesEvent = { readonly added: IUserDataProfile[]; readonly removed: IUserDataProfile[]; readonly all: IUserDataProfile[] };
export type DidChangeProfilesEvent = { readonly added: IUserDataProfile[]; readonly removed: IUserDataProfile[]; readonly updated: IUserDataProfile[]; readonly all: IUserDataProfile[] };
export type WillCreateProfileEvent = {
profile: IUserDataProfile;
@ -91,6 +92,7 @@ export interface IUserDataProfilesService {
readonly profiles: IUserDataProfile[];
createProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile>;
updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile>;
setProfileForWorkspace(profile: IUserDataProfile, workspaceIdentifier: WorkspaceIdentifier): Promise<void>;
getProfile(workspaceIdentifier: WorkspaceIdentifier): IUserDataProfile;
removeProfile(profile: IUserDataProfile): Promise<void>;
@ -240,7 +242,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf
throw new Error(`Profile with name ${name} already exists`);
}
const profile = toUserDataProfile(name, joinPath(this.profilesHome, hash(name).toString(16)), useDefaultFlags);
const profile = toUserDataProfile(name, joinPath(this.profilesHome, hash(generateUuid()).toString(16)), useDefaultFlags);
await this.fileService.createFolder(profile.location);
const joiners: Promise<void>[] = [];
@ -252,7 +254,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf
});
await Promises.settled(joiners);
this.updateProfiles([profile], []);
this.updateProfiles([profile], [], []);
if (workspaceIdentifier) {
await this.setProfileForWorkspace(profile, workspaceIdentifier);
@ -261,6 +263,22 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf
return profile;
}
async updateProfile(profileToUpdate: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile> {
if (!this.enabled) {
throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`);
}
let profile = this.profiles.find(p => p.id === profileToUpdate.id);
if (!profile) {
throw new Error(`Profile '${profileToUpdate.name}' does not exist`);
}
profile = toUserDataProfile(name, profile.location, useDefaultFlags);
this.updateProfiles([], [], [profile]);
return profile;
}
async setProfileForWorkspace(profileToSet: IUserDataProfile, workspaceIdentifier: WorkspaceIdentifier): Promise<void> {
if (!this.enabled) {
throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`);
@ -312,7 +330,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf
}
this.updateStoredProfileAssociations();
this.updateProfiles([], [profile]);
this.updateProfiles([], [profile], []);
try {
if (this.profiles.length === 1) {
@ -325,24 +343,25 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf
}
}
private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[]) {
private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]) {
const storedProfiles: StoredUserDataProfile[] = [];
for (const profile of [...this.profilesObject.profiles, ...added]) {
for (let profile of [...this.profilesObject.profiles, ...added]) {
if (profile.isDefault) {
continue;
}
if (removed.some(p => profile.id === p.id)) {
continue;
}
profile = updated.find(p => profile.id === p.id) ?? profile;
storedProfiles.push({ location: profile.location, name: profile.name, useDefaultFlags: profile.useDefaultFlags });
}
this.saveStoredProfiles(storedProfiles);
this._profilesObject = undefined;
this.triggerProfilesChanges(added, removed);
this.triggerProfilesChanges(added, removed, updated);
}
protected triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[]) {
this._onDidChangeProfiles.fire({ added, removed, all: this.profiles });
protected triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]) {
this._onDidChangeProfiles.fire({ added, removed, updated, all: this.profiles });
}
private updateWorkspaceAssociation(workspaceIdentifier: WorkspaceIdentifier, newProfile?: IUserDataProfile) {

View File

@ -41,8 +41,9 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa
this._register(this.channel.listen<DidChangeProfilesEvent>('onDidChangeProfiles')(e => {
const added = e.added.map(profile => reviveProfile(profile, this.profilesHome.scheme));
const removed = e.removed.map(profile => reviveProfile(profile, this.profilesHome.scheme));
const updated = e.updated.map(profile => reviveProfile(profile, this.profilesHome.scheme));
this._profiles = e.all.map(profile => reviveProfile(profile, this.profilesHome.scheme));
this._onDidChangeProfiles.fire({ added, removed, all: this.profiles });
this._onDidChangeProfiles.fire({ added, removed, updated, all: this.profiles });
}));
}
@ -59,6 +60,11 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa
return this.channel.call('removeProfile', [profile]);
}
async updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags): Promise<IUserDataProfile> {
const result = await this.channel.call<UriDto<IUserDataProfile>>('updateProfile', [profile, name, useDefaultFlags]);
return reviveProfile(result, this.profilesHome.scheme);
}
getProfile(workspaceIdentifier: WorkspaceIdentifier): IUserDataProfile { throw new Error('Not implemented'); }
}

View File

@ -52,7 +52,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements
this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1)));
this.updateStatus();
this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfilesService.onDidChangeProfiles)(() => this.updateStatus()));
this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfileService.onDidUpdateCurrentProfile, this.userDataProfilesService.onDidChangeProfiles)(() => this.updateStatus()));
this.registerActions();
}

View File

@ -20,14 +20,85 @@ import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userData
import { CATEGORIES } from 'vs/workbench/common/actions';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ICommandService } from 'vs/platform/commands/common/commands';
registerAction2(class CreateFromCurrentProfileAction extends Action2 {
class CreateFromCurrentProfileAction extends Action2 {
static readonly ID = 'workbench.profiles.actions.createFromCurrentProfile';
static readonly TITLE = {
value: localize('save profile as', "Create from Current Settings Profile..."),
original: 'Create from Current Profile...'
};
constructor() {
super({
id: 'workbench.profiles.actions.createFromCurrentProfile',
id: CreateFromCurrentProfileAction.ID,
title: CreateFromCurrentProfileAction.TITLE,
category: PROFILES_CATEGORY,
f1: true,
precondition: PROFILES_ENABLEMENT_CONTEXT
});
}
async run(accessor: ServicesAccessor) {
const quickInputService = accessor.get(IQuickInputService);
const notificationService = accessor.get(INotificationService);
const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService);
const name = await quickInputService.input({
placeHolder: localize('name', "Profile name"),
title: localize('save profile as', "Create from Current Settings Profile..."),
});
if (name) {
try {
await userDataProfileManagementService.createAndEnterProfile(name, undefined, true);
} catch (error) {
notificationService.error(error);
}
}
}
}
registerAction2(CreateFromCurrentProfileAction);
class CreateEmptyProfileAction extends Action2 {
static readonly ID = 'workbench.profiles.actions.createEmptyProfile';
static readonly TITLE = {
value: localize('create empty profile', "Create an Empty Settings Profile..."),
original: 'Create an Empty Settings Profile...'
};
constructor() {
super({
id: CreateEmptyProfileAction.ID,
title: CreateEmptyProfileAction.TITLE,
category: PROFILES_CATEGORY,
f1: true,
precondition: PROFILES_ENABLEMENT_CONTEXT
});
}
async run(accessor: ServicesAccessor) {
const quickInputService = accessor.get(IQuickInputService);
const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService);
const notificationService = accessor.get(INotificationService);
const name = await quickInputService.input({
placeHolder: localize('name', "Profile name"),
title: localize('create and enter empty profile', "Create an Empty Profile..."),
});
if (name) {
try {
await userDataProfileManagementService.createAndEnterProfile(name);
} catch (error) {
notificationService.error(error);
}
}
}
}
registerAction2(CreateEmptyProfileAction);
registerAction2(class CreateProfileAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.createProfile',
title: {
value: localize('save profile as', "Create from Current Settings Profile..."),
original: 'Create from Current Profile...'
value: localize('create profile', "Create..."),
original: 'Create...'
},
category: PROFILES_CATEGORY,
f1: true,
@ -35,7 +106,7 @@ registerAction2(class CreateFromCurrentProfileAction extends Action2 {
menu: [
{
id: ManageProfilesSubMenu,
group: '1_create_profiles',
group: '2_manage_profiles',
when: PROFILES_ENABLEMENT_CONTEXT,
order: 1
}
@ -45,59 +116,28 @@ registerAction2(class CreateFromCurrentProfileAction extends Action2 {
async run(accessor: ServicesAccessor) {
const quickInputService = accessor.get(IQuickInputService);
const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService);
const name = await quickInputService.input({
placeHolder: localize('name', "Profile name"),
title: localize('save profile as', "Create from Current Settings Profile..."),
});
if (name) {
await userDataProfileManagementService.createAndEnterProfile(name, undefined, true);
const commandService = accessor.get(ICommandService);
const pick = await quickInputService.pick(
[{
id: CreateFromCurrentProfileAction.ID,
label: CreateFromCurrentProfileAction.TITLE.value,
}, {
id: CreateEmptyProfileAction.ID,
label: CreateEmptyProfileAction.TITLE.value,
}], { canPickMany: false, title: localize('create settings profile', "{0}: Create...", PROFILES_CATEGORY) });
if (pick) {
return commandService.executeCommand(pick.id);
}
}
});
registerAction2(class CreateEmptyProfileAction extends Action2 {
registerAction2(class RenameProfileAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.createProfile',
id: 'workbench.profiles.actions.renameProfile',
title: {
value: localize('create profile', "Create an Empty Settings Profile..."),
original: 'Create an Empty Profile...'
},
category: PROFILES_CATEGORY,
f1: true,
precondition: PROFILES_ENABLEMENT_CONTEXT,
menu: [
{
id: ManageProfilesSubMenu,
group: '1_create_profiles',
when: PROFILES_ENABLEMENT_CONTEXT,
order: 2
}
]
});
}
async run(accessor: ServicesAccessor) {
const quickInputService = accessor.get(IQuickInputService);
const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService);
const name = await quickInputService.input({
placeHolder: localize('name', "Profile name"),
title: localize('create and enter empty profile', "Create an Empty Profile..."),
});
if (name) {
await userDataProfileManagementService.createAndEnterProfile(name);
}
}
});
registerAction2(class RemoveProfileAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.removeProfile',
title: {
value: localize('remove profile', "Remove Settings Profile..."),
original: 'Remove Profile...'
value: localize('rename profile', "Rename..."),
original: 'Rename...'
},
category: PROFILES_CATEGORY,
f1: true,
@ -106,7 +146,65 @@ registerAction2(class RemoveProfileAction extends Action2 {
{
id: ManageProfilesSubMenu,
group: '2_manage_profiles',
when: PROFILES_ENABLEMENT_CONTEXT
when: PROFILES_ENABLEMENT_CONTEXT,
order: 1
}
]
});
}
async run(accessor: ServicesAccessor) {
const quickInputService = accessor.get(IQuickInputService);
const userDataProfileService = accessor.get(IUserDataProfileService);
const userDataProfilesService = accessor.get(IUserDataProfilesService);
const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService);
const notificationService = accessor.get(INotificationService);
const profiles = userDataProfilesService.profiles.filter(p => !p.isDefault);
if (profiles.length) {
const pick = await quickInputService.pick(
profiles.map(profile => ({
label: profile.name,
description: profile.id === userDataProfileService.currentProfile.id ? localize('current', "Current") : undefined,
profile
})),
{
placeHolder: localize('pick profile to rename', "Select Settings Profile to Rename"),
});
if (pick) {
const name = await quickInputService.input({
value: pick.profile.name,
title: localize('edit settings profile', "Rename Settings Profile..."),
});
if (name && name !== pick.profile.name) {
try {
await userDataProfileManagementService.renameProfile(pick.profile, name);
} catch (error) {
notificationService.error(error);
}
}
}
}
}
});
registerAction2(class DeleteProfileAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.deleteProfile',
title: {
value: localize('delete profile', "Delete..."),
original: 'Delete...'
},
category: PROFILES_CATEGORY,
f1: true,
precondition: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT),
menu: [
{
id: ManageProfilesSubMenu,
group: '2_manage_profiles',
when: PROFILES_ENABLEMENT_CONTEXT,
order: 2
}
]
});
@ -128,7 +226,7 @@ registerAction2(class RemoveProfileAction extends Action2 {
profile
})),
{
placeHolder: localize('pick profile', "Select Settings Profile"),
placeHolder: localize('pick profile to delete', "Select Settings Profiles to Delete"),
canPickMany: true
});
if (picks) {
@ -147,12 +245,12 @@ registerAction2(class SwitchProfileAction extends Action2 {
super({
id: 'workbench.profiles.actions.switchProfile',
title: {
value: localize('switch profile', "Switch Settings Profile..."),
original: 'Switch Settings Profile...'
value: localize('switch profile', "Switch..."),
original: 'Switch...'
},
category: PROFILES_CATEGORY,
f1: true,
precondition: PROFILES_ENABLEMENT_CONTEXT,
precondition: ContextKeyExpr.and(PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT),
});
}
@ -207,8 +305,8 @@ registerAction2(class ExportProfileAction extends Action2 {
super({
id: 'workbench.profiles.actions.exportProfile',
title: {
value: localize('export profile', "Export Settings Profile..."),
original: 'Export Settings Profile...'
value: localize('export profile', "Export..."),
original: 'Export...'
},
category: PROFILES_CATEGORY,
menu: [
@ -252,8 +350,8 @@ registerAction2(class ImportProfileAction extends Action2 {
super({
id: 'workbench.profiles.actions.importProfile',
title: {
value: localize('import profile', "Import Settings Profile..."),
original: 'Import Settings Profile...'
value: localize('import profile', "Import..."),
original: 'Import...'
},
category: PROFILES_CATEGORY,
menu: [

View File

@ -43,6 +43,16 @@ export class UserDataProfileManagementService extends Disposable implements IUse
return profile;
}
async renameProfile(profile: IUserDataProfile, name: string): Promise<void> {
if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) {
throw new Error(`Settings profile ${profile.name} does not exist`);
}
if (profile.isDefault) {
throw new Error(localize('cannotRenameDefaultProfile', "Cannot rename the default settings profile"));
}
await this.userDataProfilesService.updateProfile(profile, name);
}
async removeProfile(profile: IUserDataProfile): Promise<void> {
if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) {
throw new Error(`Settings profile ${profile.name} does not exist`);

View File

@ -22,6 +22,7 @@ export interface DidChangeUserDataProfileEvent {
export const IUserDataProfileService = createDecorator<IUserDataProfileService>('IUserDataProfileService');
export interface IUserDataProfileService {
readonly _serviceBrand: undefined;
readonly onDidUpdateCurrentProfile: Event<void>;
readonly onDidChangeCurrentProfile: Event<DidChangeUserDataProfileEvent>;
readonly currentProfile: IUserDataProfile;
updateCurrentProfile(currentProfile: IUserDataProfile, preserveData: boolean): Promise<void>;
@ -33,6 +34,7 @@ export interface IUserDataProfileManagementService {
createAndEnterProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, fromExisting?: boolean): Promise<IUserDataProfile>;
removeProfile(profile: IUserDataProfile): Promise<void>;
renameProfile(profile: IUserDataProfile, name: string): Promise<void>;
switchProfile(profile: IUserDataProfile): Promise<void>;
}

View File

@ -16,6 +16,9 @@ export class UserDataProfileService extends Disposable implements IUserDataProfi
private readonly _onDidChangeCurrentProfile = this._register(new Emitter<DidChangeUserDataProfileEvent>());
readonly onDidChangeCurrentProfile = this._onDidChangeCurrentProfile.event;
private readonly _onDidUpdateCurrentProfile = this._register(new Emitter<void>());
readonly onDidUpdateCurrentProfile = this._onDidUpdateCurrentProfile.event;
private _currentProfile: IUserDataProfile;
get currentProfile(): IUserDataProfile { return this._currentProfile; }
@ -25,13 +28,20 @@ export class UserDataProfileService extends Disposable implements IUserDataProfi
) {
super();
this._currentProfile = currentProfile;
this._register(userDataProfilesService.onDidChangeProfiles(() => {
this._register(userDataProfilesService.onDidChangeProfiles(e => {
/**
* If the current profile is default profile, then reset it because,
* In Desktop the extensions resource will be set/unset in the default profile when profiles are changed.
*/
if (this._currentProfile.isDefault) {
this._currentProfile = userDataProfilesService.defaultProfile;
return;
}
const updatedCurrentProfile = e.updated.find(p => this._currentProfile.id === p.id);
if (updatedCurrentProfile) {
this._currentProfile = updatedCurrentProfile;
this._onDidUpdateCurrentProfile.fire();
}
}));
}

View File

@ -2009,6 +2009,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens
export class TestUserDataProfileService implements IUserDataProfileService {
readonly _serviceBrand: undefined;
readonly onDidUpdateCurrentProfile = Event.None;
readonly onDidChangeCurrentProfile = Event.None;
readonly currentProfile = toUserDataProfile('test', URI.file('tests').with({ scheme: 'vscode-tests' }));
async updateCurrentProfile(): Promise<void> { }