userDataSync.ts 49.7 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6 7 8
import { Action } from 'vs/base/common/actions';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { canceled, isPromiseCanceledError } from 'vs/base/common/errors';
9
import { Event } from 'vs/base/common/event';
10
import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle';
11
import { isWeb } from 'vs/base/common/platform';
12
import { isEqual } from 'vs/base/common/resources';
13 14
import { URI } from 'vs/base/common/uri';
import type { ICodeEditor } from 'vs/editor/browser/editorBrowser';
15
import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
16 17
import type { IEditorContribution } from 'vs/editor/common/editorCommon';
import type { ITextModel } from 'vs/editor/common/model';
18
import { AuthenticationSession } from 'vs/editor/common/modes';
S
Sandeep Somavarapu 已提交
19 20
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
21 22
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
import { localize } from 'vs/nls';
23 24
import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
25
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
26
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
27 28 29 30
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
31
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
32
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
33
import { CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncSource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, ResourceKey, getSyncSourceFromPreviewResource, CONTEXT_SYNC_ENABLEMENT, toRemoteSyncResourceFromSource, PREVIEW_QUERY, resolveSyncResource, getSyncSourceFromResourceKey } from 'vs/platform/userDataSync/common/userDataSync';
34
import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets';
35 36
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
S
Sandeep Somavarapu 已提交
37
import { IEditorInput, toResource, SideBySideEditor } from 'vs/workbench/common/editor';
38 39 40 41
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
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';
42
import { IActivityService, IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity';
43 44 45
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';
S
Sandeep Somavarapu 已提交
46
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
47
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
48
import { fromNow } from 'vs/base/common/date';
49
import { IProductService } from 'vs/platform/product/common/productService';
50 51
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IOpenerService } from 'vs/platform/opener/common/opener';
S
Sandeep Somavarapu 已提交
52
import { timeout } from 'vs/base/common/async';
53

54
const enum AuthStatus {
55 56
	Initializing = 'Initializing',
	SignedIn = 'SignedIn',
57 58
	SignedOut = 'SignedOut',
	Unavailable = 'Unavailable'
59
}
60
const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey<string>('authTokenStatus', AuthStatus.Initializing);
61
const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources', '');
62

S
Sandeep Somavarapu 已提交
63
type ConfigureSyncQuickPickItem = { id: ResourceKey, label: string, description?: string };
64

65 66 67
function getSyncAreaLabel(source: SyncSource): string {
	switch (source) {
		case SyncSource.Settings: return localize('settings', "Settings");
68
		case SyncSource.Keybindings: return localize('keybindings', "Keyboard Shortcuts");
69
		case SyncSource.Extensions: return localize('extensions', "Extensions");
S
Sandeep Somavarapu 已提交
70
		case SyncSource.GlobalState: return localize('ui state label', "UI State");
71 72 73
	}
}

S
Sandeep Somavarapu 已提交
74 75 76 77 78
type SyncConflictsClassification = {
	source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
	action?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};

S
Sandeep Somavarapu 已提交
79 80 81 82
type FirstTimeSyncClassification = {
	action: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};

83 84 85 86 87 88 89 90 91
const getActivityTitle = (label: string, userDataSyncService: IUserDataSyncService): string => {
	if (userDataSyncService.status === SyncStatus.Syncing) {
		return localize('sync is on with syncing', "{0} (syncing)", label);
	}
	if (userDataSyncService.lastSyncTime) {
		return localize('sync is on with time', "{0} (synced {1})", label, fromNow(userDataSyncService.lastSyncTime, true));
	}
	return label;
};
S
Sandeep Somavarapu 已提交
92 93
const getIdentityTitle = (label: string, authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService): string => {
	return account ? `${label} (${authenticationService.getDisplayName(authenticationProviderId)}:${account.accountName})` : label;
94
};
95 96
const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Sync: Turn on Sync") };
const signInCommand = { id: 'workbench.userData.actions.signin', title: localize('sign in', "Sync: Sign in to sync") };
S
Sandeep Somavarapu 已提交
97
const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Sync: Turn off Sync"), authenticationProviderId, account, authenticationService); } };
98 99 100
const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Sync: Show Settings Conflicts") };
const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Sync: Show Keybindings Conflicts") };
const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Sync: Configure") };
101 102
const showSyncActivityCommand = {
	id: 'workbench.userData.actions.showSyncActivity', title(userDataSyncService: IUserDataSyncService): string {
S
Sandeep Somavarapu 已提交
103
		return getActivityTitle(localize('show sync log', "Sync: Show Log"), userDataSyncService);
104 105
	}
};
106 107
const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Sync: Settings"), };

108 109
export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution {

110
	private readonly userDataSyncStore: IUserDataSyncStore | undefined;
S
Sandeep Somavarapu 已提交
111
	private readonly syncEnablementContext: IContextKey<boolean>;
112
	private readonly syncStatusContext: IContextKey<string>;
113
	private readonly authenticationState: IContextKey<string>;
114
	private readonly conflictsSources: IContextKey<string>;
S
Sandeep Somavarapu 已提交
115

116 117
	private readonly badgeDisposable = this._register(new MutableDisposable());
	private readonly signInNotificationDisposable = this._register(new MutableDisposable());
118
	private _activeAccount: AuthenticationSession | undefined;
119 120

	constructor(
S
Sandeep Somavarapu 已提交
121
		@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
122
		@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
123
		@IAuthenticationService private readonly authenticationService: IAuthenticationService,
124 125 126
		@IContextKeyService contextKeyService: IContextKeyService,
		@IActivityService private readonly activityService: IActivityService,
		@INotificationService private readonly notificationService: INotificationService,
S
Sandeep Somavarapu 已提交
127
		@IConfigurationService configurationService: IConfigurationService,
128 129
		@IEditorService private readonly editorService: IEditorService,
		@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
130
		@IDialogService private readonly dialogService: IDialogService,
131
		@IQuickInputService private readonly quickInputService: IQuickInputService,
132
		@IInstantiationService instantiationService: IInstantiationService,
S
Sandeep Somavarapu 已提交
133
		@IOutputService private readonly outputService: IOutputService,
134
		@IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService,
S
Sandeep Somavarapu 已提交
135
		@IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService,
S
Sandeep Somavarapu 已提交
136
		@ITextModelService textModelResolverService: ITextModelService,
S
Sandeep Somavarapu 已提交
137
		@IPreferencesService private readonly preferencesService: IPreferencesService,
S
Sandeep Somavarapu 已提交
138
		@ITelemetryService private readonly telemetryService: ITelemetryService,
139
		@IFileService private readonly fileService: IFileService,
140
		@IProductService private readonly productService: IProductService,
141 142
		@IStorageService private readonly storageService: IStorageService,
		@IOpenerService private readonly openerService: IOpenerService,
143 144
	) {
		super();
145
		this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
S
Sandeep Somavarapu 已提交
146
		this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService);
147
		this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
148
		this.authenticationState = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService);
