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
This commit is contained in:
Joyce Er 2022-06-16 19:13:42 -07:00 committed by GitHub
parent 5e26d5f9b3
commit 482bc7c146
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 101 additions and 16 deletions

View file

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

View file

@ -64,6 +64,9 @@ export interface IEnvironmentService {
userDataSyncLogResource: URI;
sync: 'on' | 'off' | undefined;
// --- continue edit session
editSessionId?: string;
// --- extension development
debugExtensionHost: IExtensionHostDebugParams;
isExtensionDevelopment: boolean;

View file

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

View file

@ -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.

View file

@ -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<void> {
// 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<void> {
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<string | undefined> {
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) {

View file

@ -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<ISessionSyncWorkbenchService>() { });
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<IWorkspaceContextService>() {
override getWorkspace() {

View file

@ -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<string, string> | undefined;
constructor(

View file

@ -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<void> {
async write(editSession: EditSession): Promise<string> {
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);
}
/**

View file

@ -13,7 +13,7 @@ export interface ISessionSyncWorkbenchService {
_serviceBrand: undefined;
read(ref: string | undefined): Promise<EditSession | undefined>;
write(editSession: EditSession): Promise<void>;
write(editSession: EditSession): Promise<string>;
}
export enum ChangeType {