Skip to content

Commit 6189b6f

Browse files
authored
Implement workspaceState sync (microsoft#182107)
Implement workspaceState sync
1 parent cc244e0 commit 6189b6f

File tree

14 files changed

+324
-174
lines changed

14 files changed

+324
-174
lines changed

src/vs/platform/userDataSync/common/userDataSync.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export const enum SyncResource {
142142
Extensions = 'extensions',
143143
GlobalState = 'globalState',
144144
Profiles = 'profiles',
145+
WorkspaceState = 'workspaceState',
145146
}
146147
export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Tasks, SyncResource.Extensions, SyncResource.GlobalState, SyncResource.Profiles];
147148

@@ -173,7 +174,7 @@ export interface IResourceRefHandle {
173174
created: number;
174175
}
175176

176-
export type ServerResource = SyncResource | 'machines' | 'editSessions';
177+
export type ServerResource = SyncResource | 'machines' | 'editSessions' | 'workspaceState';
177178
export type UserDataSyncStoreType = 'insiders' | 'stable';
178179

179180
export const IUserDataSyncStoreManagementService = createDecorator<IUserDataSyncStoreManagementService>('IUserDataSyncStoreManagementService');
@@ -359,6 +360,16 @@ export interface IGlobalState {
359360
storage: IStringDictionary<IStorageValue>;
360361
}
361362

363+
export interface IWorkspaceState {
364+
folders: IWorkspaceStateFolder[];
365+
storage: IStringDictionary<string>;
366+
}
367+
368+
export interface IWorkspaceStateFolder {
369+
resourceUri: string;
370+
workspaceFolderIdentity: string;
371+
}
372+
362373
export const enum SyncStatus {
363374
Uninitialized = 'uninitialized',
364375
Idle = 'idle',

src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc
119119
case SyncResource.GlobalState: return this.getGlobalStateAssociatedResources(uri, profile);
120120
case SyncResource.Extensions: return this.getExtensionsAssociatedResources(uri, profile);
121121
case SyncResource.Profiles: return this.getProfilesAssociatedResources(uri, profile);
122+
case SyncResource.WorkspaceState: return [];
122123
}
123124
}
124125

@@ -187,6 +188,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc
187188
case SyncResource.GlobalState: return this.resolveGlobalStateNodeContent(syncData, node);
188189
case SyncResource.Extensions: return this.resolveExtensionsNodeContent(syncData, node);
189190
case SyncResource.Profiles: return this.resolveProfileNodeContent(syncData, node);
191+
case SyncResource.WorkspaceState: return null;
190192
}
191193
}
192194

@@ -203,6 +205,7 @@ export class UserDataSyncResourceProviderService implements IUserDataSyncResourc
203205
case SyncResource.Keybindings: return null;
204206
case SyncResource.Tasks: return null;
205207
case SyncResource.Snippets: return null;
208+
case SyncResource.WorkspaceState: return null;
206209
}
207210
}
208211

src/vs/platform/userDataSync/common/userDataSyncService.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
470470
}
471471