149
		this.conflictsSources = CONTEXT_CONFLICTS_SOURCES.bindTo(contextKeyService);
150 151 152
		if (this.userDataSyncStore) {
			registerConfiguration();
			this.onDidChangeSyncStatus(this.userDataSyncService.status);
153
			this.onDidChangeConflicts(this.userDataSyncService.conflictsSources);
S
Sandeep Somavarapu 已提交
154
			this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled());
155
			this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status)));
156
			this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflictsSources)));
S
Sandeep Somavarapu 已提交
157
			this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors)));
158
			this._register(this.authTokenService.onTokenFailed(_ => this.onTokenFailed()));
S
Sandeep Somavarapu 已提交
159
			this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled)));
160 161
			this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e)));
			this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e)));
162
			this._register(this.authenticationService.onDidChangeSessions(e => this.onDidChangeSessions(e)));
163
			this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error)));
164
			this.registerActions();
165
			this.initializeActiveAccount().then(_ => {
166
				if (!isWeb) {
167
					this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source])));
168 169
				}
			});
S
Sandeep Somavarapu 已提交
170 171

			textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider));
172
			registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution);
173
		}
174 175
	}

176
	private async initializeActiveAccount(): Promise<void> {
177
		const sessions = await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId);
178
		// Auth provider has not yet been registered
179
		if (!sessions) {
180 181 182
			return;
		}

183
		if (sessions.length === 0) {
184
			await this.setActiveAccount(undefined);
185
			return;
186
		}
187

188
		if (sessions.length === 1) {
189
			this.logAuthenticatedEvent(sessions[0]);
190
			await this.setActiveAccount(sessions[0]);
191 192 193
			return;
		}

194
		const selectedAccount = await this.quickInputService.pick(sessions.map(session => {
195
			return {
196 197
				id: session.id,
				label: session.accountName
198 199 200 201
			};
		}), { canPickMany: false });

		if (selectedAccount) {
202 203
			const selected = sessions.filter(account => selectedAccount.id === account.id)[0];
			this.logAuthenticatedEvent(selected);
204
			await this.setActiveAccount(selected);
205 206 207
		}
	}

208 209 210 211 212 213 214 215 216 217 218 219 220
	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 });
	}

221
	get activeAccount(): AuthenticationSession | undefined {
222 223 224
		return this._activeAccount;
	}

225
	async setActiveAccount(account: AuthenticationSession | undefined) {
226 227 228
		this._activeAccount = account;

		if (account) {
229
			try {
230
				const token = await account.getAccessToken();
231
				this.authTokenService.setToken(token);
232 233
				this.authenticationState.set(AuthStatus.SignedIn);
			} catch (e) {
234
				this.authTokenService.setToken(undefined);
235 236
				this.authenticationState.set(AuthStatus.Unavailable);
			}
237
		} else {
238
			this.authTokenService.setToken(undefined);
239
			this.authenticationState.set(AuthStatus.SignedOut);
240 241
		}

242 243 244
		this.updateBadge();
	}

245
	private async onDidChangeSessions(providerId: string): Promise<void> {
246
		if (providerId === this.userDataSyncStore!.authenticationProviderId) {
247 248
			if (this.activeAccount) {
				// Try to update existing account, case where access token has been refreshed
249
				const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []);
250
				const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0];
251
				this.setActiveAccount(matchingAccount);
252 253 254 255 256 257
			} else {
				this.initializeActiveAccount();
			}
		}
	}

258 259 260 261 262 263 264 265 266 267
	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);
		}
	}

268
	private async onDidRegisterAuthenticationProvider(providerId: string) {
269
		if (providerId === this.userDataSyncStore!.authenticationProviderId) {
270 271 272 273 274
			await this.initializeActiveAccount();
		}
	}

	private onDidUnregisterAuthenticationProvider(providerId: string) {
275
		if (providerId === this.userDataSyncStore!.authenticationProviderId) {
276
			this.setActiveAccount(undefined);
277 278 279 280
			this.authenticationState.reset();
		}
	}

281 282
	private onDidChangeSyncStatus(status: SyncStatus) {
		this.syncStatusContext.set(status);
283
		this.updateBadge();
284
	}
285

S
Sandeep Somavarapu 已提交
286
	private readonly conflictsDisposables = new Map<SyncSource, IDisposable>();
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
	private onDidChangeConflicts(conflicts: SyncSource[]) {
		this.updateBadge();
		if (conflicts.length) {
			this.conflictsSources.set(this.userDataSyncService.conflictsSources.join(','));

			// Clear and dispose conflicts those were cleared
			this.conflictsDisposables.forEach((disposable, conflictsSource) => {
				if (this.userDataSyncService.conflictsSources.indexOf(conflictsSource) === -1) {
					disposable.dispose();
					this.conflictsDisposables.delete(conflictsSource);
				}
			});

			for (const conflictsSource of this.userDataSyncService.conflictsSources) {
				const conflictsEditorInput = this.getConflictsEditorInput(conflictsSource);
				if (!conflictsEditorInput && !this.conflictsDisposables.has(conflictsSource)) {
					const conflictsArea = getSyncAreaLabel(conflictsSource);
304
					const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea.toLowerCase()),
305
						[
306 307
							{
								label: localize('accept remote', "Accept Remote"),
S
Sandeep Somavarapu 已提交
308 309 310 311
								run: () => {
									this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: 'acceptRemote' });
									this.acceptRemote(conflictsSource);
								}
312 313 314
							},
							{
								label: localize('accept local', "Accept Local"),
S
Sandeep Somavarapu 已提交
315 316 317 318
								run: () => {
									this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: 'acceptLocal' });
									this.acceptLocal(conflictsSource);
								}
319
							},
