- implement profile CRUD operations

- enable profile actions in dev mode
This commit is contained in:
Sandeep Somavarapu
2022-06-15 09:54:31 +02:00
parent 4192006454
commit 917f6978e9
15 changed files with 359 additions and 157 deletions

View File

@@ -102,7 +102,6 @@ import { IExtensionsScannerService } from 'vs/platform/extensionManagement/commo
import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService';
import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
import { revive } from 'vs/base/common/marshalling';
import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc';
import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy';
import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender';
@@ -233,7 +232,7 @@ class SharedProcessMain extends Disposable {
fileService.registerProvider(Schemas.vscodeUserData, userDataFileSystemProvider);
// User Data Profiles
const userDataProfilesService = this._register(new UserDataProfilesService(revive(this.configuration.profiles.default), revive(this.configuration.profiles.current), environmentService, fileService, logService));
const userDataProfilesService = this._register(new UserDataProfilesService(this.configuration.defaultProfile, undefined, environmentService, fileService, logService));
services.set(IUserDataProfilesService, userDataProfilesService);
// Configuration

View File

@@ -68,6 +68,8 @@ import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/pol
import { NativePolicyService } from 'vs/platform/policy/node/nativePolicyService';
import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
/**
* The main VS Code entry point.
@@ -170,6 +172,10 @@ class CodeMain {
const diskFileSystemProvider = new DiskFileSystemProvider(logService);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
// URI Identity
const uriIdentityService = new UriIdentityService(fileService);
services.set(IUriIdentityService, uriIdentityService);
// Logger
services.set(ILoggerService, new LoggerService(logService, fileService));
@@ -178,7 +184,7 @@ class CodeMain {
services.set(IStateMainService, stateMainService);
// User Data Profiles
const userDataProfilesMainService = new UserDataProfilesMainService(stateMainService, environmentMainService, fileService, logService);
const userDataProfilesMainService = new UserDataProfilesMainService(stateMainService, uriIdentityService, environmentMainService, fileService, logService);
services.set(IUserDataProfilesService, userDataProfilesMainService);
// Policy
@@ -244,10 +250,7 @@ class CodeMain {
].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)),
// State service
stateMainService.init(),
// User Data Profiles Service
userDataProfilesMainService.init(),
stateMainService.init().then(() => userDataProfilesMainService.init()),
// Configuration service
configurationService.initialize()

View File

@@ -242,7 +242,7 @@ export class SharedProcess extends Disposable implements ISharedProcess {
appRoot: this.environmentMainService.appRoot,
codeCachePath: this.environmentMainService.codeCachePath,
backupWorkspacesPath: this.environmentMainService.backupWorkspacesPath,
profiles: this.userDataProfilesService.serialize(),
defaultProfile: this.userDataProfilesService.defaultProfile,
userEnv: this.userEnv,
args: this.environmentMainService.args,
logLevel: this.logService.getLevel(),

View File

@@ -7,8 +7,9 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { LogLevel } from 'vs/platform/log/common/log';
import { IUserDataProfilesDto } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy';
import { UriDto } from 'vs/base/common/types';
export interface ISharedProcess {
@@ -28,7 +29,7 @@ export interface ISharedProcessConfiguration extends ISandboxConfiguration {
readonly backupWorkspacesPath: string;
readonly profiles: IUserDataProfilesDto;
readonly defaultProfile: UriDto<IUserDataProfile>;
readonly policiesData?: IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }>;
}

View File

@@ -5,9 +5,10 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { UriDto } from 'vs/base/common/types';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
import { IUserDataProfileDto, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ISerializedSingleFolderWorkspaceIdentifier, ISerializedWorkspaceIdentifier, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
export type Key = string;
@@ -21,7 +22,7 @@ export interface IBaseSerializableStorageRequest {
* workspace is provided. Can be undefined to denote
* application scope.
*/
readonly profile: IUserDataProfileDto | undefined;
readonly profile: UriDto<IUserDataProfile> | undefined;
/**
* Workspace to correlate storage. Can be undefined to
@@ -46,7 +47,7 @@ abstract class BaseStorageDatabaseClient extends Disposable implements IStorageD
constructor(
protected channel: IChannel,
protected profile: IUserDataProfileDto | undefined,
protected profile: UriDto<IUserDataProfile> | undefined,
protected workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier | undefined
) {
super();
@@ -81,7 +82,7 @@ abstract class BaseProfileAwareStorageDatabaseClient extends BaseStorageDatabase
private readonly _onDidChangeItemsExternal = this._register(new Emitter<IStorageItemsChangeEvent>());
readonly onDidChangeItemsExternal = this._onDidChangeItemsExternal.event;
constructor(channel: IChannel, profile: IUserDataProfileDto | undefined) {
constructor(channel: IChannel, profile: UriDto<IUserDataProfile> | undefined) {
super(channel, profile, undefined);
this.registerListeners();
@@ -119,7 +120,7 @@ class ApplicationStorageDatabaseClient extends BaseProfileAwareStorageDatabaseCl
class GlobalStorageDatabaseClient extends BaseProfileAwareStorageDatabaseClient {
constructor(channel: IChannel, profile: IUserDataProfileDto) {
constructor(channel: IChannel, profile: UriDto<IUserDataProfile>) {
super(channel, profile);
}

View File

@@ -3,17 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { coalesce } from 'vs/base/common/arrays';
import { Emitter, Event } from 'vs/base/common/event';
import { hash } from 'vs/base/common/hash';
import { Disposable } from 'vs/base/common/lifecycle';
import { joinPath } from 'vs/base/common/resources';
import { UriDto } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files';
import { IFileService } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
export type ProfileOptions = {
settings?: boolean;
keybindings?: boolean;
tasks?: boolean;
snippets?: boolean;
extensions?: boolean;
uiState?: boolean;
};
export const DefaultOptions: ProfileOptions = {
settings: true,
keybindings: true,
tasks: true,
snippets: true,
extensions: true,
uiState: true
};
export interface IUserDataProfile {
readonly id: string;
@@ -28,30 +46,23 @@ export interface IUserDataProfile {
readonly extensionsResource: URI | undefined;
}
export type IUserDataProfileDto = UriDto<IUserDataProfile>;
export type IUserDataProfilesDto = {
readonly current: IUserDataProfileDto;
readonly default: IUserDataProfileDto;
};
export const IUserDataProfilesService = createDecorator<IUserDataProfilesService>('IUserDataProfilesService');
export interface IUserDataProfilesService {
readonly _serviceBrand: undefined;
readonly profilesHome: URI;
readonly defaultProfile: IUserDataProfile;
readonly onDidChangeCurrentProfile: Event<IUserDataProfile>;
readonly currentProfile: IUserDataProfile;
createProfile(name: string): IUserDataProfile;
setProfile(name: string): Promise<void>;
newProfile(name: string, options?: ProfileOptions): IUserDataProfile;
createProfile(profile: IUserDataProfile, options: ProfileOptions, workspaceIdentifier?: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile>;
setProfileForWorkspace(profile: IUserDataProfile, workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile>;
getProfile(workspace: URI): IUserDataProfile;
getAllProfiles(): Promise<IUserDataProfile[]>;
serialize(): IUserDataProfilesDto;
removeProfile(profile: IUserDataProfile): Promise<void>;
}
function reviveProfile(profile: IUserDataProfile, scheme: string): IUserDataProfile {
export function reviveProfile(profile: UriDto<IUserDataProfile>, scheme: string): IUserDataProfile {
return {
id: profile.id,
isDefault: profile.isDefault,
@@ -69,74 +80,49 @@ function reviveProfile(profile: IUserDataProfile, scheme: string): IUserDataProf
export class UserDataProfilesService extends Disposable implements IUserDataProfilesService {
readonly _serviceBrand: undefined;
protected static DEFAULT_PROFILE_NAME = 'default';
readonly profilesHome: URI;
protected _currentProfile: IUserDataProfile;
get currentProfile(): IUserDataProfile { return this._currentProfile; }
readonly profilesHome: URI;
protected _defaultProfile: IUserDataProfile;
get defaultProfile(): IUserDataProfile { return this._defaultProfile; }
private readonly _onDidChangeCurrentProfile = this._register(new Emitter<IUserDataProfile>());
readonly onDidChangeCurrentProfile = this._onDidChangeCurrentProfile.event;
constructor(
defaultProfile: IUserDataProfile | undefined,
currentProfile: IUserDataProfile | undefined,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
defaultProfile: UriDto<IUserDataProfile> | undefined,
currentProfile: UriDto<IUserDataProfile> | undefined,
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
@IFileService protected readonly fileService: IFileService,
@ILogService protected readonly logService: ILogService
) {
super();
this.profilesHome = joinPath(this.environmentService.userRoamingDataHome, 'profiles');
this._defaultProfile = defaultProfile ? reviveProfile(defaultProfile, this.profilesHome.scheme) : this.createProfile(undefined);
this._defaultProfile = defaultProfile ? reviveProfile(defaultProfile, this.profilesHome.scheme) : this.toUserDataProfile(localize('defaultProfile', "Default"), environmentService.userRoamingDataHome, { ...DefaultOptions, extensions: false }, true);
this._currentProfile = currentProfile ? reviveProfile(currentProfile, this.profilesHome.scheme) : this._defaultProfile;
}
createProfile(name: string | undefined): IUserDataProfile {
const isDefault = !name || name === UserDataProfilesService.DEFAULT_PROFILE_NAME;
const location = name && name !== UserDataProfilesService.DEFAULT_PROFILE_NAME ? joinPath(this.profilesHome, name) : this.environmentService.userRoamingDataHome;
newProfile(name: string, options: ProfileOptions = DefaultOptions): IUserDataProfile {
return this.toUserDataProfile(name, joinPath(this.profilesHome, hash(name).toString(16)), options, this.defaultProfile);
}
protected toUserDataProfile(name: string, location: URI, options: ProfileOptions, defaultProfile: true | IUserDataProfile): IUserDataProfile {
return {
id: hash(location.toString()).toString(16),
isDefault,
name: name ?? UserDataProfilesService.DEFAULT_PROFILE_NAME,
location,
globalStorageHome: joinPath(location, 'globalStorage'),
settingsResource: joinPath(location, 'settings.json'),
keybindingsResource: joinPath(location, 'keybindings.json'),
tasksResource: joinPath(location, 'tasks.json'),
snippetsHome: joinPath(location, 'snippets'),
extensionsResource: name ? joinPath(location, 'extensions.json') : undefined
name: name,
location: location,
isDefault: defaultProfile === true,
globalStorageHome: defaultProfile === true || options.uiState ? joinPath(location, 'globalStorage') : defaultProfile.globalStorageHome,
settingsResource: defaultProfile === true || options.settings ? joinPath(location, 'settings.json') : defaultProfile.settingsResource,
keybindingsResource: defaultProfile === true || options.keybindings ? joinPath(location, 'keybindings.json') : defaultProfile.keybindingsResource,
tasksResource: defaultProfile === true || options.tasks ? joinPath(location, 'tasks.json') : defaultProfile.tasksResource,
snippetsHome: defaultProfile === true || options.snippets ? joinPath(location, 'snippets') : defaultProfile.snippetsHome,
extensionsResource: defaultProfile === true && !options.extensions ? undefined : joinPath(location, 'extensions.json'),
};
}
async getAllProfiles(): Promise<IUserDataProfile[]> {
try {
const stat = await this.fileService.resolve(this.profilesHome);
const profiles = coalesce(stat.children?.map(stat => stat.isDirectory ? this.createProfile(stat.name) : undefined) ?? []);
if (profiles.length) {
profiles.unshift(this._defaultProfile);
}
return profiles;
} catch (error) {
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
this.logService.error('Error while getting all profiles', error);
}
}
return [];
}
protected createCurrentProfile(profile: string | undefined): IUserDataProfile {
return profile === UserDataProfilesService.DEFAULT_PROFILE_NAME ? this._defaultProfile : this.createProfile(profile);
}
setProfile(name: string): Promise<void> { throw new Error('Not implemented'); }
serialize(): IUserDataProfilesDto {
return {
default: this.defaultProfile,
current: this.currentProfile
};
}
getAllProfiles(): Promise<IUserDataProfile[]> { throw new Error('Not implemented'); }
createProfile(profile: IUserDataProfile, options: ProfileOptions, workspaceIdentifier?: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile> { throw new Error('Not implemented'); }
setProfileForWorkspace(profile: IUserDataProfile, workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile> { throw new Error('Not implemented'); }
getProfile(workspace: URI): IUserDataProfile { throw new Error('Not implemented'); }
removeProfile(profile: IUserDataProfile): Promise<void> { throw new Error('Not implemented'); }
}

View File

@@ -3,18 +3,42 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ResourceMap } from 'vs/base/common/map';
import { revive } from 'vs/base/common/marshalling';
import { UriDto } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IStateMainService } from 'vs/platform/state/electron-main/state';
import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { ProfileOptions, DefaultOptions, IUserDataProfile, IUserDataProfilesService, UserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
type UserDataProfiles = {
profiles: IUserDataProfile[];
workspaces: ResourceMap<IUserDataProfile>;
};
type StoredUserDataProfile = {
name: string;
location: URI;
options: ProfileOptions;
};
type StoredWorkspaceInfo = {
workspace: URI;
profile: URI;
};
export class UserDataProfilesMainService extends UserDataProfilesService implements IUserDataProfilesService {
private static CURRENT_PROFILE_KEY = 'currentUserDataProfile';
private static readonly PROFILES_KEY = 'userDataProfiles';
private static readonly WORKSPACE_PROFILE_INFO_KEY = 'workspaceAndProfileInfo';
constructor(
@IStateMainService private readonly stateMainService: IStateMainService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@@ -22,23 +46,90 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme
super(undefined, undefined, environmentService, fileService, logService);
}
async init(): Promise<void> {
const profileName = this.stateMainService.getItem<string>(UserDataProfilesMainService.CURRENT_PROFILE_KEY);
if (profileName) {
const profiles = await this.getAllProfiles();
const profile = profiles.find(p => p.name === profileName);
if (profile || (profileName === UserDataProfilesMainService.DEFAULT_PROFILE_NAME && profiles.length > 1)) {
this._defaultProfile = this.createProfile(UserDataProfilesService.DEFAULT_PROFILE_NAME);
this._currentProfile = profileName === UserDataProfilesMainService.DEFAULT_PROFILE_NAME ? this._defaultProfile : profile ?? this._defaultProfile;
} else {
this.stateMainService?.removeItem(UserDataProfilesMainService.CURRENT_PROFILE_KEY);
}
init(): void {
if (this.storedProfiles.length) {
this._defaultProfile = this.toUserDataProfile(this.defaultProfile.name, this.defaultProfile.location, DefaultOptions, true);
}
}
override async setProfile(name: string): Promise<void> {
this.stateMainService?.setItem(UserDataProfilesMainService.CURRENT_PROFILE_KEY, name);
private _profiles: UserDataProfiles | undefined;
private get profiles(): UserDataProfiles {
if (!this._profiles) {
const profiles = this.storedProfiles.map(storedProfile => this.toUserDataProfile(storedProfile.name, storedProfile.location, storedProfile.options, this.defaultProfile));
profiles.unshift(this.defaultProfile);
const workspaces = this.storedWorskpaceInfos.reduce((workspaces, workspaceProfileInfo) => {
const profile = profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, workspaceProfileInfo.profile));
if (profile) {
workspaces.set(workspaceProfileInfo.workspace, profile);
}
return workspaces;
}, new ResourceMap<IUserDataProfile>());
this._profiles = { profiles: profiles, workspaces: workspaces };
}
return this._profiles;
}
override async getAllProfiles(): Promise<IUserDataProfile[]> {
return this.profiles.profiles;
}
override getProfile(workspace: URI): IUserDataProfile {
return this.profiles.workspaces.get(workspace) ?? this.defaultProfile;
}
override async createProfile(profile: IUserDataProfile, options: ProfileOptions, workspaceIdentifier?: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile> {
profile = reviveProfile(profile, this.profilesHome.scheme);
if (this.storedProfiles.some(p => p.name === profile.name)) {
throw new Error(`Profile with name ${profile.name} already exists`);
}
const storedProfile: StoredUserDataProfile = { name: profile.name, location: profile.location, options };
const storedProfiles = [...this.storedProfiles, storedProfile];
this.storedProfiles = storedProfiles;
if (workspaceIdentifier) {
await this.setProfileForWorkspace(profile, workspaceIdentifier);
}
return this.profiles.profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, profile.location))!;
}
override async setProfileForWorkspace(profile: IUserDataProfile, workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile> {
profile = reviveProfile(profile, this.profilesHome.scheme);
const workspace = isSingleFolderWorkspaceIdentifier(workspaceIdentifier) ? workspaceIdentifier.uri : workspaceIdentifier.configPath;
const storedWorkspaceInfos = this.storedWorskpaceInfos.filter(info => !this.uriIdentityService.extUri.isEqual(info.workspace, workspace));
if (!profile.isDefault) {
storedWorkspaceInfos.push({ workspace, profile: profile.location });
}
this.storedWorskpaceInfos = storedWorkspaceInfos;
return this.profiles.profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, profile.location))!;
}
override async removeProfile(profile: IUserDataProfile): Promise<void> {
if (profile.isDefault) {
throw new Error('Cannot remove default profile');
}
profile = reviveProfile(profile, this.profilesHome.scheme);
if (!this.storedProfiles.some(p => this.uriIdentityService.extUri.isEqual(p.location, profile.location))) {
throw new Error(`Profile with name ${profile.name} does not exist`);
}
this.storedWorskpaceInfos = this.storedWorskpaceInfos.filter(p => !this.uriIdentityService.extUri.isEqual(p.profile, profile.location));
this.storedProfiles = this.storedProfiles.filter(p => !this.uriIdentityService.extUri.isEqual(p.location, profile.location));
}
private get storedProfiles(): StoredUserDataProfile[] {
return revive(this.stateMainService.getItem<UriDto<StoredUserDataProfile>[]>(UserDataProfilesMainService.PROFILES_KEY, []));
}
private set storedProfiles(storedProfiles: StoredUserDataProfile[]) {
this.stateMainService.setItem(UserDataProfilesMainService.PROFILES_KEY, storedProfiles);
this._profiles = undefined;
}
private get storedWorskpaceInfos(): StoredWorkspaceInfo[] {
return revive(this.stateMainService.getItem<UriDto<StoredWorkspaceInfo>[]>(UserDataProfilesMainService.WORKSPACE_PROFILE_INFO_KEY, []));
}
private set storedWorskpaceInfos(storedWorkspaceInfos: StoredWorkspaceInfo[]) {
this.stateMainService.setItem(UserDataProfilesMainService.WORKSPACE_PROFILE_INFO_KEY, storedWorkspaceInfos);
this._profiles = undefined;
}
}

View File

@@ -3,11 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriDto } from 'vs/base/common/types';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IUserDataProfile, IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ProfileOptions, IUserDataProfile, IUserDataProfilesService, reviveProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
export class UserDataProfilesNativeService extends UserDataProfilesService implements IUserDataProfilesService {
@@ -22,8 +24,23 @@ export class UserDataProfilesNativeService extends UserDataProfilesService imple
super(defaultProfile, currentProfile, environmentService, fileService, logService);
}
override setProfile(name: string): Promise<void> {
return this.channel.call('setProfile', [name]);
override async createProfile(profile: IUserDataProfile, options: ProfileOptions, workspaceIdentifier?: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile> {
const result = await this.channel.call<UriDto<IUserDataProfile>>('createProfile', [profile, options, workspaceIdentifier]);
return reviveProfile(result, this.profilesHome.scheme);
}
override async setProfileForWorkspace(profile: IUserDataProfile, workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): Promise<IUserDataProfile> {
const result = await this.channel.call<UriDto<IUserDataProfile>>('setProfileForWorkspace', [profile, workspaceIdentifier]);
return reviveProfile(result, this.profilesHome.scheme);
}
override async getAllProfiles(): Promise<IUserDataProfile[]> {
const result = await this.channel.call<UriDto<IUserDataProfile>[]>('getAllProfiles');
return result.map(profile => reviveProfile(profile, this.profilesHome.scheme));
}
override removeProfile(profile: IUserDataProfile): Promise<void> {
return this.channel.call('removeProfile', [profile]);
}
}

View File

@@ -6,6 +6,7 @@
import { IStringDictionary } from 'vs/base/common/collections';
import { PerformanceMark } from 'vs/base/common/performance';
import { isLinux, isMacintosh, isNative, isWeb, isWindows } from 'vs/base/common/platform';
import { UriDto } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -16,7 +17,7 @@ import { FileType } from 'vs/platform/files/common/files';
import { LogLevel } from 'vs/platform/log/common/log';
import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy';
import { IPartsSplash } from 'vs/platform/theme/common/themeService';
import { IUserDataProfilesDto } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
export const WindowMinimumSize = {
@@ -283,7 +284,10 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native
execPath: string;
backupPath?: string;
profiles: IUserDataProfilesDto;
profiles: {
default: UriDto<IUserDataProfile>;
current: UriDto<IUserDataProfile>;
};
homeDir: string;
tmpDir: string;

View File

@@ -40,7 +40,7 @@ import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electro
import { IWindowState, ICodeWindow, ILoadEvent, WindowMode, WindowError, LoadReason, defaultWindowState } from 'vs/platform/window/electron-main/window';
import { Color } from 'vs/base/common/color';
import { IPolicyService } from 'vs/platform/policy/common/policy';
import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { revive } from 'vs/base/common/marshalling';
export interface IWindowCreationOptions {
@@ -156,6 +156,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
@ILogService private readonly logService: ILogService,
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@IPolicyService private readonly policyService: IPolicyService,
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
@IFileService private readonly fileService: IFileService,
@IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService,
@IStorageMainService private readonly storageMainService: IStorageMainService,
@@ -882,6 +883,10 @@ export class CodeWindow extends Disposable implements ICodeWindow {
configuration.isInitialStartup = false; // since this is a reload
configuration.policiesData = this.policyService.serialize(); // set policies data again
configuration.profiles = {
default: this.userDataProfilesService.defaultProfile,
current: configuration.workspace ? this.userDataProfilesService.getProfile(isWorkspaceIdentifier(configuration.workspace) ? configuration.workspace.configPath : configuration.workspace.uri) : this.userDataProfilesService.defaultProfile,
};
// Load config
this.load(configuration, { isReload: true, disableExtensions: cli?.['disable-extensions'] });

View File

@@ -1300,7 +1300,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// loading the window.
backupPath: options.emptyWindowBackupInfo ? join(this.environmentMainService.backupHome, options.emptyWindowBackupInfo.backupFolder) : undefined,
profiles: this.userDataProfilesService.serialize(),
profiles: {
default: this.userDataProfilesService.defaultProfile,
current: options.workspace ? this.userDataProfilesService.getProfile(isWorkspaceIdentifier(options.workspace) ? options.workspace.configPath : options.workspace.uri) : this.userDataProfilesService.defaultProfile,
},
homeDir: this.environmentMainService.userHome.fsPath,
tmpDir: this.environmentMainService.tmpDir.fsPath,

View File

@@ -3,28 +3,33 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
import { PROFILES_CATEGORY } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import '../common/profileActions';
// import '../common/userDataProfileActions';
import '../common/userDataProfileActions';
class UserDataProfileStatusBarEntryContribution implements IWorkbenchContribution {
class UserDataProfileStatusBarEntryContribution extends Disposable implements IWorkbenchContribution {
constructor(
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
@IStatusbarService private readonly statusBarService: IStatusbarService
@IStatusbarService private readonly statusBarService: IStatusbarService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
) {
super();
this.updateStatus();
this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => this.updateStatus()));
}
private async updateStatus(): Promise<void> {
const profiles = await this.userDataProfilesService.getAllProfiles();
if (profiles.length) {
if (profiles.length > 1 && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY) {
this.statusBarService.addEntry({
name: this.userDataProfilesService.currentProfile.name!,
command: 'workbench.profiles.actions.switchProfile',

View File

@@ -17,6 +17,11 @@ import { asJson, asText, IRequestService } from 'vs/platform/request/common/requ
import { IUserDataProfileTemplate, isProfile, IUserDataProfileManagementService, IUserDataProfileWorkbenchService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
registerAction2(class SaveProfileAsAction extends Action2 {
constructor() {
@@ -27,7 +32,8 @@ registerAction2(class SaveProfileAsAction extends Action2 {
original: 'Save Settings Profile As...'
},
category: PROFILES_CATEGORY,
f1: true
f1: true,
precondition: ContextKeyExpr.and(IsDevelopmentContext, WorkbenchStateContext.notEqualsTo('empty')),
});
}
@@ -39,7 +45,7 @@ registerAction2(class SaveProfileAsAction extends Action2 {
title: localize('save profile as', "Save Settings Profile As..."),
});
if (name) {
await userDataProfileManagementService.createAndEnterProfile(name);
await userDataProfileManagementService.createAndEnterProfile(name, undefined, true);
}
}
});
@@ -53,7 +59,8 @@ registerAction2(class SwitchProfileAction extends Action2 {
original: 'Switch Settings Profile'
},
category: PROFILES_CATEGORY,
f1: true
f1: true,
precondition: ContextKeyExpr.and(IsDevelopmentContext, WorkbenchStateContext.notEqualsTo('empty')),
});
}
@@ -85,7 +92,8 @@ registerAction2(class RemoveProfileAction extends Action2 {
original: 'Remove Settings Profile'
},
category: PROFILES_CATEGORY,
f1: true
f1: true,
precondition: ContextKeyExpr.and(IsDevelopmentContext, WorkbenchStateContext.notEqualsTo('empty')),
});
}
@@ -104,16 +112,70 @@ registerAction2(class RemoveProfileAction extends Action2 {
}
});
registerAction2(class CleanupProfilesAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.cleanupProfiles',
title: {
value: localize('cleanup profile', "Cleanup Profiles"),
original: 'Cleanup Profiles'
},
category: CATEGORIES.Developer,
f1: true,
precondition: IsDevelopmentContext,
});
}
async run(accessor: ServicesAccessor) {
const userDataProfilesService = accessor.get(IUserDataProfilesService);
const fileService = accessor.get(IFileService);
const uriIdentityService = accessor.get(IUriIdentityService);
const allProfiles = await userDataProfilesService.getAllProfiles();
const stat = await fileService.resolve(userDataProfilesService.profilesHome);
await Promise.all((stat.children || [])?.filter(child => child.isDirectory && allProfiles.every(p => !uriIdentityService.extUri.isEqual(p.location, child.resource)))
.map(child => fileService.del(child.resource, { recursive: true })));
}
});
registerAction2(class CreateAndEnterEmptyProfileAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.createAndEnterEmptyProfile',
title: {
value: localize('create and enter empty profile', "Create and Enter Empty Profile..."),
original: 'Create and Enter Empty Profile...'
},
category: PROFILES_CATEGORY,
f1: true,
precondition: ContextKeyExpr.and(IsDevelopmentContext, WorkbenchStateContext.notEqualsTo('empty')),
});
}
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 and Enter Empty Profile..."),
});
if (name) {
await userDataProfileManagementService.createAndEnterProfile(name);
}
}
});
registerAction2(class ExportProfileAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.exportProfile',
id: 'workbench.profiles.actions.exportProfile2',
title: {
value: localize('export profile', "Export Settings Profile as a Template..."),
original: 'Export Settings as a Profile as a Template...'
value: localize('export profile', "Export Settings as a Profile (2)..."),
original: 'Export Settings as a Profile as a Profile (2)...'
},
category: PROFILES_CATEGORY,
f1: true
f1: true,
precondition: ContextKeyExpr.and(IsDevelopmentContext, WorkbenchStateContext.notEqualsTo('empty')),
});
}
@@ -140,16 +202,17 @@ registerAction2(class ExportProfileAction extends Action2 {
}
});
registerAction2(class CreateProfileFromTemplateAction extends Action2 {
registerAction2(class ImportProfileAction extends Action2 {
constructor() {
super({
id: 'workbench.profiles.actions.createProfileFromTemplate',
id: 'workbench.profiles.actions.importProfile2',
title: {
value: localize('create profile from template', "Create Settings Profile from Template..."),
original: 'Create Settings Profile from Template...'
value: localize('import profile', "Import Settings from a Profile (2)..."),
original: 'Import Settings from a Profile (2)...'
},
category: PROFILES_CATEGORY,
f1: true
f1: true,
precondition: ContextKeyExpr.and(IsDevelopmentContext, WorkbenchStateContext.notEqualsTo('empty')),
});
}

View File

@@ -17,6 +17,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceContextService, IWorkspaceIdentifier, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IExtensionManagementServerService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { CreationOptions, IUserDataProfileManagementService, IUserDataProfileTemplate, PROFILES_CATEGORY } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
@@ -42,38 +43,48 @@ export class UserDataProfileManagementService extends Disposable implements IUse
@IHostService private readonly hostService: IHostService,
@IDialogService private readonly dialogService: IDialogService,
@IProgressService private readonly progressService: IProgressService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@ILogService logService: ILogService
) {
super();
}
async createAndEnterProfile(name: string, options: CreationOptions = DefaultOptions): Promise<void> {
async createAndEnterProfile(name: string, options: CreationOptions = DefaultOptions, fromExisting?: boolean): Promise<void> {
const workspaceIdentifier = this.getWorkspaceIdentifier();
if (!workspaceIdentifier) {
throw new Error(localize('cannotCreateProfileInEmptyWorkbench', "Cannot create a profile in an empty workspace"));
}
const promises: Promise<any>[] = [];
const newProfile = this.userDataProfilesService.createProfile(name);
const newProfile = this.userDataProfilesService.newProfile(name);
await this.fileService.createFolder(newProfile.location);
if (options?.uiState) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.globalStorageHome, newProfile.globalStorageHome));
}
if (options?.settings) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.settingsResource, newProfile.settingsResource));
}
if (options?.extensions && newProfile.extensionsResource) {
promises.push((async () => {
const extensionsProfileResource = this.userDataProfilesService.currentProfile.extensionsResource ?? await this.createDefaultExtensionsProfile(joinPath(this.userDataProfilesService.defaultProfile.location, basename(newProfile.extensionsResource!)));
this.fileService.copy(extensionsProfileResource, newProfile.extensionsResource!);
})());
}
if (options?.keybindings) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.keybindingsResource, newProfile.keybindingsResource));
}
if (options?.tasks) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.tasksResource, newProfile.tasksResource));
}
if (options?.snippets) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.snippetsHome, newProfile.snippetsHome));
if (fromExisting) {
if (options?.uiState) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.globalStorageHome, newProfile.globalStorageHome));
}
if (options?.settings) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.settingsResource, newProfile.settingsResource));
}
if (options?.extensions && newProfile.extensionsResource) {
promises.push((async () => {
const extensionsProfileResource = this.userDataProfilesService.currentProfile.extensionsResource ?? await this.createDefaultExtensionsProfile(joinPath(this.userDataProfilesService.defaultProfile.location, basename(newProfile.extensionsResource!)));
this.fileService.copy(extensionsProfileResource, newProfile.extensionsResource!);
})());
}
if (options?.keybindings) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.keybindingsResource, newProfile.keybindingsResource));
}
if (options?.tasks) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.tasksResource, newProfile.tasksResource));
}
if (options?.snippets) {
promises.push(this.fileService.copy(this.userDataProfilesService.currentProfile.snippetsHome, newProfile.snippetsHome));
}
} else {
promises.push(this.fileService.createFolder(newProfile.globalStorageHome));
}
await Promise.allSettled(promises);
await this.doSwitchProfile(name);
await this.userDataProfilesService.createProfile(newProfile, options, workspaceIdentifier);
await this.enterProfile();
}
async removeProfile(name: string): Promise<void> {
@@ -88,6 +99,7 @@ export class UserDataProfileManagementService extends Disposable implements IUse
if (!profile) {
throw new Error(`Profile ${name} does not exist`);
}
await this.userDataProfilesService.removeProfile(profile);
if (profiles.length === 2) {
await this.fileService.del(this.userDataProfilesService.profilesHome, { recursive: true });
} else {
@@ -96,21 +108,30 @@ export class UserDataProfileManagementService extends Disposable implements IUse
}
async switchProfile(name: string): Promise<void> {
const workspaceIdentifier = this.getWorkspaceIdentifier();
if (!workspaceIdentifier) {
throw new Error(localize('cannotSwitchProfileInEmptyWorkbench', "Cannot switch a profile in an empty workspace"));
}
const profiles = await this.userDataProfilesService.getAllProfiles();
const profile = profiles.find(p => p.name === name);
if (!profile) {
throw new Error(`Profile ${name} does not exist`);
}
await this.doSwitchProfile(name);
await this.userDataProfilesService.setProfileForWorkspace(profile, workspaceIdentifier);
await this.enterProfile();
}
async createAndEnterProfileFromTemplate(name: string, template: IUserDataProfileTemplate, options: CreationOptions = DefaultOptions): Promise<void> {
const workspaceIdentifier = this.getWorkspaceIdentifier();
if (!workspaceIdentifier) {
throw new Error(localize('cannotCreateProfileInEmptyWorkbench', "Cannot create a profile in an empty workspace"));
}
await this.progressService.withProgress({
location: ProgressLocation.Notification,
title: localize('profiles.creating', "{0}: Creating...", PROFILES_CATEGORY),
}, async progress => {
const promises: Promise<any>[] = [];
const newProfile = this.userDataProfilesService.createProfile(name);
const newProfile = this.userDataProfilesService.newProfile(name);
await this.fileService.createFolder(newProfile.location);
if (template.globalState) {
// todo: create global state
@@ -122,26 +143,30 @@ export class UserDataProfileManagementService extends Disposable implements IUse
promises.push(this.fileService.writeFile(newProfile.extensionsResource, VSBuffer.fromString(template.extensions)));
}
await Promise.allSettled(promises);
await this.userDataProfilesService.createProfile(newProfile, options, workspaceIdentifier);
});
await this.doSwitchProfile(name);
await this.enterProfile();
}
async reset(): Promise<void> {
if (this.userDataProfilesService.currentProfile.name !== this.userDataProfilesService.defaultProfile.name) {
throw new Error('Please switch to default profile to reset');
private getWorkspaceIdentifier(): ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier | undefined {
const workspace = this.workspaceContextService.getWorkspace();
switch (this.workspaceContextService.getWorkbenchState()) {
case WorkbenchState.FOLDER:
return { uri: workspace.folders[0].uri, id: workspace.id };
case WorkbenchState.WORKSPACE:
return { configPath: workspace.configuration!, id: workspace.id };
}
await this.fileService.del(this.userDataProfilesService.profilesHome);
return undefined;
}
private async doSwitchProfile(name: string): Promise<void> {
await this.userDataProfilesService.setProfile(name);
private async enterProfile(): Promise<void> {
const result = await this.dialogService.confirm({
type: 'info',
message: localize('restart message', "Switching a profile requires restarting VS Code."),
primaryButton: localize('restart button', "&&Restart"),
message: localize('reload message', "Switching a profile requires reloading VS Code."),
primaryButton: localize('reload button', "&&Reload"),
});
if (result.confirmed) {
await this.hostService.restart();
await this.hostService.reload();
}
}

View File

@@ -20,12 +20,11 @@ export const IUserDataProfileManagementService = createDecorator<IUserDataProfil
export interface IUserDataProfileManagementService {
readonly _serviceBrand: undefined;
createAndEnterProfile(name: string, options?: CreationOptions): Promise<void>;
createAndEnterProfile(name: string, options?: CreationOptions, fromExisting?: boolean): Promise<void>;
createAndEnterProfileFromTemplate(name: string, template: IUserDataProfileTemplate, options?: CreationOptions): Promise<void>;
removeProfile(name: string): Promise<void>;
switchProfile(name: string): Promise<void>;
reset(): Promise<void>;
}
export interface IUserDataProfileTemplate {