Path: blob/main/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';6import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js';7import { Registry } from '../../../../platform/registry/common/platform.js';8import { ILifecycleService, LifecyclePhase, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js';9import { Action2, IAction2Options, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';10import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';11import { localize, localize2 } from '../../../../nls.js';12import { 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_DATA_VIEW_ID, decodeEditSessionFileContent, hashedEditSessionId, editSessionsLogId, EDIT_SESSIONS_PENDING } from '../common/editSessions.js';13import { ISCMRepository, ISCMService } from '../../scm/common/scm.js';14import { IFileService } from '../../../../platform/files/common/files.js';15import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';16import { URI } from '../../../../base/common/uri.js';17import { basename, joinPath, relativePath } from '../../../../base/common/resources.js';18import { encodeBase64 } from '../../../../base/common/buffer.js';19import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';20import { IProgress, IProgressService, IProgressStep, ProgressLocation } from '../../../../platform/progress/common/progress.js';21import { EditSessionsWorkbenchService } from './editSessionsStorageService.js';22import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';23import { UserDataSyncErrorCode, UserDataSyncStoreError } from '../../../../platform/userDataSync/common/userDataSync.js';24import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';25import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';26import { getFileNamesMessage, IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';27import { IProductService } from '../../../../platform/product/common/productService.js';28import { IOpenerService } from '../../../../platform/opener/common/opener.js';29import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';30import { workbenchConfigurationNodeBase } from '../../../common/configuration.js';31import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';32import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';33import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';34import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';35import { ICommandService } from '../../../../platform/commands/common/commands.js';36import { getVirtualWorkspaceLocation } from '../../../../platform/workspace/common/virtualWorkspace.js';37import { Schemas } from '../../../../base/common/network.js';38import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';39import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';40import { EditSessionsLogService } from '../common/editSessionsLogService.js';41import { IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation } from '../../../common/views.js';42import { IViewsService } from '../../../services/views/common/viewsService.js';43import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';44import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';45import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';46import { EditSessionsDataViews } from './editSessionsViews.js';47import { EditSessionsFileSystemProvider } from './editSessionsFileSystemProvider.js';48import { isNative, isWeb } from '../../../../base/common/platform.js';49import { VirtualWorkspaceContext, WorkspaceFolderCountContext } from '../../../common/contextkeys.js';50import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';51import { equals } from '../../../../base/common/objects.js';52import { EditSessionIdentityMatch, IEditSessionIdentityService } from '../../../../platform/workspace/common/editSessions.js';53import { ThemeIcon } from '../../../../base/common/themables.js';54import { IOutputService } from '../../../services/output/common/output.js';55import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';56import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js';57import { IEditorService } from '../../../services/editor/common/editorService.js';58import { ILocalizedString } from '../../../../platform/action/common/action.js';59import { Codicon } from '../../../../base/common/codicons.js';60import { CancellationError } from '../../../../base/common/errors.js';61import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';62import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';63import { WorkspaceStateSynchroniser } from '../common/workspaceStateSync.js';64import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';65import { IRequestService } from '../../../../platform/request/common/request.js';66import { EditSessionsStoreClient } from '../common/editSessionsStorageClient.js';67import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';68import { IWorkspaceIdentityService } from '../../../services/workspaces/common/workspaceIdentityService.js';69import { hashAsync } from '../../../../base/common/hash.js';70import { ResourceSet } from '../../../../base/common/map.js';7172registerSingleton(IEditSessionsLogService, EditSessionsLogService, InstantiationType.Delayed);73registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService, InstantiationType.Delayed);747576const continueWorkingOnCommand: IAction2Options = {77id: '_workbench.editSessions.actions.continueEditSession',78title: localize2('continue working on', 'Continue Working On...'),79precondition: WorkspaceFolderCountContext.notEqualsTo('0'),80f1: true81};82const openLocalFolderCommand: IAction2Options = {83id: '_workbench.editSessions.actions.continueEditSession.openLocalFolder',84title: localize2('continue edit session in local folder', 'Open In Local Folder'),85category: EDIT_SESSION_SYNC_CATEGORY,86precondition: ContextKeyExpr.and(IsWebContext.toNegated(), VirtualWorkspaceContext)87};88const showOutputChannelCommand: IAction2Options = {89id: 'workbench.editSessions.actions.showOutputChannel',90title: localize2('show log', "Show Log"),91category: EDIT_SESSION_SYNC_CATEGORY92};93const installAdditionalContinueOnOptionsCommand = {94id: 'workbench.action.continueOn.extensions',95title: localize('continueOn.installAdditional', 'Install additional development environment options'),96};97registerAction2(class extends Action2 {98constructor() {99super({ ...installAdditionalContinueOnOptionsCommand, f1: false });100}101102async run(accessor: ServicesAccessor): Promise<void> {103return accessor.get(IExtensionsWorkbenchService).openSearch('@tag:continueOn');104}105});106107const resumeProgressOptionsTitle = `[${localize('resuming working changes window', 'Resuming working changes...')}](command:${showOutputChannelCommand.id})`;108const resumeProgressOptions = {109location: ProgressLocation.Window,110type: 'syncing',111};112const queryParamName = 'editSessionId';113114const useEditSessionsWithContinueOn = 'workbench.editSessions.continueOn';115export class EditSessionsContribution extends Disposable implements IWorkbenchContribution {116117private continueEditSessionOptions: ContinueEditSessionItem[] = [];118119private readonly shouldShowViewsContext: IContextKey<boolean>;120private readonly pendingEditSessionsContext: IContextKey<boolean>;121122private static APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY = 'applicationLaunchedViaContinueOn';123private readonly accountsMenuBadgeDisposable = this._register(new MutableDisposable());124125private registeredCommands = new Set<string>();126127private workspaceStateSynchronizer: WorkspaceStateSynchroniser | undefined;128private editSessionsStorageClient: EditSessionsStoreClient | undefined;129130constructor(131@IEditSessionsStorageService private readonly editSessionsStorageService: IEditSessionsStorageService,132@IFileService private readonly fileService: IFileService,133@IProgressService private readonly progressService: IProgressService,134@IOpenerService private readonly openerService: IOpenerService,135@ITelemetryService private readonly telemetryService: ITelemetryService,136@ISCMService private readonly scmService: ISCMService,137@INotificationService private readonly notificationService: INotificationService,138@IDialogService private readonly dialogService: IDialogService,139@IEditSessionsLogService private readonly logService: IEditSessionsLogService,140@IEnvironmentService private readonly environmentService: IEnvironmentService,141@IInstantiationService private readonly instantiationService: IInstantiationService,142@IProductService private readonly productService: IProductService,143@IConfigurationService private configurationService: IConfigurationService,144@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,145@IEditSessionIdentityService private readonly editSessionIdentityService: IEditSessionIdentityService,146@IQuickInputService private readonly quickInputService: IQuickInputService,147@ICommandService private commandService: ICommandService,148@IContextKeyService private readonly contextKeyService: IContextKeyService,149@IFileDialogService private readonly fileDialogService: IFileDialogService,150@ILifecycleService private readonly lifecycleService: ILifecycleService,151@IStorageService private readonly storageService: IStorageService,152@IActivityService private readonly activityService: IActivityService,153@IEditorService private readonly editorService: IEditorService,154@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,155@IExtensionService private readonly extensionService: IExtensionService,156@IRequestService private readonly requestService: IRequestService,157@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,158@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,159@IWorkspaceIdentityService private readonly workspaceIdentityService: IWorkspaceIdentityService,160) {161super();162163this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService);164this.pendingEditSessionsContext = EDIT_SESSIONS_PENDING.bindTo(this.contextKeyService);165this.pendingEditSessionsContext.set(false);166167if (!this.productService['editSessions.store']?.url) {168return;169}170171this.editSessionsStorageClient = new EditSessionsStoreClient(URI.parse(this.productService['editSessions.store'].url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService);172this.editSessionsStorageService.storeClient = this.editSessionsStorageClient;173this.workspaceStateSynchronizer = new WorkspaceStateSynchroniser(this.userDataProfilesService.defaultProfile, undefined, this.editSessionsStorageClient, this.logService, this.fileService, this.environmentService, this.telemetryService, this.configurationService, this.storageService, this.uriIdentityService, this.workspaceIdentityService, this.editSessionsStorageService);174175this.autoResumeEditSession();176177this.registerActions();178this.registerViews();179this.registerContributedEditSessionOptions();180181this._register(this.fileService.registerProvider(EditSessionsFileSystemProvider.SCHEMA, new EditSessionsFileSystemProvider(this.editSessionsStorageService)));182this.lifecycleService.onWillShutdown((e) => {183if (e.reason !== ShutdownReason.RELOAD && this.editSessionsStorageService.isSignedIn && this.configurationService.getValue('workbench.experimental.cloudChanges.autoStore') === 'onShutdown' && !isWeb) {184e.join(this.autoStoreEditSession(), { id: 'autoStoreWorkingChanges', label: localize('autoStoreWorkingChanges', 'Storing current working changes...') });185}186});187this._register(this.editSessionsStorageService.onDidSignIn(() => this.updateAccountsMenuBadge()));188this._register(this.editSessionsStorageService.onDidSignOut(() => this.updateAccountsMenuBadge()));189}190191private async autoResumeEditSession() {192const shouldAutoResumeOnReload = this.configurationService.getValue('workbench.cloudChanges.autoResume') === 'onReload';193194if (this.environmentService.editSessionId !== undefined) {195this.logService.info(`Resuming cloud changes, reason: found editSessionId ${this.environmentService.editSessionId} in environment service...`);196await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(this.environmentService.editSessionId, undefined, undefined, undefined, progress).finally(() => this.environmentService.editSessionId = undefined));197} else if (shouldAutoResumeOnReload && this.editSessionsStorageService.isSignedIn) {198this.logService.info('Resuming cloud changes, reason: cloud changes enabled...');199// Attempt to resume edit session based on edit workspace identifier200// Note: at this point if the user is not signed into edit sessions,201// we don't want them to be prompted to sign in and should just return early202await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(undefined, true, undefined, undefined, progress));203} else if (shouldAutoResumeOnReload) {204// The application has previously launched via a protocol URL Continue On flow205const hasApplicationLaunchedFromContinueOnFlow = this.storageService.getBoolean(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION, false);206this.logService.info(`Prompting to enable cloud changes, has application previously launched from Continue On flow: ${hasApplicationLaunchedFromContinueOnFlow}`);207208const handlePendingEditSessions = () => {209// display a badge in the accounts menu but do not prompt the user to sign in again210this.logService.info('Showing badge to enable cloud changes in accounts menu...');211this.updateAccountsMenuBadge();212this.pendingEditSessionsContext.set(true);213// attempt a resume if we are in a pending state and the user just signed in214const disposable = this.editSessionsStorageService.onDidSignIn(async () => {215disposable.dispose();216this.logService.info('Showing badge to enable cloud changes in accounts menu succeeded, resuming cloud changes...');217await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(undefined, true, undefined, undefined, progress));218this.storageService.remove(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, StorageScope.APPLICATION);219this.environmentService.continueOn = undefined;220});221};222223if ((this.environmentService.continueOn !== undefined) &&224!this.editSessionsStorageService.isSignedIn &&225// and user has not yet been prompted to sign in on this machine226hasApplicationLaunchedFromContinueOnFlow === false227) {228// store the fact that we prompted the user229this.storageService.store(EditSessionsContribution.APPLICATION_LAUNCHED_VIA_CONTINUE_ON_STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);230this.logService.info('Prompting to enable cloud changes...');231await this.editSessionsStorageService.initialize('read');232if (this.editSessionsStorageService.isSignedIn) {233this.logService.info('Prompting to enable cloud changes succeeded, resuming cloud changes...');234await this.progressService.withProgress(resumeProgressOptions, async (progress) => await this.resumeEditSession(undefined, true, undefined, undefined, progress));235} else {236handlePendingEditSessions();237}238} else if (!this.editSessionsStorageService.isSignedIn &&239// and user has been prompted to sign in on this machine240hasApplicationLaunchedFromContinueOnFlow === true241) {242handlePendingEditSessions();243}244} else {245this.logService.debug('Auto resuming cloud changes disabled.');246}247}248249private updateAccountsMenuBadge() {250if (this.editSessionsStorageService.isSignedIn) {251return this.accountsMenuBadgeDisposable.clear();252}253254const badge = new NumberBadge(1, () => localize('check for pending cloud changes', 'Check for pending cloud changes'));255this.accountsMenuBadgeDisposable.value = this.activityService.showAccountsActivity({ badge });256}257258private async autoStoreEditSession() {259const cancellationTokenSource = new CancellationTokenSource();260await this.progressService.withProgress({261location: ProgressLocation.Window,262type: 'syncing',263title: localize('store working changes', 'Storing working changes...')264}, async () => this.storeEditSession(false, cancellationTokenSource.token), () => {265cancellationTokenSource.cancel();266cancellationTokenSource.dispose();267});268}269270private registerViews() {271const container = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer(272{273id: EDIT_SESSIONS_CONTAINER_ID,274title: EDIT_SESSIONS_TITLE,275ctorDescriptor: new SyncDescriptor(276ViewPaneContainer,277[EDIT_SESSIONS_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]278),279icon: EDIT_SESSIONS_VIEW_ICON,280hideIfEmpty: true281}, ViewContainerLocation.Sidebar, { doNotRegisterOpenCommand: true }282);283this._register(this.instantiationService.createInstance(EditSessionsDataViews, container));284}285286private registerActions() {287this.registerContinueEditSessionAction();288289this.registerResumeLatestEditSessionAction();290this.registerStoreLatestEditSessionAction();291292this.registerContinueInLocalFolderAction();293294this.registerShowEditSessionViewAction();295this.registerShowEditSessionOutputChannelAction();296}297298private registerShowEditSessionOutputChannelAction() {299this._register(registerAction2(class ShowEditSessionOutput extends Action2 {300constructor() {301super(showOutputChannelCommand);302}303304run(accessor: ServicesAccessor, ...args: any[]) {305const outputChannel = accessor.get(IOutputService);306void outputChannel.showChannel(editSessionsLogId);307}308}));309}310311private registerShowEditSessionViewAction() {312const that = this;313this._register(registerAction2(class ShowEditSessionView extends Action2 {314constructor() {315super({316id: 'workbench.editSessions.actions.showEditSessions',317title: localize2('show cloud changes', 'Show Cloud Changes'),318category: EDIT_SESSION_SYNC_CATEGORY,319f1: true320});321}322323async run(accessor: ServicesAccessor) {324that.shouldShowViewsContext.set(true);325const viewsService = accessor.get(IViewsService);326await viewsService.openView(EDIT_SESSIONS_DATA_VIEW_ID);327}328}));329}330331private registerContinueEditSessionAction() {332const that = this;333this._register(registerAction2(class ContinueEditSessionAction extends Action2 {334constructor() {335super(continueWorkingOnCommand);336}337338async run(accessor: ServicesAccessor, workspaceUri: URI | undefined, destination: string | undefined): Promise<void> {339type ContinueOnEventOutcome = { outcome: string; hashedId?: string };340type ContinueOnClassificationOutcome = {341owner: 'joyceerhl'; comment: 'Reporting the outcome of invoking the Continue On action.';342outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The outcome of invoking continue edit session.' };343hashedId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The hash of the stored edit session id, for correlating success of stores and resumes.' };344};345346// First ask the user to pick a destination, if necessary347let uri: URI | 'noDestinationUri' | undefined = workspaceUri;348if (!destination && !uri) {349destination = await that.pickContinueEditSessionDestination();350if (!destination) {351that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.pick.outcome', { outcome: 'noSelection' });352return;353}354}355356// Determine if we need to store an edit session, asking for edit session auth if necessary357const shouldStoreEditSession = await that.shouldContinueOnWithEditSession();358359// Run the store action to get back a ref360let ref: string | undefined;361if (shouldStoreEditSession) {362type ContinueWithEditSessionEvent = {};363type ContinueWithEditSessionClassification = {364owner: 'joyceerhl'; comment: 'Reporting when storing an edit session as part of the Continue On flow.';365};366that.telemetryService.publicLog2<ContinueWithEditSessionEvent, ContinueWithEditSessionClassification>('continueOn.editSessions.store');367368const cancellationTokenSource = new CancellationTokenSource();369try {370ref = await that.progressService.withProgress({371location: ProgressLocation.Notification,372cancellable: true,373type: 'syncing',374title: localize('store your working changes', 'Storing your working changes...')375}, async () => {376const ref = await that.storeEditSession(false, cancellationTokenSource.token);377if (ref !== undefined) {378that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeSucceeded', hashedId: hashedEditSessionId(ref) });379} else {380that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeSkipped' });381}382return ref;383}, () => {384cancellationTokenSource.cancel();385cancellationTokenSource.dispose();386that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeCancelledByUser' });387});388} catch (ex) {389that.telemetryService.publicLog2<ContinueOnEventOutcome, ContinueOnClassificationOutcome>('continueOn.editSessions.store.outcome', { outcome: 'storeFailed' });390throw ex;391}392}393394// Append the ref to the URI395uri = destination ? await that.resolveDestination(destination) : uri;396if (uri === undefined) {397return;398}399400if (ref !== undefined && uri !== 'noDestinationUri') {401const encodedRef = encodeURIComponent(ref);402uri = uri.with({403query: uri.query.length > 0 ? (uri.query + `&${queryParamName}=${encodedRef}&continueOn=1`) : `${queryParamName}=${encodedRef}&continueOn=1`404});405406// Open the URI407that.logService.info(`Opening ${uri.toString()}`);408await that.openerService.open(uri, { openExternal: true });409} else if ((!shouldStoreEditSession || ref === undefined) && uri !== 'noDestinationUri') {410// Open the URI without an edit session ref411that.logService.info(`Opening ${uri.toString()}`);412await that.openerService.open(uri, { openExternal: true });413} else if (ref === undefined && shouldStoreEditSession) {414that.logService.warn(`Failed to store working changes when invoking ${continueWorkingOnCommand.id}.`);415}416}417}));418}419420private registerResumeLatestEditSessionAction(): void {421const that = this;422this._register(registerAction2(class ResumeLatestEditSessionAction extends Action2 {423constructor() {424super({425id: 'workbench.editSessions.actions.resumeLatest',426title: localize2('resume latest cloud changes', 'Resume Latest Changes from Cloud'),427category: EDIT_SESSION_SYNC_CATEGORY,428f1: true,429});430}431432async run(accessor: ServicesAccessor, editSessionId?: string, forceApplyUnrelatedChange?: boolean): Promise<void> {433await that.progressService.withProgress({ ...resumeProgressOptions, title: resumeProgressOptionsTitle }, async () => await that.resumeEditSession(editSessionId, undefined, forceApplyUnrelatedChange));434}435}));436this._register(registerAction2(class ResumeLatestEditSessionAction extends Action2 {437constructor() {438super({439id: 'workbench.editSessions.actions.resumeFromSerializedPayload',440title: localize2('resume cloud changes', 'Resume Changes from Serialized Data'),441category: 'Developer',442f1: true,443});444}445446async run(accessor: ServicesAccessor, editSessionId?: string): Promise<void> {447const data = await that.quickInputService.input({ prompt: 'Enter serialized data' });448if (data) {449that.editSessionsStorageService.lastReadResources.set('editSessions', { content: data, ref: '' });450}451await that.progressService.withProgress({ ...resumeProgressOptions, title: resumeProgressOptionsTitle }, async () => await that.resumeEditSession(editSessionId, undefined, undefined, undefined, undefined, data));452}453}));454}455456private registerStoreLatestEditSessionAction(): void {457const that = this;458this._register(registerAction2(class StoreLatestEditSessionAction extends Action2 {459constructor() {460super({461id: 'workbench.editSessions.actions.storeCurrent',462title: localize2('store working changes in cloud', 'Store Working Changes in Cloud'),463category: EDIT_SESSION_SYNC_CATEGORY,464f1: true,465});466}467468async run(accessor: ServicesAccessor): Promise<void> {469const cancellationTokenSource = new CancellationTokenSource();470await that.progressService.withProgress({471location: ProgressLocation.Notification,472title: localize('storing working changes', 'Storing working changes...')473}, async () => {474type StoreEvent = {};475type StoreClassification = {476owner: 'joyceerhl'; comment: 'Reporting when the store edit session action is invoked.';477};478that.telemetryService.publicLog2<StoreEvent, StoreClassification>('editSessions.store');479480await that.storeEditSession(true, cancellationTokenSource.token);481}, () => {482cancellationTokenSource.cancel();483cancellationTokenSource.dispose();484});485}486}));487}488489async resumeEditSession(ref?: string, silent?: boolean, forceApplyUnrelatedChange?: boolean, applyPartialMatch?: boolean, progress?: IProgress<IProgressStep>, serializedData?: string): Promise<void> {490// Wait for the remote environment to become available, if any491await this.remoteAgentService.getEnvironment();492493// Edit sessions are not currently supported in empty workspaces494// https://github.com/microsoft/vscode/issues/159220495if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {496return;497}498499this.logService.info(ref !== undefined ? `Resuming changes from cloud with ref ${ref}...` : 'Checking for pending cloud changes...');500501if (silent && !(await this.editSessionsStorageService.initialize('read', true))) {502return;503}504505type ResumeEvent = { outcome: string; hashedId?: string };506type ResumeClassification = {507owner: 'joyceerhl'; comment: 'Reporting when an edit session is resumed from an edit session identifier.';508outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The outcome of resuming the edit session.' };509hashedId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The hash of the stored edit session id, for correlating success of stores and resumes.' };510};511this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume');512513performance.mark('code/willResumeEditSessionFromIdentifier');514515progress?.report({ message: localize('checkingForWorkingChanges', 'Checking for pending cloud changes...') });516const data = serializedData ? { content: serializedData, ref: '' } : await this.editSessionsStorageService.read('editSessions', ref);517if (!data) {518if (ref === undefined && !silent) {519this.notificationService.info(localize('no cloud changes', 'There are no changes to resume from the cloud.'));520} else if (ref !== undefined) {521this.notificationService.warn(localize('no cloud changes for ref', 'Could not resume changes from the cloud for ID {0}.', ref));522}523this.logService.info(ref !== undefined ? `Aborting resuming changes from cloud as no edit session content is available to be applied from ref ${ref}.` : `Aborting resuming edit session as no edit session content is available to be applied`);524return;525}526527progress?.report({ message: resumeProgressOptionsTitle });528const editSession = JSON.parse(data.content);529ref = data.ref;530531if (editSession.version > EditSessionSchemaVersion) {532this.notificationService.error(localize('client too old', "Please upgrade to a newer version of {0} to resume your working changes from the cloud.", this.productService.nameLong));533this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'clientUpdateNeeded' });534return;535}536537try {538const { changes, conflictingChanges } = await this.generateChanges(editSession, ref, forceApplyUnrelatedChange, applyPartialMatch);539if (changes.length === 0) {540return;541}542543// TODO@joyceerhl Provide the option to diff files which would be overwritten by edit session contents544if (conflictingChanges.length > 0) {545// Allow to show edit sessions546547const { confirmed } = await this.dialogService.confirm({548type: Severity.Warning,549message: conflictingChanges.length > 1 ?550localize('resume edit session warning many', 'Resuming your working changes from the cloud will overwrite the following {0} files. Do you want to proceed?', conflictingChanges.length) :551localize('resume edit session warning 1', 'Resuming your working changes from the cloud will overwrite {0}. Do you want to proceed?', basename(conflictingChanges[0].uri)),552detail: conflictingChanges.length > 1 ? getFileNamesMessage(conflictingChanges.map((c) => c.uri)) : undefined553});554555if (!confirmed) {556return;557}558}559560for (const { uri, type, contents } of changes) {561if (type === ChangeType.Addition) {562await this.fileService.writeFile(uri, decodeEditSessionFileContent(editSession.version, contents!));563} else if (type === ChangeType.Deletion && await this.fileService.exists(uri)) {564await this.fileService.del(uri);565}566}567568await this.workspaceStateSynchronizer?.apply();569570this.logService.info(`Deleting edit session with ref ${ref} after successfully applying it to current workspace...`);571await this.editSessionsStorageService.delete('editSessions', ref);572this.logService.info(`Deleted edit session with ref ${ref}.`);573574this.telemetryService.publicLog2<ResumeEvent, ResumeClassification>('editSessions.resume.outcome', { hashedId: hashedEditSessionId(ref), outcome: 'resumeSucceeded' });575} catch (ex) {576this.logService.error('Failed to resume edit session, reason: ', (ex as Error).toString());577this.notificationService.error(localize('resume failed', "Failed to resume your working changes from the cloud."));578}579580performance.mark('code/didResumeEditSessionFromIdentifier');581}582583private async generateChanges(editSession: EditSession, ref: string, forceApplyUnrelatedChange = false, applyPartialMatch = false) {584const changes: ({ uri: URI; type: ChangeType; contents: string | undefined })[] = [];585const conflictingChanges = [];586const workspaceFolders = this.contextService.getWorkspace().folders;587const cancellationTokenSource = new CancellationTokenSource();588589for (const folder of editSession.folders) {590let folderRoot: IWorkspaceFolder | undefined;591592if (folder.canonicalIdentity) {593// Look for an edit session identifier that we can use594for (const f of workspaceFolders) {595const identity = await this.editSessionIdentityService.getEditSessionIdentifier(f, cancellationTokenSource.token);596this.logService.info(`Matching identity ${identity} against edit session folder identity ${folder.canonicalIdentity}...`);597598if (equals(identity, folder.canonicalIdentity) || forceApplyUnrelatedChange) {599folderRoot = f;600break;601}602603if (identity !== undefined) {604const match = await this.editSessionIdentityService.provideEditSessionIdentityMatch(f, identity, folder.canonicalIdentity, cancellationTokenSource.token);605if (match === EditSessionIdentityMatch.Complete) {606folderRoot = f;607break;608} else if (match === EditSessionIdentityMatch.Partial &&609this.configurationService.getValue('workbench.experimental.cloudChanges.partialMatches.enabled') === true610) {611if (!applyPartialMatch) {612// Surface partially matching edit session613this.notificationService.prompt(614Severity.Info,615localize('editSessionPartialMatch', 'You have pending working changes in the cloud for this workspace. Would you like to resume them?'),616[{ label: localize('resume', 'Resume'), run: () => this.resumeEditSession(ref, false, undefined, true) }]617);618} else {619folderRoot = f;620break;621}622}623}624}625} else {626folderRoot = workspaceFolders.find((f) => f.name === folder.name);627}628629if (!folderRoot) {630this.logService.info(`Skipping applying ${folder.workingChanges.length} changes from edit session with ref ${ref} as no matching workspace folder was found.`);631return { changes: [], conflictingChanges: [], contributedStateHandlers: [] };632}633634const localChanges = new Set<string>();635for (const repository of this.scmService.repositories) {636if (repository.provider.rootUri !== undefined &&637this.contextService.getWorkspaceFolder(repository.provider.rootUri)?.name === folder.name638) {639const repositoryChanges = this.getChangedResources(repository);640repositoryChanges.forEach((change) => localChanges.add(change.toString()));641}642}643644for (const change of folder.workingChanges) {645const uri = joinPath(folderRoot.uri, change.relativeFilePath);646647changes.push({ uri, type: change.type, contents: change.contents });648if (await this.willChangeLocalContents(localChanges, uri, change)) {649conflictingChanges.push({ uri, type: change.type, contents: change.contents });650}651}652}653654return { changes, conflictingChanges };655}656657private async willChangeLocalContents(localChanges: Set<string>, uriWithIncomingChanges: URI, incomingChange: Change) {658if (!localChanges.has(uriWithIncomingChanges.toString())) {659return false;660}661662const { contents, type } = incomingChange;663664switch (type) {665case (ChangeType.Addition): {666const [originalContents, incomingContents] = await Promise.all([667hashAsync(contents),668hashAsync(encodeBase64((await this.fileService.readFile(uriWithIncomingChanges)).value))669]);670return originalContents !== incomingContents;671}672case (ChangeType.Deletion): {673return await this.fileService.exists(uriWithIncomingChanges);674}675default:676throw new Error('Unhandled change type.');677}678}679680async storeEditSession(fromStoreCommand: boolean, cancellationToken: CancellationToken): Promise<string | undefined> {681const folders: Folder[] = [];682let editSessionSize = 0;683let hasEdits = false;684685// Save all saveable editors before building edit session contents686await this.editorService.saveAll();687688// Do a first pass over all repositories to ensure that the edit session identity is created for each.689// This may change the working changes that need to be stored later690const createdEditSessionIdentities = new ResourceSet();691for (const repository of this.scmService.repositories) {692const changedResources = this.getChangedResources(repository);693if (!changedResources.size) {694continue;695}696for (const uri of changedResources) {697const workspaceFolder = this.contextService.getWorkspaceFolder(uri);698if (!workspaceFolder || createdEditSessionIdentities.has(uri)) {699continue;700}701createdEditSessionIdentities.add(uri);702await this.editSessionIdentityService.onWillCreateEditSessionIdentity(workspaceFolder, cancellationToken);703}704}705706for (const repository of this.scmService.repositories) {707// Look through all resource groups and compute which files were added/modified/deleted708const trackedUris = this.getChangedResources(repository); // A URI might appear in more than one resource group709710const workingChanges: Change[] = [];711712const { rootUri } = repository.provider;713const workspaceFolder = rootUri ? this.contextService.getWorkspaceFolder(rootUri) : undefined;714let name = workspaceFolder?.name;715716for (const uri of trackedUris) {717const workspaceFolder = this.contextService.getWorkspaceFolder(uri);718if (!workspaceFolder) {719this.logService.info(`Skipping working change ${uri.toString()} as no associated workspace folder was found.`);720721continue;722}723724name = name ?? workspaceFolder.name;725const relativeFilePath = relativePath(workspaceFolder.uri, uri) ?? uri.path;726727// Only deal with file contents for now728try {729if (!(await this.fileService.stat(uri)).isFile) {730continue;731}732} catch { }733734hasEdits = true;735736737if (await this.fileService.exists(uri)) {738const contents = encodeBase64((await this.fileService.readFile(uri)).value);739editSessionSize += contents.length;740if (editSessionSize > this.editSessionsStorageService.SIZE_LIMIT) {741this.notificationService.error(localize('payload too large', 'Your working changes exceed the size limit and cannot be stored.'));742return undefined;743}744745workingChanges.push({ type: ChangeType.Addition, fileType: FileType.File, contents: contents, relativeFilePath: relativeFilePath });746} else {747// Assume it's a deletion748workingChanges.push({ type: ChangeType.Deletion, fileType: FileType.File, contents: undefined, relativeFilePath: relativeFilePath });749}750}751752let canonicalIdentity = undefined;753if (workspaceFolder !== null && workspaceFolder !== undefined) {754canonicalIdentity = await this.editSessionIdentityService.getEditSessionIdentifier(workspaceFolder, cancellationToken);755}756757// TODO@joyceerhl debt: don't store working changes as a child of the folder758folders.push({ workingChanges, name: name ?? '', canonicalIdentity: canonicalIdentity ?? undefined, absoluteUri: workspaceFolder?.uri.toString() });759}760761// Store contributed workspace state762await this.workspaceStateSynchronizer?.sync();763764if (!hasEdits) {765this.logService.info('Skipped storing working changes in the cloud as there are no edits to store.');766if (fromStoreCommand) {767this.notificationService.info(localize('no working changes to store', 'Skipped storing working changes in the cloud as there are no edits to store.'));768}769return undefined;770}771772const data: EditSession = { folders, version: 2, workspaceStateId: this.editSessionsStorageService.lastWrittenResources.get('workspaceState')?.ref };773774try {775this.logService.info(`Storing edit session...`);776const ref = await this.editSessionsStorageService.write('editSessions', data);777this.logService.info(`Stored edit session with ref ${ref}.`);778return ref;779} catch (ex) {780this.logService.error(`Failed to store edit session, reason: `, (ex as Error).toString());781782type UploadFailedEvent = { reason: string };783type UploadFailedClassification = {784owner: 'joyceerhl'; comment: 'Reporting when Continue On server request fails.';785reason?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason that the server request failed.' };786};787788if (ex instanceof UserDataSyncStoreError) {789switch (ex.code) {790case UserDataSyncErrorCode.TooLarge:791// Uploading a payload can fail due to server size limits792this.telemetryService.publicLog2<UploadFailedEvent, UploadFailedClassification>('editSessions.upload.failed', { reason: 'TooLarge' });793this.notificationService.error(localize('payload too large', 'Your working changes exceed the size limit and cannot be stored.'));794break;795default:796this.telemetryService.publicLog2<UploadFailedEvent, UploadFailedClassification>('editSessions.upload.failed', { reason: 'unknown' });797this.notificationService.error(localize('payload failed', 'Your working changes cannot be stored.'));798break;799}800}801}802803return undefined;804}805806private getChangedResources(repository: ISCMRepository) {807return repository.provider.groups.reduce((resources, resourceGroups) => {808resourceGroups.resources.forEach((resource) => resources.add(resource.sourceUri));809return resources;810}, new Set<URI>()); // A URI might appear in more than one resource group811}812813private hasEditSession() {814for (const repository of this.scmService.repositories) {815if (this.getChangedResources(repository).size > 0) {816return true;817}818}819return false;820}821822private async shouldContinueOnWithEditSession(): Promise<boolean> {823type EditSessionsAuthCheckEvent = { outcome: string };824type EditSessionsAuthCheckClassification = {825owner: 'joyceerhl'; comment: 'Reporting whether we can and should store edit session as part of Continue On.';826outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The outcome of checking whether we can store an edit session as part of the Continue On flow.' };827};828829// If the user is already signed in, we should store edit session830if (this.editSessionsStorageService.isSignedIn) {831return this.hasEditSession();832}833834// If the user has been asked before and said no, don't use edit sessions835if (this.configurationService.getValue(useEditSessionsWithContinueOn) === 'off') {836this.telemetryService.publicLog2<EditSessionsAuthCheckEvent, EditSessionsAuthCheckClassification>('continueOn.editSessions.canStore.outcome', { outcome: 'disabledEditSessionsViaSetting' });837return false;838}839840// Prompt the user to use edit sessions if they currently could benefit from using it841if (this.hasEditSession()) {842const disposables = new DisposableStore();843const quickpick = disposables.add(this.quickInputService.createQuickPick<IQuickPickItem>());844quickpick.placeholder = localize('continue with cloud changes', "Select whether to bring your working changes with you");845quickpick.ok = false;846quickpick.ignoreFocusOut = true;847const withCloudChanges = { label: localize('with cloud changes', "Yes, continue with my working changes") };848const withoutCloudChanges = { label: localize('without cloud changes', "No, continue without my working changes") };849quickpick.items = [withCloudChanges, withoutCloudChanges];850851const continueWithCloudChanges = await new Promise<boolean>((resolve, reject) => {852disposables.add(quickpick.onDidAccept(() => {853resolve(quickpick.selectedItems[0] === withCloudChanges);854disposables.dispose();855}));856disposables.add(quickpick.onDidHide(() => {857reject(new CancellationError());858disposables.dispose();859}));860quickpick.show();861});862863if (!continueWithCloudChanges) {864this.telemetryService.publicLog2<EditSessionsAuthCheckEvent, EditSessionsAuthCheckClassification>('continueOn.editSessions.canStore.outcome', { outcome: 'didNotEnableEditSessionsWhenPrompted' });865return continueWithCloudChanges;866}867868const initialized = await this.editSessionsStorageService.initialize('write');869if (!initialized) {870this.telemetryService.publicLog2<EditSessionsAuthCheckEvent, EditSessionsAuthCheckClassification>('continueOn.editSessions.canStore.outcome', { outcome: 'didNotEnableEditSessionsWhenPrompted' });871}872return initialized;873}874875return false;876}877878//#region Continue Edit Session extension contribution point879880private registerContributedEditSessionOptions() {881continueEditSessionExtPoint.setHandler(extensions => {882const continueEditSessionOptions: ContinueEditSessionItem[] = [];883for (const extension of extensions) {884if (!isProposedApiEnabled(extension.description, 'contribEditSessions')) {885continue;886}887if (!Array.isArray(extension.value)) {888continue;889}890for (const contribution of extension.value) {891const command = MenuRegistry.getCommand(contribution.command);892if (!command) {893return;894}895896const icon = command.icon;897const title = typeof command.title === 'string' ? command.title : command.title.value;898const when = ContextKeyExpr.deserialize(contribution.when);899900continueEditSessionOptions.push(new ContinueEditSessionItem(901ThemeIcon.isThemeIcon(icon) ? `$(${icon.id}) ${title}` : title,902command.id,903command.source?.title,904when,905contribution.documentation906));907908if (contribution.qualifiedName) {909this.generateStandaloneOptionCommand(command.id, contribution.qualifiedName, contribution.category ?? command.category, when, contribution.remoteGroup);910}911}912}913this.continueEditSessionOptions = continueEditSessionOptions;914});915}916917private generateStandaloneOptionCommand(commandId: string, qualifiedName: string, category: string | ILocalizedString | undefined, when: ContextKeyExpression | undefined, remoteGroup: string | undefined) {918const command: IAction2Options = {919id: `${continueWorkingOnCommand.id}.${commandId}`,920title: { original: qualifiedName, value: qualifiedName },921category: typeof category === 'string' ? { original: category, value: category } : category,922precondition: when,923f1: true924};925926if (!this.registeredCommands.has(command.id)) {927this.registeredCommands.add(command.id);928929this._register(registerAction2(class StandaloneContinueOnOption extends Action2 {930constructor() {931super(command);932}933934async run(accessor: ServicesAccessor): Promise<void> {935return accessor.get(ICommandService).executeCommand(continueWorkingOnCommand.id, undefined, commandId);936}937}));938939if (remoteGroup !== undefined) {940MenuRegistry.appendMenuItem(MenuId.StatusBarRemoteIndicatorMenu, {941group: remoteGroup,942command: command,943when: command.precondition944});945}946}947}948949private registerContinueInLocalFolderAction(): void {950const that = this;951this._register(registerAction2(class ContinueInLocalFolderAction extends Action2 {952constructor() {953super(openLocalFolderCommand);954}955956async run(accessor: ServicesAccessor): Promise<URI | undefined> {957const selection = await that.fileDialogService.showOpenDialog({958title: localize('continueEditSession.openLocalFolder.title.v2', 'Select a local folder to continue working in'),959canSelectFolders: true,960canSelectMany: false,961canSelectFiles: false,962availableFileSystems: [Schemas.file]963});964965return selection?.length !== 1 ? undefined : URI.from({966scheme: that.productService.urlProtocol,967authority: Schemas.file,968path: selection[0].path969});970}971}));972973if (getVirtualWorkspaceLocation(this.contextService.getWorkspace()) !== undefined && isNative) {974this.generateStandaloneOptionCommand(openLocalFolderCommand.id, localize('continueWorkingOn.existingLocalFolder', 'Continue Working in Existing Local Folder'), undefined, openLocalFolderCommand.precondition, undefined);975}976}977978private async pickContinueEditSessionDestination(): Promise<string | undefined> {979const disposables = new DisposableStore();980const quickPick = disposables.add(this.quickInputService.createQuickPick<ContinueEditSessionItem>({ useSeparators: true }));981982const workspaceContext = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER983? this.contextService.getWorkspace().folders[0].name984: this.contextService.getWorkspace().folders.map((folder) => folder.name).join(', ');985quickPick.placeholder = localize('continueEditSessionPick.title.v2', "Select a development environment to continue working on {0} in", `'${workspaceContext}'`);986quickPick.items = this.createPickItems();987this.extensionService.onDidChangeExtensions(() => {988quickPick.items = this.createPickItems();989});990991const command = await new Promise<string | undefined>((resolve, reject) => {992disposables.add(quickPick.onDidHide(() => {993disposables.dispose();994resolve(undefined);995}));996997disposables.add(quickPick.onDidAccept((e) => {998const selection = quickPick.activeItems[0].command;9991000if (selection === installAdditionalContinueOnOptionsCommand.id) {1001void this.commandService.executeCommand(installAdditionalContinueOnOptionsCommand.id);1002} else {1003resolve(selection);1004quickPick.hide();1005}1006}));10071008quickPick.show();10091010disposables.add(quickPick.onDidTriggerItemButton(async (e) => {1011if (e.item.documentation !== undefined) {1012const uri = URI.isUri(e.item.documentation) ? URI.parse(e.item.documentation) : await this.commandService.executeCommand(e.item.documentation);1013void this.openerService.open(uri, { openExternal: true });1014}1015}));1016});10171018quickPick.dispose();10191020return command;1021}10221023private async resolveDestination(command: string): Promise<URI | 'noDestinationUri' | undefined> {1024type EvaluateContinueOnDestinationEvent = { outcome: string; selection: string };1025type EvaluateContinueOnDestinationClassification = {1026owner: 'joyceerhl'; comment: 'Reporting the outcome of evaluating a selected Continue On destination option.';1027selection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The selected Continue On destination option.' };1028outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The outcome of evaluating the selected Continue On destination option.' };1029};10301031try {1032const uri = await this.commandService.executeCommand(command);10331034// Some continue on commands do not return a URI1035// to support extensions which want to be in control1036// of how the destination is opened1037if (uri === undefined) {1038this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'noDestinationUri' });1039return 'noDestinationUri';1040}10411042if (URI.isUri(uri)) {1043this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'resolvedUri' });1044return uri;1045}10461047this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'invalidDestination' });1048return undefined;1049} catch (ex) {1050if (ex instanceof CancellationError) {1051this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'cancelled' });1052} else {1053this.telemetryService.publicLog2<EvaluateContinueOnDestinationEvent, EvaluateContinueOnDestinationClassification>('continueOn.openDestination.outcome', { selection: command, outcome: 'unknownError' });1054}1055return undefined;1056}1057}10581059private createPickItems(): (ContinueEditSessionItem | IQuickPickSeparator)[] {1060const items = [...this.continueEditSessionOptions].filter((option) => option.when === undefined || this.contextKeyService.contextMatchesRules(option.when));10611062if (getVirtualWorkspaceLocation(this.contextService.getWorkspace()) !== undefined && isNative) {1063items.push(new ContinueEditSessionItem(1064'$(folder) ' + localize('continueEditSessionItem.openInLocalFolder.v2', 'Open in Local Folder'),1065openLocalFolderCommand.id,1066localize('continueEditSessionItem.builtin', 'Built-in')1067));1068}10691070const sortedItems: (ContinueEditSessionItem | IQuickPickSeparator)[] = items.sort((item1, item2) => item1.label.localeCompare(item2.label));1071return sortedItems.concat({ type: 'separator' }, new ContinueEditSessionItem(installAdditionalContinueOnOptionsCommand.title, installAdditionalContinueOnOptionsCommand.id));1072}1073}10741075const infoButtonClass = ThemeIcon.asClassName(Codicon.info);1076class ContinueEditSessionItem implements IQuickPickItem {1077public readonly buttons: IQuickInputButton[] | undefined;10781079constructor(1080public readonly label: string,1081public readonly command: string,1082public readonly description?: string,1083public readonly when?: ContextKeyExpression,1084public readonly documentation?: string,1085) {1086if (documentation !== undefined) {1087this.buttons = [{1088iconClass: infoButtonClass,1089tooltip: localize('learnMoreTooltip', 'Learn More'),1090}];1091}1092}1093}10941095interface ICommand {1096command: string;1097group: string;1098when: string;1099documentation?: string;1100qualifiedName?: string;1101category?: string;1102remoteGroup?: string;1103}11041105const continueEditSessionExtPoint = ExtensionsRegistry.registerExtensionPoint<ICommand[]>({1106extensionPoint: 'continueEditSession',1107jsonSchema: {1108description: localize('continueEditSessionExtPoint', 'Contributes options for continuing the current edit session in a different environment'),1109type: 'array',1110items: {1111type: 'object',1112properties: {1113command: {1114description: localize('continueEditSessionExtPoint.command', 'Identifier of the command to execute. The command must be declared in the \'commands\'-section and return a URI representing a different environment where the current edit session can be continued.'),1115type: 'string'1116},1117group: {1118description: localize('continueEditSessionExtPoint.group', 'Group into which this item belongs.'),1119type: 'string'1120},1121qualifiedName: {1122description: localize('continueEditSessionExtPoint.qualifiedName', 'A fully qualified name for this item which is used for display in menus.'),1123type: 'string'1124},1125description: {1126description: localize('continueEditSessionExtPoint.description', "The url, or a command that returns the url, to the option's documentation page."),1127type: 'string'1128},1129remoteGroup: {1130description: localize('continueEditSessionExtPoint.remoteGroup', 'Group into which this item belongs in the remote indicator.'),1131type: 'string'1132},1133when: {1134description: localize('continueEditSessionExtPoint.when', 'Condition which must be true to show this item.'),1135type: 'string'1136}1137},1138required: ['command']1139}1140}1141});11421143//#endregion11441145const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);1146workbenchRegistry.registerWorkbenchContribution(EditSessionsContribution, LifecyclePhase.Restored);11471148Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({1149...workbenchConfigurationNodeBase,1150'properties': {1151'workbench.experimental.cloudChanges.autoStore': {1152enum: ['onShutdown', 'off'],1153enumDescriptions: [1154localize('autoStoreWorkingChanges.onShutdown', "Automatically store current working changes in the cloud on window close."),1155localize('autoStoreWorkingChanges.off', "Never attempt to automatically store working changes in the cloud.")1156],1157'type': 'string',1158'tags': ['experimental', 'usesOnlineServices'],1159'default': 'off',1160'markdownDescription': localize('autoStoreWorkingChangesDescription', "Controls whether to automatically store available working changes in the cloud for the current workspace. This setting has no effect in the web."),1161},1162'workbench.cloudChanges.autoResume': {1163enum: ['onReload', 'off'],1164enumDescriptions: [1165localize('autoResumeWorkingChanges.onReload', "Automatically resume available working changes from the cloud on window reload."),1166localize('autoResumeWorkingChanges.off', "Never attempt to resume working changes from the cloud.")1167],1168'type': 'string',1169'tags': ['usesOnlineServices'],1170'default': 'onReload',1171'markdownDescription': localize('autoResumeWorkingChanges', "Controls whether to automatically resume available working changes stored in the cloud for the current workspace."),1172},1173'workbench.cloudChanges.continueOn': {1174enum: ['prompt', 'off'],1175enumDescriptions: [1176localize('continueOnCloudChanges.promptForAuth', 'Prompt the user to sign in to store working changes in the cloud with Continue Working On.'),1177localize('continueOnCloudChanges.off', 'Do not store working changes in the cloud with Continue Working On unless the user has already turned on Cloud Changes.')1178],1179type: 'string',1180tags: ['usesOnlineServices'],1181default: 'prompt',1182markdownDescription: localize('continueOnCloudChanges', 'Controls whether to prompt the user to store working changes in the cloud when using Continue Working On.')1183},1184'workbench.experimental.cloudChanges.partialMatches.enabled': {1185'type': 'boolean',1186'tags': ['experimental', 'usesOnlineServices'],1187'default': false,1188'markdownDescription': localize('cloudChangesPartialMatchesEnabled', "Controls whether to surface cloud changes which partially match the current session.")1189}1190}1191});119211931194