Introduce simpler interface for IEditSessionContribution (#179921)

* Simpler interface for IEditSessionContribution

* Add doc

* More doc
This commit is contained in:
Joyce Er 2023-04-13 15:40:14 -07:00 committed by GitHub
parent 739b93cce8
commit 8e85312936
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 95 additions and 28 deletions

View file

@ -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);

View file

@ -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<void> {
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<IProgressStep>): Promise<void> {
async resumeEditSession(ref?: string, silent?: boolean, forceApplyUnrelatedChange?: boolean, applyPartialMatch?: boolean, progress?: IProgress<IProgressStep>, serializedData?: string): Promise<void> {
// 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<string, [string, EditSessionIdentityMatch]>();
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...`);

View file

@ -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';

View file

@ -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<IEditorService>() {
override saveAll = async (_options: ISaveAllEditorsOptions) => true;
});
instantiationService.stub(IEditSessionIdentityService, new class extends mock<IEditSessionIdentityService>() {
override async getEditSessionIdentifier() {
return 'test-identity';
}
});
editSessionsContribution = instantiationService.createInstance(EditSessionsContribution);
});