320 321 322
							{
								label: localize('show conflicts', "Show Conflicts"),
								run: () => {
S
Sandeep Somavarapu 已提交
323
									this.telemetryService.publicLog2<{ source: string, action?: string }, SyncConflictsClassification>('sync/showConflicts', { source: conflictsSource });
324 325
									this.handleConflicts(conflictsSource);
								}
S
Sandeep Somavarapu 已提交
326
							}
327 328 329
						],
						{
							sticky: true
330
						}
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
					);
					this.conflictsDisposables.set(conflictsSource, toDisposable(() => {

						// close the conflicts warning notification
						handle.close();

						// close opened conflicts editor previews
						const conflictsEditorInput = this.getConflictsEditorInput(conflictsSource);
						if (conflictsEditorInput) {
							conflictsEditorInput.dispose();
						}

						this.conflictsDisposables.delete(conflictsSource);
					}));
				}
346 347
			}
		} else {
348
			this.conflictsSources.reset();
S
Sandeep Somavarapu 已提交
349
			this.getAllConflictsEditorInputs().forEach(input => input.dispose());
350 351
			this.conflictsDisposables.forEach(disposable => disposable.dispose());
			this.conflictsDisposables.clear();
352 353 354
		}
	}

S
Sandeep Somavarapu 已提交
355 356
	private async acceptRemote(syncSource: SyncSource) {
		try {
357
			const contents = await this.userDataSyncService.resolveContent(toRemoteSyncResourceFromSource(syncSource).with({ query: PREVIEW_QUERY }));
S
Sandeep Somavarapu 已提交
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
			if (contents) {
				await this.userDataSyncService.accept(syncSource, contents);
			}
		} catch (e) {
			this.notificationService.error(e);
		}
	}

	private async acceptLocal(syncSource: SyncSource): Promise<void> {
		try {
			const previewResource = syncSource === SyncSource.Settings
				? this.workbenchEnvironmentService.settingsSyncPreviewResource
				: syncSource === SyncSource.Keybindings
					? this.workbenchEnvironmentService.keybindingsSyncPreviewResource
					: null;
			if (previewResource) {
				const fileContent = await this.fileService.readFile(previewResource);
				if (fileContent) {
					this.userDataSyncService.accept(syncSource, fileContent.value.toString());
				}
			}
		} catch (e) {
			this.notificationService.error(e);
		}
	}

S
Sandeep Somavarapu 已提交
384 385
	private onDidChangeEnablement(enabled: boolean) {
		this.syncEnablementContext.set(enabled);
386 387
		this.updateBadge();
		if (enabled) {
388
			if (this.authenticationState.get() === AuthStatus.SignedOut) {
389 390
				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),
391 392 393 394 395 396 397 398 399 400 401 402 403 404
					[
						{
							label: localize('Sign in', "Sign in"),
							run: () => this.signIn()
						}
					]);
				this.signInNotificationDisposable.value = toDisposable(() => handle.close());
				handle.onDidClose(() => this.signInNotificationDisposable.clear());
			}
		} else {
			this.signInNotificationDisposable.clear();
		}
	}

405 406
	private onAutoSyncError(error: UserDataSyncError): void {
		switch (error.code) {
S
Sandeep Somavarapu 已提交
407 408 409 410
			case UserDataSyncErrorCode.TurnedOff:
			case UserDataSyncErrorCode.SessionExpired:
				this.notificationService.notify({
					severity: Severity.Info,
S
Sandeep Somavarapu 已提交
411
					message: localize('turned off', "Sync was turned off from another device."),
S
Sandeep Somavarapu 已提交
412
					actions: {
S
Sandeep Somavarapu 已提交
413
						primary: [new Action('turn on sync', localize('turn on sync', "Turn on Sync"), undefined, true, () => this.turnOn())]
S
Sandeep Somavarapu 已提交
414 415 416
					}
				});
				return;
S
Sandeep Somavarapu 已提交
417
			case UserDataSyncErrorCode.TooLarge:
418
				if (error.source === SyncSource.Keybindings || error.source === SyncSource.Settings) {
S
Sandeep Somavarapu 已提交
419
					this.disableSync(error.source);
420
					const sourceArea = getSyncAreaLabel(error.source);
S
Sandeep Somavarapu 已提交
421 422
					this.notificationService.notify({
						severity: Severity.Error,
S
Sandeep Somavarapu 已提交
423
						message: localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '100kb'),
S
Sandeep Somavarapu 已提交
424
						actions: {
S
Sandeep Somavarapu 已提交
425
							primary: [new Action('open sync file', localize('open file', "Open {0} File", sourceArea), undefined, true,
426
								() => error.source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))]
S
Sandeep Somavarapu 已提交
427 428 429 430
						}
					});
				}
				return;
431 432 433 434
			case UserDataSyncErrorCode.Incompatible:
				this.disableSync();
				this.notificationService.notify({
					severity: Severity.Error,
S
Sandeep Somavarapu 已提交
435
					message: localize('error incompatible', "Turned off sync because local data is incompatible with the data in the cloud. Please update {0} and turn on sync to continue syncing.", this.productService.nameLong),
436 437
				});
				return;
S
Sandeep Somavarapu 已提交
438 439 440
		}
	}

S
Sandeep Somavarapu 已提交
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
	private readonly invalidContentErrorDisposables = new Map<SyncSource, IDisposable>();
	private onSyncErrors(errors: [SyncSource, UserDataSyncError][]): void {
		if (errors.length) {
			for (const [source, error] of errors) {
				switch (error.code) {
					case UserDataSyncErrorCode.LocalInvalidContent:
						this.handleInvalidContentError(source);
						break;
					default:
						const disposable = this.invalidContentErrorDisposables.get(source);
						if (disposable) {
							disposable.dispose();
							this.invalidContentErrorDisposables.delete(source);
						}
				}
			}
		} else {
			this.invalidContentErrorDisposables.forEach(disposable => disposable.dispose());
			this.invalidContentErrorDisposables.clear();
		}
	}

	private handleInvalidContentError(source: SyncSource): void {
S
Sandeep Somavarapu 已提交
464 465 466 467 468 469 470
		if (this.invalidContentErrorDisposables.has(source)) {
			return;
		}
		if (source !== SyncSource.Settings && source !== SyncSource.Keybindings) {
			return;
		}
		const resource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsResource : this.workbenchEnvironmentService.keybindingsResource;
S
Sandeep Somavarapu 已提交
471
		if (isEqual(resource, toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }))) {
S
Sandeep Somavarapu 已提交
472 473
			// Do not show notification if the file in error is active
			return;
S
Sandeep Somavarapu 已提交
474
		}
