From 8e853129368d59cdbe358a328407b22ad16f7cf7 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 13 Apr 2023 15:40:14 -0700 Subject: [PATCH] Introduce simpler interface for `IEditSessionContribution` (#179921) * Simpler interface for IEditSessionContribution * Add doc * More doc --- .../browser/editSessionsStorageService.ts | 27 +++++- .../browser/editSessions.contribution.ts | 84 ++++++++++++++----- .../editSessions/common/editSessions.ts | 6 +- .../test/browser/editSessions.test.ts | 6 ++ 4 files changed, 95 insertions(+), 28 deletions(-) diff --git a/src/vs/platform/workspace/browser/editSessionsStorageService.ts b/src/vs/platform/workspace/browser/editSessionsStorageService.ts index 81d8f59aad8..83c9abd71bb 100644 --- a/src/vs/platform/workspace/browser/editSessionsStorageService.ts +++ b/src/vs/platform/workspace/browser/editSessionsStorageService.ts @@ -4,12 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; export interface IEditSessionContribution { - getStateToStore(workspaceFolder: IWorkspaceFolder): unknown; - resumeState(workspaceFolder: IWorkspaceFolder, state: unknown): void; + /** + * 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 { @@ -17,7 +35,8 @@ class EditSessionStateRegistryImpl { public registerEditSessionsContribution(contributionPoint: string, editSessionsContribution: IEditSessionContribution): IDisposable { if (this._registeredEditSessionContributions.has(contributionPoint)) { - throw new Error(`Edit session contribution point with identifier ${contributionPoint} already exists`); + console.warn(`Edit session contribution point with identifier ${contributionPoint} already exists`); + return { dispose: () => { } }; } this._registeredEditSessionContributions.set(contributionPoint, editSessionsContribution); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 100c00a474d..7f4164603a6 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -15,7 +15,7 @@ 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, joinPath, relativePath } from 'vs/base/common/resources'; +import { basename, isEqualOrParent, 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'; @@ -405,6 +405,21 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo await that.progressService.withProgress({ ...resumeProgressOptions, title: resumeProgressOptionsTitle }, async () => await that.resumeEditSession(editSessionId, undefined, forceApplyUnrelatedChange)); } })); + this._register(registerAction2(class ResumeLatestEditSessionAction extends Action2 { + constructor() { + super({ + id: 'workbench.editSessions.actions.resumeFromSerializedPayload', + title: { value: localize('resume cloud changes', "Resume Changes from Serialized Data"), original: 'Resume Changes from Serialized Data' }, + category: 'Developer', + f1: true, + }); + } + + async run(accessor: ServicesAccessor, editSessionId?: string): Promise { + const data = await that.quickInputService.input({ prompt: 'Enter serialized data' }); + await that.progressService.withProgress({ ...resumeProgressOptions, title: resumeProgressOptionsTitle }, async () => await that.resumeEditSession(editSessionId, undefined, undefined, undefined, undefined, data)); + } + })); } private registerStoreLatestEditSessionAction(): void { @@ -440,7 +455,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo })); } - async resumeEditSession(ref?: string, silent?: boolean, forceApplyUnrelatedChange?: boolean, applyPartialMatch?: boolean, progress?: IProgress): Promise { + async resumeEditSession(ref?: string, silent?: boolean, forceApplyUnrelatedChange?: boolean, applyPartialMatch?: boolean, progress?: IProgress, serializedData?: string): Promise { // Wait for the remote environment to become available, if any await this.remoteAgentService.getEnvironment(); @@ -467,7 +482,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo performance.mark('code/willResumeEditSessionFromIdentifier'); progress?.report({ message: localize('checkingForWorkingChanges', 'Checking for pending cloud changes...') }); - const data = await this.editSessionsStorageService.read(ref); + const data = serializedData ? { editSession: JSON.parse(serializedData), ref: '' } : await this.editSessionsStorageService.read(ref); if (!data) { if (ref === undefined && !silent) { this.notificationService.info(localize('no cloud changes', 'There are no changes to resume from the cloud.')); @@ -541,9 +556,9 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo const contributedStateHandlers: (() => void)[] = []; const conflictingChanges = []; const workspaceFolders = this.contextService.getWorkspace().folders; + const cancellationTokenSource = new CancellationTokenSource(); for (const folder of editSession.folders) { - const cancellationTokenSource = new CancellationTokenSource(); let folderRoot: IWorkspaceFolder | undefined; if (folder.canonicalIdentity) { @@ -606,19 +621,45 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo conflictingChanges.push({ uri, type: change.type, contents: change.contents }); } } + } - const workspaceFolder = folderRoot; - if (workspaceFolder) { - // look through all registered contributions to gather additional state - EditSessionRegistry.getEditSessionContributions().forEach(([key, contrib]) => { - const state = folder[key]; - if (state) { - contributedStateHandlers.push(() => contrib.resumeState(workspaceFolder, state)); - } - }); + 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 }; } @@ -698,19 +739,20 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } let canonicalIdentity = undefined; - const contributedData: { [key: string]: unknown } = {}; if (workspaceFolder !== null && workspaceFolder !== undefined) { canonicalIdentity = await this.editSessionIdentityService.getEditSessionIdentifier(workspaceFolder, cancellationToken); - - // look through all registered contributions to gather additional state - EditSessionRegistry.getEditSessionContributions().forEach(([key, contrib]) => { - contributedData[key] = contrib.getStateToStore(workspaceFolder); - }); } - folders.push({ ...contributedData, workingChanges, name: name ?? '', canonicalIdentity: canonicalIdentity ?? undefined }); + // TODO@joyceerhl debt: don't store working changes as a child of the folder + folders.push({ workingChanges, name: name ?? '', canonicalIdentity: canonicalIdentity ?? undefined, absoluteUri: workspaceFolder?.toString() }); } + // Look through all registered contributions to gather additional state + const contributedData: { [key: string]: unknown } = {}; + EditSessionRegistry.getEditSessionContributions().forEach(([key, contrib]) => { + contributedData[key] = contrib.getStateToStore(); + }); + if (!hasEdits) { this.logService.info('Skipped storing working changes in the cloud as there are no edits to store.'); if (fromStoreCommand) { @@ -719,7 +761,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo return undefined; } - const data: EditSession = { folders, version: 2 }; + const data: EditSession = { folders, version: 2, state: contributedData }; try { this.logService.info(`Storing edit session...`); diff --git a/src/vs/workbench/contrib/editSessions/common/editSessions.ts b/src/vs/workbench/contrib/editSessions/common/editSessions.ts index bf2d50711dd..17ef4749019 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessions.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessions.ts @@ -70,16 +70,16 @@ export interface Folder { name: string; canonicalIdentity: string | undefined; workingChanges: Change[]; - // additional data collected from workbench contributions - [key: string]: unknown; + absoluteUri: string | undefined; } -export const EditSessionSchemaVersion = 2; +export const EditSessionSchemaVersion = 3; 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/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index 2818f6f9008..0898688d094 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -42,6 +42,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; 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'; const folderName = 'test-folder'; const folderUri = URI.file(`/${folderName}`); @@ -131,6 +132,11 @@ suite('Edit session sync', () => { instantiationService.stub(IEditorService, new class extends mock() { override saveAll = async (_options: ISaveAllEditorsOptions) => true; }); + instantiationService.stub(IEditSessionIdentityService, new class extends mock() { + override async getEditSessionIdentifier() { + return 'test-identity'; + } + }); editSessionsContribution = instantiationService.createInstance(EditSessionsContribution); });