472472
private async performActionWithProfileSynchronizer<T>(profileSynchronizer: ProfileSynchronizer, action: (synchroniser: IUserDataSynchroniser) => Promise<T | undefined>, disposables: DisposableStore): Promise<T | undefined> {
473-
const allSynchronizers = [...profileSynchronizer.enabled, ...profileSynchronizer.disabled.map(syncResource => disposables.add(profileSynchronizer.createSynchronizer(syncResource)))];
473+
const allSynchronizers = [...profileSynchronizer.enabled, ...profileSynchronizer.disabled.reduce<(IUserDataSynchroniser & IDisposable)[]>((synchronizers, syncResource) => {
474+
if (syncResource !== SyncResource.WorkspaceState) {
475+
synchronizers.push(disposables.add(profileSynchronizer.createSynchronizer(syncResource)));
476+
}
477+
return synchronizers;
478+
}, [])];
474479
for (const synchronizer of allSynchronizers) {
475480
const result = await action(synchronizer);
476481
if (!isUndefined(result)) {
@@ -614,6 +619,9 @@ class ProfileSynchronizer extends Disposable {
614619
return;
615620
}
616621
}
622+
if (syncResource === SyncResource.WorkspaceState) {
623+
return;
624+
}
617625
const disposables = new DisposableStore();
618626
const synchronizer = disposables.add(this.createSynchronizer(syncResource));
619627
disposables.add(synchronizer.onDidChangeStatus(() => this.updateStatus()));
@@ -634,7 +642,7 @@ class ProfileSynchronizer extends Disposable {
634642
}
635643
}
636644

637-
createSynchronizer(syncResource: SyncResource): IUserDataSynchroniser & IDisposable {
645+
createSynchronizer(syncResource: Exclude<SyncResource, SyncResource.WorkspaceState>): IUserDataSynchroniser & IDisposable {
638646
switch (syncResource) {
639647
case SyncResource.Settings: return this.instantiationService.createInstance(SettingsSynchroniser, this.profile, this.collection);
640648
case SyncResource.Keybindings: return this.instantiationService.createInstance(KeybindingsSynchroniser, this.profile, this.collection);
@@ -802,6 +810,7 @@ class ProfileSynchronizer extends Disposable {
802810
case SyncResource.GlobalState: return 4;
803811
case SyncResource.Extensions: return 5;
804812
case SyncResource.Profiles: return 6;
813+
case SyncResource.WorkspaceState: return 7;
805814
}
806815
}
807816

src/vs/platform/workspace/browser/editSessionsStorageService.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts

Lines changed: 36 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm
1515
import { IFileService } from 'vs/platform/files/common/files';
1616
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
1717
import { URI } from 'vs/base/common/uri';
18-
import { basename, isEqualOrParent, joinPath, relativePath } from 'vs/base/common/resources';
18+
import { basename, joinPath, relativePath } from 'vs/base/common/resources';
1919
import { encodeBase64 } from 'vs/base/common/buffer';
2020
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2121
import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress';
2222
import { EditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService';
2323
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
24-
import { UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync';
24+
import { UserDataSyncErrorCode, UserDataSyncStoreError, IUserDataSynchroniser } from 'vs/platform/userDataSync/common/userDataSync';
2525
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2626
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
2727
import { getFileNamesMessage, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
@@ -62,7 +62,12 @@ import { CancellationError } from 'vs/base/common/errors';
6262
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
6363
import { IExtensionsViewPaneContainer, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
6464
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
65-
import { EditSessionRegistry } from 'vs/platform/workspace/browser/editSessionsStorageService';
65+
import { WorkspaceStateSynchroniser } from 'vs/workbench/contrib/editSessions/common/workspaceStateSync';
66+
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
67+
import { IRequestService } from 'vs/platform/request/common/request';
68+
import { EditSessionsStoreClient } from 'vs/workbench/contrib/editSessions/common/editSessionsStorageClient';
69+
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
70+
import { IWorkspaceIdentityService } from 'vs/workbench/services/workspaces/common/workspaceIdentityService';
6671

6772
registerSingleton(IEditSessionsLogService, EditSessionsLogService, InstantiationType.Delayed);
6873
registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService, InstantiationType.Delayed);
@@ -121,6 +126,9 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
121126

122127
private registeredCommands = new Set<string>();
123128

129+
private workspaceStateSynchronizer: IUserDataSynchroniser | undefined;
130+
private editSessionsStorageClient: EditSessionsStoreClient | undefined;
131+
124132
constructor(
125133
@IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService,
126134
@IFileService private readonly fileService: IFileService,
@@ -147,17 +155,29 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
147155
@IEditorService private readonly editorService: IEditorService,
148156
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
149157
@IExtensionService private readonly extensionService: IExtensionService,
158+
@IRequestService private readonly requestService: IRequestService,
159+
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
160+
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
161+
@IWorkspaceIdentityService private readonly workspaceIdentityService: IWorkspaceIdentityService,
150162
) {
151163
super();
152164

165+
this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService);
166+
167+
if (!this.productService['editSessions.store']?.url) {
168+
return;
169+
}
170+
171+
this.editSessionsStorageClient = new EditSessionsStoreClient(URI.parse(this.productService['editSessions.store'].url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService);
172+
this.editSessionsStorageService.storeClient = this.editSessionsStorageClient;
173+
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);
174+
153175
this.autoResumeEditSession();
154176

155177
this.registerActions();
156178
this.registerViews();
157179
this.registerContributedEditSessionOptions();
158180

159-
this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService);
160-
161181
this._register(this.fileService.registerProvider(EditSessionsFileSystemProvider.SCHEMA, new EditSessionsFileSystemProvider(this.editSessionsStorageService)));
162182
this.lifecycleService.onWillShutdown((e) => {
163183
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
482502
performance.mark('code/willResumeEditSessionFromIdentifier');
483503

484504
progress?.report({ message: localize('checkingForWorkingChanges', 'Checking for pending cloud changes...') });
485-
const data = serializedData ? { editSession: JSON.parse(serializedData), ref: '' } : await this.editSessionsStorageService.read(ref);
505+
const data = serializedData ? { content: serializedData, ref: '' } : await this.editSessionsStorageService.read('editSessions', ref);
486506
if (!data) {
487507
if (ref === undefined && !silent) {
488508
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
494514
}
495515

496516
progress?.report({ message: resumeProgressOptionsTitle });
497-
const editSession = data.editSession;
517+
const editSession = JSON.parse(data.content);
498518
ref = data.ref;
499519

500520
if (editSession.version > EditSessionSchemaVersion) {
@@ -504,8 +524,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
504524
}
505525

506526
try {
507-
const { changes, conflictingChanges, contributedStateHandlers } = await this.generateChanges(editSession, ref, forceApplyUnrelatedChange, applyPartialMatch);
508-
if (changes.length === 0 && contributedStateHandlers.length === 0) {
527+
const { changes, conflictingChanges } = await this.generateChanges(editSession, ref, forceApplyUnrelatedChange, applyPartialMatch);
528+
if (changes.length === 0) {
509529
return;
510530
}
511531

@@ -534,12 +554,10 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
534554
}
535555
}
536556

537-
for (const handleContributedState of contributedStateHandlers) {
538-
handleContributedState();
539-
}
557+
await this.workspaceStateSynchronizer?.apply(false, {});
540558

541559
this.logService.info(`Deleting edit session with ref ${ref} after successfully applying it to current workspace...`);
542-
await this.editSessionsStorageService.delete(ref);
560+
await this.editSessionsStorageService.delete('editSessions', ref);
543561
this.logService.info(`Deleted edit session with ref ${ref}.`);
544562

545563
this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'resumeSucceeded' });
@@ -553,7 +571,6 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
553571

554572
private async generateChanges(editSession: EditSession, ref: string, forceApplyUnrelatedChange = false, applyPartialMatch = false) {
555573
const changes: ({ uri: URI; type: ChangeType; contents: string | undefined })[] = [];
556-
const contributedStateHandlers: (() => void)[] = [];
557574
const conflictingChanges = [];
558575
const workspaceFolders = this.contextService.getWorkspace().folders;
559576
const cancellationTokenSource = new CancellationTokenSource();
@@ -623,44 +640,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
623640
}
624641
}
625642

626-
const incomingFolderUrisToIdentifiers = new Map<string, [string, EditSessionIdentityMatch]>();
627-
for (const folder of editSession.folders) {
628-
const { canonicalIdentity } = folder;
629-
for (const workspaceFolder of workspaceFolders) {
630-
const identity = await this.editSessionIdentityService.getEditSessionIdentifier(workspaceFolder, cancellationTokenSource.token);
631-
if (!identity || !canonicalIdentity || !folder.absoluteUri) {
632-
continue;
633-
}
634-
const match = identity === canonicalIdentity
635-
? EditSessionIdentityMatch.Complete
636-
: await this.editSessionIdentityService.provideEditSessionIdentityMatch(workspaceFolder, identity, canonicalIdentity, cancellationTokenSource.token);
637-
if (!match) {
638-
continue;
639-
}
640-
incomingFolderUrisToIdentifiers.set(folder.absoluteUri.toString(), [workspaceFolder.uri.toString(), match]);
641-
}
642-
}
643-
644-
EditSessionRegistry.getEditSessionContributions().forEach(([key, contrib]) => {
645-
const state = editSession.state[key];
646-
if (state) {
647-
contributedStateHandlers.push(() => contrib.resumeState(state, (incomingUri: URI) => {
648-
for (const absoluteUri of incomingFolderUrisToIdentifiers.keys()) {
649-
if (isEqualOrParent(incomingUri, URI.parse(absoluteUri))) {
650-
const [workspaceFolderUri, match] = incomingFolderUrisToIdentifiers.get(absoluteUri)!;
651-
if (match === EditSessionIdentityMatch.Complete) {
652-
const relativeFilePath = relativePath(URI.parse(absoluteUri), incomingUri);
653-
return relativeFilePath ? joinPath(URI.parse(workspaceFolderUri), relativeFilePath) : incomingUri;
654-
}
655-
656-
}
657-
}
658-
return incomingUri;
659-
}));
660-
}
661-
});
662-
663-
return { changes, conflictingChanges, contributedStateHandlers };
643+
return { changes, conflictingChanges };
664644
}
665645

666646
private async willChangeLocalContents(localChanges: Set<string>, uriWithIncomingChanges: URI, incomingChange: Change) {
@@ -747,11 +727,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo
747727
folders.push({ workingChanges, name: name ?? '', canonicalIdentity: canonicalIdentity ?? undefined, absoluteUri: workspaceFolder?.uri.toString() });
748728
}
749729

750-
// Look through all registered contributions to gather additional state
751-
const contributedData: { [key: string]: unknown } = {};
752-
EditSessionRegistry.getEditSessionContributions().forEach(([key, contrib]) => {
753-
contributedData[key] = contrib.getStateToStore();
754-
});
730+
// Store contributed workspace state
731+
await this.workspaceStateSynchronizer?.sync(null, {});
755732

756733
if (!hasEdits) {
757734
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
761738
return undefined;
762739
}
763740

764-
const data: EditSession = { folders, version: 2, state: contributedData };
741+
const data: EditSession = { folders, version: 2 };
765742

766743
try {
767744
this.logService.info(`Storing edit session...`);
768-
const ref = await this.editSessionsStorageService.write(data);
745+
const ref = await this.editSessionsStorageService.write('editSessions', data);
769746
this.logService.info(`Stored edit session with ref ${ref}.`);
770747
return ref;
771748
} catch (ex) {

0 commit comments

Comments
 (0)