Merge from vscode 2e5312cd61ff99c570299ecc122c52584265eda2

This commit is contained in:
ADS Merger
2020-04-23 02:50:35 +00:00
committed by Anthony Dresser
parent 3603f55d97
commit 7f1d8fc32f
659 changed files with 22709 additions and 12497 deletions

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4315 3.32315L5.96154 13.3232L5.17083 13.2874L1.82083 8.51736L2.63918 7.94263L5.617 12.1827L13.6685 2.67683L14.4315 3.32315Z" fill="#C5C5C5"/>
</svg>

Before

Width:  |  Height:  |  Size: 298 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4315 3.32317L5.96154 13.3232L5.17083 13.2874L1.82083 8.51737L2.63918 7.94264L5.617 12.1827L13.6685 2.67685L14.4315 3.32317Z" fill="#424242"/>
</svg>

Before

Width:  |  Height:  |  Size: 298 B

View File

@@ -15,7 +15,6 @@ import type { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import type { IEditorContribution } from 'vs/editor/common/editorCommon';
import type { ITextModel } from 'vs/editor/common/model';
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
@@ -30,7 +29,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import {
CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration,
CONTEXT_SYNC_STATE, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration,
SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT,
SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview
} from 'vs/platform/userDataSync/common/userDataSync';
@@ -43,7 +42,6 @@ import * as Constants from 'vs/workbench/contrib/logs/common/logConstants';
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger';
import { IActivityService, IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
@@ -52,19 +50,11 @@ import { fromNow } from 'vs/base/common/date';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { distinct } from 'vs/base/common/arrays';
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { UserDataSyncAccounts, AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount';
const enum AuthStatus {
Initializing = 'Initializing',
SignedIn = 'SignedIn',
SignedOut = 'SignedOut',
Unavailable = 'Unavailable'
}
const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey<string>('authTokenStatus', AuthStatus.Initializing);
const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources', '');
const USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY = 'userDataSyncAccountPreference';
type ConfigureSyncQuickPickItem = { id: SyncResource, label: string, description?: string };
function getSyncAreaLabel(source: SyncResource): string {
@@ -95,12 +85,12 @@ const getActivityTitle = (label: string, userDataSyncService: IUserDataSyncServi
}
return label;
};
const getIdentityTitle = (label: string, authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService): string => {
return account ? `${label} (${authenticationService.getDisplayName(authenticationProviderId)}:${account.accountName})` : label;
const getIdentityTitle = (label: string, userDataSyncAccountService: UserDataSyncAccounts, authenticationService: IAuthenticationService) => {
const account = userDataSyncAccountService.current;
return account ? `${label} (${authenticationService.getDisplayName(account.authenticationProviderId)}:${account.accountName})` : label;
};
const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Preferences Sync: Turn on...") };
const signInCommand = { id: 'workbench.userData.actions.signin', title: localize('sign in', "Preferences Sync: Sign in to sync") };
const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn Off"), authenticationProviderId, account, authenticationService); } };
const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Preferences Sync: Turn On...") };
const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(userDataSyncAccountService: UserDataSyncAccounts, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn Off"), userDataSyncAccountService, authenticationService); } };
const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Preferences Sync: Show Settings Conflicts") };
const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") };
const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") };
@@ -112,34 +102,31 @@ const showSyncActivityCommand = {
};
const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Preferences Sync: Show Settings"), };
const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Uninitialized);
export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution {
private readonly userDataSyncStore: IUserDataSyncStore | undefined;
private readonly syncEnablementContext: IContextKey<boolean>;
private readonly syncStatusContext: IContextKey<string>;
private readonly authenticationState: IContextKey<string>;
private readonly accountStatusContext: IContextKey<string>;
private readonly conflictsSources: IContextKey<string>;
private readonly userDataSyncAccounts: UserDataSyncAccounts;
private readonly badgeDisposable = this._register(new MutableDisposable());
private readonly signInNotificationDisposable = this._register(new MutableDisposable());
private _activeAccount: AuthenticationSession | undefined;
private loginInProgress: boolean = false;
constructor(
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IActivityService private readonly activityService: IActivityService,
@INotificationService private readonly notificationService: INotificationService,
@IConfigurationService configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IDialogService private readonly dialogService: IDialogService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService instantiationService: IInstantiationService,
@IOutputService private readonly outputService: IOutputService,
@IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService,
@IAuthenticationTokenService readonly authTokenService: IAuthenticationTokenService,
@IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService,
@ITextModelService private readonly textModelResolverService: ITextModelService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@@ -147,210 +134,48 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
@IProductService private readonly productService: IProductService,
@IStorageService private readonly storageService: IStorageService,
@IOpenerService private readonly openerService: IOpenerService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
) {
super();
this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService);
this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
this.authenticationState = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService);
this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService);
this.conflictsSources = CONTEXT_CONFLICTS_SOURCES.bindTo(contextKeyService);
if (this.userDataSyncStore) {
this.userDataSyncAccounts = instantiationService.createInstance(UserDataSyncAccounts);
if (this.userDataSyncAccounts.authenticationProviders.length) {
registerConfiguration();
this.onDidChangeSyncStatus(this.userDataSyncService.status);
this.onDidChangeConflicts(this.userDataSyncService.conflicts);
this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled());
this.onDidChangeAccountStatus(this.userDataSyncAccounts.status);
this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status)));
this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts)));
this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors)));
this._register(this.authTokenService.onTokenFailed(_ => this.onTokenFailed()));
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled)));
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e)));
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e)));
this._register(this.authenticationService.onDidChangeSessions(e => this.onDidChangeSessions(e)));
this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error)));
this._register(this.userDataSyncAccounts.onDidChangeStatus(status => this.onDidChangeAccountStatus(status)));
this._register(this.userDataSyncAccounts.onDidSignOut(() => this.doTurnOff(false)));
this.registerActions();
this.initializeActiveAccount().then(_ => {
if (!isWeb) {
this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source])));
}
});
textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider));
registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution);
}
}
private async initializeActiveAccount(): Promise<void> {
const sessions = await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId);
// Auth provider has not yet been registered
if (!sessions) {
return;
}
if (sessions.length === 0) {
await this.setActiveAccount(undefined);
return;
}
if (sessions.length === 1) {
this.logAuthenticatedEvent(sessions[0]);
await this.setActiveAccount(sessions[0]);
return;
}
const accountPreference = this.storageService.get(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, StorageScope.GLOBAL);
if (accountPreference) {
const matchingSession = sessions.find(session => session.id === accountPreference);
if (matchingSession) {
this.setActiveAccount(matchingSession);
return;
if (!isWeb) {
this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source])));
}
}
await this.showSwitchAccountPicker(sessions);
}
private logAuthenticatedEvent(session: AuthenticationSession): void {
type UserAuthenticatedClassification = {
id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
};
type UserAuthenticatedEvent = {
id: string;
};
const id = session.id.split('/')[1];
this.telemetryService.publicLog2<UserAuthenticatedEvent, UserAuthenticatedClassification>('user.authenticated', { id });
}
get activeAccount(): AuthenticationSession | undefined {
return this._activeAccount;
}
async setActiveAccount(account: AuthenticationSession | undefined) {
this._activeAccount = account;
if (account) {
try {
const token = await account.getAccessToken();
this.authTokenService.setToken(token);
this.authenticationState.set(AuthStatus.SignedIn);
} catch (e) {
this.authTokenService.setToken(undefined);
this.authenticationState.set(AuthStatus.Unavailable);
}
} else {
this.authTokenService.setToken(undefined);
this.authenticationState.set(AuthStatus.SignedOut);
}
private onDidChangeAccountStatus(status: AccountStatus): void {
this.accountStatusContext.set(status);
this.updateBadge();
}
private async showSwitchAccountPicker(sessions: readonly AuthenticationSession[]): Promise<void> {
return new Promise((resolve, _) => {
const quickPick = this.quickInputService.createQuickPick<{ label: string, session: AuthenticationSession }>();
quickPick.title = localize('chooseAccountTitle', "Preferences Sync: Choose Account");
quickPick.placeholder = localize('chooseAccount', "Choose an account you would like to use for preferences sync");
const dedupedSessions = distinct(sessions, (session) => session.accountName);
quickPick.items = dedupedSessions.map(session => {
return {
label: session.accountName,
session: session
};
});
quickPick.onDidHide(() => {
quickPick.dispose();
resolve();
});
quickPick.onDidAccept(() => {
const selected = quickPick.selectedItems[0];
this.setActiveAccount(selected.session);
this.storageService.store(USER_DATA_SYNC_ACCOUNT_PREFERENCE_KEY, selected.session.id, StorageScope.GLOBAL);
quickPick.dispose();
resolve();
});
quickPick.show();
});
}
private async onDidChangeSessions(e: { providerId: string, event: AuthenticationSessionsChangeEvent }): Promise<void> {
const { providerId, event } = e;
if (providerId === this.userDataSyncStore!.authenticationProviderId) {
if (this.loginInProgress) {
return;
}
if (this.activeAccount) {
if (event.removed.length) {
const activeWasRemoved = !!event.removed.find(removed => removed === this.activeAccount!.id);
if (activeWasRemoved) {
this.setActiveAccount(undefined);
this.notificationService.notify({
severity: Severity.Info,
message: localize('turned off on logout', "Sync has stopped because you are no longer signed in."),
actions: {
primary: [new Action('turn on sync', localize('turn on sync', "Turn on Sync"), undefined, true, () => this.turnOn())]
}
});
return;
return;
}
}
if (event.added.length) {
// Offer to switch accounts
const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
await this.showSwitchAccountPicker(accounts);
return;
}
if (event.changed.length) {
const activeWasChanged = !!event.changed.find(changed => changed === this.activeAccount!.id);
if (activeWasChanged) {
// Try to update existing account, case where access token has been refreshed
const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0];
this.setActiveAccount(matchingAccount);
}
}
} else {
await this.initializeActiveAccount();
// If logged in for the first time from accounts menu, prompt if sync should be turned on
if (this.activeAccount) {
this.turnOn(true);
}
}
}
}
private async onTokenFailed(): Promise<void> {
if (this.activeAccount) {
const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0];
this.setActiveAccount(matchingAccount);
} else {
this.setActiveAccount(undefined);
}
}
private async onDidRegisterAuthenticationProvider(providerId: string) {
if (providerId === this.userDataSyncStore!.authenticationProviderId) {
await this.initializeActiveAccount();
}
}
private onDidUnregisterAuthenticationProvider(providerId: string) {
if (providerId === this.userDataSyncStore!.authenticationProviderId) {
this.setActiveAccount(undefined);
this.authenticationState.reset();
}
}
private onDidChangeSyncStatus(status: SyncStatus) {
this.syncStatusContext.set(status);
this.updateBadge();
@@ -467,22 +292,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
private onDidChangeEnablement(enabled: boolean) {
this.syncEnablementContext.set(enabled);
this.updateBadge();
if (enabled) {
if (this.authenticationState.get() === AuthStatus.SignedOut) {
const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId);
const handle = this.notificationService.prompt(Severity.Info, localize('sign in message', "Please sign in with your {0} account to continue sync", displayName),
[
{
label: localize('Sign in', "Sign in"),
run: () => this.signIn()
}
]);
this.signInNotificationDisposable.value = toDisposable(() => handle.close());
handle.onDidClose(() => this.signInNotificationDisposable.clear());
}
} else {
this.signInNotificationDisposable.clear();
}
}
private onAutoSyncError(error: UserDataSyncError): void {
@@ -491,9 +300,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
case UserDataSyncErrorCode.SessionExpired:
this.notificationService.notify({
severity: Severity.Info,
message: localize('turned off', "Sync was turned off from another device."),
message: localize('turned off', "Preferences sync was turned off from another device."),
actions: {
primary: [new Action('turn on sync', localize('turn on sync', "Turn on Sync"), undefined, true, () => this.turnOn())]
primary: [new Action('turn on sync', localize('turn on sync', "Turn on Preferences Sync..."), undefined, true, () => this.turnOn())]
}
});
return;
@@ -578,8 +387,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
let clazz: string | undefined;
let priority: number | undefined = undefined;
if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.authenticationState.get() === AuthStatus.SignedOut) {
badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync"));
if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncAccounts.status === AccountStatus.Unavailable) {
badge = new NumberBadge(1, () => localize('sign in to sync preferences', "Sign in to Sync Preferences"));
} else if (this.userDataSyncService.conflicts.length) {
badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, syncResourceConflict) => { return result + syncResourceConflict.conflicts.length; }, 0), () => localize('has conflicts', "Preferences Sync: Conflicts Detected"));
}
@@ -589,14 +398,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
}
private async turnOn(skipAccountPick?: boolean): Promise<void> {
private async turnOn(): Promise<void> {
if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) {
const result = await this.dialogService.show(
Severity.Info,
localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."),
[
localize('open doc', "Open Documentation"),
localize('turn on sync', "Turn on Sync"),
localize('turn on', "Turn On"),
localize('cancel', "Cancel"),
],
{
@@ -607,28 +416,22 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
case 0: this.openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); return;
case 2: return;
}
} else if (skipAccountPick) {
const result = await this.dialogService.confirm({
type: 'info',
message: localize('turn on sync confirmation', "Do you want to turn on preferences sync?"),
primaryButton: localize('turn on', "Turn On")
});
if (!result.confirmed) {
return;
}
}
return new Promise((c, e) => {
const disposables: DisposableStore = new DisposableStore();
const quickPick = this.quickInputService.createQuickPick<ConfigureSyncQuickPickItem>();
disposables.add(quickPick);
quickPick.title = localize('turn on title', "Preferences Sync: Turn On");
quickPick.title = localize('Preferences Sync Title', "Preferences Sync");
quickPick.ok = false;
quickPick.customButton = true;
if (this.authenticationState.get() === AuthStatus.SignedIn) {
if (this.userDataSyncAccounts.all.length) {
quickPick.customLabel = localize('turn on', "Turn On");
} else {
const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId);
const orTerm = localize({ key: 'or', comment: ['Here is the context where it is used - Sign in with your A or B or C account to synchronize your data across devices.'] }, "or");
const displayName = this.userDataSyncAccounts.authenticationProviders.length === 1
? this.authenticationService.getDisplayName(this.userDataSyncAccounts.authenticationProviders[0].id)
: this.userDataSyncAccounts.authenticationProviders.map(({ id }) => this.authenticationService.getDisplayName(id)).join(` ${orTerm} `);
quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName);
quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on");
}
@@ -641,7 +444,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => {
if (quickPick.selectedItems.length) {
this.updateConfiguration(items, quickPick.selectedItems);
this.doTurnOn(skipAccountPick).then(c, e);
this.doTurnOn().then(c, e);
quickPick.hide();
}
}));
@@ -650,43 +453,17 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
});
}
private async doTurnOn(skipAccountPick?: boolean): Promise<void> {
if (this.authenticationState.get() === AuthStatus.SignedIn && !skipAccountPick) {
await new Promise((c, e) => {
const disposables: DisposableStore = new DisposableStore();
const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId);
const quickPick = this.quickInputService.createQuickPick<{ id: string, label: string, description?: string, detail?: string }>();
disposables.add(quickPick);
const chooseAnotherItemId = 'chooseAnother';
quickPick.title = localize('pick account', "{0}: Pick an account", displayName);
quickPick.ok = false;
quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing");
quickPick.ignoreFocusOut = true;
quickPick.items = [{
id: 'existing',
label: localize('existing', "{0}", this.activeAccount!.accountName),
detail: localize('signed in', "Signed in"),
}, {
id: chooseAnotherItemId,
label: localize('choose another', "Use another account")
}];
disposables.add(quickPick.onDidAccept(async () => {
if (quickPick.selectedItems.length) {
if (quickPick.selectedItems[0].id === chooseAnotherItemId) {
await this.authenticationService.logout(this.userDataSyncStore!.authenticationProviderId, this.activeAccount!.id);
await this.setActiveAccount(undefined);
}
quickPick.hide();
c();
}
}));
disposables.add(quickPick.onDidHide(() => disposables.dispose()));
quickPick.show();
});
private async doTurnOn(): Promise<void> {
const picked = await this.userDataSyncAccounts.pick();
if (!picked) {
throw canceled();
}
if (this.authenticationState.get() === AuthStatus.SignedOut) {
await this.signIn();
// User did not pick an account or login failed
if (this.userDataSyncAccounts.status !== AccountStatus.Available) {
throw new Error(localize('no account', "No account available"));
}
await this.handleFirstTimeSync();
this.userDataSyncEnablementService.setEnablement(true);
this.notificationService.info(localize('sync turned on', "Preferences sync is turned on"));
@@ -792,16 +569,26 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
});
if (result.confirmed) {
if (result.checkboxChecked) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
this.disableSync();
return this.doTurnOff(!!result.checkboxChecked);
}
}
private async doTurnOff(turnOffEveryWhere: boolean): Promise<void> {
if (!this.userDataSyncEnablementService.isEnabled()) {
return;
}
if (!this.userDataSyncEnablementService.canToggleEnablement()) {
return;
}
if (turnOffEveryWhere) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
this.disableSync();
}
private disableSync(source?: SyncResource): void {
if (source === undefined) {
this.userDataSyncEnablementService.setEnablement(false);
@@ -816,17 +603,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
}
private async signIn(): Promise<void> {
try {
this.loginInProgress = true;
await this.setActiveAccount(await this.authenticationService.login(this.userDataSyncStore!.authenticationProviderId, ['https://management.core.windows.net/.default', 'offline_access']));
this.loginInProgress = false;
} catch (e) {
this.notificationService.error(localize('loginFailed', "Logging in failed: {0}", e));
throw e;
}
}
private getConflictsEditorInputs(syncResource: SyncResource): DiffEditorInput[] {
return this.editorService.editors.filter(input => {
const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource;
@@ -876,21 +652,23 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
private registerActions(): void {
this.registerTurnOnSyncAction();
this.registerSignInAction();
if (this.userDataSyncEnablementService.canToggleEnablement()) {
this.registerTurnOnSyncAction();
this.registerTurnOffSyncAction();
}
this.registerSignInAction(); // When Sync is turned on from CLI
this.registerShowSettingsConflictsAction();
this.registerShowKeybindingsConflictsAction();
this.registerShowSnippetsConflictsAction();
this.registerSyncStatusAction();
this.registerTurnOffSyncAction();
this.registerConfigureSyncAction();
this.registerShowActivityAction();
this.registerShowSettingsAction();
}
private registerTurnOnSyncAction(): void {
const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_AUTH_TOKEN_STATE.notEqualsTo(AuthStatus.Initializing));
const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized));
CommandsRegistry.registerCommand(turnOnSyncCommand.id, async () => {
try {
await this.turnOn();
@@ -921,31 +699,49 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
},
when: turnOnSyncWhenContext,
});
MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '1_sync',
command: {
id: turnOnSyncCommand.id,
title: localize('global activity turn on sync', "Turn on Preferences Sync...")
},
when: turnOnSyncWhenContext
});
}
private registerSignInAction(): void {
const that = this;
const id = 'workbench.userData.actions.signin';
const when = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Unavailable));
this._register(registerAction2(class StopSyncAction extends Action2 {
constructor() {
super({
id: signInCommand.id,
title: localize('sign in 2', "Preferences Sync: Sign in to sync (1)"),
id: 'workbench.userData.actions.signin',
title: localize('sign in global', "Sign in to Sync Preferences(1)"),
menu: {
group: '5_sync',
id: MenuId.GlobalActivity,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT, CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedOut)),
when,
order: 2
},
}
});
}
async run(): Promise<any> {
try {
await that.signIn();
await that.userDataSyncAccounts.pick();
} catch (e) {
that.notificationService.error(e);
}
}
}));
this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '1_sync',
command: {
id,
title: localize('sign in accounts', "Sign in to Sync Preferences"),
},
when
}));
}
private registerShowSettingsConflictsAction(): void {
@@ -1034,7 +830,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
private registerSyncStatusAction(): void {
const that = this;
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
this._register(registerAction2(class SyncStatusAction extends Action2 {
constructor() {
super({
@@ -1052,6 +848,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
group: '5_sync',
when,
order: 3,
},
{
id: MenuId.AccountsContext,
group: '1_sync',
when,
}
],
});
@@ -1084,7 +885,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title });
items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title(that.userDataSyncService) });
items.push({ type: 'separator' });
items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService) });
if (that.userDataSyncEnablementService.canToggleEnablement()) {
items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncAccounts, that.authenticationService) });
}
quickPick.items = items;
disposables.add(quickPick.onDidAccept(() => {
if (quickPick.selectedItems[0] && quickPick.selectedItems[0].id) {
@@ -1108,7 +911,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
constructor() {
super({
id: stopSyncCommand.id,
title: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService),
title: stopSyncCommand.title(that.userDataSyncAccounts, that.authenticationService),
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT),
@@ -1260,7 +1063,6 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
return false;
}
private createAcceptChangesWidgetRenderer(): void {
if (!this.acceptChangesButton) {
const resource = this.editor.getModel()!.uri;

View File

@@ -0,0 +1,326 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { localize } from 'vs/nls';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
import { Event, Emitter } from 'vs/base/common/event';
import { getUserDataSyncStore, IUserDataSyncEnablementService, IAuthenticationProvider, isAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { values } from 'vs/base/common/map';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { flatten } from 'vs/base/common/arrays';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
type UserAccountClassification = {
id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
};
type UserAccountEvent = {
id: string;
};
type AccountQuickPickItem = { label: string, authenticationProvider: IAuthenticationProvider, account?: IUserDataSyncAccount, detail?: string };
export interface IUserDataSyncAccount {
readonly authenticationProviderId: string;
readonly sessionId: string;
readonly accountName: string;
}
export const enum AccountStatus {
Uninitialized = 'uninitialized',
Unavailable = 'unavailable',
Available = 'available',
}
export class UserDataSyncAccounts extends Disposable {
private static DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY = 'userDataSyncAccount.donotUseWorkbenchSession';
private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference';
_serviceBrand: any;
readonly authenticationProviders: IAuthenticationProvider[];
private _status: AccountStatus = AccountStatus.Uninitialized;
get status(): AccountStatus { return this._status; }
private readonly _onDidChangeStatus = this._register(new Emitter<AccountStatus>());
readonly onDidChangeStatus = this._onDidChangeStatus.event;
private readonly _onDidSignOut = this._register(new Emitter<void>());
readonly onDidSignOut = this._onDidSignOut.event;
private _all: Map<string, IUserDataSyncAccount[]> = new Map<string, IUserDataSyncAccount[]>();
get all(): IUserDataSyncAccount[] { return flatten(values(this._all)); }
get current(): IUserDataSyncAccount | undefined { return this.all.filter(account => this.isCurrentAccount(account))[0]; }
constructor(
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IAuthenticationTokenService private readonly authenticationTokenService: IAuthenticationTokenService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IExtensionService extensionService: IExtensionService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
) {
super();
this.authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || [];
if (this.authenticationProviders.length) {
extensionService.whenInstalledExtensionsRegistered().then(() => {
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
this.initialize();
} else {
const disposable = this.authenticationService.onDidRegisterAuthenticationProvider(() => {
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
disposable.dispose();
this.initialize();
}
});
}
});
}
}
private async initialize(): Promise<void> {
if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) {
this.currentSessionId = this.environmentService.options.authenticationSessionId;
this.useWorkbenchSessionId = false;
}
await this.update();
this._register(
Event.any(
Event.filter(
Event.any(
this.authenticationService.onDidRegisterAuthenticationProvider,
this.authenticationService.onDidUnregisterAuthenticationProvider,
), authenticationProviderId => this.isSupportedAuthenticationProviderId(authenticationProviderId)),
this.authenticationTokenService.onTokenFailed)
(() => this.update()));
this._register(Event.filter(this.authenticationService.onDidChangeSessions, e => this.isSupportedAuthenticationProviderId(e.providerId))(({ event }) => this.onDidChangeSessions(event)));
this._register(this.storageService.onDidChangeStorage(e => this.onDidChangeStorage(e)));
}
private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean {
return this.authenticationProviders.some(({ id }) => id === authenticationProviderId);
}
private async update(): Promise<void> {
const allAccounts: Map<string, IUserDataSyncAccount[]> = new Map<string, IUserDataSyncAccount[]>();
for (const { id } of this.authenticationProviders) {
const accounts = await this.getAccounts(id);
allAccounts.set(id, accounts);
}
this._all = allAccounts;
const status = this.current ? AccountStatus.Available : AccountStatus.Unavailable;
if (this._status === AccountStatus.Unavailable) {
await this.authenticationTokenService.setToken(undefined);
}
if (this._status !== status) {
const previous = this._status;
this.logService.debug('Sync account status changed', previous, status);
if (previous === AccountStatus.Available && status === AccountStatus.Unavailable) {
this._onDidSignOut.fire();
}
this._status = status;
this._onDidChangeStatus.fire(status);
}
}
private async getAccounts(authenticationProviderId: string): Promise<IUserDataSyncAccount[]> {
let accounts: Map<string, IUserDataSyncAccount> = new Map<string, IUserDataSyncAccount>();
let currentAccount: IUserDataSyncAccount | null = null;
let currentSession: AuthenticationSession | undefined = undefined;
const sessions = await this.authenticationService.getSessions(authenticationProviderId) || [];
for (const session of sessions) {
const account: IUserDataSyncAccount = { authenticationProviderId, sessionId: session.id, accountName: session.account.displayName };
accounts.set(account.accountName, account);
if (this.isCurrentAccount(account)) {
currentAccount = account;
currentSession = session;
}
}
if (currentAccount) {
// Always use current account if available
accounts.set(currentAccount.accountName, currentAccount);
}
// update access token
if (currentSession) {
try {
const token = await currentSession.getAccessToken();
await this.authenticationTokenService.setToken({ token, authenticationProviderId });
} catch (e) {
this.logService.error(e);
}
}
return values(accounts);
}
private isCurrentAccount(account: IUserDataSyncAccount): boolean {
return account.sessionId === this.currentSessionId;
}
async pick(): Promise<boolean> {
const result = await this.doPick();
if (!result) {
return false;
}
let sessionId: string, accountName: string;
if (isAuthenticationProvider(result)) {
const session = await this.authenticationService.login(result.id, result.scopes);
sessionId = session.id;
accountName = session.account.displayName;
} else {
sessionId = result.sessionId;
accountName = result.accountName;
}
await this.switch(sessionId, accountName);
return true;
}
private async doPick(): Promise<IUserDataSyncAccount | IAuthenticationProvider | undefined> {
if (this.authenticationProviders.length === 0) {
return undefined;
}
await this.update();
// Single auth provider and no accounts available
if (this.authenticationProviders.length === 1 && !this.all.length) {
return this.authenticationProviders[0];
}
return new Promise<IUserDataSyncAccount | IAuthenticationProvider | undefined>(async (c, e) => {
let result: IUserDataSyncAccount | IAuthenticationProvider | undefined;
const disposables: DisposableStore = new DisposableStore();
const quickPick = this.quickInputService.createQuickPick<AccountQuickPickItem>();
disposables.add(quickPick);
quickPick.title = localize('pick an account', "Preferences Sync");
quickPick.ok = false;
quickPick.placeholder = localize('choose account placeholder', "Select an account");
quickPick.ignoreFocusOut = true;
quickPick.items = this.createQuickpickItems();
disposables.add(quickPick.onDidAccept(() => {
result = quickPick.selectedItems[0]?.account ? quickPick.selectedItems[0]?.account : quickPick.selectedItems[0]?.authenticationProvider;
quickPick.hide();
}));
disposables.add(quickPick.onDidHide(() => {
disposables.dispose();
c(result);
}));
quickPick.show();
});
}
private createQuickpickItems(): (AccountQuickPickItem | IQuickPickSeparator)[] {
const quickPickItems: (AccountQuickPickItem | IQuickPickSeparator)[] = [];
const authenticationProviders = [...this.authenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1);
for (const authenticationProvider of authenticationProviders) {
const providerName = this.authenticationService.getDisplayName(authenticationProvider.id);
if (this.all.length) {
quickPickItems.push({ type: 'separator', label: providerName });
const accounts = this._all.get(authenticationProvider.id) || [];
for (const account of accounts) {
quickPickItems.push({
label: account.accountName,
detail: account.sessionId === this.current?.sessionId ? localize('last used', "Last Used") : undefined,
account,
authenticationProvider,
});
}
quickPickItems.push({
label: accounts.length ? localize('use another', "Use another {0} Account", providerName) : localize('use provider account', "Use {0} Account", providerName),
authenticationProvider,
});
} else {
quickPickItems.push({ label: providerName, authenticationProvider });
}
}
return quickPickItems;
}
private async switch(sessionId: string, accountName: string): Promise<void> {
const currentAccount = this.current;
if (this.userDataSyncEnablementService.isEnabled() && (currentAccount && currentAccount.accountName !== accountName)) {
// accounts are switched while sync is enabled.
}
this.currentSessionId = sessionId;
this.telemetryService.publicLog2<UserAccountEvent, UserAccountClassification>('sync.userAccount', { id: sessionId.split('/')[1] });
await this.update();
}
private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void {
if (this.currentSessionId && e.removed.includes(this.currentSessionId)) {
this.currentSessionId = undefined;
}
this.update();
}
private onDidChangeStorage(e: IWorkspaceStorageChangeEvent): void {
if (e.key === UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY && e.scope === StorageScope.GLOBAL
&& this.currentSessionId !== this.getStoredCachedSessionId() /* This checks if current window changed the value or not */) {
this._cachedCurrentSessionId = null;
this.update();
}
}
private _cachedCurrentSessionId: string | undefined | null = null;
private get currentSessionId(): string | undefined {
if (this._cachedCurrentSessionId === null) {
this._cachedCurrentSessionId = this.getStoredCachedSessionId();
}
return this._cachedCurrentSessionId;
}
private set currentSessionId(cachedSessionId: string | undefined) {
if (this._cachedCurrentSessionId !== cachedSessionId) {
this._cachedCurrentSessionId = cachedSessionId;
if (cachedSessionId === undefined) {
this.storageService.remove(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
} else {
this.storageService.store(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, cachedSessionId, StorageScope.GLOBAL);
}
}
}
private getStoredCachedSessionId(): string | undefined {
return this.storageService.get(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
}
private get useWorkbenchSessionId(): boolean {
return !this.storageService.getBoolean(UserDataSyncAccounts.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, StorageScope.GLOBAL, false);
}
private set useWorkbenchSessionId(useWorkbenchSession: boolean) {
this.storageService.store(UserDataSyncAccounts.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, !useWorkbenchSession, StorageScope.GLOBAL);
}
}

View File

@@ -19,6 +19,7 @@ import { FolderThemeIcon } from 'vs/platform/theme/common/themeService';
import { fromNow } from 'vs/base/common/date';
import { pad, uppercaseFirstLetter } from 'vs/base/common/strings';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { Codicon } from 'vs/base/common/codicons';
export class UserDataSyncViewContribution implements IWorkbenchContribution {
@@ -39,9 +40,9 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution {
name: localize('sync preferences', "Preferences Sync"),
ctorDescriptor: new SyncDescriptor(
ViewPaneContainer,
['workbench.view.sync', `workbench.view.sync.state`, { mergeViewWithContainerWhenSingleView: true }]
['workbench.view.sync', { mergeViewWithContainerWhenSingleView: true }]
),
icon: 'codicon-sync',
icon: Codicon.sync.classNames,
hideIfEmpty: true,
}, ViewContainerLocation.Sidebar);
}