Skip to content

Commit fa4ff15

Browse files
authored
Implement and adopt edit session identifier API proposal (microsoft#157733)
* Add canonical workspace identifier proposed API * Use canonical id to store and resume edit sessions * Add git extension workspace identity provider * Fix warning incorrectly showing up * Make auto resume behavior opt in * * Create a separate service * Accept WorkspaceFolder instead of URI * Return string instead of object * Make edit session restores resilient to provider registration races * Introduce an activation event * Activate contributing extension before using provider * `CanonicalWorkspaceIdentity` -> `EditSessionIdentity` * Show progress while resuming edit session * Store edit session even if extension will take care of opening target workspace * Address most of PR feedback * `IEditSessionsWorkbenchService` -> `IEditSessionsStorageService` * Unregister provider in renderer * Split out proposal into new `editSessionIdentityProvider.d.ts` * Fix bad merge * Always show progress in window * Convert URI schemes
1 parent 4291d2b commit fa4ff15

23 files changed

+411
-83
lines changed

extensions/git/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"diffCommand",
1414
"contribMergeEditorToolbar",
1515
"contribViewsWelcome",
16+
"editSessionIdentityProvider",
1617
"scmActionButton",
1718
"scmSelectedProvider",
1819
"scmValidation",
@@ -24,6 +25,7 @@
2425
],
2526
"activationEvents": [
2627
"*",
28+
"onEditSession:file",
2729
"onFileSystem:git",
2830
"onFileSystem:git-show"
2931
],
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as path from 'path';
7+
import * as vscode from 'vscode';
8+
import { Model } from './model';
9+
10+
export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentityProvider, vscode.Disposable {
11+
12+
private providerRegistration: vscode.Disposable;
13+
14+
constructor(private model: Model) {
15+
this.providerRegistration = vscode.workspace.registerEditSessionIdentityProvider('file', this);
16+
}
17+
18+
dispose() {
19+
this.providerRegistration.dispose();
20+
}
21+
22+
async provideEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder, _token: vscode.CancellationToken): Promise<string | undefined> {
23+
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));
24+
25+
const repository = this.model.getRepository(workspaceFolder.uri);
26+
await repository?.status();
27+
28+
if (!repository || !repository?.HEAD?.upstream) {
29+
return undefined;
30+
}
31+
32+
return JSON.stringify({
33+
remote: repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl ?? null,
34+
ref: repository.HEAD?.name ?? null,
35+
sha: repository.HEAD?.commit ?? null,
36+
});
37+
}
38+
}

extensions/git/src/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { OutputChannelLogger } from './log';
2828
import { createIPCServer, IPCServer } from './ipc/ipcServer';
2929
import { GitEditor } from './gitEditor';
3030
import { GitPostCommitCommandsProvider } from './postCommitCommands';
31+
import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider';
3132

3233
const deactivateTasks: { (): Promise<any> }[] = [];
3334

@@ -115,7 +116,8 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
115116
new GitFileSystemProvider(model),
116117
new GitDecorations(model),
117118
new GitProtocolHandler(),
118-
new GitTimelineProvider(model, cc)
119+
new GitTimelineProvider(model, cc),
120+
new GitEditSessionIdentityProvider(model)
119121
);
120122

