Skip to content

Commit

Permalink
Implement and adopt edit session identifier API proposal (#157733)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
joyceerhl committed Aug 19, 2022
1 parent 4291d2b commit fa4ff15
Show file tree
Hide file tree
Showing 23 changed files with 411 additions and 83 deletions.
2 changes: 2 additions & 0 deletions extensions/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"diffCommand",
"contribMergeEditorToolbar",
"contribViewsWelcome",
"editSessionIdentityProvider",
"scmActionButton",
"scmSelectedProvider",
"scmValidation",
Expand All @@ -24,6 +25,7 @@
],
"activationEvents": [
"*",
"onEditSession:file",
"onFileSystem:git",
"onFileSystem:git-show"
],
Expand Down
38 changes: 38 additions & 0 deletions extensions/git/src/editSessionIdentityProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as vscode from 'vscode';
import { Model } from './model';

export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentityProvider, vscode.Disposable {

private providerRegistration: vscode.Disposable;

constructor(private model: Model) {
this.providerRegistration = vscode.workspace.registerEditSessionIdentityProvider('file', this);
}

dispose() {
this.providerRegistration.dispose();
}

async provideEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder, _token: vscode.CancellationToken): Promise<string | undefined> {
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));

const repository = this.model.getRepository(workspaceFolder.uri);
await repository?.status();

if (!repository || !repository?.HEAD?.upstream) {
return undefined;
}

return JSON.stringify({
remote: repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl ?? null,
ref: repository.HEAD?.name ?? null,
sha: repository.HEAD?.commit ?? null,
});
}
}
4 changes: 3 additions & 1 deletion extensions/git/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { OutputChannelLogger } from './log';
import { createIPCServer, IPCServer } from './ipc/ipcServer';
import { GitEditor } from './gitEditor';
import { GitPostCommitCommandsProvider } from './postCommitCommands';
import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider';

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

Expand Down Expand Up @@ -115,7 +116,8 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
new GitFileSystemProvider(model),
new GitDecorations(model),
new GitProtocolHandler(),
new GitTimelineProvider(model, cc)
new GitTimelineProvider(model, cc),
new GitEditSessionIdentityProvider(model)
);

const postCommitCommandsProvider = new GitPostCommitCommandsProvider();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

declare module 'vscode' {

// https://github.com/microsoft/vscode/issues/157734

export namespace workspace {
/**
*
* @param scheme The URI scheme that this provider can provide edit session identities for.
* @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to
* an edit session identifier which is stable across machines. This enables edit sessions to be resolved.
*/
export function registerEditSessionIdentityProvider(scheme: string, provider: EditSessionIdentityProvider): Disposable;
}

export interface EditSessionIdentityProvider {
/**
*
* @param workspaceFolder The workspace folder to provide an edit session identity for.
* @param token A cancellation token for the request.
* @returns An string representing the edit session identity for the requested workspace folder.
*/
provideEditSessionIdentity(workspaceFolder: WorkspaceFolder, token: CancellationToken): ProviderResult<string>;
}
}
23 changes: 23 additions & 0 deletions src/vs/platform/workspace/common/editSessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';

export interface IEditSessionIdentityProvider {
readonly scheme: string;
getEditSessionIdentifier(workspaceFolder: IWorkspaceFolder, token: CancellationToken): Promise<string | undefined>;
}

export const IEditSessionIdentityService = createDecorator<IEditSessionIdentityService>('editSessionIdentityService');

export interface IEditSessionIdentityService {
readonly _serviceBrand: undefined;

registerEditSessionIdentityProvider(provider: IEditSessionIdentityProvider): void;
unregisterEditSessionIdentityProvider(scheme: string): void;
getEditSessionIdentifier(workspaceFolder: IWorkspaceFolder, cancellationTokenSource: CancellationTokenSource): Promise<string | undefined>;
}
18 changes: 17 additions & 1 deletion src/vs/workbench/api/browser/mainThreadWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IRequestService } from 'vs/platform/request/common/request';
import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust';
import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspace } from 'vs/platform/workspace/common/workspace';
import { IWorkspace, IWorkspaceContextService, WorkbenchState, isUntitledWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { checkGlobFileExists } from 'vs/workbench/services/extensions/common/workspaceContains';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
import { ExtHostContext, ExtHostWorkspaceShape, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol';
import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions';

@extHostNamedCustomer(MainContext.MainThreadWorkspace)
export class MainThreadWorkspace implements MainThreadWorkspaceShape {
Expand All @@ -38,6 +39,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
extHostContext: IExtHostContext,
@ISearchService private readonly _searchService: ISearchService,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@IEditSessionIdentityService private readonly _editSessionIdentityService: IEditSessionIdentityService,
@IEditorService private readonly _editorService: IEditorService,
@IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService,
@INotificationService private readonly _notificationService: INotificationService,
Expand Down Expand Up @@ -220,4 +222,18 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
private _onDidGrantWorkspaceTrust(): void {
this._proxy.$onDidGrantWorkspaceTrust();
}

// --- edit sessions ---
$registerEditSessionIdentityProvider(scheme: string) {
this._editSessionIdentityService.registerEditSessionIdentityProvider({
scheme: scheme,
getEditSessionIdentifier: async (workspaceFolder: WorkspaceFolder, token: CancellationToken) => {
return this._proxy.$getEditSessionIdentifier(workspaceFolder.uri, token);
}
});
}

$unregisterEditSessionIdentityProvider(scheme: string) {
this._editSessionIdentityService.unregisterEditSessionIdentityProvider(scheme);
}
}
4 changes: 4 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
},
onDidGrantWorkspaceTrust: (listener, thisArgs?, disposables?) => {
return extHostWorkspace.onDidGrantWorkspaceTrust(listener, thisArgs, disposables);
},
registerEditSessionIdentityProvider: (scheme: string, provider: vscode.EditSessionIdentityProvider) => {
checkProposedApiEnabled(extension, 'editSessionIdentityProvider');
return extHostWorkspace.registerEditSessionIdentityProvider(scheme, provider);
}
};

Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,8 @@ export interface MainThreadWorkspaceShape extends IDisposable {
$updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents; name?: string }[]): Promise<void>;
$resolveProxy(url: string): Promise<string | undefined>;
$requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise<boolean | undefined>;
$registerEditSessionIdentityProvider(scheme: string): void;
$unregisterEditSessionIdentityProvider(scheme: string): void;
}

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