S
Sandeep Somavarapu 已提交
475 476 477 478 479 480 481 482 483 484 485 486 487 488
		const errorArea = getSyncAreaLabel(source);
		const handle = this.notificationService.notify({
			severity: Severity.Error,
			message: localize('errorInvalidConfiguration', "Unable to sync {0} because there are some errors/warnings in the file. Please open the file to correct errors/warnings in it.", errorArea.toLowerCase()),
			actions: {
				primary: [new Action('open sync file', localize('open file', "Open {0} File", errorArea), undefined, true,
					() => source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))]
			}
		});
		this.invalidContentErrorDisposables.set(source, toDisposable(() => {
			// close the error warning notification
			handle.close();
			this.invalidContentErrorDisposables.delete(source);
		}));
S
Sandeep Somavarapu 已提交
489 490
	}

491
	private async updateBadge(): Promise<void> {
492 493 494 495
		this.badgeDisposable.clear();

		let badge: IBadge | undefined = undefined;
		let clazz: string | undefined;
S
Sandeep Somavarapu 已提交
496
		let priority: number | undefined = undefined;
497

S
Sandeep Somavarapu 已提交
498
		if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.authenticationState.get() === AuthStatus.SignedOut) {
S
Sandeep Somavarapu 已提交
499
			badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync"));
500 501
		} else if (this.userDataSyncService.conflictsSources.length) {
			badge = new NumberBadge(this.userDataSyncService.conflictsSources.length, () => localize('has conflicts', "Sync: Conflicts Detected"));
502 503 504
		}

		if (badge) {
S
Sandeep Somavarapu 已提交
505
			this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz, priority);
506 507 508
		}
	}

509
	private async turnOn(): Promise<void> {
510 511 512
		if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) {
			const result = await this.dialogService.show(
				Severity.Info,
D
Daniel Imms 已提交
513
				localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."),
514 515
				[
					localize('open doc', "Open Documentation"),
S
Sandeep Somavarapu 已提交
516
					localize('turn on sync', "Turn on Sync"),
517 518 519 520 521 522 523
					localize('cancel', "Cancel"),
				],
				{
					cancelId: 2
				}
			);
			switch (result.choice) {
S
Sandeep Somavarapu 已提交
524
				case 0: this.openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); return;
525 526 527
				case 2: return;
			}
		}
528 529 530 531
		return new Promise((c, e) => {
			const disposables: DisposableStore = new DisposableStore();
			const quickPick = this.quickInputService.createQuickPick<ConfigureSyncQuickPickItem>();
			disposables.add(quickPick);
S
Sandeep Somavarapu 已提交
532
			quickPick.title = localize('turn on title', "Sync: Turn On");
533 534
			quickPick.ok = false;
			quickPick.customButton = true;
535
			if (this.authenticationState.get() === AuthStatus.SignedIn) {
S
Sandeep Somavarapu 已提交
536
				quickPick.customLabel = localize('turn on', "Turn On");
537
			} else {
538
				const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId);
S
Sandeep Somavarapu 已提交
539
				quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName);
540 541 542 543 544 545 546
				quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on");
			}
			quickPick.placeholder = localize('configure sync placeholder', "Choose what to sync");
			quickPick.canSelectMany = true;
			quickPick.ignoreFocusOut = true;
			const items = this.getConfigureSyncQuickPickItems();
			quickPick.items = items;
S
Sandeep Somavarapu 已提交
547
			quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id));
548 549
			disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => {
				if (quickPick.selectedItems.length) {
S
Sandeep Somavarapu 已提交
550
					this.updateConfiguration(items, quickPick.selectedItems);
S
Sandeep Somavarapu 已提交
551
					this.doTurnOn().then(c, e);
552 553 554
					quickPick.hide();
				}
			}));
S
Sandeep Somavarapu 已提交
555
			disposables.add(quickPick.onDidHide(() => disposables.dispose()));
556 557 558 559 560
			quickPick.show();
		});
	}

	private async doTurnOn(): Promise<void> {
S
Sandeep Somavarapu 已提交
561 562 563 564
		if (this.authenticationState.get() === AuthStatus.SignedIn) {
			await new Promise((c, e) => {
				const disposables: DisposableStore = new DisposableStore();
				const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId);
565
				const quickPick = this.quickInputService.createQuickPick<{ id: string, label: string, description?: string, detail?: string }>();
S
Sandeep Somavarapu 已提交
566
				disposables.add(quickPick);
S
Sandeep Somavarapu 已提交
567
				const chooseAnotherItemId = 'chooseAnother';
568
				quickPick.title = localize('pick account', "{0}: Pick an account", displayName);
S
Sandeep Somavarapu 已提交
569
				quickPick.ok = false;
570
				quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing");
S
Sandeep Somavarapu 已提交
571 572 573
				quickPick.ignoreFocusOut = true;
				quickPick.items = [{
					id: 'existing',
574 575
					label: localize('existing', "{0}", this.activeAccount!.accountName),
					detail: localize('signed in', "Signed in"),
S
Sandeep Somavarapu 已提交
576 577
				}, {
					id: chooseAnotherItemId,
578
					label: localize('choose another', "Use another account")
S
Sandeep Somavarapu 已提交
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
				}];
				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();
			});
		}
594
		if (this.authenticationState.get() === AuthStatus.SignedOut) {
S
Sandeep Somavarapu 已提交
595
			await this.signIn();
596
		}
597
		await this.handleFirstTimeSync();
S
Sandeep Somavarapu 已提交
598
		this.userDataSyncEnablementService.setEnablement(true);
S
Sandeep Somavarapu 已提交
599
		this.notificationService.info(localize('sync turned on', "Sync will happen automatically from now on."));