121123
const postCommitCommandsProvider = new GitPostCommitCommandsProvider();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
declare module 'vscode' {
7+
8+
// https://github.com/microsoft/vscode/issues/157734
9+
10+
export namespace workspace {
11+
/**
12+
*
13+
* @param scheme The URI scheme that this provider can provide edit session identities for.
14+
* @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to
15+
* an edit session identifier which is stable across machines. This enables edit sessions to be resolved.
16+
*/
17+
export function registerEditSessionIdentityProvider(scheme: string, provider: EditSessionIdentityProvider): Disposable;
18+
}
19+
20+
export interface EditSessionIdentityProvider {
21+
/**
22+
*
23+
* @param workspaceFolder The workspace folder to provide an edit session identity for.
24+
* @param token A cancellation token for the request.
25+
* @returns An string representing the edit session identity for the requested workspace folder.
26+
*/
27+
provideEditSessionIdentity(workspaceFolder: WorkspaceFolder, token: CancellationToken): ProviderResult<string>;
28+
}
29+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
7+
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
8+
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
9+
10+
export interface IEditSessionIdentityProvider {
11+
readonly scheme: string;
12+
getEditSessionIdentifier(workspaceFolder: IWorkspaceFolder, token: CancellationToken): Promise<string | undefined>;
13+
}
14+
15+
export const IEditSessionIdentityService = createDecorator<IEditSessionIdentityService>('editSessionIdentityService');
16+
17+
export interface IEditSessionIdentityService {
18+
readonly _serviceBrand: undefined;
19+
20+
registerEditSessionIdentityProvider(provider: IEditSessionIdentityProvider): void;
21+
unregisterEditSessionIdentityProvider(scheme: string): void;
22+
getEditSessionIdentifier(workspaceFolder: IWorkspaceFolder, cancellationTokenSource: CancellationTokenSource): Promise<string | undefined>;
23+
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ import { ILabelService } from 'vs/platform/label/common/label';
1717
import { INotificationService } from 'vs/platform/notification/common/notification';
1818
import { IRequestService } from 'vs/platform/request/common/request';
1919
import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust';
20-
import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspace } from 'vs/platform/workspace/common/workspace';
20+
import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
2121
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
2222
import { checkGlobFileExists } from 'vs/workbench/services/extensions/common/workspaceContains';
2323
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder';
2424
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2525
import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search';
2626
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
2727
import { ExtHostContext, ExtHostWorkspaceShape, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol';
28+
import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions';
2829

2930
@extHostNamedCustomer(MainContext.MainThreadWorkspace)
3031
export class MainThreadWorkspace implements MainThreadWorkspaceShape {
@@ -38,6 +39,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
3839
extHostContext: IExtHostContext,
3940
@ISearchService private readonly _searchService: ISearchService,
4041
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
42+
@IEditSessionIdentityService private readonly _editSessionIdentityService: IEditSessionIdentityService,
4143
@IEditorService private readonly _editorService: IEditorService,
4244
@IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService,
4345
@INotificationService private readonly _notificationService: INotificationService,
@@ -220,4 +222,18 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
220222
private _onDidGrantWorkspaceTrust(): void {
221223
this._proxy.$onDidGrantWorkspaceTrust();
222224
}
225+
226+
// --- edit sessions ---
227+
$registerEditSessionIdentityProvider(scheme: string) {
228+
this._editSessionIdentityService.registerEditSessionIdentityProvider({
229+
scheme: scheme,
230+
getEditSessionIdentifier: async (workspaceFolder: WorkspaceFolder, token: CancellationToken) => {
231+
return this._proxy.$getEditSessionIdentifier(workspaceFolder.uri, token);
232+
}
233+
});
234+
}
235+
236+
$unregisterEditSessionIdentityProvider(scheme: string) {
237+
this._editSessionIdentityService.unregisterEditSessionIdentityProvider(scheme);
238+
}
223239
}

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
10431043
},
10441044
onDidGrantWorkspaceTrust: (listener, thisArgs?, disposables?) => {
10451045
return extHostWorkspace.onDidGrantWorkspaceTrust(listener, thisArgs, disposables);
1046+
},
1047+
registerEditSessionIdentityProvider: (scheme: string, provider: vscode.EditSessionIdentityProvider) => {
1048+
checkProposedApiEnabled(extension, 'editSessionIdentityProvider');
1049+
return extHostWorkspace.registerEditSessionIdentityProvider(scheme, provider);
10461050
}
10471051
};
10481052

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,8 @@ export interface MainThreadWorkspaceShape extends IDisposable {
10861086
$updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents; name?: string }[]): Promise<void>;
10871087
$resolveProxy(url: string): Promise<string | undefined>;
10881088
$requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise<boolean | undefined>;
1089+
$registerEditSessionIdentityProvider(scheme: string): void;
1090+
$unregisterEditSessionIdentityProvider(scheme: string): void;
10891091
}
10901092

