diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index a399f2b836011..40366891a2766 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -29,7 +29,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; -import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -57,10 +57,9 @@ import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; registerSingleton(IEditSessionsLogService, EditSessionsLogService, false); registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService, false); -const continueEditSessionCommand: IAction2Options = { +const continueWorkingOnCommand: IAction2Options = { id: '_workbench.experimental.editSessions.actions.continueEditSession', - title: { value: localize('continue edit session', "Continue Edit Session..."), original: 'Continue Edit Session...' }, - category: EDIT_SESSION_SYNC_CATEGORY, + title: { value: localize('continue working on', "Continue Working On..."), original: 'Continue Working On...' }, precondition: WorkspaceFolderCountContext.notEqualsTo('0'), f1: true }; @@ -83,6 +82,7 @@ const resumingProgressOptions = { const queryParamName = 'editSessionId'; const experimentalSettingName = 'workbench.experimental.editSessions.enabled'; +const useEditSessionsWithContinueOn = 'workbench.experimental.editSessions.continueOn'; export class EditSessionsContribution extends Disposable implements IWorkbenchContribution { private registered = false; @@ -242,7 +242,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo const that = this; this._register(registerAction2(class ContinueEditSessionAction extends Action2 { constructor() { - super(continueEditSessionCommand); + super(continueWorkingOnCommand); } async run(accessor: ServicesAccessor, workspaceUri: URI | undefined): Promise { @@ -252,11 +252,16 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo }; that.telemetryService.publicLog2('editSessions.continue.store'); + const shouldStoreEditSession = await that.shouldContinueOnWithEditSession(); + 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 ref: string | undefined; + if (shouldStoreEditSession) { + ref = await that.storeEditSession(false); + } // Append the ref to the URI if (ref !== undefined && uri !== 'noDestinationUri') { @@ -268,8 +273,12 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo // Open the URI that.logService.info(`Opening ${uri.toString()}`); await that.openerService.open(uri, { openExternal: true }); - } else if (ref === undefined) { - that.logService.warn(`Failed to store edit session when invoking ${continueEditSessionCommand.id}.`); + } else if (!shouldStoreEditSession && uri !== 'noDestinationUri') { + // Open the URI without an edit session ref + that.logService.info(`Opening ${uri.toString()}`); + await that.openerService.open(uri, { openExternal: true }); + } else if (ref === undefined && shouldStoreEditSession) { + that.logService.warn(`Failed to store edit session when invoking ${continueWorkingOnCommand.id}.`); } } })); @@ -287,7 +296,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo }); } - async run(accessor: ServicesAccessor, editSessionId?: string): Promise { + async run(accessor: ServicesAccessor, editSessionId?: string, silent?: boolean): Promise { await that.progressService.withProgress(resumingProgressOptions, async () => { type ResumeEvent = {}; type ResumeClassification = { @@ -295,7 +304,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo }; that.telemetryService.publicLog2('editSessions.resume'); - await that.resumeEditSession(editSessionId); + await that.resumeEditSession(editSessionId, silent); }); } })); @@ -529,6 +538,34 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo return [...trackedUris]; } + private hasEditSession() { + for (const repository of this.scmService.repositories) { + if (this.getChangedResources(repository).length > 0) { + return true; + } + } + return false; + } + + private async shouldContinueOnWithEditSession(): Promise { + // If the user is already signed in, we should store edit session + if (this.editSessionsStorageService.isSignedIn) { + return true; + } + + // If the user has been asked before and said no, don't use edit sessions + if (this.configurationService.getValue(useEditSessionsWithContinueOn) === 'off') { + return false; + } + + // Prompt the user to use edit sessions if they currently could benefit from using it + if (this.hasEditSession()) { + return this.editSessionsStorageService.initialize(true); + } + + return false; + } + //#region Continue Edit Session extension contribution point private registerContributedEditSessionOptions() { @@ -690,7 +727,7 @@ const continueEditSessionExtPoint = ExtensionsRegistry.registerExtensionPoint(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(EditSessionsContribution, 'EditSessionsContribution', LifecyclePhase.Restored); -Registry.as(Extensions.Configuration).registerConfiguration({ +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ ...workbenchConfigurationNodeBase, 'properties': { 'workbench.experimental.editSessions.autoStore': { @@ -721,5 +758,16 @@ Registry.as(Extensions.Configuration).registerConfigurat 'default': 'onReload', 'markdownDescription': localize('autoResume', "Controls whether to automatically resume an available edit session for the current workspace."), }, + 'workbench.experimental.editSessions.continueOn': { + enum: ['prompt', 'off'], + enumDescriptions: [ + localize('continueOn.promptForAuth', 'Prompt the user to sign in to store edit sessions with Continue Working On.'), + localize('continueOn.off', 'Do not use edit sessions with Continue Working On unless the user has already turned on edit sessions.') + ], + type: 'string', + tags: ['experimental', 'usesOnlineServices'], + default: 'prompt', + markdownDescription: localize('continueOn', 'Controls whether to prompt the user to store edit sessions when using Continue Working On.') + } } }); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 6f4d49ddd20d5..c47c73e27d381 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -24,10 +24,17 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { getCurrentAuthenticationSessionInfo } from 'vs/workbench/services/authentication/browser/authenticationService'; import { isWeb } from 'vs/base/common/platform'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { Codicon } from 'vs/base/common/codicons'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { WorkspaceFolderCountContext } from 'vs/workbench/common/contextkeys'; type ExistingSession = IQuickPickItem & { session: AuthenticationSession & { providerId: string } }; type AuthenticationProviderOption = IQuickPickItem & { provider: IAuthenticationProvider }; +const configureContinueOnPreference = { iconClass: Codicon.settingsGear.classNames, tooltip: localize('configure continue on', 'Configure this preference in settings') }; +const turnOnEditSessionsTitle = localize('sign in', 'Turn on Edit Sessions...'); + export class EditSessionsWorkbenchService extends Disposable implements IEditSessionsStorageService { _serviceBrand = undefined; @@ -45,6 +52,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes return this.existingSessionId !== undefined; } + private globalActivityBadgeDisposable = this._register(new MutableDisposable()); + constructor( @IFileService private readonly fileService: IFileService, @IStorageService private readonly storageService: IStorageService, @@ -58,6 +67,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes @IRequestService private readonly requestService: IRequestService, @IDialogService private readonly dialogService: IDialogService, @ICredentialsService private readonly credentialsService: ICredentialsService, + @ICommandService private readonly commandService: ICommandService, + @IActivityService private readonly activityService: IActivityService, ) { super(); @@ -67,11 +78,13 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes // If another window changes the preferred session storage, reset our cached auth state in memory this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorage(e))); - this.registerSignInAction(); + this.registerTurnOnAction(); this.registerResetAuthenticationAction(); this.signedInContext = EDIT_SESSIONS_SIGNED_IN.bindTo(this.contextKeyService); this.signedInContext.set(this.existingSessionId !== undefined); + + this.updateGlobalActivityBadge(); } /** @@ -80,7 +93,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes * @returns The ref of the stored edit session state. */ async write(editSession: EditSession): Promise { - await this.initialize(); + await this.initialize(false); if (!this.initialized) { throw new Error('Please sign in to store your edit session.'); } @@ -95,7 +108,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes * @returns An object representing the requested or latest edit session state, if any. */ async read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined> { - await this.initialize(); + await this.initialize(false); if (!this.initialized) { throw new Error('Please sign in to apply your latest edit session.'); } @@ -119,7 +132,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } async delete(ref: string | null) { - await this.initialize(); + await this.initialize(false); if (!this.initialized) { throw new Error(`Unable to delete edit session with ref ${ref}.`); } @@ -132,7 +145,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } async list(): Promise { - await this.initialize(); + await this.initialize(false); if (!this.initialized) { throw new Error(`Unable to list edit sessions.`); } @@ -146,12 +159,15 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes return []; } - private async initialize() { + public async initialize(fromContinueOn: boolean) { if (this.initialized) { - return; + return true; } - this.initialized = await this.doInitialize(); + this.initialized = await this.doInitialize(fromContinueOn); this.signedInContext.set(this.initialized); + this.updateGlobalActivityBadge(); + return this.initialized; + } /** @@ -160,7 +176,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes * meaning that authentication is configured and it * can be used to communicate with the remote storage service */ - private async doInitialize(): Promise { + private async doInitialize(fromContinueOn: boolean): Promise { // Wait for authentication extensions to be registered await this.extensionService.whenInstalledExtensionsRegistered(); @@ -181,7 +197,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes return true; } - const authenticationSession = await this.getAuthenticationSession(); + const authenticationSession = await this.getAuthenticationSession(fromContinueOn); if (authenticationSession !== undefined) { this.#authenticationInfo = authenticationSession; this.storeClient.setAuthToken(authenticationSession.token, authenticationSession.providerId); @@ -190,7 +206,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes return authenticationSession !== undefined; } - private async getAuthenticationSession() { + private async getAuthenticationSession(fromContinueOn: boolean) { // If the user signed in previously and the session is still available, reuse that without prompting the user again if (this.existingSessionId) { this.logService.info(`Searching for existing authentication session with ID ${this.existingSessionId}`); @@ -213,7 +229,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } // Ask the user to pick a preferred account - const authenticationSession = await this.getAccountPreference(); + const authenticationSession = await this.getAccountPreference(fromContinueOn); if (authenticationSession !== undefined) { this.existingSessionId = authenticationSession.id; return { sessionId: authenticationSession.id, token: authenticationSession.idToken ?? authenticationSession.accessToken, providerId: authenticationSession.providerId }; @@ -230,13 +246,13 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes * * Prompts the user to pick an authentication option for storing and getting edit sessions. */ - private async getAccountPreference(): Promise { - const quickpick = this.quickInputService.createQuickPick(); - quickpick.title = localize('account preference', 'Sign In to Use Edit Sessions'); + private async getAccountPreference(fromContinueOn: boolean): Promise { + const quickpick = this.quickInputService.createQuickPick(); + quickpick.title = localize('account preference', 'Turn on Edit Sessions to bring your working changes with you'); quickpick.ok = false; quickpick.placeholder = localize('choose account placeholder', "Select an account to sign in"); quickpick.ignoreFocusOut = true; - quickpick.items = await this.createQuickpickItems(); + quickpick.items = await this.createQuickpickItems(fromContinueOn); return new Promise((resolve, reject) => { quickpick.onDidHide((e) => { @@ -246,17 +262,23 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes quickpick.onDidAccept(async (e) => { const selection = quickpick.selectedItems[0]; - const session = 'provider' in selection ? { ...await this.authenticationService.createSession(selection.provider.id, selection.provider.scopes), providerId: selection.provider.id } : selection.session; + const session = 'provider' in selection ? { ...await this.authenticationService.createSession(selection.provider.id, selection.provider.scopes), providerId: selection.provider.id } : ('session' in selection ? selection.session : undefined); resolve(session); quickpick.hide(); }); + quickpick.onDidTriggerItemButton(async (e) => { + if (e.button.tooltip === configureContinueOnPreference.tooltip) { + await this.commandService.executeCommand('workbench.action.openSettings', 'workbench.experimental.editSessions.continueOn'); + } + }); + quickpick.show(); }); } - private async createQuickpickItems(): Promise<(ExistingSession | AuthenticationProviderOption | IQuickPickSeparator)[]> { - const options: (ExistingSession | AuthenticationProviderOption | IQuickPickSeparator)[] = []; + private async createQuickpickItems(fromContinueOn: boolean): Promise<(ExistingSession | AuthenticationProviderOption | IQuickPickSeparator | IQuickPickItem & { canceledAuthentication: boolean })[]> { + const options: (ExistingSession | AuthenticationProviderOption | IQuickPickSeparator | IQuickPickItem & { canceledAuthentication: boolean })[] = []; options.push({ type: 'separator', label: localize('signed in', "Signed In") }); @@ -273,6 +295,14 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } } + if (fromContinueOn) { + return options.concat([{ type: 'separator' }, { + label: localize('continue without', 'Continue without my working changes'), + canceledAuthentication: true, + buttons: [configureContinueOnPreference] + }]); + } + return options; } @@ -378,23 +408,51 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } } - private registerSignInAction() { + private registerTurnOnAction() { const that = this; - this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 { + const when = ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false); + this._register(registerAction2(class TurnOnEditSessionsAction extends Action2 { constructor() { super({ id: 'workbench.editSessions.actions.signIn', - title: localize('sign in', 'Sign In'), + title: turnOnEditSessionsTitle, category: EDIT_SESSION_SYNC_CATEGORY, precondition: ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false), menu: [{ id: MenuId.CommandPalette, + }, + { + id: MenuId.AccountsContext, + group: '2_editSessions', + when, }] }); } async run() { - await that.initialize(); + return await that.initialize(false); + } + })); + + this._register(registerAction2(class TurnOnEditSessionsAndResumeAction extends Action2 { + constructor() { + super({ + id: 'workbench.editSessions.actions.turnOnAndResume', + title: turnOnEditSessionsTitle, + menu: { + group: '6_editSessions', + id: MenuId.GlobalActivity, + // Do not push for edit sessions when there are no workspace folders open + when: ContextKeyExpr.and(when, WorkspaceFolderCountContext.notEqualsTo(0)), + order: 2 + } + }); + } + + async run() { + if (await that.initialize(false)) { + await that.commandService.executeCommand('workbench.experimental.editSessions.actions.resumeLatest', undefined, true); + } } })); } @@ -405,7 +463,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes constructor() { super({ id: 'workbench.editSessions.actions.resetAuth', - title: localize('reset auth.v2', 'Sign Out of Edit Sessions'), + title: localize('reset auth.v3', 'Turn off Edit Sessions...'), category: EDIT_SESSION_SYNC_CATEGORY, precondition: ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, true), menu: [{ @@ -422,8 +480,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes async run() { const result = await that.dialogService.confirm({ type: 'info', - message: localize('sign out of edit sessions clear data prompt', 'Do you want to sign out of edit sessions?'), - checkbox: { label: localize('delete all edit sessions', 'Delete all stored edit sessions from the cloud.') }, + message: localize('sign out of edit sessions clear data prompt.v2', 'Do you want to turn off Edit Sessions?'), + checkbox: { label: localize('delete all edit sessions.v2', 'Delete all stored data from the cloud.') }, primaryButton: localize('clear data confirm', 'Yes'), }); if (result.confirmed) { @@ -435,4 +493,13 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } })); } + + private updateGlobalActivityBadge() { + if (this.initialized) { + return this.globalActivityBadgeDisposable.clear(); + } + + const badge = new NumberBadge(1, () => turnOnEditSessionsTitle); + this.globalActivityBadgeDisposable.value = this.activityService.showGlobalActivity({ badge, priority: 1 }); + } } diff --git a/src/vs/workbench/contrib/editSessions/common/editSessions.ts b/src/vs/workbench/contrib/editSessions/common/editSessions.ts index c6fc8fda976f0..99f664e11795d 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessions.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessions.ts @@ -24,6 +24,7 @@ export interface IEditSessionsStorageService { readonly isSignedIn: boolean; + initialize(fromContinueOn: boolean): Promise; read(ref: string | undefined): Promise<{ ref: string; editSession: EditSession } | undefined>; write(editSession: EditSession): Promise; delete(ref: string | null): Promise;