600
		this.storageService.store('sync.donotAskPreviewConfirmation', true, StorageScope.GLOBAL);
601 602
	}

603 604
	private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] {
		return [{
S
Sandeep Somavarapu 已提交
605
			id: 'settings',
606
			label: getSyncAreaLabel(SyncSource.Settings)
607
		}, {
S
Sandeep Somavarapu 已提交
608
			id: 'keybindings',
609
			label: getSyncAreaLabel(SyncSource.Keybindings)
610
		}, {
S
Sandeep Somavarapu 已提交
611
			id: 'extensions',
612
			label: getSyncAreaLabel(SyncSource.Extensions)
613
		}, {
S
Sandeep Somavarapu 已提交
614
			id: 'globalState',
S
Sandeep Somavarapu 已提交
615
			label: getSyncAreaLabel(SyncSource.GlobalState),
S
Sandeep Somavarapu 已提交
616
			description: localize('ui state description', "only 'Display Language' for now")
617 618 619
		}];
	}

S
Sandeep Somavarapu 已提交
620
	private updateConfiguration(items: ConfigureSyncQuickPickItem[], selectedItems: ReadonlyArray<ConfigureSyncQuickPickItem>): void {
621
		for (const item of items) {
S
Sandeep Somavarapu 已提交
622
			const wasEnabled = this.userDataSyncEnablementService.isResourceEnabled(item.id);
623 624
			const isEnabled = !!selectedItems.filter(selected => selected.id === item.id)[0];
			if (wasEnabled !== isEnabled) {
S
Sandeep Somavarapu 已提交
625
				this.userDataSyncEnablementService.setResourceEnablement(item.id!, isEnabled);
626
			}
S
Sandeep Somavarapu 已提交
627 628 629
		}
	}

630
	private async configureSyncOptions(): Promise<ISyncConfiguration> {
631 632
		return new Promise((c, e) => {
			const disposables: DisposableStore = new DisposableStore();
633
			const quickPick = this.quickInputService.createQuickPick<ConfigureSyncQuickPickItem>();
634
			disposables.add(quickPick);
635
			quickPick.title = localize('turn on sync', "Turn on Sync");
636
			quickPick.placeholder = localize('configure sync placeholder', "Choose what to sync");
637
			quickPick.canSelectMany = true;
S
Sandeep Somavarapu 已提交
638
			quickPick.ignoreFocusOut = true;
S
Sandeep Somavarapu 已提交
639
			quickPick.ok = true;
640
			const items = this.getConfigureSyncQuickPickItems();
641
			quickPick.items = items;
S
Sandeep Somavarapu 已提交
642
			quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id));
643 644
			disposables.add(quickPick.onDidAccept(async () => {
				if (quickPick.selectedItems.length) {
645
					await this.updateConfiguration(items, quickPick.selectedItems);
646
					quickPick.hide();
647
				}
S
Sandeep Somavarapu 已提交
648 649
			}));
			disposables.add(quickPick.onDidHide(() => {
650 651 652 653 654
				disposables.dispose();
				c();
			}));
			quickPick.show();
		});
655 656
	}

657
	private async handleFirstTimeSync(): Promise<void> {
658 659
		const isFirstSyncWithMerge = await this.userDataSyncService.isFirstTimeSyncWithMerge();
		if (!isFirstSyncWithMerge) {
660 661 662 663
			return;
		}
		const result = await this.dialogService.show(
			Severity.Info,
S
Sandeep Somavarapu 已提交
664
			localize('firs time sync', "Sync"),
665 666 667
			[
				localize('merge', "Merge"),
				localize('cancel', "Cancel"),
S
Sandeep Somavarapu 已提交
668
				localize('replace', "Replace Local"),
669 670 671
			],
			{
				cancelId: 1,
S
Sandeep Somavarapu 已提交
672
				detail: localize('first time sync detail', "It looks like this is the first time sync is set up.\nWould you like to merge or replace with the data from the cloud?"),
673
			}
674 675
		);
		switch (result.choice) {
S
Sandeep Somavarapu 已提交
676 677 678 679 680 681 682 683 684 685
			case 0:
				this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'merge' });
				break;
			case 1:
				this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'cancelled' });
				throw canceled();
			case 2:
				this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'replace-local' });
				await this.userDataSyncService.pull();
				break;
686 687 688
		}
	}

689
	private async turnOff(): Promise<void> {
690 691 692
		const result = await this.dialogService.confirm({
			type: 'info',
			message: localize('turn off sync confirmation', "Turn off Sync"),
S
Sandeep Somavarapu 已提交
693
			detail: localize('turn off sync detail', "Your settings, keybindings, extensions and UI State will no longer be synced."),
S
Sandeep Somavarapu 已提交
694
			primaryButton: localize('turn off', "Turn Off"),
S
Sandeep Somavarapu 已提交
695
			checkbox: {
S
Sandeep Somavarapu 已提交
696
				label: localize('turn off sync everywhere', "Turn off sync on all your devices and clear the data from the cloud.")
S
Sandeep Somavarapu 已提交
697
			}
698 699
		});
		if (result.confirmed) {
S
Sandeep Somavarapu 已提交
700
			if (result.checkboxChecked) {
S
Sandeep Somavarapu 已提交
701
				this.telemetryService.publicLog2('sync/turnOffEveryWhere');
S
Sandeep Somavarapu 已提交
702
				await this.userDataSyncService.reset();
S
Sandeep Somavarapu 已提交
703 704
			} else {
				await this.userDataSyncService.resetLocal();
S
Sandeep Somavarapu 已提交
705
			}
S
Sandeep Somavarapu 已提交
706
			this.disableSync();
707
		}
708 709
	}

S
Sandeep Somavarapu 已提交
710 711 712 713 714 715 716 717 718 719
	private disableSync(source?: SyncSource): void {
		if (source === undefined) {
			this.userDataSyncEnablementService.setEnablement(false);
		} else {
			switch (source) {
				case SyncSource.Settings: return this.userDataSyncEnablementService.setResourceEnablement('settings', false);
				case SyncSource.Keybindings: return this.userDataSyncEnablementService.setResourceEnablement('keybindings', false);
				case SyncSource.Extensions: return this.userDataSyncEnablementService.setResourceEnablement('extensions', false);
				case SyncSource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement('globalState', false);
			}
S
Sandeep Somavarapu 已提交
720
		}
S
Sandeep Somavarapu 已提交
721 722
	}

