diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index e341f36b058..bf95d8ced69 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -105,7 +105,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; -import { UserDataTransientProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataTransientProfilesHandler'; +import { UserDataProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataProfilesHandler'; import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc'; import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; import { resolveMachineId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; @@ -558,7 +558,7 @@ export class CodeApplication extends Disposable { this._register(instantiationService.createInstance(ProxyAuthHandler)); // Transient profiles handler - this._register(instantiationService.createInstance(UserDataTransientProfilesHandler)); + this._register(instantiationService.createInstance(UserDataProfilesHandler)); } private setupSharedProcess(machineId: string): { sharedProcess: SharedProcess; sharedProcessReady: Promise; sharedProcessClient: Promise } { diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 09a8ac88bd2..a8d3c445baa 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -13,7 +13,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' 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, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; +import { IAnyWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { ResourceMap } from 'vs/base/common/map'; import { IStringDictionary } from 'vs/base/common/collections'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -69,9 +69,6 @@ export function isUserDataProfile(thing: unknown): thing is IUserDataProfile { export const PROFILES_ENABLEMENT_CONFIG = 'workbench.experimental.settingsProfiles.enabled'; -export type EmptyWindowWorkspaceIdentifier = 'empty-window'; -export type WorkspaceIdentifier = ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier | EmptyWindowWorkspaceIdentifier; - export type DidChangeProfilesEvent = { readonly added: readonly IUserDataProfile[]; readonly removed: readonly IUserDataProfile[]; readonly updated: readonly IUserDataProfile[]; readonly all: readonly IUserDataProfile[] }; export type WillCreateProfileEvent = { @@ -107,13 +104,13 @@ export interface IUserDataProfilesService { readonly onDidResetWorkspaces: Event; isEnabled(): boolean; - createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise; - createTransientProfile(workspaceIdentifier?: WorkspaceIdentifier): Promise; - createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise; + createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise; + createTransientProfile(workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise; + createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise; updateProfile(profile: IUserDataProfile, options?: IUserDataProfileUpdateOptions,): Promise; removeProfile(profile: IUserDataProfile): Promise; - setProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier, profile: IUserDataProfile): Promise; + setProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, profile: IUserDataProfile): Promise; resetWorkspaces(): Promise; cleanUp(): Promise; @@ -159,7 +156,7 @@ export function toUserDataProfile(id: string, name: string, location: URI, optio export type UserDataProfilesObject = { profiles: IUserDataProfile[]; workspaces: ResourceMap; - emptyWindow?: IUserDataProfile; + emptyWindows: Map; }; export type StoredUserDataProfile = { @@ -171,7 +168,7 @@ export type StoredUserDataProfile = { export type StoredProfileAssociations = { workspaces?: IStringDictionary; - emptyWindow?: string; + emptyWindows?: IStringDictionary; }; export class UserDataProfilesService extends Disposable implements IUserDataProfilesService { @@ -203,7 +200,8 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf protected readonly transientProfilesObject: UserDataProfilesObject = { profiles: [], - workspaces: new ResourceMap() + workspaces: new ResourceMap(), + emptyWindows: new Map() }; constructor( @@ -240,14 +238,14 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf profiles.push(toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, { shortName: storedProfile.shortName, useDefaultFlags: storedProfile.useDefaultFlags })); } } - let emptyWindow: IUserDataProfile | undefined; const workspaces = new ResourceMap(); + const emptyWindows = new Map(); const defaultProfile = toUserDataProfile(hash(this.environmentService.userRoamingDataHome.path).toString(16), localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome); profiles.unshift({ ...defaultProfile, extensionsResource: this.getDefaultProfileExtensionsLocation() ?? defaultProfile.extensionsResource, isDefault: true }); if (profiles.length) { - const profileAssicaitions = this.getStoredProfileAssociations(); - if (profileAssicaitions.workspaces) { - for (const [workspacePath, profilePath] of Object.entries(profileAssicaitions.workspaces)) { + const profileAssociaitions = this.getStoredProfileAssociations(); + if (profileAssociaitions.workspaces) { + for (const [workspacePath, profilePath] of Object.entries(profileAssociaitions.workspaces)) { const workspace = URI.parse(workspacePath); const profileLocation = URI.parse(profilePath); const profile = profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, profileLocation)); @@ -256,17 +254,22 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } } } - if (profileAssicaitions.emptyWindow) { - const emptyWindowProfileLocation = URI.parse(profileAssicaitions.emptyWindow); - emptyWindow = profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, emptyWindowProfileLocation)); + if (profileAssociaitions.emptyWindows) { + for (const [windowId, profilePath] of Object.entries(profileAssociaitions.emptyWindows)) { + const profileLocation = URI.parse(profilePath); + const profile = profiles.find(p => this.uriIdentityService.extUri.isEqual(p.location, profileLocation)); + if (profile) { + emptyWindows.set(windowId, profile); + } + } } } - this._profilesObject = { profiles, workspaces, emptyWindow }; + this._profilesObject = { profiles, workspaces, emptyWindows }; } return this._profilesObject; } - async createTransientProfile(workspaceIdentifier?: WorkspaceIdentifier): Promise { + async createTransientProfile(workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise { const namePrefix = `Temp`; const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s(\\d+)`); let nameIndex = 0; @@ -279,11 +282,11 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return this.createProfile(hash(generateUuid()).toString(16), name, { transient: true }, workspaceIdentifier); } - async createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { + async createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise { return this.createProfile(hash(generateUuid()).toString(16), name, options, workspaceIdentifier); } - async createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { + async createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } @@ -375,8 +378,10 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf this.logService.error(error); } - if (profile.id === this.profilesObject.emptyWindow?.id) { - this.profilesObject.emptyWindow = undefined; + for (const windowId of [...this.profilesObject.emptyWindows.keys()]) { + if (profile.id === this.profilesObject.emptyWindows.get(windowId)?.id) { + this.profilesObject.emptyWindows.delete(windowId); + } } for (const workspace of [...this.profilesObject.workspaces.keys()]) { if (profile.id === this.profilesObject.workspaces.get(workspace)?.id) { @@ -398,28 +403,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } } - getOrSetProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier, profileToSet: IUserDataProfile = this.defaultProfile): IUserDataProfile { - if (!this.enabled) { - return this.defaultProfile; - } - - let profile = this.getProfileForWorkspace(workspaceIdentifier); - if (!profile) { - profile = profileToSet; - // Associate the profile to workspace only if there are user profiles - // If there are no profiles, workspaces are associated to default profile by default - if (this.profiles.length > 1) { - this.setProfileForWorkspaceSync(workspaceIdentifier, profile); - } - } - return profile; - } - - async setProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier, profileToSet: IUserDataProfile): Promise { - this.setProfileForWorkspaceSync(workspaceIdentifier, profileToSet); - } - - setProfileForWorkspaceSync(workspaceIdentifier: WorkspaceIdentifier, profileToSet: IUserDataProfile): void { + async setProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, profileToSet: IUserDataProfile): Promise { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } @@ -432,7 +416,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf this.updateWorkspaceAssociation(workspaceIdentifier, profile); } - unsetWorkspace(workspaceIdentifier: WorkspaceIdentifier, transient?: boolean): void { + unsetWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, transient?: boolean): void { if (!this.enabled) { throw new Error(`Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } @@ -442,9 +426,9 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf async resetWorkspaces(): Promise { this.transientProfilesObject.workspaces.clear(); - this.transientProfilesObject.emptyWindow = undefined; + this.transientProfilesObject.emptyWindows.clear(); this.profilesObject.workspaces.clear(); - this.profilesObject.emptyWindow = undefined; + this.profilesObject.emptyWindows.clear(); this.updateStoredProfileAssociations(); this._onDidResetWorkspaces.fire(); } @@ -469,29 +453,29 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf await Promise.allSettled(unAssociatedTransientProfiles.map(p => this.removeProfile(p))); } - private getProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier): IUserDataProfile | undefined { + getProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): IUserDataProfile | undefined { const workspace = this.getWorkspace(workspaceIdentifier); - return URI.isUri(workspace) ? this.transientProfilesObject.workspaces.get(workspace) ?? this.profilesObject.workspaces.get(workspace) : this.transientProfilesObject.emptyWindow ?? this.profilesObject.emptyWindow; + return URI.isUri(workspace) ? this.transientProfilesObject.workspaces.get(workspace) ?? this.profilesObject.workspaces.get(workspace) : this.transientProfilesObject.emptyWindows.get(workspace) ?? this.profilesObject.emptyWindows.get(workspace); } - protected getWorkspace(workspaceIdentifier: WorkspaceIdentifier): URI | EmptyWindowWorkspaceIdentifier { + protected getWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): URI | string { if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { return workspaceIdentifier.uri; } if (isWorkspaceIdentifier(workspaceIdentifier)) { return workspaceIdentifier.configPath; } - return 'empty-window'; + return workspaceIdentifier.id; } private isProfileAssociatedToWorkspace(profile: IUserDataProfile): boolean { - if (this.uriIdentityService.extUri.isEqual(this.transientProfilesObject.emptyWindow?.location, profile.location)) { + if ([...this.transientProfilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { return true; } if ([...this.transientProfilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { return true; } - if (this.uriIdentityService.extUri.isEqual(this.profilesObject.emptyWindow?.location, profile.location)) { + if ([...this.profilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { return true; } if ([...this.profilesObject.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { @@ -527,7 +511,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf this._onDidChangeProfiles.fire({ added, removed, updated, all: this.profiles }); } - private updateWorkspaceAssociation(workspaceIdentifier: WorkspaceIdentifier, newProfile?: IUserDataProfile, transient?: boolean): void { + private updateWorkspaceAssociation(workspaceIdentifier: IAnyWorkspaceIdentifier, newProfile?: IUserDataProfile, transient?: boolean): void { // Force transient if the new profile to associate is transient transient = newProfile?.isTransient ? true : transient; @@ -548,7 +532,10 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf } // Empty Window else { - profilesObject.emptyWindow = newProfile; + profilesObject.emptyWindows.delete(workspace); + if (newProfile) { + profilesObject.emptyWindows.set(workspace, newProfile); + } } if (!transient) { @@ -561,8 +548,11 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf for (const [workspace, profile] of this.profilesObject.workspaces.entries()) { workspaces[workspace.toString()] = profile.location.toString(); } - const emptyWindow = this.profilesObject.emptyWindow?.location.toString(); - this.saveStoredProfileAssociations({ workspaces, emptyWindow }); + const emptyWindows: IStringDictionary = {}; + for (const [windowId, profile] of this.profilesObject.emptyWindows.entries()) { + emptyWindows[windowId.toString()] = profile.location.toString(); + } + this.saveStoredProfileAssociations({ workspaces, emptyWindows }); this._profilesObject = undefined; } diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index c947e37ca28..1f286fbd939 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -11,15 +11,16 @@ import { refineServiceDecorator } from 'vs/platform/instantiation/common/instant import { ILogService } from 'vs/platform/log/common/log'; import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IUserDataProfilesService, WorkspaceIdentifier, StoredUserDataProfile, StoredProfileAssociations, WillCreateProfileEvent, WillRemoveProfileEvent, IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfilesService, StoredUserDataProfile, StoredProfileAssociations, WillCreateProfileEvent, WillRemoveProfileEvent, IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesService } from 'vs/platform/userDataProfile/node/userDataProfile'; import { IStringDictionary } from 'vs/base/common/collections'; +import { IAnyWorkspaceIdentifier, IEmptyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export const IUserDataProfilesMainService = refineServiceDecorator(IUserDataProfilesService); export interface IUserDataProfilesMainService extends IUserDataProfilesService { - getOrSetProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier, profileToSet?: IUserDataProfile): IUserDataProfile; - setProfileForWorkspaceSync(workspaceIdentifier: WorkspaceIdentifier, profileToSet: IUserDataProfile): void; - unsetWorkspace(workspaceIdentifier: WorkspaceIdentifier, transient?: boolean): void; + getProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier): IUserDataProfile | undefined; + unsetWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, transient?: boolean): void; + getAssociatedEmptyWindows(): IEmptyWorkspaceIdentifier[]; readonly onWillCreateProfile: Event; readonly onWillRemoveProfile: Event; } @@ -45,6 +46,14 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } } + getAssociatedEmptyWindows(): IEmptyWorkspaceIdentifier[] { + const emptyWindows: IEmptyWorkspaceIdentifier[] = []; + for (const id of this.profilesObject.emptyWindows.keys()) { + emptyWindows.push({ id }); + } + return emptyWindows; + } + protected override saveStoredProfiles(storedProfiles: StoredUserDataProfile[]): void { if (storedProfiles.length) { this.stateMainService.setItem(UserDataProfilesMainService.PROFILES_KEY, storedProfiles); @@ -54,7 +63,7 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } protected override saveStoredProfileAssociations(storedProfileAssociations: StoredProfileAssociations): void { - if (storedProfileAssociations.emptyWindow || storedProfileAssociations.workspaces) { + if (storedProfileAssociations.emptyWindows || storedProfileAssociations.workspaces) { this.stateMainService.setItem(UserDataProfilesMainService.PROFILE_ASSOCIATIONS_KEY, storedProfileAssociations); } else { this.stateMainService.removeItem(UserDataProfilesMainService.PROFILE_ASSOCIATIONS_KEY); diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfilesHandler.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfilesHandler.ts new file mode 100644 index 00000000000..4fdd18f6dc7 --- /dev/null +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfilesHandler.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILifecycleMainService, } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ICodeWindow, LoadReason } from 'vs/platform/window/electron-main/window'; +import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; +import { IAnyWorkspaceIdentifier, isEmptyWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; + +export class UserDataProfilesHandler extends Disposable { + + constructor( + @ILifecycleMainService lifecycleMainService: ILifecycleMainService, + @IUserDataProfilesMainService private readonly userDataProfilesService: IUserDataProfilesMainService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + ) { + super(); + this._register(lifecycleMainService.onWillLoadWindow(e => { + if (e.reason === LoadReason.LOAD) { + this.unsetProfileForWorkspace(e.window); + } + })); + this._register(lifecycleMainService.onBeforeCloseWindow(window => this.unsetProfileForWorkspace(window))); + this._register(new RunOnceScheduler(() => this.cleanUpEmptyWindowAssociations(), 30 * 1000 /* after 30s */)).schedule(); + } + + private async unsetProfileForWorkspace(window: ICodeWindow): Promise { + const workspace = this.getWorkspace(window); + const profile = this.userDataProfilesService.getProfileForWorkspace(workspace); + if (profile && (isEmptyWorkspaceIdentifier(workspace) || profile.isTransient)) { + this.userDataProfilesService.unsetWorkspace(workspace, profile.isTransient); + if (profile.isTransient) { + await this.userDataProfilesService.cleanUpTransientProfiles(); + } + } + } + + private getWorkspace(window: ICodeWindow): IAnyWorkspaceIdentifier { + return window.openedWorkspace ?? toWorkspaceIdentifier(window.backupPath, window.isExtensionDevelopmentHost); + } + + private cleanUpEmptyWindowAssociations(): void { + const associatedEmptyWindows = this.userDataProfilesService.getAssociatedEmptyWindows(); + if (associatedEmptyWindows.length === 0) { + return; + } + const openedWorkspaces = this.windowsMainService.getWindows().map(window => this.getWorkspace(window)); + for (const associatedEmptyWindow of associatedEmptyWindows) { + if (openedWorkspaces.some(openedWorkspace => openedWorkspace.id === associatedEmptyWindow.id)) { + continue; + } + this.userDataProfilesService.unsetWorkspace(associatedEmptyWindow, false); + } + } + +} diff --git a/src/vs/platform/userDataProfile/electron-main/userDataTransientProfilesHandler.ts b/src/vs/platform/userDataProfile/electron-main/userDataTransientProfilesHandler.ts deleted file mode 100644 index 8eef8ef44e4..00000000000 --- a/src/vs/platform/userDataProfile/electron-main/userDataTransientProfilesHandler.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ILifecycleMainService, } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { LoadReason } from 'vs/platform/window/electron-main/window'; -import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; - -export class UserDataTransientProfilesHandler extends Disposable { - - constructor( - @ILifecycleMainService lifecycleMainService: ILifecycleMainService, - @IUserDataProfilesMainService private readonly userDataProfilesService: IUserDataProfilesMainService, - ) { - super(); - this._register(lifecycleMainService.onWillLoadWindow(e => { - if (e.reason === LoadReason.LOAD) { - this.unsetTransientProfileForWorkspace(e.window.openedWorkspace ?? 'empty-window'); - } - })); - this._register(lifecycleMainService.onBeforeCloseWindow(window => this.unsetTransientProfileForWorkspace(window.openedWorkspace ?? 'empty-window'))); - } - - private async unsetTransientProfileForWorkspace(workspace: WorkspaceIdentifier): Promise { - const profile = this.userDataProfilesService.getOrSetProfileForWorkspace(workspace); - if (profile.isTransient) { - this.userDataProfilesService.unsetWorkspace(workspace, true); - await this.userDataProfilesService.cleanUpTransientProfiles(); - } - } - -} diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts index 6bcd8ab1c3e..60767bf59ef 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts @@ -10,7 +10,8 @@ import { URI, UriDto } from 'vs/base/common/uri'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; -import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions, reviveProfile, WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; export class UserDataProfilesNativeService extends Disposable implements IUserDataProfilesService { @@ -58,22 +59,22 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa return this.enabled; } - async createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { + async createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise { const result = await this.channel.call>('createNamedProfile', [name, options, workspaceIdentifier]); return reviveProfile(result, this.profilesHome.scheme); } - async createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { + async createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise { const result = await this.channel.call>('createProfile', [id, name, options, workspaceIdentifier]); return reviveProfile(result, this.profilesHome.scheme); } - async createTransientProfile(workspaceIdentifier?: WorkspaceIdentifier): Promise { + async createTransientProfile(workspaceIdentifier?: IAnyWorkspaceIdentifier): Promise { const result = await this.channel.call>('createTransientProfile', [workspaceIdentifier]); return reviveProfile(result, this.profilesHome.scheme); } - async setProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier, profile: IUserDataProfile): Promise { + async setProfileForWorkspace(workspaceIdentifier: IAnyWorkspaceIdentifier, profile: IUserDataProfile): Promise { await this.channel.call>('setProfileForWorkspace', [workspaceIdentifier, profile]); } diff --git a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts index b0a0e384e55..d184f77020d 100644 --- a/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts +++ b/src/vs/platform/userDataProfile/test/electron-main/userDataProfileMainService.test.ts @@ -70,4 +70,39 @@ suite('UserDataProfileMainService', () => { assert.strictEqual(testObject.defaultProfile.isDefault, true); }); + test('when no profile is set', async () => { + await testObject.createNamedProfile('profile1'); + + assert.equal(testObject.getProfileForWorkspace({ id: 'id' }), undefined); + assert.equal(testObject.getProfileForWorkspace({ id: 'id', configPath: environmentService.userRoamingDataHome }), undefined); + assert.equal(testObject.getProfileForWorkspace({ id: 'id', uri: environmentService.userRoamingDataHome }), undefined); + }); + + test('set profile to a workspace', async () => { + const workspace = { id: 'id', configPath: environmentService.userRoamingDataHome }; + const profile = await testObject.createNamedProfile('profile1'); + + testObject.setProfileForWorkspace(workspace, profile); + + assert.deepStrictEqual(testObject.getProfileForWorkspace(workspace), profile); + }); + + test('set profile to a folder', async () => { + const workspace = { id: 'id', uri: environmentService.userRoamingDataHome }; + const profile = await testObject.createNamedProfile('profile1'); + + testObject.setProfileForWorkspace(workspace, profile); + + assert.deepStrictEqual(testObject.getProfileForWorkspace(workspace), profile); + }); + + test('set profile to a window', async () => { + const workspace = { id: 'id' }; + const profile = await testObject.createNamedProfile('profile1'); + + testObject.setProfileForWorkspace(workspace, profile); + + assert.deepStrictEqual(testObject.getProfileForWorkspace(workspace), profile); + }); + }); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 7d716747e51..176aedc9803 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -35,7 +35,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { getMenuBarVisibility, getTitleBarStyle, IFolderToOpen, INativeWindowConfiguration, IWindowSettings, IWorkspaceToOpen, MenuBarVisibility, useWindowControlsOverlay, WindowMinimumSize, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IWindowState, ICodeWindow, ILoadEvent, WindowMode, WindowError, LoadReason, defaultWindowState } from 'vs/platform/window/electron-main/window'; import { Color } from 'vs/base/common/color'; @@ -139,7 +139,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { return profile; } - return this.userDataProfilesService.getOrSetProfileForWorkspace(this.config.workspace ?? 'empty-window', this.userDataProfilesService.defaultProfile); + return this.userDataProfilesService.getProfileForWorkspace(this.config.workspace ?? toWorkspaceIdentifier(this.backupPath, this.isExtensionDevelopmentHost)) ?? this.userDataProfilesService.defaultProfile; } get remoteAuthority(): string | undefined { return this._config?.remoteAuthority; } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index dbd5cc80e5f..5117c5d7acb 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -44,7 +44,7 @@ import { IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from 'vs/platform/windows/electron-main/windowsFinder'; import { IWindowState, WindowsStateHandler } from 'vs/platform/windows/electron-main/windowsStateHandler'; import { IRecent } from 'vs/platform/workspaces/common/workspaces'; -import { hasWorkspaceFileExtension, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; +import { hasWorkspaceFileExtension, IAnyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { createEmptyWorkspaceIdentifier, getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/platform/workspaces/node/workspaces'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; @@ -1350,7 +1350,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic profiles: { all: this.userDataProfilesMainService.profiles, - profile: window?.isExtensionDevelopmentHost && window?.profile ? window.profile : await this.resolveProfileForBrowserWindow(options) + // Set to default profile first and resolve and update the profile + // only after the workspace-backup is registered. + // Because, workspace identifier of an empty window is known only then. + profile: this.userDataProfilesMainService.defaultProfile }, homeDir: this.environmentMainService.userHome.fsPath, @@ -1453,19 +1456,19 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // first and only load the new configuration if that was // not vetoed if (window.isReady) { - this.lifecycleMainService.unload(window, UnloadReason.LOAD).then(veto => { + this.lifecycleMainService.unload(window, UnloadReason.LOAD).then(async veto => { if (!veto) { - this.doOpenInBrowserWindow(window!, configuration, options); + await this.doOpenInBrowserWindow(window!, configuration, options); } }); } else { - this.doOpenInBrowserWindow(window, configuration, options); + await this.doOpenInBrowserWindow(window, configuration, options); } return window; } - private doOpenInBrowserWindow(window: ICodeWindow, configuration: INativeWindowConfiguration, options: IOpenBrowserWindowOptions): void { + private async doOpenInBrowserWindow(window: ICodeWindow, configuration: INativeWindowConfiguration, options: IOpenBrowserWindowOptions): Promise { // Register window for backups unless the window // is for extension development, where we do not @@ -1498,27 +1501,34 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } } + if (this.userDataProfilesMainService.isEnabled()) { + const workspace = configuration.workspace ?? toWorkspaceIdentifier(configuration.backupPath, false); + const profilePromise = this.resolveProfileForBrowserWindow(options, workspace); + const profile = profilePromise instanceof Promise ? await profilePromise : profilePromise; + configuration.profiles.profile = profile; + + if (!configuration.extensionDevelopmentPath) { + // Associate the configured profile to the workspace + // unless the window is for extension development, + // where we do not persist the associations + await this.userDataProfilesMainService.setProfileForWorkspace(workspace, profile); + } + } + // Load it window.load(configuration); } - private async resolveProfileForBrowserWindow(options: IOpenBrowserWindowOptions): Promise { - let profile: IUserDataProfile | undefined; - if (this.userDataProfilesMainService.isEnabled()) { - if (options.forceProfile) { - profile = this.userDataProfilesMainService.profiles.find(p => p.name === options.forceProfile) ?? await this.userDataProfilesMainService.createNamedProfile(options.forceProfile); - } else if (options.forceTempProfile) { - profile = await this.userDataProfilesMainService.createTransientProfile(); - } + private resolveProfileForBrowserWindow(options: IOpenBrowserWindowOptions, workspace: IAnyWorkspaceIdentifier): Promise | IUserDataProfile { + if (options.forceProfile) { + return this.userDataProfilesMainService.profiles.find(p => p.name === options.forceProfile) ?? this.userDataProfilesMainService.createNamedProfile(options.forceProfile); } - if (profile) { - if (!options.cli?.extensionDevelopmentPath) { - this.userDataProfilesMainService.setProfileForWorkspaceSync(options.workspace ?? 'empty-window', profile); - } - } else { - profile = this.userDataProfilesMainService.getOrSetProfileForWorkspace(options.workspace ?? 'empty-window', this.userDataProfilesMainService.defaultProfile); + + if (options.forceTempProfile) { + return this.userDataProfilesMainService.createTransientProfile(); } - return profile; + + return this.userDataProfilesMainService.getProfileForWorkspace(workspace) ?? this.userDataProfilesMainService.defaultProfile; } private onWindowClosed(window: ICodeWindow): void { diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index d5610838ee2..172d1438518 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -140,6 +140,13 @@ export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleF return typeof singleFolderIdentifier?.id === 'string' && URI.isUri(singleFolderIdentifier.uri); } +export function isEmptyWorkspaceIdentifier(obj: unknown): obj is IEmptyWorkspaceIdentifier { + const emptyWorkspaceIdentifier = obj as IEmptyWorkspaceIdentifier | undefined; + return typeof emptyWorkspaceIdentifier?.id === 'string' + && !isSingleFolderWorkspaceIdentifier(obj) + && !isWorkspaceIdentifier(obj); +} + export const EXTENSION_DEVELOPMENT_EMPTY_WINDOW_WORKSPACE: IEmptyWorkspaceIdentifier = { id: 'ext-dev' }; export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index b9df7a24293..ef36c008a8e 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -22,7 +22,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas, connectionTokenCookieName } from 'vs/base/common/network'; -import { IAnyWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IAnyWorkspaceIdentifier, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { onUnexpectedError } from 'vs/base/common/errors'; import { setFullscreen } from 'vs/base/browser/browser'; @@ -297,8 +297,7 @@ export class BrowserMain extends Disposable { } } - const lastActiveProfile = environmentService.lastActiveProfile ? userDataProfilesService.profiles.find(p => p.id === environmentService.lastActiveProfile) : undefined; - const currentProfile = userDataProfilesService.getOrSetProfileForWorkspace(isWorkspaceIdentifier(workspace) || isSingleFolderWorkspaceIdentifier(workspace) ? workspace : 'empty-window', lastActiveProfile ?? userDataProfilesService.defaultProfile); + const currentProfile = userDataProfilesService.getProfileForWorkspace(workspace) ?? userDataProfilesService.defaultProfile; const userDataProfileService = new UserDataProfileService(currentProfile, userDataProfilesService); serviceCollection.set(IUserDataProfileService, userDataProfileService); diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index 0bc2e066433..0ae20185f0f 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -8,8 +8,8 @@ import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions, WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IAnyWorkspaceIdentifier, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -113,7 +113,7 @@ export class UserDataProfileManagementService extends Disposable implements IUse this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'switchProfile' }); } - private getWorkspaceIdentifier(): WorkspaceIdentifier { + private getWorkspaceIdentifier(): IAnyWorkspaceIdentifier { const workspace = this.workspaceContextService.getWorkspace(); switch (this.workspaceContextService.getWorkbenchState()) { case WorkbenchState.FOLDER: @@ -121,7 +121,7 @@ export class UserDataProfileManagementService extends Disposable implements IUse case WorkbenchState.WORKSPACE: return { configPath: workspace.configuration!, id: workspace.id }; } - return 'empty-window'; + return { id: workspace.id }; } private async enterProfile(profile: IUserDataProfile, preserveData: boolean, reloadMessage?: string): Promise {