Skip to content

Commit 3038b56

Browse files
authored
Merge pull request microsoft#154756 from microsoft/sandy081/profiles-enable-web
Enable profiles in Web
2 parents 5ccf7c4 + 11a70bd commit 3038b56

File tree

24 files changed

+403
-314
lines changed

24 files changed

+403
-314
lines changed

src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class SharedProcessMain extends Disposable {
232232
fileService.registerProvider(Schemas.vscodeUserData, userDataFileSystemProvider);
233233

234234
// User Data Profiles
235-
const userDataProfilesService = this._register(new UserDataProfilesNativeService(this.configuration.profiles, mainProcessService, environmentService, fileService, logService));
235+
const userDataProfilesService = this._register(new UserDataProfilesNativeService(this.configuration.profiles, mainProcessService, environmentService));
236236
services.set(IUserDataProfilesService, userDataProfilesService);
237237

238238
// Configuration

src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
1818
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
1919
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
2020
import { IProductService } from 'vs/platform/product/common/productService';
21+
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
2122
import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
2223

2324
let translations: Translations = Object.create(null);
@@ -72,7 +73,7 @@ suite('NativeExtensionsScanerService Test', () => {
7273
});
7374
instantiationService.stub(IProductService, { version: '1.66.0' });
7475
instantiationService.stub(IExtensionsProfileScannerService, new ExtensionsProfileScannerService(fileService, logService));
75-
instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, logService));
76+
instantiationService.stub(IUserDataProfilesService, new UserDataProfilesService(environmentService, fileService, new UriIdentityService(fileService), logService));
7677
await fileService.createFolder(systemExtensionsLocation);
7778
await fileService.createFolder(userExtensionsLocation);
7879
});

src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { AbstractNativeEnvironmentService } from 'vs/platform/environment/common
1919
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
2020
import product from 'vs/platform/product/common/product';
2121
import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
22+
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
2223

2324
const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' });
2425