export interface ExtHostFileSystemInfoShape {
Expand Down
53 changes: 52 additions & 1 deletion src/vs/workbench/api/common/extHostWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { delta as arrayDelta, mapArrayOrNot } from 'vs/base/common/arrays';
import { Barrier } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { toDisposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { Counter } from 'vs/base/common/numbers';
import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from 'vs/base/common/resources';
import { compare } from 'vs/base/common/strings';
import { withUndefinedAsNull } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
Expand All @@ -26,6 +27,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { GlobPattern } from 'vs/workbench/api/common/extHostTypeConverters';
import { Range } from 'vs/workbench/api/common/extHostTypes';
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder';
import { IRawFileMatch2, resultIsMatch } from 'vs/workbench/services/search/common/search';
import * as vscode from 'vscode';
Expand Down Expand Up @@ -182,19 +184,24 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
private readonly _proxy: MainThreadWorkspaceShape;
private readonly _messageService: MainThreadMessageServiceShape;
private readonly _extHostFileSystemInfo: IExtHostFileSystemInfo;
private readonly _uriTransformerService: IURITransformerService;

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

private _trusted: boolean = false;

private readonly _editSessionIdentityProviders = new Map<string, vscode.EditSessionIdentityProvider>();

constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService,
@IExtHostFileSystemInfo extHostFileSystemInfo: IExtHostFileSystemInfo,
@ILogService logService: ILogService,
@IURITransformerService uriTransformerService: IURITransformerService,
) {
this._logService = logService;
this._extHostFileSystemInfo = extHostFileSystemInfo;
this._uriTransformerService = uriTransformerService;
this._requestIdProvider = new Counter();
this._barrier = new Barrier();

Expand Down Expand Up @@ -573,6 +580,50 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
this._onDidGrantWorkspaceTrust.fire();
}
}

// --- edit sessions ---

// called by ext host
registerEditSessionIdentityProvider(scheme: string, provider: vscode.EditSessionIdentityProvider) {
if (this._editSessionIdentityProviders.has(scheme)) {
throw new Error(`A provider has already been registered for scheme ${scheme}`);
}

this._editSessionIdentityProviders.set(scheme, provider);
const outgoingScheme = this._uriTransformerService.transformOutgoingScheme(scheme);
this._proxy.$registerEditSessionIdentityProvider(outgoingScheme);

return toDisposable(() => {
this._editSessionIdentityProviders.delete(scheme);
this._proxy.$unregisterEditSessionIdentityProvider(outgoingScheme);
});
}

// called by main thread
async $getEditSessionIdentifier(workspaceFolder: UriComponents, cancellationToken: CancellationToken): Promise<string | undefined> {
this._logService.info('Getting edit session identifier for workspaceFolder', workspaceFolder);
const folder = await this.resolveWorkspaceFolder(URI.revive(workspaceFolder));
if (!folder) {
this._logService.warn('Unable to resolve workspace folder');
return undefined;
}

this._logService.info('Invoking #provideEditSessionIdentity for workspaceFolder', folder);

const provider = this._editSessionIdentityProviders.get(folder.uri.scheme);
this._logService.info(`Provider for scheme ${folder.uri.scheme} is defined: `, !!provider);
if (!provider) {
return undefined;
}

const result = await provider.provideEditSessionIdentity(folder, cancellationToken);
this._logService.info('Provider returned edit session identifier: ', result);
if (!result) {
return undefined;
}

return result;
}
}

export const IExtHostWorkspace = createDecorator<IExtHostWorkspace>('IExtHostWorkspace');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';

suite('ExtHostConfiguration', function () {

Expand All @@ -30,7 +31,7 @@ suite('ExtHostConfiguration', function () {
}

function createExtHostWorkspace(): ExtHostWorkspace {
return new ExtHostWorkspace(new TestRPCProtocol(), new class extends mock<IExtHostInitDataService>() { }, new class extends mock<IExtHostFileSystemInfo>() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, new NullLogService());
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>() { });
}

function createExtHostConfiguration(contents: any = Object.create(null), shape?: MainThreadConfigurationShape) {
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/test/browser/extHostWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import { isLinux, isWindows } from 'vs/base/common/platform';
import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
import { FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { nullExtensionDescription as extensionDescriptor } from 'vs/workbench/services/extensions/common/extensions';
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';

function createExtHostWorkspace(mainContext: IMainContext, data: IWorkspaceData, logService: ILogService): ExtHostWorkspace {
const result = new ExtHostWorkspace(
new ExtHostRpcService(mainContext),
new class extends mock<IExtHostInitDataService>() { override workspace = data; },
new class extends mock<IExtHostFileSystemInfo>() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } },
logService,
new class extends mock<IURITransformerService>() { }
);
result.$initializeWorkspace(data, true);
return result;
Expand Down
Loading

0 comments on commit fa4ff15

Please sign in to comment.