10911093
export interface IFileChangeDto {
@@ -1414,6 +1416,7 @@ export interface ExtHostWorkspaceShape {
14141416
$acceptWorkspaceData(workspace: IWorkspaceData | null): void;
14151417
$handleTextSearchResult(result: search.IRawFileMatch2, requestId: number): void;
14161418
$onDidGrantWorkspaceTrust(): void;
1419+
$getEditSessionIdentifier(folder: UriComponents, token: CancellationToken): Promise<string | undefined>;
14171420
}
14181421

14191422
export interface ExtHostFileSystemInfoShape {

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { delta as arrayDelta, mapArrayOrNot } from 'vs/base/common/arrays';
77
import { Barrier } from 'vs/base/common/async';
88
import { CancellationToken } from 'vs/base/common/cancellation';
99
import { Emitter, Event } from 'vs/base/common/event';
10+
import { toDisposable } from 'vs/base/common/lifecycle';
1011
import { TernarySearchTree } from 'vs/base/common/map';
1112
import { Schemas } from 'vs/base/common/network';
1213
import { Counter } from 'vs/base/common/numbers';
1314
import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from 'vs/base/common/resources';
1415
import { compare } from 'vs/base/common/strings';
1516
import { withUndefinedAsNull } from 'vs/base/common/types';
16-
import { URI } from 'vs/base/common/uri';
17+
import { URI, UriComponents } from 'vs/base/common/uri';
1718
import { localize } from 'vs/nls';
1819
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
1920
import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
@@ -26,6 +27,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
2627
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
2728
import { GlobPattern } from 'vs/workbench/api/common/extHostTypeConverters';
2829
import { Range } from 'vs/workbench/api/common/extHostTypes';
30+
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
2931
import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder';
3032
import { IRawFileMatch2, resultIsMatch } from 'vs/workbench/services/search/common/search';
3133
import * as vscode from 'vscode';
@@ -182,19 +184,24 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
182184
private readonly _proxy: MainThreadWorkspaceShape;
183185
private readonly _messageService: MainThreadMessageServiceShape;
184186
private readonly _extHostFileSystemInfo: IExtHostFileSystemInfo;
187+
private readonly _uriTransformerService: IURITransformerService;
185188

186189
private readonly _activeSearchCallbacks: ((match: IRawFileMatch2) => any)[] = [];
187190

188191
private _trusted: boolean = false;
189192

193+
private readonly _editSessionIdentityProviders = new Map<string, vscode.EditSessionIdentityProvider>();
194+
190195
constructor(
191196
@IExtHostRpcService extHostRpc: IExtHostRpcService,
192197
@IExtHostInitDataService initData: IExtHostInitDataService,
193198
@IExtHostFileSystemInfo extHostFileSystemInfo: IExtHostFileSystemInfo,
194199
@ILogService logService: ILogService,
200+
@IURITransformerService uriTransformerService: IURITransformerService,
195201
) {
196202
this._logService = logService;
197203
this._extHostFileSystemInfo = extHostFileSystemInfo;
204+
this._uriTransformerService = uriTransformerService;
198205
this._requestIdProvider = new Counter();
199206
this._barrier = new Barrier();
200207

@@ -573,6 +580,50 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
573580
this._onDidGrantWorkspaceTrust.fire();
574581
}
575582
}
583+
584+
// --- edit sessions ---
585+
586+
// called by ext host
587+
registerEditSessionIdentityProvider(scheme: string, provider: vscode.EditSessionIdentityProvider) {
588+
if (this._editSessionIdentityProviders.has(scheme)) {
589+
throw new Error(`A provider has already been registered for scheme ${scheme}`);
590+
}
591+
592+
this._editSessionIdentityProviders.set(scheme, provider);
593+
const outgoingScheme = this._uriTransformerService.transformOutgoingScheme(scheme);
594+
this._proxy.$registerEditSessionIdentityProvider(outgoingScheme);
595+
596+
return toDisposable(() => {
597+
this._editSessionIdentityProviders.delete(scheme);
598+
this._proxy.$unregisterEditSessionIdentityProvider(outgoingScheme);
599+
});
600+
}
601+
602+
// called by main thread
603+
async $getEditSessionIdentifier(workspaceFolder: UriComponents, cancellationToken: CancellationToken): Promise<string | undefined> {
604+
this._logService.info('Getting edit session identifier for workspaceFolder', workspaceFolder);
605+
const folder = await this.resolveWorkspaceFolder(URI.revive(workspaceFolder));
606+
if (!folder) {
607+
this._logService.warn('Unable to resolve workspace folder');
608+
return undefined;
609+
}
610+
611+
this._logService.info('Invoking #provideEditSessionIdentity for workspaceFolder', folder);
612+
613+
const provider = this._editSessionIdentityProviders.get(folder.uri.scheme);
614+
this._logService.info(`Provider for scheme ${folder.uri.scheme} is defined: `, !!provider);
615+
if (!provider) {
616+
return undefined;
617+
}
618+
619+
const result = await provider.provideEditSessionIdentity(folder, cancellationToken);
620+
this._logService.info('Provider returned edit session identifier: ', result);
621+
if (!result) {
622+
return undefined;
623+
}
624+
625+
return result;
626+
}
576627
}
577628

578629
export const IExtHostWorkspace = createDecorator<IExtHostWorkspace>('IExtHostWorkspace');

src/vs/workbench/api/test/browser/extHostConfiguration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
1818
import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
1919
import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
2020
import { isLinux } from 'vs/base/common/platform';
21+
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
2122

2223
suite('ExtHostConfiguration', function () {
2324

@@ -30,7 +31,7 @@ suite('ExtHostConfiguration', function () {
3031
}
3132

3233
function createExtHostWorkspace(): ExtHostWorkspace {
33-
return new ExtHostWorkspace(new TestRPCProtocol(), new class extends mock<IExtHostInitDataService>() { }, new class extends mock<IExtHostFileSystemInfo>() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, new NullLogService());
34+
return new ExtHostWorkspace(new TestRPCProtocol(), new class extends mock<IExtHostInitDataService>() { }, new class extends mock<IExtHostFileSystemInfo>() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, new NullLogService(), new class extends mock<IURITransformerService>() { });
3435
}
3536

3637
function createExtHostConfiguration(contents: any = Object.create(null), shape?: MainThreadConfigurationShape) {

0 commit comments

Comments
 (0)