723
	private async signIn(): Promise<void> {
S
Sandeep Somavarapu 已提交
724
		try {
725
			await this.setActiveAccount(await this.authenticationService.login(this.userDataSyncStore!.authenticationProviderId, ['https://management.core.windows.net/.default', 'offline_access']));
S
Sandeep Somavarapu 已提交
726
		} catch (e) {
727
			this.notificationService.error(localize('loginFailed', "Logging in failed: {0}", e));
S
Sandeep Somavarapu 已提交
728 729
			throw e;
		}
730 731
	}

S
Sandeep Somavarapu 已提交
732 733 734 735
	private getConflictsEditorInput(source: SyncSource): IEditorInput | undefined {
		const previewResource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsSyncPreviewResource
			: source === SyncSource.Keybindings ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource
				: null;
736
		return previewResource ? this.editorService.editors.filter(input => input instanceof DiffEditorInput && isEqual(previewResource, input.master.resource))[0] : undefined;
737 738
	}

S
Sandeep Somavarapu 已提交
739
	private getAllConflictsEditorInputs(): IEditorInput[] {
S
Sandeep Somavarapu 已提交
740
		return this.editorService.editors.filter(input => {
741
			const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource;
S
Sandeep Somavarapu 已提交
742
			return isEqual(resource, this.workbenchEnvironmentService.settingsSyncPreviewResource) || isEqual(resource, this.workbenchEnvironmentService.keybindingsSyncPreviewResource);
S
Sandeep Somavarapu 已提交
743
		});
S
Sandeep Somavarapu 已提交
744 745
	}

746
	private async handleConflicts(source: SyncSource): Promise<void> {
S
inline  
Sandeep Somavarapu 已提交
747
		let previewResource: URI | undefined = undefined;
S
Sandeep Somavarapu 已提交
748
		let label: string = '';
749
		if (source === SyncSource.Settings) {
S
inline  
Sandeep Somavarapu 已提交
750
			previewResource = this.workbenchEnvironmentService.settingsSyncPreviewResource;
S
Sandeep Somavarapu 已提交
751
			label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)");
752
		} else if (source === SyncSource.Keybindings) {
S
Sandeep Somavarapu 已提交
753
			previewResource = this.workbenchEnvironmentService.keybindingsSyncPreviewResource;
S
Sandeep Somavarapu 已提交
754 755
			label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)");
		}
S
inline  
Sandeep Somavarapu 已提交
756
		if (previewResource) {
757
			const remoteContentResource = toRemoteSyncResourceFromSource(source).with({ query: PREVIEW_QUERY });
S
Sandeep Somavarapu 已提交
758
			await this.editorService.openEditor({
S
inline  
Sandeep Somavarapu 已提交
759 760
				leftResource: remoteContentResource,
				rightResource: previewResource,
S
Sandeep Somavarapu 已提交
761 762 763
				label,
				options: {
					preserveFocus: false,
S
Sandeep Somavarapu 已提交
764
					pinned: true,
S
Sandeep Somavarapu 已提交
765 766 767
					revealIfVisible: true,
				},
			});
S
Sandeep Somavarapu 已提交
768
		}
769 770
	}

771
	private showSyncActivity(): Promise<void> {
S
Sandeep Somavarapu 已提交
772 773 774
		return this.outputService.showChannel(Constants.userDataSyncLogChannelId);
	}

775
	private registerActions(): void {
776 777 778 779
		this.registerTurnOnSyncAction();
		this.registerSignInAction();
		this.registerShowSettingsConflictsAction();
		this.registerShowKeybindingsConflictsAction();
780
		this.registerSyncStatusAction();
781

782
		this.registerTurnOffSyncAction();
783 784 785 786 787 788
		this.registerConfigureSyncAction();
		this.registerShowActivityAction();
		this.registerShowSettingsAction();
	}

	private registerTurnOnSyncAction(): void {
S
Sandeep Somavarapu 已提交
789
		const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_AUTH_TOKEN_STATE.notEqualsTo(AuthStatus.Initializing));
790
		CommandsRegistry.registerCommand(turnOnSyncCommand.id, async () => {
791 792 793 794
			try {
				await this.turnOn();
			} catch (e) {
				if (!isPromiseCanceledError(e)) {
795
					this.notificationService.error(localize('turn on failed', "Error while starting Sync: {0}", toErrorMessage(e)));
796 797 798
				}
			}
		});
S
Sandeep Somavarapu 已提交
799
		MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
800 801
			group: '5_sync',
			command: {
802
				id: turnOnSyncCommand.id,
S
Sandeep Somavarapu 已提交
803
				title: localize('global activity turn on sync', "Turn on Sync...")
804
			},
S
Sandeep Somavarapu 已提交
805
			when: turnOnSyncWhenContext,
806
			order: 1
S
Sandeep Somavarapu 已提交
807 808
		});
		MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
809
			command: turnOnSyncCommand,
S
Sandeep Somavarapu 已提交
810 811
			when: turnOnSyncWhenContext,
		});
S
Sandeep Somavarapu 已提交
812 813 814
		MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
			group: '5_sync',
			command: {
815
				id: turnOnSyncCommand.id,
S
Sandeep Somavarapu 已提交
816 817 818 819
				title: localize('global activity turn on sync', "Turn on Sync...")
			},
			when: turnOnSyncWhenContext,
		});
820
	}
821

822 823
	private registerSignInAction(): void {
		const that = this;
824
		this._register(registerAction2(class StopSyncAction extends Action2 {
825 826 827 828 829 830 831 832
			constructor() {
				super({
					id: signInCommand.id,
					title: signInCommand.title,
					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)),
833
						order: 2
834 835 836 837 838 839 840 841
					},
				});
			}
			async run(): Promise<any> {
				try {
					await that.signIn();
				} catch (e) {
					that.notificationService.error(e);
S
Sandeep Somavarapu 已提交
842 843
				}
			}
844
		}));
845
	}
S
Sandeep Somavarapu 已提交
846

847
	private registerShowSettingsConflictsAction(): void {
848
		const resolveSettingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*settings.*/i);
