diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 5c310f46e86eb..e5bdab04b3fcf 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -142,6 +142,7 @@ export const enum SyncResource { Extensions = 'extensions', GlobalState = 'globalState', Profiles = 'profiles', + WorkspaceState = 'workspaceState', } export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Tasks, SyncResource.Extensions, SyncResource.GlobalState, SyncResource.Profiles]; @@ -173,7 +174,7 @@ export interface IResourceRefHandle { created: number; } -export type ServerResource = SyncResource | 'machines' | 'editSessions'; +export type ServerResource = SyncResource | 'machines' | 'editSessions' | 'workspaceState'; export type UserDataSyncStoreType = 'insiders' | 'stable'; export const IUserDataSyncStoreManagementService = createDecorator('IUserDataSyncStoreManagementService'); @@ -359,6 +360,16 @@ export interface IGlobalState { storage: IStringDictionary; } +export interface IWorkspaceState { + folders: IWorkspaceStateFolder[]; + storage: IStringDictionary; +} + +export interface IWorkspaceStateFolder { + resourceUri: string; + workspaceFolderIdentity: string; +} + export const enum SyncStatus { Uninitialized = 'uninitialized', Idle = 'idle', diff --git a/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts b/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts index 8daa8147b1134..7086de977e6c9 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts @@ -119,6 +119,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc case SyncResource.GlobalState: return this.getGlobalStateAssociatedResources(uri, profile); case SyncResource.Extensions: return this.getExtensionsAssociatedResources(uri, profile); case SyncResource.Profiles: return this.getProfilesAssociatedResources(uri, profile); + case SyncResource.WorkspaceState: return []; } } @@ -187,6 +188,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc case SyncResource.GlobalState: return this.resolveGlobalStateNodeContent(syncData, node); case SyncResource.Extensions: return this.resolveExtensionsNodeContent(syncData, node); case SyncResource.Profiles: return this.resolveProfileNodeContent(syncData, node); + case SyncResource.WorkspaceState: return null; } } @@ -203,6 +205,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc case SyncResource.Keybindings: return null; case SyncResource.Tasks: return null; case SyncResource.Snippets: return null; + case SyncResource.WorkspaceState: return null; } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 9358953629d77..8612189e0f7ff 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -470,7 +470,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private async performActionWithProfileSynchronizer(profileSynchronizer: ProfileSynchronizer, action: (synchroniser: IUserDataSynchroniser) => Promise, disposables: DisposableStore): Promise { - const allSynchronizers = [...profileSynchronizer.enabled, ...profileSynchronizer.disabled.map(syncResource => disposables.add(profileSynchronizer.createSynchronizer(syncResource)))]; + const allSynchronizers = [...profileSynchronizer.enabled, ...profileSynchronizer.disabled.reduce<(IUserDataSynchroniser & IDisposable)[]>((synchronizers, syncResource) => { + if (syncResource !== SyncResource.WorkspaceState) { + synchronizers.push(disposables.add(profileSynchronizer.createSynchronizer(syncResource))); + } + return synchronizers; + }, [])]; for (const synchronizer of allSynchronizers) { const result = await action(synchronizer); if (!isUndefined(result)) { @@ -614,6 +619,9 @@ class ProfileSynchronizer extends Disposable { return; } } + if (syncResource === SyncResource.WorkspaceState) { + return; + } const disposables = new DisposableStore(); const synchronizer = disposables.add(this.createSynchronizer(syncResource)); disposables.add(synchronizer.onDidChangeStatus(() => this.updateStatus())); @@ -634,7 +642,7 @@ class ProfileSynchronizer extends Disposable { } } - createSynchronizer(syncResource: SyncResource): IUserDataSynchroniser & IDisposable { + createSynchronizer(syncResource: Exclude): IUserDataSynchroniser & IDisposable { switch (syncResource) { case SyncResource.Settings: return this.instantiationService.createInstance(SettingsSynchroniser, this.profile, this.collection); case SyncResource.Keybindings: return this.instantiationService.createInstance(KeybindingsSynchroniser, this.profile, this.collection); @@ -802,6 +810,7 @@ class ProfileSynchronizer extends Disposable { case SyncResource.GlobalState: return 4; case SyncResource.Extensions: return 5; case SyncResource.Profiles: return 6; + case SyncResource.WorkspaceState: return 7; } } diff --git a/src/vs/platform/workspace/browser/editSessionsStorageService.ts b/src/vs/platform/workspace/browser/editSessionsStorageService.ts deleted file mode 100644 index 83c9abd71bb80..0000000000000 --- a/src/vs/platform/workspace/browser/editSessionsStorageService.ts +++ /dev/null @@ -1,56 +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 { IDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { Registry } from 'vs/platform/registry/common/platform'; - -export interface IEditSessionContribution { - /** - * Called as part of storing an edit session. - * @returns An opaque object representing state that this contribution - * knows how to restore. Stored state will be passed back to this - * contribution when an edit session is resumed via {@link resumeState}. - */ - getStateToStore(): unknown; - - /** - * - * Called as part of resuming an edit session. - * @param state State that this contribution has previously provided in - * {@link getStateToStore}. - * @param uriResolver A handler capable of converting URIs which may have - * originated on another filesystem to URIs which exist in the current - * workspace. If no conversion is possible, e.g. because the specified - * URI bears no relation to the current workspace, this returns the original - * URI that was passed in. - */ - resumeState(state: unknown, uriResolver: (uri: URI) => URI): void; -} - -class EditSessionStateRegistryImpl { - private _registeredEditSessionContributions: Map = new Map(); - - public registerEditSessionsContribution(contributionPoint: string, editSessionsContribution: IEditSessionContribution): IDisposable { - if (this._registeredEditSessionContributions.has(contributionPoint)) { - console.warn(`Edit session contribution point with identifier ${contributionPoint} already exists`); - return { dispose: () => { } }; - } - - this._registeredEditSessionContributions.set(contributionPoint, editSessionsContribution); - return { - dispose: () => { - this._registeredEditSessionContributions.delete(contributionPoint); - } - }; - } - - public getEditSessionContributions(): [string, IEditSessionContribution][] { - return Array.from(this._registeredEditSessionContributions.entries()); - } -} - -Registry.add('editSessionStateRegistry', new EditSessionStateRegistryImpl()); -export const EditSessionRegistry: EditSessionStateRegistryImpl = Registry.as('editSessionStateRegistry'); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 072aaa8c586fc..406edde8e45f1 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -15,13 +15,13 @@ import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; -import { basename, isEqualOrParent, joinPath, relativePath } from 'vs/base/common/resources'; +import { basename, joinPath, relativePath } from 'vs/base/common/resources'; import { encodeBase64 } from 'vs/base/common/buffer'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; import { EditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncErrorCode, UserDataSyncStoreError, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { getFileNamesMessage, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -62,7 +62,12 @@ import { CancellationError } from 'vs/base/common/errors'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IExtensionsViewPaneContainer, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { EditSessionRegistry } from 'vs/platform/workspace/browser/editSessionsStorageService'; +import { WorkspaceStateSynchroniser } from 'vs/workbench/contrib/editSessions/common/workspaceStateSync'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { EditSessionsStoreClient } from 'vs/workbench/contrib/editSessions/common/editSessionsStorageClient'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IWorkspaceIdentityService } from 'vs/workbench/services/workspaces/common/workspaceIdentityService'; registerSingleton(IEditSessionsLogService, EditSessionsLogService, InstantiationType.Delayed); registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService, InstantiationType.Delayed); @@ -121,6 +126,9 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo private registeredCommands = new Set(); + private workspaceStateSynchronizer: IUserDataSynchroniser | undefined; + private editSessionsStorageClient: EditSessionsStoreClient | undefined; + constructor( @IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService, @IFileService private readonly fileService: IFileService, @@ -147,17 +155,29 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo @IEditorService private readonly editorService: IEditorService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IExtensionService private readonly extensionService: IExtensionService, + @IRequestService private readonly requestService: IRequestService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IWorkspaceIdentityService private readonly workspaceIdentityService: IWorkspaceIdentityService, ) { super(); + this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService); + + if (!this.productService['editSessions.store']?.url) { + return; + } + + this.editSessionsStorageClient = new EditSessionsStoreClient(URI.parse(this.productService['editSessions.store'].url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); + this.editSessionsStorageService.storeClient = this.editSessionsStorageClient; + this.workspaceStateSynchronizer = new WorkspaceStateSynchroniser(this.userDataProfilesService.defaultProfile, undefined, this.editSessionsStorageClient, this.logService, this.fileService, this.environmentService, this.telemetryService, this.configurationService, this.storageService, this.uriIdentityService, this.workspaceIdentityService, this.editSessionsStorageService); + this.autoResumeEditSession(); this.registerActions(); this.registerViews(); this.registerContributedEditSessionOptions(); - this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService); - this._register(this.fileService.registerProvider(EditSessionsFileSystemProvider.SCHEMA, new EditSessionsFileSystemProvider(this.editSessionsStorageService))); this.lifecycleService.onWillShutdown((e) => { if (e.reason !== ShutdownReason.RELOAD && this.editSessionsStorageService.isSignedIn && this.configurationService.getValue('workbench.experimental.cloudChanges.autoStore') === 'onShutdown' && !isWeb) { @@ -482,7 +502,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo performance.mark('code/willResumeEditSessionFromIdentifier'); progress?.report({ message: localize('checkingForWorkingChanges', 'Checking for pending cloud changes...') }); - const data = serializedData ? { editSession: JSON.parse(serializedData), ref: '' } : await this.editSessionsStorageService.read(ref); + const data = serializedData ? { content: serializedData, ref: '' } : await this.editSessionsStorageService.read('editSessions', ref); if (!data) { if (ref === undefined && !silent) { this.notificationService.info(localize('no cloud changes', 'There are no changes to resume from the cloud.')); @@ -494,7 +514,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } progress?.report({ message: resumeProgressOptionsTitle }); - const editSession = data.editSession; + const editSession = JSON.parse(data.content); ref = data.ref; if (editSession.version > EditSessionSchemaVersion) { @@ -504,8 +524,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } try { - const { changes, conflictingChanges, contributedStateHandlers } = await this.generateChanges(editSession, ref, forceApplyUnrelatedChange, applyPartialMatch); - if (changes.length === 0 && contributedStateHandlers.length === 0) { + const { changes, conflictingChanges } = await this.generateChanges(editSession, ref, forceApplyUnrelatedChange, applyPartialMatch); + if (changes.length === 0) { return; } @@ -534,12 +554,10 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } } - for (const handleContributedState of contributedStateHandlers) { - handleContributedState(); - } + await this.workspaceStateSynchronizer?.apply(false, {}); this.logService.info(`Deleting edit session with ref ${ref} after successfully applying it to current workspace...`); - await this.editSessionsStorageService.delete(ref); + await this.editSessionsStorageService.delete('editSessions', ref); this.logService.info(`Deleted edit session with ref ${ref}.`); this.telemetryService.publicLog2('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'resumeSucceeded' }); @@ -553,7 +571,6 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo private async generateChanges(editSession: EditSession, ref: string, forceApplyUnrelatedChange = false, applyPartialMatch = false) { const changes: ({ uri: URI; type: ChangeType; contents: string | undefined })[] = []; - const contributedStateHandlers: (() => void)[] = []; const conflictingChanges = []; const workspaceFolders = this.contextService.getWorkspace().folders; const cancellationTokenSource = new CancellationTokenSource(); @@ -623,44 +640,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } } - const incomingFolderUrisToIdentifiers = new Map(); - for (const folder of editSession.folders) { - const { canonicalIdentity } = folder; - for (const workspaceFolder of workspaceFolders) { - const identity = await this.editSessionIdentityService.getEditSessionIdentifier(workspaceFolder, cancellationTokenSource.token); - if (!identity || !canonicalIdentity || !folder.absoluteUri) { - continue; - } - const match = identity === canonicalIdentity - ? EditSessionIdentityMatch.Complete - : await this.editSessionIdentityService.provideEditSessionIdentityMatch(workspaceFolder, identity, canonicalIdentity, cancellationTokenSource.token); - if (!match) { - continue; - } - incomingFolderUrisToIdentifiers.set(folder.absoluteUri.toString(), [workspaceFolder.uri.toString(), match]); - } - } - - EditSessionRegistry.getEditSessionContributions().forEach(([key, contrib]) => { - const state = editSession.state[key]; - if (state) { - contributedStateHandlers.push(() => contrib.resumeState(state, (incomingUri: URI) => { - for (const absoluteUri of incomingFolderUrisToIdentifiers.keys()) { - if (isEqualOrParent(incomingUri, URI.parse(absoluteUri))) { - const [workspaceFolderUri, match] = incomingFolderUrisToIdentifiers.get(absoluteUri)!; - if (match === EditSessionIdentityMatch.Complete) { - const relativeFilePath = relativePath(URI.parse(absoluteUri), incomingUri); - return relativeFilePath ? joinPath(URI.parse(workspaceFolderUri), relativeFilePath) : incomingUri; - } - - } - } - return incomingUri; - })); - } - }); - - return { changes, conflictingChanges, contributedStateHandlers }; + return { changes, conflictingChanges }; } private async willChangeLocalContents(localChanges: Set, uriWithIncomingChanges: URI, incomingChange: Change) { @@ -747,11 +727,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo folders.push({ workingChanges, name: name ?? '', canonicalIdentity: canonicalIdentity ?? undefined, absoluteUri: workspaceFolder?.uri.toString() }); } - // Look through all registered contributions to gather additional state - const contributedData: { [key: string]: unknown } = {}; - EditSessionRegistry.getEditSessionContributions().forEach(([key, contrib]) => { - contributedData[key] = contrib.getStateToStore(); - }); + // Store contributed workspace state + await this.workspaceStateSynchronizer?.sync(null, {}); if (!hasEdits) { this.logService.info('Skipped storing working changes in the cloud as there are no edits to store.'); @@ -761,11 +738,11 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo return undefined; } - const data: EditSession = { folders, version: 2, state: contributedData }; + const data: EditSession = { folders, version: 2 }; try { this.logService.info(`Storing edit session...`); - const ref = await this.editSessionsStorageService.write(data); + const ref = await this.editSessionsStorageService.write('editSessions', data); this.logService.info(`Stored edit session with ref ${ref}.`); return ref; } catch (ex) { diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts index 47b1ca77bb6ca..2410ed5501619 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts @@ -7,7 +7,7 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; -import { ChangeType, decodeEditSessionFileContent, EDIT_SESSIONS_SCHEME, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { ChangeType, decodeEditSessionFileContent, EDIT_SESSIONS_SCHEME, EditSession, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { NotSupportedError } from 'vs/base/common/errors'; export class EditSessionsFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability { @@ -26,15 +26,16 @@ export class EditSessionsFileSystemProvider implements IFileSystemProviderWithFi throw FileSystemProviderErrorCode.FileNotFound; } const { ref, folderName, filePath } = match.groups; - const data = await this.editSessionsStorageService.read(ref); + const data = await this.editSessionsStorageService.read('editSessions', ref); if (!data) { throw FileSystemProviderErrorCode.FileNotFound; } - const change = data?.editSession.folders.find((f) => f.name === folderName)?.workingChanges.find((change) => change.relativeFilePath === filePath); + const content: EditSession = JSON.parse(data.content); + const change = content.folders.find((f) => f.name === folderName)?.workingChanges.find((change) => change.relativeFilePath === filePath); if (!change || change.type === ChangeType.Deletion) { throw FileSystemProviderErrorCode.FileNotFound; } - return decodeEditSessionFileContent(data.editSession.version, change.contents).buffer; + return decodeEditSessionFileContent(content.version, change.contents).buffer; } async stat(resource: URI): Promise { diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 1d4cd88a8cf35..7f2eff58d9168 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -12,13 +11,11 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; -import { IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createSyncHeaders, IAuthenticationProvider, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; -import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService, SyncResource } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { generateUuid } from 'vs/base/common/uuid'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; @@ -27,6 +24,7 @@ import { isWeb } from 'vs/base/common/platform'; import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; import { Emitter } from 'vs/base/common/event'; import { CancellationError } from 'vs/base/common/errors'; +import { EditSessionsStoreClient } from 'vs/workbench/contrib/editSessions/common/editSessionsStorageClient'; type ExistingSession = IQuickPickItem & { session: AuthenticationSession & { providerId: string } }; type AuthenticationProviderOption = IQuickPickItem & { provider: IAuthenticationProvider }; @@ -38,7 +36,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes public readonly SIZE_LIMIT = 1024 * 1024 * 2; // 2 MB private serverConfiguration = this.productService['editSessions.store']; - private storeClient: EditSessionsStoreClient | undefined; private machineClient: IUserDataSyncMachinesService | undefined; #authenticationInfo: { sessionId: string; token: string; providerId: string } | undefined; @@ -61,6 +58,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes return this._didSignOut.event; } + storeClient: EditSessionsStoreClient | undefined; // TODO@joyceerhl lifecycle hack + constructor( @IFileService private readonly fileService: IFileService, @IStorageService private readonly storageService: IStorageService, @@ -71,7 +70,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes @IEditSessionsLogService private readonly logService: IEditSessionsLogService, @IProductService private readonly productService: IProductService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IRequestService private readonly requestService: IRequestService, @IDialogService private readonly dialogService: IDialogService, @ICredentialsService private readonly credentialsService: ICredentialsService ) { @@ -95,17 +93,17 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes * @param editSession An object representing edit session state to be restored. * @returns The ref of the stored edit session state. */ - async write(editSession: EditSession): Promise { + async write(resource: SyncResource, content: string | EditSession): Promise { await this.initialize(false); if (!this.initialized) { throw new Error('Please sign in to store your edit session.'); } - if (editSession.machine === undefined) { - editSession.machine = await this.getOrCreateCurrentMachineId(); + if (typeof content !== 'string' && content.machine === undefined) { + content.machine = await this.getOrCreateCurrentMachineId(); } - return this.storeClient!.writeResource('editSessions', JSON.stringify(editSession), null, undefined, createSyncHeaders(generateUuid())); + return this.storeClient!.writeResource(resource, typeof content === 'string' ? content : JSON.stringify(content), null, undefined, createSyncHeaders(generateUuid())); } /** @@ -114,7 +112,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes * * @returns An object representing the requested or latest edit session state, if any. */ - async read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined> { + async read(resource: SyncResource, ref: string | undefined): Promise<{ ref: string; content: string } | undefined> { await this.initialize(false); if (!this.initialized) { throw new Error('Please sign in to apply your latest edit session.'); @@ -124,9 +122,9 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes const headers = createSyncHeaders(generateUuid()); try { if (ref !== undefined) { - content = await this.storeClient?.resolveResourceContent('editSessions', ref, undefined, headers); + content = await this.storeClient?.resolveResourceContent(resource, ref, undefined, headers); } else { - const result = await this.storeClient?.readResource('editSessions', null, undefined, headers); + const result = await this.storeClient?.readResource(resource, null, undefined, headers); content = result?.content; ref = result?.ref; } @@ -135,30 +133,30 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } // TODO@joyceerhl Validate session data, check schema version - return (content !== undefined && content !== null && ref !== undefined) ? { ref: ref, editSession: JSON.parse(content) } : undefined; + return (content !== undefined && content !== null && ref !== undefined) ? { ref, content } : undefined; } - async delete(ref: string | null) { + async delete(resource: SyncResource, ref: string | null) { await this.initialize(false); if (!this.initialized) { throw new Error(`Unable to delete edit session with ref ${ref}.`); } try { - await this.storeClient?.deleteResource('editSessions', ref); + await this.storeClient?.deleteResource(resource, ref); } catch (ex) { this.logService.error(ex); } } - async list(): Promise { + async list(resource: SyncResource): Promise { await this.initialize(false); if (!this.initialized) { throw new Error(`Unable to list edit sessions.`); } try { - return this.storeClient?.getAllResourceRefs('editSessions') ?? []; + return this.storeClient?.getAllResourceRefs(resource) ?? []; } catch (ex) { this.logService.error(ex); } @@ -193,14 +191,15 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes throw new Error('Unable to initialize sessions sync as session sync preference is not configured in product.json.'); } - if (!this.storeClient) { - this.storeClient = new EditSessionsStoreClient(URI.parse(this.serverConfiguration.url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); - this._register(this.storeClient.onTokenFailed(() => { - this.logService.info('Clearing edit sessions authentication preference because of successive token failures.'); - this.clearAuthenticationPreference(); - })); + if (this.storeClient === undefined) { + return false; } + this._register(this.storeClient.onTokenFailed(() => { + this.logService.info('Clearing edit sessions authentication preference because of successive token failures.'); + this.clearAuthenticationPreference(); + })); + if (this.machineClient === undefined) { this.machineClient = new UserDataSyncMachinesService(this.environmentService, this.fileService, this.storageService, this.storeClient!, this.logService, this.productService); } @@ -499,7 +498,3 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes })); } } - -class EditSessionsStoreClient extends UserDataSyncStoreClient { - _serviceBrand: any; -} diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts index 21eb04f24d73c..a043ec34b849d 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts @@ -10,7 +10,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { Registry } from 'vs/platform/registry/common/platform'; import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewsRegistry, TreeItemCollapsibleState, TreeViewItemHandleArg, ViewContainer } from 'vs/workbench/common/views'; -import { ChangeType, EDIT_SESSIONS_DATA_VIEW_ID, EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_TITLE, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { ChangeType, EDIT_SESSIONS_DATA_VIEW_ID, EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_TITLE, EditSession, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { URI } from 'vs/base/common/uri'; import { fromNow } from 'vs/base/common/date'; import { Codicon } from 'vs/base/common/codicons'; @@ -131,7 +131,7 @@ export class EditSessionsDataViews extends Disposable { title: EDIT_SESSIONS_TITLE }); if (result.confirmed) { - await editSessionStorageService.delete(editSessionId); + await editSessionStorageService.delete('editSessions', editSessionId); await treeView.refresh(); } } @@ -160,7 +160,7 @@ export class EditSessionsDataViews extends Disposable { title: EDIT_SESSIONS_TITLE }); if (result.confirmed) { - await editSessionStorageService.delete(null); + await editSessionStorageService.delete('editSessions', null); await treeView.refresh(); } } @@ -198,15 +198,19 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { } private async getAllEditSessions(): Promise { - const allEditSessions = await this.editSessionsStorageService.list(); + const allEditSessions = await this.editSessionsStorageService.list('editSessions'); this.editSessionsCount.set(allEditSessions.length); const editSessions = []; for (const session of allEditSessions) { const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${session.ref}` }); - const sessionData = await this.editSessionsStorageService.read(session.ref); - const label = sessionData?.editSession.folders.map((folder) => folder.name).join(', ') ?? session.ref; - const machineId = sessionData?.editSession.machine; + const sessionData = await this.editSessionsStorageService.read('editSessions', session.ref); + if (!sessionData) { + continue; + } + const content: EditSession = JSON.parse(sessionData.content); + const label = content.folders.map((folder) => folder.name).join(', ') ?? session.ref; + const machineId = content.machine; const machineName = machineId ? await this.editSessionsStorageService.getMachineById(machineId) : undefined; const description = machineName === undefined ? fromNow(session.created, true) : `${fromNow(session.created, true)}\u00a0\u00a0\u2022\u00a0\u00a0${machineName}`; @@ -224,18 +228,19 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { } private async getEditSession(ref: string): Promise { - const data = await this.editSessionsStorageService.read(ref); + const data = await this.editSessionsStorageService.read('editSessions', ref); if (!data) { return []; } + const content: EditSession = JSON.parse(data.content); - if (data.editSession.folders.length === 1) { - const folder = data.editSession.folders[0]; + if (content.folders.length === 1) { + const folder = content.folders[0]; return this.getEditSessionFolderContents(ref, folder.name); } - return data.editSession.folders.map((folder) => { + return content.folders.map((folder) => { const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folder.name}` }); return { handle: resource.toString(), @@ -247,14 +252,15 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { } private async getEditSessionFolderContents(ref: string, folderName: string): Promise { - const data = await this.editSessionsStorageService.read(ref); + const data = await this.editSessionsStorageService.read('editSessions', ref); if (!data) { return []; } + const content: EditSession = JSON.parse(data.content); const currentWorkspaceFolder = this.workspaceContextService.getWorkspace().folders.find((folder) => folder.name === folderName); - const editSessionFolder = data.editSession.folders.find((folder) => folder.name === folderName); + const editSessionFolder = content.folders.find((folder) => folder.name === folderName); if (!editSessionFolder) { return []; diff --git a/src/vs/workbench/contrib/editSessions/common/editSessions.ts b/src/vs/workbench/contrib/editSessions/common/editSessions.ts index 17ef4749019a5..a8eb11fa6c8ae 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessions.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessions.ts @@ -14,12 +14,15 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { StringSHA1 } from 'vs/base/common/hash'; +import { EditSessionsStoreClient } from 'vs/workbench/contrib/editSessions/common/editSessionsStorageClient'; export const EDIT_SESSION_SYNC_CATEGORY: ILocalizedString = { original: 'Cloud Changes', value: localize('cloud changes', 'Cloud Changes') }; +export type SyncResource = 'editSessions' | 'workspaceState'; + export const IEditSessionsStorageService = createDecorator('IEditSessionsStorageService'); export interface IEditSessionsStorageService { _serviceBrand: undefined; @@ -30,11 +33,13 @@ export interface IEditSessionsStorageService { readonly onDidSignIn: Event; readonly onDidSignOut: Event; + storeClient: EditSessionsStoreClient | undefined; + initialize(silent?: boolean): Promise; - read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined>; - write(editSession: EditSession): Promise; - delete(ref: string | null): Promise; - list(): Promise; + read(resource: SyncResource, ref: string | undefined): Promise<{ ref: string; content: string } | undefined>; + write(resource: SyncResource, content: string | EditSession): Promise; + delete(resource: SyncResource, ref: string | null): Promise; + list(resource: SyncResource): Promise; getMachineById(machineId: string): Promise; } @@ -79,7 +84,6 @@ export interface EditSession { version: number; machine?: string; folders: Folder[]; - state: { [key: string]: unknown }; } export const EDIT_SESSIONS_SIGNED_IN_KEY = 'editSessionsSignedIn'; diff --git a/src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts b/src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts new file mode 100644 index 0000000000000..d573fe7897f48 --- /dev/null +++ b/src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; + +export class EditSessionsStoreClient extends UserDataSyncStoreClient { + _serviceBrand: any; +} diff --git a/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts b/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts new file mode 100644 index 0000000000000..5473144123b45 --- /dev/null +++ b/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { Emitter, Event } from 'vs/base/common/event'; +import { parse, stringify } from 'vs/base/common/marshalling'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IRemoteUserData, IResourceRefHandle, IUserDataSyncBackupStoreService, IUserDataSyncConfiguration, IUserDataSyncEnablementService, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSynchroniser, IWorkspaceState, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { IWorkspaceIdentityService } from 'vs/workbench/services/workspaces/common/workspaceIdentityService'; + + +class NullBackupStoreService implements IUserDataSyncBackupStoreService { + _serviceBrand: undefined; + async backup(profile: IUserDataProfile, resource: SyncResource, content: string): Promise { + return; + } + async getAllRefs(profile: IUserDataProfile, resource: SyncResource): Promise { + return []; + } + async resolveContent(profile: IUserDataProfile, resource: SyncResource, ref: string): Promise { + return null; + } + +} + +class NullEnablementService implements IUserDataSyncEnablementService { + _serviceBrand: any; + + private _onDidChangeEnablement = new Emitter(); + readonly onDidChangeEnablement: Event = this._onDidChangeEnablement.event; + + private _onDidChangeResourceEnablement = new Emitter<[SyncResource, boolean]>(); + readonly onDidChangeResourceEnablement: Event<[SyncResource, boolean]> = this._onDidChangeResourceEnablement.event; + + isEnabled(): boolean { return true; } + canToggleEnablement(): boolean { return true; } + setEnablement(_enabled: boolean): void { } + isResourceEnabled(_resource: SyncResource): boolean { return true; } + setResourceEnablement(_resource: SyncResource, _enabled: boolean): void { } + getResourceSyncStateVersion(_resource: SyncResource): string | undefined { return undefined; } + +} + +export class WorkspaceStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { + protected override version: number = 1; + + constructor( + profile: IUserDataProfile, + collection: string | undefined, + userDataSyncStoreService: IUserDataSyncStoreService, + logService: IUserDataSyncLogService, + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, + @IStorageService storageService: IStorageService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IWorkspaceIdentityService private readonly workspaceIdentityService: IWorkspaceIdentityService, + @IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService, + ) { + const userDataSyncBackupStoreService = new NullBackupStoreService(); + const userDataSyncEnablementService = new NullEnablementService(); + super({ syncResource: SyncResource.WorkspaceState, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService); + } + + override async sync(): Promise { + const cancellationTokenSource = new CancellationTokenSource(); + const folders = await this.workspaceIdentityService.getWorkspaceStateFolders(cancellationTokenSource.token); + if (!folders.length) { + return; + } + + const keys = this.storageService.keys(StorageScope.WORKSPACE, StorageTarget.USER); + if (!keys.length) { + return; + } + + const contributedData: IStringDictionary = {}; + keys.forEach((key) => { + const data = this.storageService.get(key, StorageScope.WORKSPACE); + if (data) { + contributedData[key] = data; + } + }); + + const content: IWorkspaceState = { folders, storage: contributedData }; + this.editSessionsStorageService.write('workspaceState', stringify(content)); + } + + protected override async applyResult(remoteUserData: IRemoteUserData): Promise { + const cancellationTokenSource = new CancellationTokenSource(); + const remoteWorkspaceState: IWorkspaceState = remoteUserData.syncData ? parse(remoteUserData.syncData.content) : null; + if (!remoteWorkspaceState) { + this.logService.info('Skipping initializing workspace state because remote workspace state does not exist.'); + return; + } + + const storage: IStringDictionary = {}; + for (const key of Object.keys(remoteWorkspaceState.storage)) { + storage[key] = remoteWorkspaceState.storage[key]; + } + + if (Object.keys(storage).length) { + // Evaluate whether storage is applicable for current workspace + const replaceUris = await this.workspaceIdentityService.matches(remoteWorkspaceState.folders, cancellationTokenSource.token); + // If so, initialize storage with remote storage + for (const key of Object.keys(storage)) { + // Deserialize the stored state + try { + const value = parse(storage[key]); + // Run URI conversion on the stored state + replaceUris(value); + // Write the stored state to the storage service + this.storageService.store(key, value, StorageScope.WORKSPACE, StorageTarget.USER); + } catch { + this.storageService.store(key, storage[key], StorageScope.WORKSPACE, StorageTarget.USER); + } + } + } + } + + protected override async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration, token: CancellationToken): Promise { + return []; + } + protected override getMergeResult(resourcePreview: IResourcePreview, token: CancellationToken): Promise { + throw new Error('Method not implemented.'); + } + protected override getAcceptResult(resourcePreview: IResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise { + throw new Error('Method not implemented.'); + } + protected override async hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise { + return true; + } + override async hasLocalData(): Promise { + return false; + } + override async resolveContent(uri: URI): Promise { + return null; + } +} diff --git a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index 15efba18fcf56..c4f3e61cfd548 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -43,6 +43,13 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { IWorkspaceIdentityService, WorkspaceIdentityService } from 'vs/workbench/services/workspaces/common/workspaceIdentityService'; const folderName = 'test-folder'; const folderUri = URI.file(`/${folderName}`); @@ -73,6 +80,9 @@ suite('Edit session sync', () => { override onWillShutdown = Event.None; }); instantiationService.stub(INotificationService, new TestNotificationService()); + instantiationService.stub(IProductService, >{ 'editSessions.store': { url: 'https://test.com', canSwitch: true, authenticationProviders: {} } }); + instantiationService.stub(IStorageService, new TestStorageService()); + instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); instantiationService.stub(IEditSessionsStorageService, new class extends mock() { override onDidSignIn = Event.None; override onDidSignOut = Event.None; @@ -137,6 +147,22 @@ suite('Edit session sync', () => { return 'test-identity'; } }); + instantiationService.set(IWorkspaceIdentityService, instantiationService.createInstance(WorkspaceIdentityService)); + instantiationService.stub(IUserDataProfilesService, new class extends mock() { + override defaultProfile = { + id: 'default', + name: 'Default', + isDefault: true, + location: URI.file('location'), + globalStorageHome: URI.file('globalStorageHome'), + settingsResource: URI.file('settingsResource'), + keybindingsResource: URI.file('keybindingsResource'), + tasksResource: URI.file('tasksResource'), + snippetsHome: URI.file('snippetsHome'), + extensionsResource: URI.file('extensionsResource'), + cacheHome: URI.file('cacheHome'), + }; + }); editSessionsContribution = instantiationService.createInstance(EditSessionsContribution); }); @@ -167,7 +193,7 @@ suite('Edit session sync', () => { }; // Stub sync service to return edit session data - const readStub = sandbox.stub().returns({ editSession, ref: '0' }); + const readStub = sandbox.stub().returns({ content: JSON.stringify(editSession), ref: '0' }); instantiationService.stub(IEditSessionsStorageService, 'read', readStub); // Create root folder diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index 2b412374d3e11..9a709322d3423 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -55,6 +55,7 @@ export function getSyncAreaLabel(source: SyncResource): string { case SyncResource.Extensions: return localize('extensions', "Extensions"); case SyncResource.GlobalState: return localize('ui state label', "UI State"); case SyncResource.Profiles: return localize('profiles', "Profiles"); + case SyncResource.WorkspaceState: return localize('workspace state label', "Workspace State"); } } diff --git a/src/vs/workbench/services/workspaces/common/workspaceIdentityService.ts b/src/vs/workbench/services/workspaces/common/workspaceIdentityService.ts index c97207fa0cea6..921bca90518bc 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceIdentityService.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceIdentityService.ts @@ -9,18 +9,15 @@ import { isEqualOrParent, joinPath, relativePath } from 'vs/base/common/resource import { URI } from 'vs/base/common/uri'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceStateFolder } from 'vs/platform/userDataSync/common/userDataSync'; import { EditSessionIdentityMatch, IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -export interface IWorkspaceStateFolder { - resourceUri: string; - workspaceFolderIdentity: string; -} - export const IWorkspaceIdentityService = createDecorator('IWorkspaceIdentityService'); export interface IWorkspaceIdentityService { _serviceBrand: undefined; matches(folders: IWorkspaceStateFolder[], cancellationToken: CancellationToken): Promise<(obj: any) => any>; + getWorkspaceStateFolders(cancellationToken: CancellationToken): Promise; } export class WorkspaceIdentityService implements IWorkspaceIdentityService { @@ -31,6 +28,18 @@ export class WorkspaceIdentityService implements IWorkspaceIdentityService { @IEditSessionIdentityService private readonly editSessionIdentityService: IEditSessionIdentityService ) { } + async getWorkspaceStateFolders(cancellationToken: CancellationToken): Promise { + const workspaceStateFolders: IWorkspaceStateFolder[] = []; + + for (const workspaceFolder of this.workspaceContextService.getWorkspace().folders) { + const workspaceFolderIdentity = await this.editSessionIdentityService.getEditSessionIdentifier(workspaceFolder, cancellationToken); + if (!workspaceFolderIdentity) { continue; } + workspaceStateFolders.push({ resourceUri: workspaceFolder.uri.toString(), workspaceFolderIdentity }); + } + + return workspaceStateFolders; + } + async matches(incomingWorkspaceFolders: IWorkspaceStateFolder[], cancellationToken: CancellationToken): Promise<(value: any) => any> { const incomingToCurrentWorkspaceFolderUris: { [key: string]: string } = {}; @@ -123,6 +132,8 @@ export class WorkspaceIdentityService implements IWorkspaceIdentityService { } } } + + return obj; }; return uriReplacer;