From 482bc7c14645d4a033cfa440f4e65d65b0d7a4cd Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 16 Jun 2022 19:13:42 -0700 Subject: [PATCH] Add experimental Continue Edit Session API command (#152375) * Implement `vscode.experimental.editSession.continue` API command * Read `editSessionId` from protocol url query params Pass it down to `environmentService` for later access Read it from `environmentService` when attempting to apply edit session * Pass `editSessionId` to environmentService in web * Set and clear edit session ID * Add logging and encode ref in query parameters * Update test --- src/vs/code/electron-main/app.ts | 7 ++ .../environment/common/environment.ts | 3 + .../api/common/extHostApiCommands.ts | 6 ++ src/vs/workbench/browser/web.api.ts | 5 ++ .../browser/sessionSync.contribution.ts | 83 ++++++++++++++++--- .../test/browser/sessionSync.test.ts | 3 + .../environment/browser/environmentService.ts | 3 + .../browser/sessionSyncWorkbenchService.ts | 5 +- .../sessionSync/common/sessionSync.ts | 2 +- 9 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 8fb281645b9..6db5653c631 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -876,6 +876,13 @@ export class CodeApplication extends Disposable { // or if no window is open (macOS only) shouldOpenInNewWindow ||= isMacintosh && windowsMainService.getWindowCount() === 0; + // Pass along edit session id + if (params.get('edit-session-id') !== null) { + environmentService.editSessionId = params.get('edit-session-id') ?? undefined; + params.delete('edit-session-id'); + uri = uri.with({ query: params.toString() }); + } + // Check for URIs to open in window const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri); logService.trace('app#handleURL: windowOpenableFromProtocolLink = ', windowOpenableFromProtocolLink); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 54d4836668a..5ed69a092ef 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -64,6 +64,9 @@ export interface IEnvironmentService { userDataSyncLogResource: URI; sync: 'on' | 'off' | undefined; + // --- continue edit session + editSessionId?: string; + // --- extension development debugExtensionHost: IExtensionHostDebugParams; isExtensionDevelopment: boolean; diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 4b866a477f3..6b1771703ea 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -436,6 +436,12 @@ const newCommands: ApiCommand[] = [ 'vscode.revealTestInExplorer', '_revealTestInExplorer', 'Reveals a test instance in the explorer', [ApiCommandArgument.TestItem], ApiCommandResult.Void + ), + // --- continue edit session + new ApiCommand( + 'vscode.experimental.editSession.continue', '_workbench.experimental.sessionSync.actions.continueEditSession', 'Continue the current edit session in a different workspace', + [ApiCommandArgument.Uri.with('workspaceUri', 'The target workspace to continue the current edit session in')], + ApiCommandResult.Void ) ]; diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 4eddbcbba88..99c7b044635 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -169,6 +169,11 @@ export interface IWorkbenchConstructionOptions { */ readonly codeExchangeProxyEndpoints?: { [providerId: string]: string }; + /** + * The identifier of an edit session associated with the current workspace. + */ + readonly editSessionId?: string; + /** * [TEMPORARY]: This will be removed soon. * Endpoints to be used for proxying repository tarball download calls in the browser. diff --git a/src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts b/src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts index 24aec32b2b6..7ea8636ceae 100644 --- a/src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts +++ b/src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts @@ -27,17 +27,24 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; registerSingleton(ISessionSyncWorkbenchService, SessionSyncWorkbenchService); const applyLatestCommand = { - id: 'workbench.sessionSync.actions.applyLatest', + id: 'workbench.experimental.sessionSync.actions.applyLatest', title: localize('apply latest', "{0}: Apply Latest Edit Session", EDIT_SESSION_SYNC_TITLE), }; const storeLatestCommand = { - id: 'workbench.sessionSync.actions.storeLatest', + id: 'workbench.experimental.sessionSync.actions.storeLatest', title: localize('store latest', "{0}: Store Latest Edit Session", EDIT_SESSION_SYNC_TITLE), }; +const continueEditSessionCommand = { + id: '_workbench.experimental.sessionSync.actions.continueEditSession', + title: localize('continue edit session', "{0}: Continue Edit Session", EDIT_SESSION_SYNC_TITLE), +}; +const queryParamName = 'editSessionId'; export class SessionSyncContribution extends Disposable implements IWorkbenchContribution { @@ -47,17 +54,23 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon @ISessionSyncWorkbenchService private readonly sessionSyncWorkbenchService: ISessionSyncWorkbenchService, @IFileService private readonly fileService: IFileService, @IProgressService private readonly progressService: IProgressService, + @IOpenerService private readonly openerService: IOpenerService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ISCMService private readonly scmService: ISCMService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @ILogService private readonly logService: ILogService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @IProductService private readonly productService: IProductService, @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, ) { super(); + if (this.environmentService.editSessionId !== undefined) { + void this.applyEditSession(this.environmentService.editSessionId).then(() => this.environmentService.editSessionId = undefined); + } + this.configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('workbench.experimental.sessionSync.enabled')) { this.registerActions(); @@ -72,15 +85,50 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon return; } - this.registerApplyEditSessionAction(); - this.registerStoreEditSessionAction(); + this.registerContinueEditSessionAction(); + + this.registerApplyLatestEditSessionAction(); + this.registerStoreLatestEditSessionAction(); this.registered = true; } - private registerApplyEditSessionAction(): void { + private registerContinueEditSessionAction() { const that = this; - this._register(registerAction2(class ApplyEditSessionAction extends Action2 { + this._register(registerAction2(class ContinueEditSessionAction extends Action2 { + constructor() { + super({ + id: continueEditSessionCommand.id, + title: continueEditSessionCommand.title + }); + } + + async run(accessor: ServicesAccessor, workspaceUri: URI): Promise { + // Run the store action to get back a ref + const ref = await that.storeEditSession(); + + // Append the ref to the URI + if (ref !== undefined) { + const encodedRef = encodeURIComponent(ref); + workspaceUri = workspaceUri.with({ + query: workspaceUri.query.length > 0 ? (workspaceUri + `&${queryParamName}=${encodedRef}`) : `${queryParamName}=${encodedRef}` + }); + + that.environmentService.editSessionId = ref; + } else { + that.logService.warn(`Edit Sessions: Failed to store edit session when invoking ${continueEditSessionCommand.id}.`); + } + + // Open the URI + that.logService.info(`Edit Sessions: opening ${workspaceUri.toString()}`); + await that.openerService.open(workspaceUri, { openExternal: true }); + } + })); + } + + private registerApplyLatestEditSessionAction(): void { + const that = this; + this._register(registerAction2(class ApplyLatestEditSessionAction extends Action2 { constructor() { super({ id: applyLatestCommand.id, @@ -100,9 +148,9 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon })); } - private registerStoreEditSessionAction(): void { + private registerStoreLatestEditSessionAction(): void { const that = this; - this._register(registerAction2(class StoreEditSessionAction extends Action2 { + this._register(registerAction2(class StoreLatestEditSessionAction extends Action2 { constructor() { super({ id: storeLatestCommand.id, @@ -122,8 +170,12 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon })); } - async applyEditSession() { - const editSession = await this.sessionSyncWorkbenchService.read(undefined); + async applyEditSession(ref?: string): Promise { + if (ref !== undefined) { + this.logService.info(`Edit Sessions: Applying edit session with ref ${ref}.`); + } + + const editSession = await this.sessionSyncWorkbenchService.read(ref); if (!editSession) { return; } @@ -160,6 +212,7 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon } if (hasLocalUncommittedChanges) { + // TODO@joyceerhl Provide the option to diff files which would be overwritten by edit session contents const result = await this.dialogService.confirm({ message: localize('apply edit session warning', 'Applying your edit session may overwrite your existing uncommitted changes. Do you want to proceed?'), type: 'warning', @@ -178,12 +231,12 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon } } } catch (ex) { - this.logService.error(ex); + this.logService.error('Edit Sessions:', (ex as Error).toString()); this.notificationService.error(localize('apply failed', "Failed to apply your edit session.")); } } - async storeEditSession() { + async storeEditSession(): Promise { const folders: Folder[] = []; for (const repository of this.scmService.repositories) { @@ -223,7 +276,9 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon const data: EditSession = { folders, version: 1 }; try { - await this.sessionSyncWorkbenchService.write(data); + const ref = await this.sessionSyncWorkbenchService.write(data); + this.logService.info(`Edit Sessions: Stored edit session with ref ${ref}.`); + return ref; } catch (ex) { type UploadFailedEvent = { reason: string }; type UploadFailedClassification = { @@ -245,6 +300,8 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon } } } + + return undefined; } private getChangedResources(repository: ISCMRepository) { diff --git a/src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts b/src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts index b4f169a12f9..83cfaf3e64a 100644 --- a/src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts +++ b/src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts @@ -26,6 +26,8 @@ import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; const folderName = 'test-folder'; const folderUri = URI.file(`/${folderName}`); @@ -53,6 +55,7 @@ suite('Edit session sync', () => { instantiationService.stub(ISessionSyncWorkbenchService, new class extends mock() { }); instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(ISCMService, SCMService); + instantiationService.stub(IEnvironmentService, TestEnvironmentService); instantiationService.stub(IConfigurationService, new TestConfigurationService({ workbench: { experimental: { sessionSync: { enabled: true } } } })); instantiationService.stub(IWorkspaceContextService, new class extends mock() { override getWorkspace() { diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 1efe771ec2c..adbb3c495c8 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -205,6 +205,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get disableWorkspaceTrust(): boolean { return !this.options.enableWorkspaceTrust; } + @memoize + get editSessionId(): string | undefined { return this.options.editSessionId; } + private payload: Map | undefined; constructor( diff --git a/src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts b/src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts index 894141cce99..68a8dd657bb 100644 --- a/src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts +++ b/src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts @@ -60,14 +60,15 @@ export class SessionSyncWorkbenchService extends Disposable implements ISessionS /** * * @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(editSession: EditSession): Promise { this.initialized = await this.waitAndInitialize(); if (!this.initialized) { throw new Error('Please sign in to store your edit session.'); } - await this.storeClient?.write('editSessions', JSON.stringify(editSession), null); + return this.storeClient!.write('editSessions', JSON.stringify(editSession), null); } /** diff --git a/src/vs/workbench/services/sessionSync/common/sessionSync.ts b/src/vs/workbench/services/sessionSync/common/sessionSync.ts index 4b91a77f95e..7e4af5123ec 100644 --- a/src/vs/workbench/services/sessionSync/common/sessionSync.ts +++ b/src/vs/workbench/services/sessionSync/common/sessionSync.ts @@ -13,7 +13,7 @@ export interface ISessionSyncWorkbenchService { _serviceBrand: undefined; read(ref: string | undefined): Promise; - write(editSession: EditSession): Promise; + write(editSession: EditSession): Promise; } export enum ChangeType {