Skip to content

Commit 482bc7c

Browse files
authored
Add experimental Continue Edit Session API command (microsoft#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
1 parent 5e26d5f commit 482bc7c

File tree

9 files changed

+101
-16
lines changed

9 files changed

+101
-16
lines changed

src/vs/code/electron-main/app.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,13 @@ export class CodeApplication extends Disposable {
876876
// or if no window is open (macOS only)
877877
shouldOpenInNewWindow ||= isMacintosh && windowsMainService.getWindowCount() === 0;
878878

879+
// Pass along edit session id
880+
if (params.get('edit-session-id') !== null) {
881+
environmentService.editSessionId = params.get('edit-session-id') ?? undefined;
882+
params.delete('edit-session-id');
883+
uri = uri.with({ query: params.toString() });
884+
}
885+
879886
// Check for URIs to open in window
880887
const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri);
881888
logService.trace('app#handleURL: windowOpenableFromProtocolLink = ', windowOpenableFromProtocolLink);

src/vs/platform/environment/common/environment.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export interface IEnvironmentService {
6464
userDataSyncLogResource: URI;
6565
sync: 'on' | 'off' | undefined;
6666

67+
// --- continue edit session
68+
editSessionId?: string;
69+
6770
// --- extension development
6871
debugExtensionHost: IExtensionHostDebugParams;
6972
isExtensionDevelopment: boolean;

src/vs/workbench/api/common/extHostApiCommands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,12 @@ const newCommands: ApiCommand[] = [
436436
'vscode.revealTestInExplorer', '_revealTestInExplorer', 'Reveals a test instance in the explorer',
437437
[ApiCommandArgument.TestItem],
438438
ApiCommandResult.Void
439+
),
440+
// --- continue edit session
441+
new ApiCommand(
442+
'vscode.experimental.editSession.continue', '_workbench.experimental.sessionSync.actions.continueEditSession', 'Continue the current edit session in a different workspace',
443+
[ApiCommandArgument.Uri.with('workspaceUri', 'The target workspace to continue the current edit session in')],
444+
ApiCommandResult.Void
439445
)
440446
];
441447

src/vs/workbench/browser/web.api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ export interface IWorkbenchConstructionOptions {
169169
*/
170170
readonly codeExchangeProxyEndpoints?: { [providerId: string]: string };
171171

172+
/**
173+
* The identifier of an edit session associated with the current workspace.
174+
*/
175+
readonly editSessionId?: string;
176+
172177
/**
173178
* [TEMPORARY]: This will be removed soon.
174179
* Endpoints to be used for proxying repository tarball download calls in the browser.

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

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,24 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
2727
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
2828
import { ILogService } from 'vs/platform/log/common/log';
2929
import { IProductService } from 'vs/platform/product/common/productService';
30+
import { IOpenerService } from 'vs/platform/opener/common/opener';
31+
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
3032

3133
registerSingleton(ISessionSyncWorkbenchService, SessionSyncWorkbenchService);
3234

3335
const applyLatestCommand = {
34-
id: 'workbench.sessionSync.actions.applyLatest',
36+
id: 'workbench.experimental.sessionSync.actions.applyLatest',
3537
title: localize('apply latest', "{0}: Apply Latest Edit Session", EDIT_SESSION_SYNC_TITLE),
3638
};
3739
const storeLatestCommand = {
38-
id: 'workbench.sessionSync.actions.storeLatest',
40+
id: 'workbench.experimental.sessionSync.actions.storeLatest',
3941
title: localize('store latest', "{0}: Store Latest Edit Session", EDIT_SESSION_SYNC_TITLE),
4042
};
43+
const continueEditSessionCommand = {
44+
id: '_workbench.experimental.sessionSync.actions.continueEditSession',
45+
title: localize('continue edit session', "{0}: Continue Edit Session", EDIT_SESSION_SYNC_TITLE),
46+
};
47+
const queryParamName = 'editSessionId';
4148

4249
export class SessionSyncContribution extends Disposable implements IWorkbenchContribution {
4350

@@ -47,17 +54,23 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
4754
@ISessionSyncWorkbenchService private readonly sessionSyncWorkbenchService: ISessionSyncWorkbenchService,
4855
@IFileService private readonly fileService: IFileService,
4956
@IProgressService private readonly progressService: IProgressService,
57+
@IOpenerService private readonly openerService: IOpenerService,
5058
@ITelemetryService private readonly telemetryService: ITelemetryService,
5159
@ISCMService private readonly scmService: ISCMService,
5260
@INotificationService private readonly notificationService: INotificationService,
5361
@IDialogService private readonly dialogService: IDialogService,
5462
@ILogService private readonly logService: ILogService,
63+
@IEnvironmentService private readonly environmentService: IEnvironmentService,
5564
@IProductService private readonly productService: IProductService,
5665
@IConfigurationService private configurationService: IConfigurationService,
5766
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
5867
) {
5968
super();
6069

70+
if (this.environmentService.editSessionId !== undefined) {
71+
void this.applyEditSession(this.environmentService.editSessionId).then(() => this.environmentService.editSessionId = undefined);
72+
}
73+
6174
this.configurationService.onDidChangeConfiguration((e) => {
6275
if (e.affectsConfiguration('workbench.experimental.sessionSync.enabled')) {
6376
this.registerActions();
@@ -72,15 +85,50 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
7285
return;
7386
}
7487

75-
this.registerApplyEditSessionAction();
76-
this.registerStoreEditSessionAction();
88+
this.registerContinueEditSessionAction();
89+
90+
this.registerApplyLatestEditSessionAction();
91+
this.registerStoreLatestEditSessionAction();
7792

7893
this.registered = true;
7994
}
8095

81-
private registerApplyEditSessionAction(): void {
96+
private registerContinueEditSessionAction() {
97+
const that = this;
98+
this._register(registerAction2(class ContinueEditSessionAction extends Action2 {
99+
constructor() {
100+
super({
101+
id: continueEditSessionCommand.id,
102+
title: continueEditSessionCommand.title
103+
});
104+
}
105+
106+
async run(accessor: ServicesAccessor, workspaceUri: URI): Promise<void> {
107+
// Run the store action to get back a ref
108+
const ref = await that.storeEditSession();
109+
110+
// Append the ref to the URI
111+
if (ref !== undefined) {
112+
const encodedRef = encodeURIComponent(ref);
113+
workspaceUri = workspaceUri.with({
114+
query: workspaceUri.query.length > 0 ? (workspaceUri + `&${queryParamName}=${encodedRef}`) : `${queryParamName}=${encodedRef}`
115+
});
116+
117+
that.environmentService.editSessionId = ref;
118+
} else {
119+
that.logService.warn(`Edit Sessions: Failed to store edit session when invoking ${continueEditSessionCommand.id}.`);
120+
}
121+
122+
// Open the URI
123+
that.logService.info(`Edit Sessions: opening ${workspaceUri.toString()}`);
124+
await that.openerService.open(workspaceUri, { openExternal: true });
125+
}
126+
}));
127+
}
128+
129+
private registerApplyLatestEditSessionAction(): void {
82130
const that = this;
83-
this._register(registerAction2(class ApplyEditSessionAction extends Action2 {
131+
this._register(registerAction2(class ApplyLatestEditSessionAction extends Action2 {
84132
constructor() {
85133
super({
86134
id: applyLatestCommand.id,
@@ -100,9 +148,9 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
100148
}));
101149
}
102150

103-
private registerStoreEditSessionAction(): void {
151+
private registerStoreLatestEditSessionAction(): void {
104152
const that = this;
105-
this._register(registerAction2(class StoreEditSessionAction extends Action2 {
153+
this._register(registerAction2(class StoreLatestEditSessionAction extends Action2 {
106154
constructor() {
107155
super({
108156
id: storeLatestCommand.id,
@@ -122,8 +170,12 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
122170
}));
123171
}
124172

125-
async applyEditSession() {
126-
const editSession = await this.sessionSyncWorkbenchService.read(undefined);
173+
async applyEditSession(ref?: string): Promise<void> {
174+
if (ref !== undefined) {
175+
this.logService.info(`Edit Sessions: Applying edit session with ref ${ref}.`);
176+
}
177+
178+
const editSession = await this.sessionSyncWorkbenchService.read(ref);
127179
if (!editSession) {
128180
return;
129181
}
@@ -160,6 +212,7 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
160212
}
161213

162214
if (hasLocalUncommittedChanges) {
215+
// TODO@joyceerhl Provide the option to diff files which would be overwritten by edit session contents
163216
const result = await this.dialogService.confirm({
164217
message: localize('apply edit session warning', 'Applying your edit session may overwrite your existing uncommitted changes. Do you want to proceed?'),
165218
type: 'warning',
@@ -178,12 +231,12 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
178231
}
179232
}
180233
} catch (ex) {
181-
this.logService.error(ex);
234+
this.logService.error('Edit Sessions:', (ex as Error).toString());
182235
this.notificationService.error(localize('apply failed', "Failed to apply your edit session."));
183236
}
184237
}
185238

186-
async storeEditSession() {
239+
async storeEditSession(): Promise<string | undefined> {
187240
const folders: Folder[] = [];
188241

189242
for (const repository of this.scmService.repositories) {
@@ -223,7 +276,9 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
223276
const data: EditSession = { folders, version: 1 };
224277

225278
try {
226-
await this.sessionSyncWorkbenchService.write(data);
279+
const ref = await this.sessionSyncWorkbenchService.write(data);
280+
this.logService.info(`Edit Sessions: Stored edit session with ref ${ref}.`);
281+
return ref;
227282
} catch (ex) {
228283
type UploadFailedEvent = { reason: string };
229284
type UploadFailedClassification = {
@@ -245,6 +300,8 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
245300
}
246301
}
247302
}
303+
304+
return undefined;
248305
}
249306

250307
private getChangedResources(repository: ISCMRepository) {

src/vs/workbench/contrib/sessionSync/test/browser/sessionSync.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { URI } from 'vs/base/common/uri';
2626
import { joinPath } from 'vs/base/common/resources';
2727
import { INotificationService } from 'vs/platform/notification/common/notification';
2828
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
29+
import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices';
30+
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
2931

3032
const folderName = 'test-folder';
3133
const folderUri = URI.file(`/${folderName}`);
@@ -53,6 +55,7 @@ suite('Edit session sync', () => {
5355
instantiationService.stub(ISessionSyncWorkbenchService, new class extends mock<ISessionSyncWorkbenchService>() { });
5456
instantiationService.stub(IProgressService, ProgressService);
5557
instantiationService.stub(ISCMService, SCMService);
58+
instantiationService.stub(IEnvironmentService, TestEnvironmentService);
5659
instantiationService.stub(IConfigurationService, new TestConfigurationService({ workbench: { experimental: { sessionSync: { enabled: true } } } }));
5760
instantiationService.stub(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {
5861
override getWorkspace() {

src/vs/workbench/services/environment/browser/environmentService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi
205205
@memoize
206206
get disableWorkspaceTrust(): boolean { return !this.options.enableWorkspaceTrust; }
207207

208+
@memoize
209+
get editSessionId(): string | undefined { return this.options.editSessionId; }
210+
208211
private payload: Map<string, string> | undefined;
209212

210213
constructor(

src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,15 @@ export class SessionSyncWorkbenchService extends Disposable implements ISessionS
6060
/**
6161
*
6262
* @param editSession An object representing edit session state to be restored.
63+
* @returns The ref of the stored edit session state.
6364
*/
64-
async write(editSession: EditSession): Promise<void> {
65+
async write(editSession: EditSession): Promise<string> {
6566
this.initialized = await this.waitAndInitialize();
6667
if (!this.initialized) {
6768
throw new Error('Please sign in to store your edit session.');
6869
}
6970

70-
await this.storeClient?.write('editSessions', JSON.stringify(editSession), null);
71+
return this.storeClient!.write('editSessions', JSON.stringify(editSession), null);
7172
}
7273

7374
/**

src/vs/workbench/services/sessionSync/common/sessionSync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface ISessionSyncWorkbenchService {
1313
_serviceBrand: undefined;
1414

1515
read(ref: string | undefined): Promise<EditSession | undefined>;
16-
write(editSession: EditSession): Promise<void>;
16+
write(editSession: EditSession): Promise<string>;
1717
}
1818

1919
export enum ChangeType {

0 commit comments

Comments
 (0)