@@ -52,7 +53,7 @@ suite('FileUserDataProvider', () => {
5253
await testObject.createFolder(backupWorkspaceHomeOnDisk);
5354

5455
environmentService = new TestEnvironmentService(userDataHomeOnDisk);
55-
userDataProfilesService = new UserDataProfilesService(environmentService, testObject, logService);
56+
userDataProfilesService = new UserDataProfilesService(environmentService, testObject, new UriIdentityService(testObject), logService);
5657

5758
fileUserDataProvider = new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService);
5859
disposables.add(fileUserDataProvider);

src/vs/platform/userDataProfile/browser/userDataProfile.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,64 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { revive } from 'vs/base/common/marshalling';
67
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
78
import { IFileService } from 'vs/platform/files/common/files';
89
import { ILogService } from 'vs/platform/log/common/log';
9-
import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
10+
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
11+
import { IUserDataProfilesService, PROFILES_ENABLEMENT_CONFIG, StoredProfileAssociations, StoredUserDataProfile, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
1012

1113
export class BrowserUserDataProfilesService extends UserDataProfilesService implements IUserDataProfilesService {
1214

15+
protected override readonly defaultProfileShouldIncludeExtensionsResourceAlways: boolean = true;
16+
1317
constructor(
1418
@IEnvironmentService environmentService: IEnvironmentService,
1519
@IFileService fileService: IFileService,
20+
@IUriIdentityService uriIdentityService: IUriIdentityService,
1621
@ILogService logService: ILogService,
1722
) {
18-
super(environmentService, fileService, logService);
19-
this._profiles = [this.createDefaultUserDataProfile(true)];
23+
super(environmentService, fileService, uriIdentityService, logService);
24+
super.setEnablement(window.localStorage.getItem(PROFILES_ENABLEMENT_CONFIG) === 'true');
25+
}
26+
27+
override setEnablement(enabled: boolean): void {
28+
super.setEnablement(enabled);
29+
window.localStorage.setItem(PROFILES_ENABLEMENT_CONFIG, enabled ? 'true' : 'false');
30+
}
31+
32+
protected override getStoredProfiles(): StoredUserDataProfile[] {
33+
try {
34+
const value = window.localStorage.getItem(UserDataProfilesService.PROFILES_KEY);
35+
if (value) {
36+
return revive(JSON.parse(value));
37+
}
38+
} catch (error) {
39+
/* ignore */
40+
this.logService.error(error);
41+
}
42+
return [];
43+
}
44+
45+
protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void {
46+
window.localStorage.setItem(UserDataProfilesService.PROFILES_KEY, JSON.stringify(storedProfiles));
47+
}
48+
49+
protected override getStoredProfileAssociations(): StoredProfileAssociations {
50+
try {
51+
const value = window.localStorage.getItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_KEY);
52+
if (value) {
53+
return revive(JSON.parse(value));
54+
}
55+
} catch (error) {
56+
/* ignore */
57+
this.logService.error(error);
58+
}
59+
return {};
60+
}
61+
62+
protected override saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void {
63+
window.localStorage.setItem(UserDataProfilesService.PROFILE_ASSOCIATIONS_KEY, JSON.stringify(storedProfileAssociations));
2064
}
2165

2266
}

src/vs/platform/userDataProfile/common/userDataProfile.ts

Lines changed: 239 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
1414
import { IFileService } from 'vs/platform/files/common/files';
1515
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1616
import { ILogService } from 'vs/platform/log/common/log';
17-
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
17+
import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
18+
import { ResourceMap } from 'vs/base/common/map';
19+
import { IStringDictionary } from 'vs/base/common/collections';
20+
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
21+
import { Promises } from 'vs/base/common/async';
1822

1923
/**
2024
* Flags to indicate whether to use the default profile or not.
@@ -66,6 +70,16 @@ export type WorkspaceIdentifier = ISingleFolderWorkspaceIdentifier | IWorkspaceI
6670

6771
export type DidChangeProfilesEvent = { readonly added: IUserDataProfile[]; readonly removed: IUserDataProfile[]; readonly all: IUserDataProfile[] };
6872

73+
export type WillCreateProfileEvent = {
74+
profile: IUserDataProfile;
75+
join(promise: Promise<void>): void;
76+
};
77+
78+
export type WillRemoveProfileEvent = {
79+
profile: IUserDataProfile;
80+
join(promise: Promise<void>): void;
81+
};
82+
6983
export const IUserDataProfilesService = createDecorator<IUserDataProfilesService>('IUserDataProfilesService');
7084
export interface IUserDataProfilesService {
7185
readonly _serviceBrand: undefined;
@@ -115,34 +129,249 @@ export function toUserDataProfile(name: string, location: URI, useDefaultFlags?:
115129
};
116130
}
117131

132+
export type UserDataProfilesObject = {
133+
profiles: IUserDataProfile[];
134+
workspaces: ResourceMap<IUserDataProfile>;
135+
emptyWindow?: IUserDataProfile;
136+
};
137+
138+
export type StoredUserDataProfile = {
139+
name: string;
140+
location: URI;
141+
useDefaultFlags?: UseDefaultProfileFlags;
142+
};
143+
144+
export type StoredProfileAssociations = {
145+
workspaces?: IStringDictionary<string>;
146+
emptyWindow?: string;
147+
};
148+
118149
export class UserDataProfilesService extends Disposable implements IUserDataProfilesService {
150+
151+
protected static readonly PROFILES_KEY = 'userDataProfiles';
152+
protected static readonly PROFILE_ASSOCIATIONS_KEY = 'profileAssociations';
153+
119154
readonly _serviceBrand: undefined;
120155

156+
private enabled: boolean = false;
157+
protected readonly defaultProfileShouldIncludeExtensionsResourceAlways: boolean = false;
121158
readonly profilesHome: URI;
122159

123160
get defaultProfile(): IUserDataProfile { return this.profiles[0]; }
124-
protected _profiles: IUserDataProfile[] = [this.createDefaultUserDataProfile(false)];
125-
get profiles(): IUserDataProfile[] { return this._profiles; }
161+
get profiles(): IUserDataProfile[] { return this.profilesObject.profiles; }
126162

127163
protected readonly _onDidChangeProfiles = this._register(new Emitter<DidChangeProfilesEvent>());
128164
readonly onDidChangeProfiles = this._onDidChangeProfiles.event;
129165

166+
protected readonly _onWillCreateProfile = this._register(new Emitter<WillCreateProfileEvent>());
167+
readonly onWillCreateProfile = this._onWillCreateProfile.event;
168+
169+
protected readonly _onWillRemoveProfile = this._register(new Emitter<WillRemoveProfileEvent>());
170+
readonly onWillRemoveProfile = this._onWillRemoveProfile.event;
171+
130172
constructor(
131173
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
132174
@IFileService protected readonly fileService: IFileService,
175+
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
133176
@ILogService protected readonly logService: ILogService
134177
) {
135178
super();
136179
this.profilesHome = joinPath(this.environmentService.userRoamingDataHome, 'profiles');
137180
}
138181

139-
protected createDefaultUserDataProfile(extensions: boolean): IUserDataProfile {
140-
const profile = toUserDataProfile(localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome);
141-
return { ...profile, isDefault: true, extensionsResource: extensions ? profile.extensionsResource : undefined };
182+
setEnablement(enabled: boolean): void {
183+
if (this.enabled !== enabled) {
184+
this._profilesObject = undefined;
185+
this.enabled = enabled;
186+
}
187+
}
188+
189+
protected _profilesObject: UserDataProfilesObject | undefined;
190+
protected get profilesObject(): UserDataProfilesObject {
191+
if (!this._profilesObject) {
192+
const profiles = this.enabled ? this.getStoredProfiles().map<IUserDataProfile>(storedProfile => toUserDataProfile(storedProfile.name, storedProfile.location, storedProfile.useDefaultFlags)) : [];
193+
let emptyWindow: IUserDataProfile | undefined;
194+
const workspaces = new ResourceMap<IUserDataProfile>();
195+
if (profiles.length) {
196+
const profileAssicaitions = this.getStoredProfileAssociations();
197+
if (profileAssicaitions.workspaces) {
198+
for (const [workspacePath, profilePath] of Object.entries(profileAssicaitions.workspaces)) {
199+
const workspace = URI.parse(workspacePath);
200+
const profileLocation = URI.parse(profilePath);
201+
const profile = profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, profileLocation));
202+
if (profile) {
203+
workspaces.set(workspace, profile);
204+
}
205+
}
206+
}
207+
if (profileAssicaitions.emptyWindow) {
208+
const emptyWindowProfileLocation = URI.parse(profileAssicaitions.emptyWindow);
209+
emptyWindow = profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, emptyWindowProfileLocation));
210+
}
211+
}
212+
const profile = toUserDataProfile(localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome);
213+
profiles.unshift({ ...profile, isDefault: true, extensionsResource: this.defaultProfileShouldIncludeExtensionsResourceAlways || profiles.length > 0 ? profile.extensionsResource : undefined });
214+
this._profilesObject = { profiles, workspaces, emptyWindow };
215+
}
216+
return this._profilesObject;
217+
}
218+
219+
getProfile(workspaceIdentifier: WorkspaceIdentifier): IUserDataProfile {
220+
const workspace = this.getWorkspace(workspaceIdentifier);
221+
const profile = URI.isUri(workspace) ? this.profilesObject.workspaces.get(workspace) : this.profilesObject.emptyWindow;
222+
return profile ?? this.defaultProfile;
223+
}
224+
225+
protected getWorkspace(workspaceIdentifier: WorkspaceIdentifier): URI | EmptyWindowWorkspaceIdentifier {
226+
if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
227+
return workspaceIdentifier.uri;
228+
}
229+
if (isWorkspaceIdentifier(workspaceIdentifier)) {
230+
return workspaceIdentifier.configPath;
231+
}
232+
return 'empty-window';
233+
}
234+
235+
async createProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile> {
236+
if (!this.enabled) {
237+
throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`);
238+
}
239+
if (this.getStoredProfiles().some(p => p.name === name)) {
240+
throw new Error(`Profile with name ${name} already exists`);
241+
}
242+
243+
const profile = toUserDataProfile(name, joinPath(this.profilesHome, hash(name).toString(16)), useDefaultFlags);
244+
await this.fileService.createFolder(profile.location);
245+
246+
const joiners: Promise<void>[] = [];
247+
this._onWillCreateProfile.fire({
248+
profile,
249+
join(promise) {
250+
joiners.push(promise);
251+
}
252+
});
253+
await Promises.settled(joiners);
254+
255+
this.updateProfiles([profile], []);
256+
257+
if (workspaceIdentifier) {
258+
await this.setProfileForWorkspace(profile, workspaceIdentifier);
259+
}
260+
261+
return profile;
262+
}
263+
264+
async setProfileForWorkspace(profileToSet: IUserDataProfile, workspaceIdentifier: WorkspaceIdentifier): Promise<void> {
265+
if (!this.enabled) {
266+
throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`);
267+
}
268+
269+
const profile = this.profiles.find(p => p.id === profileToSet.id);
270+
if (!profile) {
271+
throw new Error(`Profile '${profileToSet.name}' does not exist`);
272+
}
273+
274+
this.updateWorkspaceAssociation(workspaceIdentifier, profile);
275+
}
276+
277+
async unsetWorkspace(workspaceIdentifier: WorkspaceIdentifier): Promise<void> {
278+
if (!this.enabled) {
279+
throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`);
280+
}
281+
this.updateWorkspaceAssociation(workspaceIdentifier);
282+
}
283+
284+
async removeProfile(profileToRemove: IUserDataProfile): Promise<void> {
285+
if (!this.enabled) {
286+
throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`);
287+
}
288+
if (profileToRemove.isDefault) {
289+
throw new Error('Cannot remove default profile');
290+
}
291+
const profile = this.profiles.find(p => p.id === profileToRemove.id);
292+
if (!profile) {
293+
throw new Error(`Profile '${profileToRemove.name}' does not exist`);
294+
}
295+
296+
const joiners: Promise<void>[] = [];
297+
this._onWillRemoveProfile.fire({
298+
profile,
299+
join(promise) {
300+
joiners.push(promise);
301+
}
302+
});
303+
await Promises.settled(joiners);
304+
305+
if (profile.id === this.profilesObject.emptyWindow?.id) {
306+
this.profilesObject.emptyWindow = undefined;
307+
}
308+
for (const workspace of [...this.profilesObject.workspaces.keys()]) {
309+
if (profile.id === this.profilesObject.workspaces.get(workspace)?.id) {
310+
this.profilesObject.workspaces.delete(workspace);
311+
}
312+
}
313+
this.updateStoredProfileAssociations();
314+
315+
this.updateProfiles([], [profile]);
316+
317+
try {
318+
if (this.profiles.length === 1) {
319+
await this.fileService.del(this.profilesHome, { recursive: true });
320+
} else {
321+
await this.fileService.del(profile.location, { recursive: true });
322+
}
323+
} catch (error) {
324+
this.logService.error(error);
325+
}
326+
}
327+
328+
private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[]) {
329+
const storedProfiles: StoredUserDataProfile[] = [];
330+
for (const profile of [...this.profilesObject.profiles, ...added]) {
331+
if (profile.isDefault) {
332+
continue;
333+
}
334+
if (removed.some(p => profile.id === p.id)) {
335+
continue;
336+
}
337+
storedProfiles.push({ location: profile.location, name: profile.name, useDefaultFlags: profile.useDefaultFlags });
338+
}
339+
this.saveStoredProfiles(storedProfiles);
340+
this._profilesObject = undefined;
341+
this._onDidChangeProfiles.fire({ added, removed, all: this.profiles });
342+
}
343+
344+
private updateWorkspaceAssociation(workspaceIdentifier: WorkspaceIdentifier, newProfile?: IUserDataProfile) {
345+
const workspace = this.getWorkspace(workspaceIdentifier);
346+
347+
// Folder or Multiroot workspace
348+
if (URI.isUri(workspace)) {
349+
this.profilesObject.workspaces.delete(workspace);
350+
if (newProfile && !newProfile.isDefault) {
351+
this.profilesObject.workspaces.set(workspace, newProfile);
352+
}
353+
}
354+
// Empty Window
355+
else {
356+
this.profilesObject.emptyWindow = !newProfile?.isDefault ? newProfile : undefined;
357+
}
358+
359+
this.updateStoredProfileAssociations();
360+
}
361+
362+
private updateStoredProfileAssociations() {
363+
const workspaces: IStringDictionary<string> = {};
364+
for (const [workspace, profile] of this.profilesObject.workspaces.entries()) {
365+
workspaces[workspace.toString()] = profile.location.toString();
366+
}
367+
const emptyWindow = this.profilesObject.emptyWindow?.location.toString();
368+
this.saveStoredProfileAssociations({ workspaces, emptyWindow });
369+
this._profilesObject = undefined;
142370
}
143371

144-
createProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise<IUserDataProfile> { throw new Error('Not implemented'); }
145-
setProfileForWorkspace(profile: IUserDataProfile, workspaceIdentifier: WorkspaceIdentifier): Promise<void> { throw new Error('Not implemented'); }
146-
getProfile(workspaceIdentifier: WorkspaceIdentifier): IUserDataProfile { throw new Error('Not implemented'); }
147-
removeProfile(profile: IUserDataProfile): Promise<void> { throw new Error('Not implemented'); }
372+
protected getStoredProfiles(): StoredUserDataProfile[] { return []; }
373+
protected saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { throw new Error('not implemented'); }
374+
375+
protected getStoredProfileAssociations(): StoredProfileAssociations { return {}; }
376+
protected saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void { throw new Error('not implemented'); }
148377
}

0 commit comments

Comments
 (0)