849
		CommandsRegistry.registerCommand(resolveSettingsConflictsCommand.id, () => this.handleConflicts(SyncSource.Settings));
S
Sandeep Somavarapu 已提交
850
		MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
851 852
			group: '5_sync',
			command: {
853
				id: resolveSettingsConflictsCommand.id,
854
				title: localize('resolveConflicts_global', "Sync: Show Settings Conflicts (1)"),
855
			},
856
			when: resolveSettingsConflictsWhenContext,
857 858 859 860 861 862 863 864 865 866
			order: 2
		});
		MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
			group: '5_sync',
			command: {
				id: resolveSettingsConflictsCommand.id,
				title: localize('resolveConflicts_global', "Sync: Show Settings Conflicts (1)"),
			},
			when: resolveSettingsConflictsWhenContext,
			order: 2
S
Sandeep Somavarapu 已提交
867 868
		});
		MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
869
			command: resolveSettingsConflictsCommand,
870 871
			when: resolveSettingsConflictsWhenContext,
		});
872
	}
873

874
	private registerShowKeybindingsConflictsAction(): void {
875
		const resolveKeybindingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*keybindings.*/i);
876
		CommandsRegistry.registerCommand(resolveKeybindingsConflictsCommand.id, () => this.handleConflicts(SyncSource.Keybindings));
877 878 879
		MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
			group: '5_sync',
			command: {
880
				id: resolveKeybindingsConflictsCommand.id,
881 882 883
				title: localize('resolveKeybindingsConflicts_global', "Sync: Show Keybindings Conflicts (1)"),
			},
			when: resolveKeybindingsConflictsWhenContext,
884 885 886 887 888 889 890 891 892 893
			order: 2
		});
		MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
			group: '5_sync',
			command: {
				id: resolveKeybindingsConflictsCommand.id,
				title: localize('resolveKeybindingsConflicts_global', "Sync: Show Keybindings Conflicts (1)"),
			},
			when: resolveKeybindingsConflictsWhenContext,
			order: 2
894 895
		});
		MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
896
			command: resolveKeybindingsConflictsCommand,
897
			when: resolveKeybindingsConflictsWhenContext,
S
Sandeep Somavarapu 已提交
898
		});
899

900 901
	}

902 903 904
	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));
905
		this._register(registerAction2(class SyncStatusAction extends Action2 {
906 907
			constructor() {
				super({
908
					id: 'workbench.userData.actions.syncStatus',
S
Sandeep Somavarapu 已提交
909
					title: localize('sync is on', "Sync is on"),
910 911 912 913 914 915
					menu: [
						{
							id: MenuId.GlobalActivity,
							group: '5_sync',
							when,
							order: 3
916 917 918 919 920 921
						},
						{
							id: MenuId.MenubarPreferencesMenu,
							group: '5_sync',
							when,
							order: 3,
922 923 924 925 926 927 928 929
						}
					],
				});
			}
			run(accessor: ServicesAccessor): any {
				return new Promise((c, e) => {
					const quickInputService = accessor.get(IQuickInputService);
					const commandService = accessor.get(ICommandService);
S
Sandeep Somavarapu 已提交
930
					const disposables = new DisposableStore();
931
					const quickPick = quickInputService.createQuickPick();
S
Sandeep Somavarapu 已提交
932
					disposables.add(quickPick);
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950
					const items: Array<IQuickPickItem | IQuickPickSeparator> = [];
					if (that.userDataSyncService.conflictsSources.length) {
						for (const source of that.userDataSyncService.conflictsSources) {
							switch (source) {
								case SyncSource.Settings:
									items.push({ id: resolveSettingsConflictsCommand.id, label: resolveSettingsConflictsCommand.title });
									break;
								case SyncSource.Keybindings:
									items.push({ id: resolveKeybindingsConflictsCommand.id, label: resolveKeybindingsConflictsCommand.title });
									break;
							}
						}
						items.push({ type: 'separator' });
					}
					items.push({ id: configureSyncCommand.id, label: configureSyncCommand.title });
					items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title });
					items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title(that.userDataSyncService) });
					items.push({ type: 'separator' });
S
Sandeep Somavarapu 已提交
951
					items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService) });
952 953 954
					quickPick.items = items;
					disposables.add(quickPick.onDidAccept(() => {
						if (quickPick.selectedItems[0] && quickPick.selectedItems[0].id) {
S
Sandeep Somavarapu 已提交
955 956
							// Introduce timeout as workaround - #91661 #91740
							timeout(0).then(() => commandService.executeCommand(quickPick.selectedItems[0].id!));
957 958 959 960 961 962 963 964 965 966
						}
						quickPick.hide();
					}));
					disposables.add(quickPick.onDidHide(() => {
						disposables.dispose();
						c();
					}));
					quickPick.show();
				});
			}
967
		}));
968 969 970 971
	}

	private registerTurnOffSyncAction(): void {
		const that = this;
972
		this._register(registerAction2(class StopSyncAction extends Action2 {
973 974 975
			constructor() {
				super({
					id: stopSyncCommand.id,
S
Sandeep Somavarapu 已提交
976
					title: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService),
977 978 979 980 981 982 983 984 985 986 987 988 989 990 991
					menu: {
						id: MenuId.CommandPalette,
						when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT),
					},
				});
			}
			async run(): Promise<any> {
				try {
					await that.turnOff();
				} catch (e) {
					if (!isPromiseCanceledError(e)) {
						that.notificationService.error(localize('turn off failed', "Error while turning off sync: {0}", toErrorMessage(e)));
					}
				}
			}
992
		}));
993 994
	}

995 996
	private registerConfigureSyncAction(): void {
		const that = this;
997
		this._register(registerAction2(class ShowSyncActivityAction extends Action2 {
998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
			constructor() {
				super({
					id: configureSyncCommand.id,
					title: configureSyncCommand.title,
					menu: {
						id: MenuId.CommandPalette,
						when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT),
					},
				});
			}
			run(): any { return that.configureSyncOptions(); }
1009
		}));
1010
	}
S
Sandeep Somavarapu 已提交
1011

