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

758 LOC · 570 covered · 188 uncovered · 114 ranges · 3436 concepts · 25 introducers · 1954 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- userDataProfile.ts ×28
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { hash } from '../../../base/common/hash.js';
7 > import { Emitter, Event } from '../../../base/common/event.js';
8 > import { Disposable } from '../../../base/common/lifecycle.js';
9 > import { basename, joinPath } from '../../../base/common/resources.js';
10 > import { URI, UriDto } from '../../../base/common/uri.js';
11 > import { localize } from '../../../nls.js';
12 > import { IEnvironmentService } from '../../environment/common/environment.js';
13 > import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';
14 > import { createDecorator } from '../../instantiation/common/instantiation.js';
15 > import { ILogService } from '../../log/common/log.js';
16 > import { IAnyWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from '../../workspace/common/workspace.js';
17 > import { IStringDictionary } from '../../../base/common/collections.js';
18 > import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
19 > import { Promises } from '../../../base/common/async.js';
20 > import { generateUuid } from '../../../base/common/uuid.js';
21 > import { escapeRegExpCharacters } from '../../../base/common/strings.js';
22 > import { isString, Mutable } from '../../../base/common/types.js';
23 >
24 > export const AGENTS_WINDOW_PROFILE_ID = 'agents';
25 >
26 > const AGENTS_WINDOW_PROFILE_FLAGS: UseDefaultProfileFlags = {
27 > settings: true,
28 > keybindings: true,
29 > prompts: true,
30 > mcp: true,
31 > languageModels: true,
32 > snippets: true,
33 > tasks: true,
34 > extensions: true,
35 > };
36 >
37 > export const enum ProfileResourceType {
38 > Settings = 'settings',
39 > Keybindings = 'keybindings',
40 > Snippets = 'snippets',
41 > Prompts = 'prompts',
42 > Tasks = 'tasks',
43 > Extensions = 'extensions',
44 > GlobalState = 'globalState',
45 > Mcp = 'mcp',
46 > LanguageModels = 'languageModels',
47 > }
48 >
49 > /**
50 > * Flags to indicate whether to use the default profile or not.
51 > */
52 > export type UseDefaultProfileFlags = { [key in ProfileResourceType]?: boolean };
53 > export type ProfileResourceTypeFlags = UseDefaultProfileFlags;
54 > export type SettingValue = string | boolean | number | undefined | null | object;
55 > export type ISettingsDictionary = Record<string, SettingValue>;
56 >
57 > export interface IUserDataProfile {
58 > readonly id: string;
59 > readonly isDefault: boolean;
60 > readonly name: string;
61 > readonly icon?: string;
62 > readonly location: URI;
63 > readonly globalStorageHome: URI;
64 > readonly settingsResource: URI;
65 > readonly keybindingsResource: URI;
66 > readonly tasksResource: URI;
67 > readonly snippetsHome: URI;
68 > readonly promptsHome: URI;
69 > readonly extensionsResource: URI;
70 > readonly mcpResource: URI;
71 > readonly languageModelsResource: URI;
72 > readonly agentPluginsHome: URI;
73 > readonly cacheHome: URI;
74 > readonly useDefaultFlags?: UseDefaultProfileFlags;
75 > readonly isInternal?: boolean;
76 > readonly isTransient?: boolean;
77 > readonly isAgentsWindowProfile?: boolean;
78 > readonly workspaces?: readonly URI[];
79 > }
80 >
81 > export function isUserDataProfile(thing: unknown): thing is IUserDataProfile {
82 const candidate = thing as IUserDataProfile | undefined;
83
84 return !!(candidate && typeof candidate === 'object'
85 && typeof candidate.id === 'string'
86 && typeof candidate.isDefault === 'boolean'
87 && typeof candidate.name === 'string'
88 && URI.isUri(candidate.location)
89 && URI.isUri(candidate.globalStorageHome)
90 && URI.isUri(candidate.settingsResource)
91 && URI.isUri(candidate.keybindingsResource)
92 && URI.isUri(candidate.tasksResource)
93 && URI.isUri(candidate.snippetsHome)
94 && URI.isUri(candidate.promptsHome)
95 && URI.isUri(candidate.extensionsResource)
96 && URI.isUri(candidate.mcpResource)
97 && URI.isUri(candidate.languageModelsResource)
98 && URI.isUri(candidate.agentPluginsHome)
99 );
100 }
102 > export interface IParsedUserDataProfileTemplate {
103 > readonly name: string;
104 > readonly icon?: string;
105 > readonly settings?: ISettingsDictionary;
106 > readonly globalState?: IStringDictionary<string>;
107 > }
108 >
109 > export interface ISystemProfileTemplate extends IParsedUserDataProfileTemplate {
110 > readonly id: string;
111 > }
112 >
113 > export type DidChangeProfilesEvent = { readonly added: readonly IUserDataProfile[]; readonly removed: readonly IUserDataProfile[]; readonly updated: readonly IUserDataProfile[]; readonly all: readonly IUserDataProfile[] };
114 >
115 > export type WillCreateProfileEvent = {
116 > profile: IUserDataProfile;
117 > join(promise: Promise<void>): void;
118 > };
119 >
120 > export type WillRemoveProfileEvent = {
121 > profile: IUserDataProfile;
122 > join(promise: Promise<void>): void;
123 > };
124 >
125 > export interface IUserDataProfileOptions {
126 > readonly icon?: string;
127 > readonly useDefaultFlags?: UseDefaultProfileFlags;
128 > readonly transient?: boolean;
129 > readonly workspaces?: readonly URI[];
130 > }
131 >
132 > export interface IUserDataProfileUpdateOptions extends Omit<IUserDataProfileOptions, 'icon'> {
133 > readonly name?: string;
134 > readonly icon?: string | null;
135 > }
136 >
137 > export const IUserDataProfilesService = createDecorator<IUserDataProfilesService>('IUserDataProfilesService');
138 > export interface IUserDataProfilesService {
139 > readonly _serviceBrand: undefined;
140 >
141 > readonly profilesHome: URI;
142 > readonly defaultProfile: IUserDataProfile;
143 >
144 > readonly onDidChangeProfiles: Event<DidChangeProfilesEvent>;
145 > readonly profiles: readonly IUserDataProfile[];
146 >
147 > readonly onDidResetWorkspaces: Event<void>;
148 >
149 > createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise<IUserDataProfile>;
150 > createTransientProfile(workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise<IUserDataProfile>;
151 > createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise<IUserDataProfile>;
152 > updateProfile(profile: IUserDataProfile, options?: IUserDataProfileUpdateOptions,): Promise<IUserDataProfile>;
153 > removeProfile(profile: IUserDataProfile): Promise<void>;
154 >
155 > setProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, profile: IUserDataProfile): Promise<void>;
156 > resetWorkspaces(): Promise<void>;
157 >
158 > cleanUp(): Promise<void>;
159 > cleanUpTransientProfiles(): Promise<void>;
160 > }
161 >
162 > export function reviveProfile(profile: UriDto<IUserDataProfile>, scheme: string): IUserDataProfile {
163 return {
164 id: profile.id,
165 isDefault: profile.isDefault,
166 name: profile.name,
167 icon: profile.icon,
168 location: URI.revive(profile.location).with({ scheme }),
169 globalStorageHome: URI.revive(profile.globalStorageHome).with({ scheme }),
170 settingsResource: URI.revive(profile.settingsResource).with({ scheme }),
171 keybindingsResource: URI.revive(profile.keybindingsResource).with({ scheme }),
172 tasksResource: URI.revive(profile.tasksResource).with({ scheme }),
173 snippetsHome: URI.revive(profile.snippetsHome).with({ scheme }),
174 promptsHome: URI.revive(profile.promptsHome).with({ scheme }),
175 extensionsResource: URI.revive(profile.extensionsResource).with({ scheme }),
176 mcpResource: URI.revive(profile.mcpResource).with({ scheme }),
177 languageModelsResource: URI.revive(profile.languageModelsResource).with({ scheme }),
178 agentPluginsHome: URI.revive(profile.agentPluginsHome),
179 cacheHome: URI.revive(profile.cacheHome).with({ scheme }),
180 useDefaultFlags: profile.useDefaultFlags,
181 isTransient: profile.isTransient,
182 isInternal: profile.isInternal,
183 isAgentsWindowProfile: profile.isAgentsWindowProfile,
184 workspaces: profile.workspaces?.map(w => URI.revive(w)),
185 };
186 }
188 > export function toUserDataProfile(id: string, name: string, location: URI, profilesCacheHome: URI, options?: IUserDataProfileOptions, defaultProfile?: IUserDataProfile): IUserDataProfile {
189 > const isAgentsWindowProfile = id === AGENTS_WINDOW_PROFILE_ID; userDataProfile.ts ×1
190 > return {
191 > id,
192 > name,
193 > location,
194 > isDefault: false,
195 > icon: options?.icon,
196 > globalStorageHome: defaultProfile && options?.useDefaultFlags?.globalState ? defaultProfile.globalStorageHome : joinPath(location, 'globalStorage'),
197 > settingsResource: defaultProfile && options?.useDefaultFlags?.settings ? defaultProfile.settingsResource : joinPath(location, 'settings.json'),
198 > keybindingsResource: defaultProfile && options?.useDefaultFlags?.keybindings ? defaultProfile.keybindingsResource : joinPath(location, 'keybindings.json'),
199 > tasksResource: defaultProfile && options?.useDefaultFlags?.tasks ? defaultProfile.tasksResource : joinPath(location, 'tasks.json'),
200 > snippetsHome: defaultProfile && options?.useDefaultFlags?.snippets ? defaultProfile.snippetsHome : joinPath(location, 'snippets'),
201 > promptsHome: defaultProfile && options?.useDefaultFlags?.prompts ? defaultProfile.promptsHome : joinPath(location, 'prompts'),
202 > extensionsResource: defaultProfile && options?.useDefaultFlags?.extensions ? defaultProfile.extensionsResource : joinPath(location, 'extensions.json'),
203 > mcpResource: defaultProfile && options?.useDefaultFlags?.mcp ? defaultProfile.mcpResource : joinPath(location, 'mcp.json'),
204 > languageModelsResource: defaultProfile && options?.useDefaultFlags?.languageModels ? defaultProfile.languageModelsResource : joinPath(location, 'chatLanguageModels.json'),
205 > agentPluginsHome: defaultProfile ? defaultProfile.agentPluginsHome : joinPath(location, 'agent-plugins'),
206 > cacheHome: joinPath(profilesCacheHome, id),
207 > useDefaultFlags: options?.useDefaultFlags,
208 > isTransient: options?.transient,
209 > isInternal: isAgentsWindowProfile || options?.transient,
210 > isAgentsWindowProfile,
211 > workspaces: options?.workspaces,
212 > };
213 > }
215 > export type UserDataProfilesObject = {
216 > profiles: IUserDataProfile[];
217 > emptyWindows: Map<string, IUserDataProfile>;
218 > };
219 >
220 > export type StoredUserDataProfile = {
221 > name: string;
222 > location: URI;
223 > icon?: string;
224 > useDefaultFlags?: UseDefaultProfileFlags;
225 > };
226 >
227 > export type StoredProfileAssociations = {
228 > workspaces?: IStringDictionary<string>;
229 > emptyWindows?: IStringDictionary<string>;
230 > };
231 >
232 > const SYSTEM_PROFILES_HOME = 'builtin';
233 >
234 > export class UserDataProfilesService extends Disposable implements IUserDataProfilesService {
235 >
236 > readonly _serviceBrand: undefined;
237 >
238 > protected static readonly PROFILES_KEY = 'userDataProfiles';
239 > protected static readonly PROFILE_ASSOCIATIONS_KEY = 'profileAssociations';
240 >
241 > readonly profilesHome: URI;
242 > private readonly profilesCacheHome: URI;
243 >
244 > get defaultProfile(): IUserDataProfile { return this.profiles[0]; }
245 > get profiles(): IUserDataProfile[] { return [...this.profilesObject.profiles, ...this.transientProfilesObject.profiles]; }
246 >
247 > protected readonly _onDidChangeProfiles = this._register(new Emitter<DidChangeProfilesEvent>());
248 > readonly onDidChangeProfiles = this._onDidChangeProfiles.event;
249 >
250 > protected readonly _onWillCreateProfile = this._register(new Emitter<WillCreateProfileEvent>());
251 > readonly onWillCreateProfile = this._onWillCreateProfile.event;
252 >
253 > protected readonly _onWillRemoveProfile = this._register(new Emitter<WillRemoveProfileEvent>());
254 > readonly onWillRemoveProfile = this._onWillRemoveProfile.event;
255 >
256 > private readonly _onDidResetWorkspaces = this._register(new Emitter<void>());
257 > readonly onDidResetWorkspaces = this._onDidResetWorkspaces.event;
258 >
259 > private profileCreationPromises = new Map<string, Promise<IUserDataProfile>>();
260 >
261 > protected readonly transientProfilesObject: UserDataProfilesObject = {
262 > profiles: [],
263 > emptyWindows: new Map()
264 > };
265 >
266 > constructor(
267 > @IEnvironmentService protected environmentService: IEnvironmentService, userDataProfile.ts ×1
268 > @IFileService protected fileService: IFileService,
269 > @IUriIdentityService protected uriIdentityService: IUriIdentityService,
270 > @ILogService protected logService: ILogService
271 > ) {
272 > super();
273 > this.profilesHome = joinPath(this.environmentService.userRoamingDataHome, 'profiles');
274 > this.profilesCacheHome = joinPath(this.environmentService.cacheHome, 'CachedProfilesData');
275 > }
277 > init(): void {
278 this._profilesObject = undefined;
279 }
281 > protected _profilesObject: UserDataProfilesObject | undefined;
282 > protected get profilesObject(): UserDataProfilesObject {
283 > if (!this._profilesObject) { userDataProfile.ts ×7
284 > const defaultProfile = this.createDefaultProfile();
285 > const profiles: Array<Mutable<IUserDataProfile>> = [defaultProfile];
286 > try {
287 > for (const storedProfile of this.getStoredProfiles()) {
288 > if (this.isInvalidProfile(storedProfile)) { userDataProfile.ts ×9
289 this.logService.warn('Skipping the invalid stored profile', storedProfile.location || storedProfile.name);
290 continue;
291 }
292 > const id = basename(storedProfile.location); userDataProfile.ts ×9
293 > profiles.push(toUserDataProfile(
294 > id,
295 > storedProfile.name,
296 > storedProfile.location,
297 > this.profilesCacheHome,
298 > {
299 > icon: storedProfile.icon,
300 > useDefaultFlags: id === AGENTS_WINDOW_PROFILE_ID ? AGENTS_WINDOW_PROFILE_FLAGS : storedProfile.useDefaultFlags,
301 > },
302 > defaultProfile));
303 > }
304 > } catch (error) { userDataProfile.ts ×7
305 this.logService.error(error);
306 }
307 > const emptyWindows = new Map<string, IUserDataProfile>(); userDataProfile.ts ×7
308 > if (profiles.length) {
309 > try {
310 > const profileAssociaitions = this.getStoredProfileAssociations();
311 > if (profileAssociaitions.workspaces) {
312 > for (const [workspacePath, profileId] of Object.entries(profileAssociaitions.workspaces)) { userDataProfile.ts ×14
313 > const workspace = URI.parse(workspacePath); userDataProfile.ts ×2
314 > const profile = profiles.find(p => p.id === profileId);
315 > if (profile) {
316 > const workspaces = profile.workspaces ? profile.workspaces.slice(0) : [];
317 > workspaces.push(workspace);
318 > profile.workspaces = workspaces;
319 > }
320 > }
322 > if (profileAssociaitions.emptyWindows) { userDataProfile.ts ×7
323 > for (const [windowId, profileId] of Object.entries(profileAssociaitions.emptyWindows)) { userDataProfile.ts ×14
324 const profile = profiles.find(p => p.id === profileId);
325 if (profile) {
326 emptyWindows.set(windowId, profile);
327 }
328 }
330 > } catch (error) { userDataProfile.ts ×7
331 this.logService.error(error);
332 }
334 > this._profilesObject = { profiles, emptyWindows };
335 > }
336 > return this._profilesObject;
337 > }
339 > private isInvalidProfile(storedProfile: StoredUserDataProfile): boolean {
340 > if (!storedProfile.name) { userDataProfile.ts ×9
341 return true;
342 }
343 > if (!isString(storedProfile.name)) { userDataProfile.ts ×9
344 return true;
345 }
346 > if (!storedProfile.location) { userDataProfile.ts ×9
347 return true;
348 }
349 > return false; userDataProfile.ts ×9
350 > }
352 > protected createDefaultProfile() {
353 > const defaultProfile = toUserDataProfile('__default__profile__', localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome, this.profilesCacheHome); userDataProfile.ts ×7
354 > return { ...defaultProfile, extensionsResource: this.getDefaultProfileExtensionsLocation() ?? defaultProfile.extensionsResource, isDefault: true };
355 > }
357 > async createTransientProfile(workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise<IUserDataProfile> {
358 > const namePrefix = `Temp`; userDataProfile.ts ×1
359 > const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s(\\d+)`);
360 > let nameIndex = 0;
361 > for (const profile of this.profiles) {
362 > const matches = nameRegEx.exec(profile.name);
363 > const index = matches ? parseInt(matches[1]) : 0;
364 > nameIndex = index > nameIndex ? index : nameIndex;
365 > }
366 > const name = `${namePrefix} ${nameIndex + 1}`;
367 > return this.createProfile(hash(generateUuid()).toString(16), name, { transient: true }, workspaceIdentifier);
368 > }
370 > async createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise<IUserDataProfile> {
371 > return this.createProfile(hash(generateUuid()).toString(16), name, options, workspaceIdentifier); userDataProfile.ts ×1
372 > }
374 > async createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise<IUserDataProfile> {
375 > const profile = await this.doCreateProfile(id, name, options, workspaceIdentifier); userDataProfile.ts ×10
376 >
377 > return profile;
378 > }
380 > private async doCreateProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise<IUserDataProfile> {
381 > if (!isString(name) || !name) { userDataProfile.ts ×10
382 throw new Error('Name of the profile is mandatory and must be of type `string`');
383 }
385 > let profileCreationPromise = this.profileCreationPromises.get(name);
386 > if (!profileCreationPromise) {
387 > profileCreationPromise = (async () => {
388 > try {
389 > const existing = this.profiles.find(p => p.id === id || (id !== AGENTS_WINDOW_PROFILE_ID && !p.isTransient && !options?.transient && p.name === name));
390 > if (existing) {
391 throw new Error(`Profile with ${name} name already exists`);
392 }
394 > const workspace = workspaceIdentifier ? this.getWorkspace(workspaceIdentifier) : undefined;
395 > if (URI.isUri(workspace)) {
396 > options = { ...options, workspaces: [workspace] }; userDataProfile.ts ×1
397 > }
399 > const profile = toUserDataProfile(
400 > id,
401 > name,
402 > this.uriIdentityService.extUri.joinPath(this.profilesHome, ...(id === AGENTS_WINDOW_PROFILE_ID ? [SYSTEM_PROFILES_HOME, id] : [id])),
403 > this.profilesCacheHome,
404 > id === AGENTS_WINDOW_PROFILE_ID ? {} : options,
405 > this.defaultProfile);
406 > await this.fileService.createFolder(profile.location);
407 >
408 > const joiners: Promise<void>[] = [];
409 > this._onWillCreateProfile.fire({
410 > profile,
411 > join(promise) {
412 joiners.push(promise);
413 }
415 > await Promises.settled(joiners);
416 >
417 > if (workspace && !URI.isUri(workspace)) {
418 this.updateEmptyWindowAssociation(workspace, profile, !!profile.isTransient);
419 }
420 > this.updateProfiles([profile], [], []); userDataProfile.ts ×10
421 > return this.profiles.find(p => p.id === profile.id) ?? profile;
422 > } finally {
423 > this.profileCreationPromises.delete(name);
424 > }
425 > })();
426 > this.profileCreationPromises.set(name, profileCreationPromise);
427 > }
428 > return profileCreationPromise;
429 > }
431 > async updateProfile(profile: IUserDataProfile, options: IUserDataProfileUpdateOptions): Promise<IUserDataProfile> {
432 > if (profile.isAgentsWindowProfile) { userDataProfile.ts ×7
433 throw new Error('Cannot update agents window profile');
434 }
436 > const profilesToUpdate: IUserDataProfile[] = [];
437 > for (const existing of this.profiles) {
438 > let profileToUpdate: Mutable<IUserDataProfile> | undefined;
439 >
440 > if (profile.id === existing.id) {
441 > if (!existing.isDefault) {
442 > profileToUpdate = toUserDataProfile(existing.id, options.name ?? existing.name, existing.location, this.profilesCacheHome, { userDataProfile.ts ×2
443 > icon: options.icon === null ? undefined : options.icon ?? existing.icon,
444 > transient: options.transient ?? existing.isTransient,
445 > useDefaultFlags: options.useDefaultFlags ?? existing.useDefaultFlags,
446 > workspaces: options.workspaces ?? existing.workspaces,
447 > }, this.defaultProfile);
448 > } else if (options.workspaces) { userDataProfile.ts ×7
449 > profileToUpdate = existing; userDataProfile.ts ×1
450 > profileToUpdate.workspaces = options.workspaces;
451 > }
454 > else if (options.workspaces) {
455 > const workspaces = existing.workspaces?.filter(w1 => !options.workspaces?.some(w2 => this.uriIdentityService.extUri.isEqual(w1, w2))); userDataProfile.ts ×2
456 > if (existing.workspaces?.length !== workspaces?.length) {
457 > profileToUpdate = existing; userDataProfile.ts ×1
458 > profileToUpdate.workspaces = workspaces;
459 > }
462 > if (profileToUpdate) {
463 > profilesToUpdate.push(profileToUpdate);
464 > }
465 > }
466 >
467 > if (!profilesToUpdate.length) {
468 if (profile.isDefault) {
469 throw new Error('Cannot update default profile');
470 }
471 throw new Error(`Profile '${profile.name}' does not exist`);
472 }
474 > this.updateProfiles([], [], profilesToUpdate);
475 >
476 > const updatedProfile = this.profiles.find(p => p.id === profile.id);
477 > if (!updatedProfile) {
478 throw new Error(`Profile '${profile.name}' was not updated`);
479 }
481 > return updatedProfile;
482 > }
484 > async removeProfile(profileToRemove: IUserDataProfile): Promise<void> {
485 > if (profileToRemove.isDefault) { userDataProfile.ts ×8
486 throw new Error('Cannot remove default profile');
487 }
488 > const profile = this.profiles.find(p => p.id === profileToRemove.id); userDataProfile.ts ×8
489 > if (!profile) {
490 throw new Error(`Profile '${profileToRemove.name}' does not exist`);
491 }
493 > const joiners: Promise<void>[] = [];
494 > this._onWillRemoveProfile.fire({
495 > profile,
496 > join(promise) {
497 joiners.push(promise);
498 }
500 >
501 > try {
502 > await Promise.allSettled(joiners);
503 > } catch (error) {
504 this.logService.error(error);
505 }
507 > this.updateProfiles([], [profile], []);
508 >
509 > try {
510 > await this.fileService.del(profile.cacheHome, { recursive: true });
511 > } catch (error) {
512 > if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
513 this.logService.error(error);
514 }
516 > }
518 > async setProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, profileToSet: IUserDataProfile): Promise<void> {
519 > const profile = this.profiles.find(p => p.id === profileToSet.id); userDataProfile.ts ×4
520 > if (!profile) {
521 throw new Error(`Profile '${profileToSet.name}' does not exist`);
522 }
524 > const workspace = this.getWorkspace(workspaceIdentifier);
525 > if (URI.isUri(workspace)) {
526 > const workspaces = profile.workspaces ? [...profile.workspaces] : [];
527 > if (!workspaces.some(w => this.uriIdentityService.extUri.isEqual(w, workspace))) {
528 > workspaces.push(workspace); userDataProfile.ts ×1
529 > await this.updateProfile(profile, { workspaces });
530 > }
531 > } else { userDataProfile.ts ×4
532 this.updateEmptyWindowAssociation(workspace, profile, false);
533 this.updateStoredProfiles(this.profiles);
534 }
537 > unsetWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, transient: boolean = false): void {
538 > const workspace = this.getWorkspace(workspaceIdentifier); userDataProfile.ts ×5
539 > if (URI.isUri(workspace)) {
540 > const currentlyAssociatedProfile = this.getProfileForWorkspace(workspaceIdentifier);
541 > if (currentlyAssociatedProfile) {
542 > this.updateProfile(currentlyAssociatedProfile, { workspaces: currentlyAssociatedProfile.workspaces?.filter(w => !this.uriIdentityService.extUri.isEqual(w, workspace)) });
543 > }
544 > } else {
545 this.updateEmptyWindowAssociation(workspace, undefined, transient);
546 this.updateStoredProfiles(this.profiles);
547 }
550 > async resetWorkspaces(): Promise<void> {
551 this.transientProfilesObject.emptyWindows.clear();
552 this.profilesObject.emptyWindows.clear();
553 for (const profile of this.profiles) {
554 (<Mutable<IUserDataProfile>>profile).workspaces = undefined;
555 }
556 this.updateProfiles([], [], this.profiles);
557 this._onDidResetWorkspaces.fire();
558 }
560 > async cleanUp(): Promise<void> {
561 try {
562 if (await this.fileService.exists(this.profilesHome)) {
563 const stat = await this.fileService.resolve(this.profilesHome);
564 await Promise.all((stat.children || [])
565 .filter(child => child.isDirectory && child.name !== SYSTEM_PROFILES_HOME && this.profiles.every(p => !this.uriIdentityService.extUri.isEqual(p.location, child.resource)))
566 .map(child => this.fileService.del(child.resource, { recursive: true })));
567 }
568 } catch (error) {
569 this.logService.error('Error deleting redundant profile folders', error);
570 }
571
572 try {
573 const existing = this.getStoredProfiles();
574 const valid: StoredUserDataProfile[] = [];
575 for (const storedProfile of this.getStoredProfiles()) {
576 if (this.isInvalidProfile(storedProfile)) {
577 this.logService.warn(`Invalid user data profile found: ${storedProfile.name}`);
578 } else {
579 valid.push(storedProfile);
580 }
581 }
582 if (existing.length !== valid.length) {
583 this.saveStoredProfiles(valid);
584 }
585 } catch (error) {
586 this.logService.error('Error removing invalid stored profiles', error);
587 }
588 }
590 > async cleanUpTransientProfiles(): Promise<void> {
591 const unAssociatedTransientProfiles = this.transientProfilesObject.profiles.filter(p => !this.isProfileAssociatedToWorkspace(p));
592 await Promise.allSettled(unAssociatedTransientProfiles.map(p => this.removeProfile(p)));
593 }
595 > getProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): IUserDataProfile | undefined {
596 > const workspace = this.getWorkspace(workspaceIdentifier); userDataProfile.ts ×5
597 >
598 > if (URI.isUri(workspace) && this.uriIdentityService.extUri.isEqual(workspace, this.environmentService.agentSessionsWorkspace)) {
599 return this.profiles.find(p => p.isAgentsWindowProfile);
600 }
602 > return URI.isUri(workspace)
603 > ? this.profiles.find(p => p.workspaces?.some(w => this.uriIdentityService.extUri.isEqual(w, workspace)))
604 : (this.profilesObject.emptyWindows.get(workspace) ?? this.transientProfilesObject.emptyWindows.get(workspace));
607 > protected getWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): URI | string {
608 > if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { userDataProfile.ts ×2
609 > return workspaceIdentifier.uri;
610 > }
611 if (isWorkspaceIdentifier(workspaceIdentifier)) {
612 return workspaceIdentifier.configPath;
613 }
614 return workspaceIdentifier.id;
617 > private isProfileAssociatedToWorkspace(profile: IUserDataProfile): boolean {
618 if (profile.workspaces?.length) {
619 return true;
620 }
621 if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) {
622 return true;
623 }
624 if ([...this.transientProfilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) {
625 return true;
626 }
627 return false;
628 }
630 > private updateProfiles(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[], donotTrigger: boolean = false): void {
631 > const allProfiles: Mutable<IUserDataProfile>[] = [...this.profiles, ...added]; userDataProfile.ts ×14
632 >
633 > const transientProfiles = this.transientProfilesObject.profiles;
634 > this.transientProfilesObject.profiles = [];
635 >
636 > const profiles: IUserDataProfile[] = [];
637 >
638 > for (let profile of allProfiles) {
639 > // removed
640 > if (removed.some(p => profile.id === p.id)) {
641 > for (const windowId of [...this.profilesObject.emptyWindows.keys()]) { userDataProfile.ts ×8
642 if (profile.id === this.profilesObject.emptyWindows.get(windowId)?.id) {
643 this.profilesObject.emptyWindows.delete(windowId);
644 }
645 }
646 > continue; userDataProfile.ts ×8
647 > }
649 > if (!profile.isDefault) {
650 > profile = updated.find(p => profile.id === p.id) ?? profile; userDataProfile.ts ×10
651 > const transientProfile = transientProfiles.find(p => profile.id === p.id);
652 > if (profile.isTransient) {
653 > this.transientProfilesObject.profiles.push(profile); userDataProfile.ts ×2
654 > } else { userDataProfile.ts ×10
655 > if (transientProfile) { userDataProfile.ts ×9
656 > // Move the empty window associations from the transient profile to the persisted profile userDataProfile.ts ×2
657 > for (const [windowId, p] of this.transientProfilesObject.emptyWindows.entries()) {
658 if (profile.id === p.id) {
659 this.transientProfilesObject.emptyWindows.delete(windowId);
660 this.profilesObject.emptyWindows.set(windowId, profile);
661 break;
662 }
663 }
668 > if (profile.workspaces?.length === 0) {
669 > profile.workspaces = undefined; userDataProfile.ts ×1
670 > }
672 > profiles.push(profile);
673 > }
674 >
675 > this.updateStoredProfiles(profiles);
676 >
677 > if (!donotTrigger) {
678 > this.triggerProfilesChanges(added, removed, updated);
679 > }
680 > }
682 > protected triggerProfilesChanges(added: IUserDataProfile[], removed: IUserDataProfile[], updated: IUserDataProfile[]) {
683 > this._onDidChangeProfiles.fire({ added, removed, updated, all: this.profiles }); userDataProfile.ts ×14
684 > }
686 > private updateEmptyWindowAssociation(windowId: string, newProfile: IUserDataProfile | undefined, transient: boolean): void {
687 // Force transient if the new profile to associate is transient
688 transient = newProfile?.isTransient ? true : transient;
689
690 if (transient) {
691 if (newProfile) {
692 this.transientProfilesObject.emptyWindows.set(windowId, newProfile);
693 } else {
694 this.transientProfilesObject.emptyWindows.delete(windowId);
695 }
696 }
697
698 else {
699 // Unset the transiet association if any
700 this.transientProfilesObject.emptyWindows.delete(windowId);
701 if (newProfile) {
702 this.profilesObject.emptyWindows.set(windowId, newProfile);
703 } else {
704 this.profilesObject.emptyWindows.delete(windowId);
705 }
706 }
707 }
709 > private updateStoredProfiles(profiles: IUserDataProfile[]): void {
710 > const storedProfiles: StoredUserDataProfile[] = []; userDataProfile.ts ×14
711 > const workspaces: IStringDictionary<string> = {};
712 > const emptyWindows: IStringDictionary<string> = {};
713 >
714 > for (const profile of profiles) {
715 > if (profile.isTransient) {
716 > continue; userDataProfile.ts ×2
717 > }
718 > if (!profile.isDefault) { userDataProfile.ts ×14
719 > storedProfiles.push({ userDataProfile.ts ×9
720 > location: profile.location,
721 > name: profile.name,
722 > icon: profile.icon,
723 > useDefaultFlags: profile.useDefaultFlags,
724 > });
725 > }
726 > if (profile.workspaces) { userDataProfile.ts ×14
727 > for (const workspace of profile.workspaces) { userDataProfile.ts ×2
728 > workspaces[workspace.toString()] = profile.id;
729 > }
730 > }
732 >
733 > for (const [windowId, profile] of this.profilesObject.emptyWindows.entries()) {
734 emptyWindows[windowId.toString()] = profile.id;
735 }
737 > this.saveStoredProfileAssociations({ workspaces, emptyWindows });
738 > this.saveStoredProfiles(storedProfiles);
739 > this._profilesObject = undefined;
740 > }
742 > protected getStoredProfiles(): StoredUserDataProfile[] { return []; }
743 > protected saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { throw new Error('not implemented'); }
744 >
745 > protected getStoredProfileAssociations(): StoredProfileAssociations { return {}; }
746 > protected saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void { throw new Error('not implemented'); }
747 > protected getDefaultProfileExtensionsLocation(): URI | undefined { return undefined; }
748 > }
749 >
750 > export class InMemoryUserDataProfilesService extends UserDataProfilesService {
751 > private storedProfiles: StoredUserDataProfile[] = []; userDataProfile.ts ×1
752 > protected override getStoredProfiles(): StoredUserDataProfile[] { return this.storedProfiles; }
753 > protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { this.storedProfiles = storedProfiles; }
754 >
755 > private storedProfileAssociations: StoredProfileAssociations = {};
756 > protected override getStoredProfileAssociations(): StoredProfileAssociations { return this.storedProfileAssociations; } userDataProfile.ts ×28
757 > protected override saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void { this.storedProfileAssociations = storedProfileAssociations; }
758 > }