diff --git a/extensions/git/package.json b/extensions/git/package.json index c0573f03bfa..f44453c91c1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -13,6 +13,7 @@ "diffCommand", "contribMergeEditorToolbar", "contribViewsWelcome", + "editSessionIdentityProvider", "scmActionButton", "scmSelectedProvider", "scmValidation", @@ -24,6 +25,7 @@ ], "activationEvents": [ "*", + "onEditSession:file", "onFileSystem:git", "onFileSystem:git-show" ], diff --git a/extensions/git/src/editSessionIdentityProvider.ts b/extensions/git/src/editSessionIdentityProvider.ts new file mode 100644 index 00000000000..82b6953cf58 --- /dev/null +++ b/extensions/git/src/editSessionIdentityProvider.ts @@ -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 { + 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, + }); + } +} diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 40147d6c0e6..4035dcf3184 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -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 }[] = []; @@ -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(); diff --git a/extensions/git/src/typings/vscode.proposed.contribEditSessions.d.ts b/extensions/git/src/typings/vscode.proposed.contribEditSessions.d.ts new file mode 100644 index 00000000000..e4d10f273f9 --- /dev/null +++ b/extensions/git/src/typings/vscode.proposed.contribEditSessions.d.ts @@ -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; + } +} diff --git a/src/vs/platform/workspace/common/editSessions.ts b/src/vs/platform/workspace/common/editSessions.ts new file mode 100644 index 00000000000..218f95f3bcf --- /dev/null +++ b/src/vs/platform/workspace/common/editSessions.ts @@ -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; +} + +export const IEditSessionIdentityService = createDecorator('editSessionIdentityService'); + +export interface IEditSessionIdentityService { + readonly _serviceBrand: undefined; + + registerEditSessionIdentityProvider(provider: IEditSessionIdentityProvider): void; + unregisterEditSessionIdentityProvider(scheme: string): void; + getEditSessionIdentifier(workspaceFolder: IWorkspaceFolder, cancellationTokenSource: CancellationTokenSource): Promise; +} diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 5fe29a26a0c..1789338133d 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -17,7 +17,7 @@ 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'; @@ -25,6 +25,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic 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 { @@ -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, @@ -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); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 6291be40e82..0a9f29de326 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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); } }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8f86b7391da..10efae0f6dc 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1086,6 +1086,8 @@ export interface MainThreadWorkspaceShape extends IDisposable { $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents; name?: string }[]): Promise; $resolveProxy(url: string): Promise; $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; + $registerEditSessionIdentityProvider(scheme: string): void; + $unregisterEditSessionIdentityProvider(scheme: string): void; } export interface IFileChangeDto { @@ -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; } export interface ExtHostFileSystemInfoShape { diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 9127c0db90e..dac46640fbf 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -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'; @@ -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'; @@ -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(); + 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(); @@ -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 { + 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'); diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index b0886289593..ec888aaa8cd 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -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 () { @@ -30,7 +31,7 @@ suite('ExtHostConfiguration', function () { } function createExtHostWorkspace(): ExtHostWorkspace { - return new ExtHostWorkspace(new TestRPCProtocol(), new class extends mock() { }, new class extends mock() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, new NullLogService()); + return new ExtHostWorkspace(new TestRPCProtocol(), new class extends mock() { }, new class extends mock() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, new NullLogService(), new class extends mock() { }); } function createExtHostConfiguration(contents: any = Object.create(null), shape?: MainThreadConfigurationShape) { diff --git a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts index 92f0b4f7246..9d980e14502 100644 --- a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts @@ -24,6 +24,7 @@ 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( @@ -31,6 +32,7 @@ function createExtHostWorkspace(mainContext: IMainContext, data: IWorkspaceData, new class extends mock() { override workspace = data; }, new class extends mock() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, logService, + new class extends mock() { } ); result.$initializeWorkspace(data, true); return result; diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 14f86859734..39c40200045 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -10,16 +10,16 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc import { Action2, IAction2Options, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; -import { IEditSessionsWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_DATA_VIEW_ID, decodeEditSessionFileContent } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { IEditSessionsStorageService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_CATEGORY, EDIT_SESSIONS_CONTAINER_ID, EditSessionSchemaVersion, IEditSessionsLogService, EDIT_SESSIONS_VIEW_ICON, EDIT_SESSIONS_TITLE, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_DATA_VIEW_ID, decodeEditSessionFileContent } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; import { IFileService } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { encodeBase64 } from 'vs/base/common/buffer'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { EditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService'; +import { EditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -47,10 +47,13 @@ import { EditSessionsDataViews } from 'vs/workbench/contrib/editSessions/browser import { EditSessionsFileSystemProvider } from 'vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider'; import { isNative } from 'vs/base/common/platform'; import { WorkspaceFolderCountContext } from 'vs/workbench/common/contextkeys'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { equals } from 'vs/base/common/objects'; +import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; registerSingleton(IEditSessionsLogService, EditSessionsLogService); -registerSingleton(IEditSessionsWorkbenchService, EditSessionsWorkbenchService); +registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService); const continueEditSessionCommand: IAction2Options = { id: '_workbench.experimental.editSessions.actions.continueEditSession', @@ -76,7 +79,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo private readonly shouldShowViewsContext: IContextKey; constructor( - @IEditSessionsWorkbenchService private readonly editSessionsWorkbenchService: IEditSessionsWorkbenchService, + @IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService, @IFileService private readonly fileService: IFileService, @IProgressService private readonly progressService: IProgressService, @IOpenerService private readonly openerService: IOpenerService, @@ -90,6 +93,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo @IProductService private readonly productService: IProductService, @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IEditSessionIdentityService private readonly editSessionIdentityService: IEditSessionIdentityService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ICommandService private commandService: ICommandService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -98,17 +102,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo ) { super(); - if (this.environmentService.editSessionId !== undefined) { - performance.mark('code/willResumeEditSessionFromIdentifier'); - type ResumeEvent = {}; - type ResumeClassification = { - owner: 'joyceerhl'; comment: 'Reporting when an action is resumed from an edit session identifier.'; - }; - this.telemetryService.publicLog2('editSessions.continue.resume'); - - void this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined); - performance.mark('code/didResumeEditSessionFromIdentifier'); - } + this.autoResumeEditSession(); this.configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration(experimentalSettingName)) { @@ -118,42 +112,41 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo this.registerActions(); this.registerViews(); - - continueEditSessionExtPoint.setHandler(extensions => { - const continueEditSessionOptions: ContinueEditSessionItem[] = []; - for (const extension of extensions) { - if (!isProposedApiEnabled(extension.description, 'contribEditSessions')) { - continue; - } - if (!Array.isArray(extension.value)) { - continue; - } - for (const contribution of extension.value) { - const command = MenuRegistry.getCommand(contribution.command); - if (!command) { - return; - } - - const icon = command.icon; - const title = typeof command.title === 'string' ? command.title : command.title.value; - - continueEditSessionOptions.push(new ContinueEditSessionItem( - ThemeIcon.isThemeIcon(icon) ? `$(${icon.id}) ${title}` : title, - command.id, - command.source, - ContextKeyExpr.deserialize(contribution.when) - )); - } - } - this.continueEditSessionOptions = continueEditSessionOptions; - }); + this.registerContributedEditSessionOptions(); this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService); - this._register(this.fileService.registerProvider(EditSessionsFileSystemProvider.SCHEMA, new EditSessionsFileSystemProvider(this.editSessionsWorkbenchService))); + this._register(this.fileService.registerProvider(EditSessionsFileSystemProvider.SCHEMA, new EditSessionsFileSystemProvider(this.editSessionsStorageService))); this.lifecycleService.onWillShutdown((e) => e.join(this.autoStoreEditSession(), { id: 'autoStoreEditSession', label: localize('autoStoreEditSession', 'Storing current edit session...') })); } + private async autoResumeEditSession() { + if (this.environmentService.editSessionId !== undefined) { + // In web, resume edit session based on an edit session GUID that + // was explicitly passed into the workbench construction options + void this.progressService.withProgress({ location: ProgressLocation.Window, type: 'syncing', title: localize('resuming edit session dialog', 'Resuming your latest edit session...') }, async () => { + performance.mark('code/willResumeEditSessionFromIdentifier'); + + type ResumeEvent = {}; + type ResumeClassification = { + owner: 'joyceerhl'; comment: 'Reporting when an action is resumed from an edit session identifier.'; + }; + this.telemetryService.publicLog2('editSessions.continue.resume'); + + await this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined); + + performance.mark('code/didResumeEditSessionFromIdentifier'); + }); + } else if (this.configurationService.getValue('workbench.experimental.editSessions.autoResume') === 'onReload' && this.editSessionsStorageService.isSignedIn) { + // Attempt to resume edit session based on edit workspace identifier + // Note: at this point if the user is not signed into edit sessions, + // we don't want them to be prompted to sign in and should just return early + void this.progressService.withProgress({ location: ProgressLocation.Window, type: 'syncing', title: localize('resuming edit session window', 'Resuming edit session...') }, async () => { + await this.resumeEditSession(undefined, true); + }); + } + } + private async autoStoreEditSession() { if (this.configurationService.getValue('workbench.experimental.editSessions.autoStore') === 'onShutdown') { await this.progressService.withProgress({ @@ -233,12 +226,12 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo }; that.telemetryService.publicLog2('editSessions.continue.store'); - let uri = workspaceUri ?? await that.pickContinueEditSessionDestination(); - if (uri === undefined) { return; } - // Run the store action to get back a ref const ref = await that.storeEditSession(false); + let uri = workspaceUri ?? await that.pickContinueEditSessionDestination(); + if (uri === undefined) { return; } + // Append the ref to the URI if (ref !== undefined) { const encodedRef = encodeURIComponent(ref); @@ -314,14 +307,14 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo })); } - async resumeEditSession(ref?: string): Promise { + async resumeEditSession(ref?: string, silent?: boolean): Promise { this.logService.info(ref !== undefined ? `Resuming edit session with ref ${ref}...` : 'Resuming edit session...'); - const data = await this.editSessionsWorkbenchService.read(ref); + const data = await this.editSessionsStorageService.read(ref); if (!data) { - if (ref === undefined) { + if (ref === undefined && !silent) { this.notificationService.info(localize('no edit session', 'There are no edit sessions to resume.')); - } else { + } else if (ref !== undefined) { this.notificationService.warn(localize('no edit session content for ref', 'Could not resume edit session contents for ID {0}.', ref)); } this.logService.info(`Aborting resuming edit session as no edit session content is available to be applied from ref ${ref}.`); @@ -338,11 +331,28 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo try { const changes: ({ uri: URI; type: ChangeType; contents: string | undefined })[] = []; let hasLocalUncommittedChanges = false; + const workspaceFolders = this.contextService.getWorkspace().folders; for (const folder of editSession.folders) { - const folderRoot = this.contextService.getWorkspace().folders.find((f) => f.name === folder.name); + const cancellationTokenSource = new CancellationTokenSource(); + let folderRoot: IWorkspaceFolder | undefined; + + if (folder.canonicalIdentity) { + // Look for an edit session identifier that we can use + for (const f of workspaceFolders) { + const identity = await this.editSessionIdentityService.getEditSessionIdentifier(f, cancellationTokenSource); + this.logService.info(`Matching identity ${identity} against edit session folder identity ${folder.canonicalIdentity}...`); + if (equals(identity, folder.canonicalIdentity)) { + folderRoot = f; + break; + } + } + } else { + folderRoot = workspaceFolders.find((f) => f.name === folder.name); + } + if (!folderRoot) { - this.logService.info(`Skipping applying ${folder.workingChanges.length} changes from edit session with ref ${ref} as no corresponding workspace folder named ${folder.name} is currently open.`); + this.logService.info(`Skipping applying ${folder.workingChanges.length} changes from edit session with ref ${ref} as no matching workspace folder was found.`); return; } @@ -383,7 +393,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } this.logService.info(`Deleting edit session with ref ${ref} after successfully applying it to current workspace...`); - await this.editSessionsWorkbenchService.delete(ref); + await this.editSessionsStorageService.delete(ref); this.logService.info(`Deleted edit session with ref ${ref}.`); } catch (ex) { this.logService.error('Failed to resume edit session, reason: ', (ex as Error).toString()); @@ -400,7 +410,10 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo const trackedUris = this.getChangedResources(repository); // A URI might appear in more than one resource group const workingChanges: Change[] = []; - let name = repository.provider.rootUri ? this.contextService.getWorkspaceFolder(repository.provider.rootUri)?.name : undefined; + + const { rootUri } = repository.provider; + const workspaceFolder = rootUri ? this.contextService.getWorkspaceFolder(rootUri) : undefined; + let name = workspaceFolder?.name; for (const uri of trackedUris) { const workspaceFolder = this.contextService.getWorkspaceFolder(uri); @@ -431,7 +444,9 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } } - folders.push({ workingChanges, name: name ?? '' }); + const canonicalIdentity = workspaceFolder ? await this.editSessionIdentityService.getEditSessionIdentifier(workspaceFolder, new CancellationTokenSource()) : undefined; + + folders.push({ workingChanges, name: name ?? '', canonicalIdentity: canonicalIdentity ?? undefined }); } if (!hasEdits) { @@ -446,7 +461,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo try { this.logService.info(`Storing edit session...`); - const ref = await this.editSessionsWorkbenchService.write(data); + const ref = await this.editSessionsStorageService.write(data); this.logService.info(`Stored edit session with ref ${ref}.`); return ref; } catch (ex) { @@ -487,6 +502,37 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo //#region Continue Edit Session extension contribution point + private registerContributedEditSessionOptions() { + continueEditSessionExtPoint.setHandler(extensions => { + const continueEditSessionOptions: ContinueEditSessionItem[] = []; + for (const extension of extensions) { + if (!isProposedApiEnabled(extension.description, 'contribEditSessions')) { + continue; + } + if (!Array.isArray(extension.value)) { + continue; + } + for (const contribution of extension.value) { + const command = MenuRegistry.getCommand(contribution.command); + if (!command) { + return; + } + + const icon = command.icon; + const title = typeof command.title === 'string' ? command.title : command.title.value; + + continueEditSessionOptions.push(new ContinueEditSessionItem( + ThemeIcon.isThemeIcon(icon) ? `$(${icon.id}) ${title}` : title, + command.id, + command.source, + ContextKeyExpr.deserialize(contribution.when) + )); + } + } + this.continueEditSessionOptions = continueEditSessionOptions; + }); + } + private registerContinueInLocalFolderAction(): void { const that = this; this._register(registerAction2(class ContinueInLocalFolderAction extends Action2 { @@ -629,5 +675,16 @@ Registry.as(Extensions.Configuration).registerConfigurat 'default': true, 'markdownDescription': localize('editSessionsEnabled', "Controls whether to display cloud-enabled actions to store and resume uncommitted changes when switching between web, desktop, or devices."), }, + 'workbench.experimental.editSessions.autoResume': { + enum: ['onReload', 'off'], + enumDescriptions: [ + localize('autoResume.onReload', "Automatically resume available edit session on window reload."), + localize('autoResume.off', "Never attempt to resume an edit session.") + ], + 'type': 'string', + 'tags': ['experimental', 'usesOnlineServices'], + 'default': 'off', + 'markdownDescription': localize('autoResume', "Controls whether to automatically resume an available edit session for the current workspace."), + }, } }); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts index ef88486cee0..ced7fbb6fbd 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsFileSystemProvider.ts @@ -7,14 +7,14 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; -import { ChangeType, decodeEditSessionFileContent, EDIT_SESSIONS_SCHEME, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { ChangeType, decodeEditSessionFileContent, EDIT_SESSIONS_SCHEME, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; export class EditSessionsFileSystemProvider implements IFileSystemProviderWithFileReadCapability { static readonly SCHEMA = EDIT_SESSIONS_SCHEME; constructor( - @IEditSessionsWorkbenchService private editSessionsWorkbenchService: IEditSessionsWorkbenchService, + @IEditSessionsStorageService private editSessionsStorageService: IEditSessionsStorageService, ) { } readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly; @@ -25,7 +25,7 @@ export class EditSessionsFileSystemProvider implements IFileSystemProviderWithFi throw FileSystemProviderErrorCode.FileNotFound; } const { ref, folderName, filePath } = match.groups; - const data = await this.editSessionsWorkbenchService.read(ref); + const data = await this.editSessionsStorageService.read(ref); if (!data) { throw FileSystemProviderErrorCode.FileNotFound; } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts similarity index 98% rename from src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts rename to src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 6cd0f8ad9e7..e9566958c72 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -18,7 +18,7 @@ import { createSyncHeaders, IAuthenticationProvider, IResourceRefHandle, IUserDa import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsWorkbenchService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { generateUuid } from 'vs/base/common/uuid'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; @@ -27,7 +27,7 @@ import { getCurrentAuthenticationSessionInfo } from 'vs/workbench/services/authe type ExistingSession = IQuickPickItem & { session: AuthenticationSession & { providerId: string } }; type AuthenticationProviderOption = IQuickPickItem & { provider: IAuthenticationProvider }; -export class EditSessionsWorkbenchService extends Disposable implements IEditSessionsWorkbenchService { +export class EditSessionsWorkbenchService extends Disposable implements IEditSessionsStorageService { _serviceBrand = undefined; @@ -40,6 +40,10 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes private initialized = false; private readonly signedInContext: IContextKey; + get isSignedIn() { + return this.existingSessionId !== undefined; + } + constructor( @IFileService private readonly fileService: IFileService, @IStorageService private readonly storageService: IStorageService, diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts index 1607260baa8..dc1b524784b 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts @@ -10,7 +10,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { Registry } from 'vs/platform/registry/common/platform'; import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewsRegistry, TreeItemCollapsibleState, TreeViewItemHandleArg, ViewContainer } from 'vs/workbench/common/views'; -import { EDIT_SESSIONS_DATA_VIEW_ID, EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_SIGNED_IN_KEY, EDIT_SESSIONS_TITLE, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { EDIT_SESSIONS_DATA_VIEW_ID, EDIT_SESSIONS_SCHEME, EDIT_SESSIONS_SHOW_VIEW, EDIT_SESSIONS_SIGNED_IN, EDIT_SESSIONS_SIGNED_IN_KEY, EDIT_SESSIONS_TITLE, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { URI } from 'vs/base/common/uri'; import { fromNow } from 'vs/base/common/date'; import { Codicon } from 'vs/base/common/codicons'; @@ -128,14 +128,14 @@ export class EditSessionsDataViews extends Disposable { async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { const editSessionId = URI.parse(handle.$treeItemHandle).path.substring(1); const dialogService = accessor.get(IDialogService); - const editSessionWorkbenchService = accessor.get(IEditSessionsWorkbenchService); + const editSessionStorageService = accessor.get(IEditSessionsStorageService); const result = await dialogService.confirm({ message: localize('confirm delete', 'Are you sure you want to permanently delete the edit session with ref {0}? You cannot undo this action.', editSessionId), type: 'warning', title: EDIT_SESSIONS_TITLE }); if (result.confirmed) { - await editSessionWorkbenchService.delete(editSessionId); + await editSessionStorageService.delete(editSessionId); await treeView.refresh(); } } @@ -156,14 +156,14 @@ export class EditSessionsDataViews extends Disposable { async run(accessor: ServicesAccessor): Promise { const dialogService = accessor.get(IDialogService); - const editSessionWorkbenchService = accessor.get(IEditSessionsWorkbenchService); + const editSessionStorageService = accessor.get(IEditSessionsStorageService); const result = await dialogService.confirm({ message: localize('confirm delete all', 'Are you sure you want to permanently delete all edit sessions? You cannot undo this action.'), type: 'warning', title: EDIT_SESSIONS_TITLE }); if (result.confirmed) { - await editSessionWorkbenchService.delete(null); + await editSessionStorageService.delete(null); await treeView.refresh(); } } @@ -176,7 +176,7 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { private editSessionsCount; constructor( - @IEditSessionsWorkbenchService private readonly editSessionsWorkbenchService: IEditSessionsWorkbenchService, + @IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService ) { this.editSessionsCount = EDIT_SESSIONS_COUNT_CONTEXT_KEY.bindTo(this.contextKeyService); @@ -199,7 +199,7 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { } private async getAllEditSessions(): Promise { - const allEditSessions = await this.editSessionsWorkbenchService.list(); + const allEditSessions = await this.editSessionsStorageService.list(); this.editSessionsCount.set(allEditSessions.length); return allEditSessions.map((session) => { const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${session.ref}` }); @@ -215,7 +215,7 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { } private async getEditSession(ref: string): Promise { - const data = await this.editSessionsWorkbenchService.read(ref); + const data = await this.editSessionsStorageService.read(ref); if (!data) { return []; @@ -233,7 +233,7 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { } private async getEditSessionFolderContents(ref: string, folderName: string): Promise { - const data = await this.editSessionsWorkbenchService.read(ref); + const data = await this.editSessionsStorageService.read(ref); if (!data) { return []; diff --git a/src/vs/workbench/contrib/editSessions/common/editSessions.ts b/src/vs/workbench/contrib/editSessions/common/editSessions.ts index 52b7d6003c6..c6fc8fda976 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessions.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessions.ts @@ -18,10 +18,12 @@ export const EDIT_SESSION_SYNC_CATEGORY: ILocalizedString = { value: localize('session sync', 'Edit Sessions') }; -export const IEditSessionsWorkbenchService = createDecorator('IEditSessionsWorkbenchService'); -export interface IEditSessionsWorkbenchService { +export const IEditSessionsStorageService = createDecorator('IEditSessionsStorageService'); +export interface IEditSessionsStorageService { _serviceBrand: undefined; + readonly isSignedIn: boolean; + read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined>; write(editSession: EditSession): Promise; delete(ref: string | null): Promise; @@ -58,6 +60,7 @@ export type Change = Addition | Deletion; export interface Folder { name: string; + canonicalIdentity: string | undefined; workingChanges: Change[]; } diff --git a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts index 4cf1596cb8e..b5f1adc7505 100644 --- a/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts +++ b/src/vs/workbench/contrib/editSessions/test/browser/editSessions.test.ts @@ -21,7 +21,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { mock } from 'vs/base/test/common/mock'; import * as sinon from 'sinon'; import * as assert from 'assert'; -import { ChangeType, FileType, IEditSessionsLogService, IEditSessionsWorkbenchService } from 'vs/workbench/contrib/editSessions/common/editSessions'; +import { ChangeType, FileType, IEditSessionsLogService, IEditSessionsStorageService } from 'vs/workbench/contrib/editSessions/common/editSessions'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -65,7 +65,7 @@ suite('Edit session sync', () => { override onWillShutdown = Event.None; }); instantiationService.stub(INotificationService, new TestNotificationService()); - instantiationService.stub(IEditSessionsWorkbenchService, new class extends mock() { }); + instantiationService.stub(IEditSessionsStorageService, new class extends mock() { }); instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(ISCMService, SCMService); instantiationService.stub(IEnvironmentService, TestEnvironmentService); @@ -128,7 +128,7 @@ suite('Edit session sync', () => { // Stub sync service to return edit session data const readStub = sandbox.stub().returns({ editSession, ref: '0' }); - instantiationService.stub(IEditSessionsWorkbenchService, 'read', readStub); + instantiationService.stub(IEditSessionsStorageService, 'read', readStub); // Create root folder await fileService.createFolder(folderUri); @@ -142,7 +142,7 @@ suite('Edit session sync', () => { test('Edit session not stored if there are no edits', async function () { const writeStub = sandbox.stub(); - instantiationService.stub(IEditSessionsWorkbenchService, 'write', writeStub); + instantiationService.stub(IEditSessionsStorageService, 'write', writeStub); // Create root folder await fileService.createFolder(folderUri); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index b7a83c396d6..0c2634d8b99 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -25,6 +25,7 @@ export const allApiProposals = Object.freeze({ documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', documentPaste: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', + editSessionIdentityProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', externalUriOpener: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 33eaef0922b..0d9ef9fa9d1 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -304,6 +304,11 @@ export const schema: IJSONSchema = { description: nls.localize('vscode.extension.activationEvents.onFileSystem', 'An activation event emitted whenever a file or folder is accessed with the given scheme.'), body: 'onFileSystem:${1:scheme}' }, + { + label: 'onEditSession', + description: nls.localize('vscode.extension.activationEvents.onEditSession', 'An activation event emitted whenever an edit session is accessed with the given scheme.'), + body: 'onEditSession:${1:scheme}' + }, { label: 'onSearch', description: nls.localize('vscode.extension.activationEvents.onSearch', 'An activation event emitted whenever a search is started in the folder with the given scheme.'), diff --git a/src/vs/workbench/services/workspaces/common/editSessionIdentityService.ts b/src/vs/workbench/services/workspaces/common/editSessionIdentityService.ts new file mode 100644 index 00000000000..b4917e74e2d --- /dev/null +++ b/src/vs/workbench/services/workspaces/common/editSessionIdentityService.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEditSessionIdentityProvider, IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export class EditSessionIdentityService implements IEditSessionIdentityService { + readonly _serviceBrand: undefined; + + private _editSessionIdentifierProviders = new Map(); + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + @ILogService private readonly _logService: ILogService, + ) { } + + registerEditSessionIdentityProvider(provider: IEditSessionIdentityProvider): void { + if (this._editSessionIdentifierProviders.get(provider.scheme)) { + throw new Error(`A provider has already been registered for scheme ${provider.scheme}`); + } + + this._editSessionIdentifierProviders.set(provider.scheme, provider); + } + + unregisterEditSessionIdentityProvider(scheme: string): void { + this._editSessionIdentifierProviders.delete(scheme); + } + + async getEditSessionIdentifier(workspaceFolder: IWorkspaceFolder, cancellationTokenSource: CancellationTokenSource): Promise { + const { scheme } = workspaceFolder.uri; + + const provider = await this.activateProvider(scheme); + this._logService.info(`EditSessionIdentityProvider for scheme ${scheme} available: ${!!provider}`); + + return provider?.getEditSessionIdentifier(workspaceFolder, cancellationTokenSource.token); + } + + private async activateProvider(scheme: string) { + const provider = this._editSessionIdentifierProviders.get(scheme); + if (provider) { + return provider; + } + + await this._extensionService.activateByEvent(`onEditSession:${scheme}`); + return this._editSessionIdentifierProviders.get(scheme); + } +} + +registerSingleton(IEditSessionIdentityService, EditSessionIdentityService, true); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index b0c26eacd7f..bdfa69387cf 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -54,6 +54,7 @@ import 'vs/workbench/browser/parts/views/viewsService'; import 'vs/platform/actions/common/actions.contribution'; import 'vs/platform/undoRedo/common/undoRedoService'; +import 'vs/workbench/services/workspaces/common/editSessionIdentityService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/decorations/browser/decorationsService'; diff --git a/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts b/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts index 254c923ef9f..a12856915ce 100644 --- a/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts +++ b/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts @@ -3,4 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// empty placeholder declaration for the `contribEditSessions`-contribution point +// empty placeholder for edit sessions contribution point from core + +// https://github.com/microsoft/vscode/issues/157734 @joyceerhl diff --git a/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts b/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts new file mode 100644 index 00000000000..e4d10f273f9 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts @@ -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; + } +}