1012 1013
	private registerShowActivityAction(): void {
		const that = this;
1014
		this._register(registerAction2(class ShowSyncActivityAction extends Action2 {
1015 1016 1017
			constructor() {
				super({
					id: showSyncActivityCommand.id,
1018
					get title() { return showSyncActivityCommand.title(that.userDataSyncService); },
1019 1020 1021 1022 1023 1024 1025
					menu: {
						id: MenuId.CommandPalette,
						when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)),
					},
				});
			}
			run(): any { return that.showSyncActivity(); }
1026
		}));
1027
	}
S
Sandeep Somavarapu 已提交
1028

1029
	private registerShowSettingsAction(): void {
1030
		this._register(registerAction2(class ShowSyncSettingsAction extends Action2 {
1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041
			constructor() {
				super({
					id: showSyncSettingsCommand.id,
					title: showSyncSettingsCommand.title,
					menu: {
						id: MenuId.CommandPalette,
						when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)),
					},
				});
			}
			run(accessor: ServicesAccessor): any {
R
Rob Lourens 已提交
1042
				accessor.get(IPreferencesService).openGlobalSettings(false, { query: '@tag:sync' });
1043
			}
1044
		}));
1045
	}
1046

1047
}
S
Sandeep Somavarapu 已提交
1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058

class UserDataRemoteContentProvider implements ITextModelContentProvider {

	constructor(
		@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
		@IModelService private readonly modelService: IModelService,
		@IModeService private readonly modeService: IModeService,
	) {
	}

	provideTextContent(uri: URI): Promise<ITextModel> | null {
1059 1060
		if (uri.scheme === USER_DATA_SYNC_SCHEME) {
			return this.userDataSyncService.resolveContent(uri).then(content => this.modelService.createModel(content || '', this.modeService.create('jsonc'), uri));
S
Sandeep Somavarapu 已提交
1061 1062 1063 1064 1065
		}
		return null;
	}
}

1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080
class AcceptChangesContribution extends Disposable implements IEditorContribution {

	static get(editor: ICodeEditor): AcceptChangesContribution {
		return editor.getContribution<AcceptChangesContribution>(AcceptChangesContribution.ID);
	}

	public static readonly ID = 'editor.contrib.acceptChangesButton';

	private acceptChangesButton: FloatingClickWidget | undefined;

	constructor(
		private editor: ICodeEditor,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
		@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
1081
		@INotificationService private readonly notificationService: INotificationService,
1082 1083
		@IDialogService private readonly dialogService: IDialogService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
S
Sandeep Somavarapu 已提交
1084
		@ITelemetryService private readonly telemetryService: ITelemetryService
1085 1086 1087 1088 1089 1090 1091 1092 1093
	) {
		super();

		this.update();
		this.registerListeners();
	}

	private registerListeners(): void {
		this._register(this.editor.onDidChangeModel(e => this.update()));
1094
		this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('diffEditor.renderSideBySide'))(() => this.update()));
1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111
	}

	private update(): void {
		if (!this.shouldShowButton(this.editor)) {
			this.disposeAcceptChangesWidgetRenderer();
			return;
		}

		this.createAcceptChangesWidgetRenderer();
	}

	private shouldShowButton(editor: ICodeEditor): boolean {
		const model = editor.getModel();
		if (!model) {
			return false; // we need a model
		}

1112
		if (getSyncSourceFromPreviewResource(model.uri, this.environmentService) !== undefined) {
1113 1114 1115
			return true;
		}

S
Sandeep Somavarapu 已提交
1116
		if (resolveSyncResource(model.uri) !== null && model.uri.query === PREVIEW_QUERY) {
1117 1118 1119 1120 1121 1122
			return this.configurationService.getValue<boolean>('diffEditor.renderSideBySide');
		}

		return false;
	}

1123 1124 1125

	private createAcceptChangesWidgetRenderer(): void {
		if (!this.acceptChangesButton) {
1126
			const isRemote = resolveSyncResource(this.editor.getModel()!.uri) !== null;
S
Sandeep Somavarapu 已提交
1127 1128
			const acceptRemoteLabel = localize('accept remote', "Accept Remote");
			const acceptLocalLabel = localize('accept local', "Accept Local");
1129
			this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null);
1130 1131 1132
			this._register(this.acceptChangesButton.onClick(async () => {
				const model = this.editor.getModel();
				if (model) {
1133
					const conflictsSource = (getSyncSourceFromPreviewResource(model.uri, this.environmentService) || getSyncSourceFromResourceKey(resolveSyncResource(model.uri)!.resourceKey))!;
1134 1135
					this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: isRemote ? 'acceptRemote' : 'acceptLocal' });
					const syncAreaLabel = getSyncAreaLabel(conflictsSource);
S
Sandeep Somavarapu 已提交
1136 1137
					const result = await this.dialogService.confirm({
						type: 'info',
1138
						title: isRemote
S
Sandeep Somavarapu 已提交
1139 1140
							? localize('Sync accept remote', "Sync: {0}", acceptRemoteLabel)
							: localize('Sync accept local', "Sync: {0}", acceptLocalLabel),
1141
						message: isRemote
1142 1143
							? localize('confirm replace and overwrite local', "Would you like to accept remote {0} and replace local {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase())
							: localize('confirm replace and overwrite remote', "Would you like to accept local {0} and replace remote {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()),
1144
						primaryButton: isRemote ? acceptRemoteLabel : acceptLocalLabel
S
Sandeep Somavarapu 已提交
1145 1146 1147
					});
					if (result.confirmed) {
						try {
1148
							await this.userDataSyncService.accept(conflictsSource, model.getValue());
S
Sandeep Somavarapu 已提交
1149
						} catch (e) {
1150
							if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) {
1151 1152 1153
								if (this.userDataSyncService.conflictsSources.indexOf(conflictsSource) !== -1) {
									this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again."));
								}
S
Sandeep Somavarapu 已提交
1154 1155 1156
							} else {
								this.notificationService.error(e);
							}
1157
						}
S
Sandeep Somavarapu 已提交
1158
					}
1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175
				}
			}));

			this.acceptChangesButton.render();
		}
	}

	private disposeAcceptChangesWidgetRenderer(): void {
		dispose(this.acceptChangesButton);
		this.acceptChangesButton = undefined;
	}

	dispose(): void {
		this.disposeAcceptChangesWidgetRenderer();
		super.dispose();
	}
}