#100346 Introduce manual sync view

This commit is contained in:
Sandeep Somavarapu
2020-07-14 13:33:14 +02:00
parent 1d745fe492
commit 9aadfc98db
4 changed files with 600 additions and 109 deletions

View File

@@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, Change } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask } from 'vs/platform/userDataSync/common/userDataSync';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNCED_DATA_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel } from 'vs/workbench/services/userDataSync/common/userDataSync';
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResourceGroup, CONTEXT_SHOW_MANUAL_SYNC_VIEW, SHOW_SYNCED_DATA_COMMAND_ID, MANUAL_SYNC_VIEW_ID } from 'vs/workbench/services/userDataSync/common/userDataSync';
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { flatten } from 'vs/base/common/arrays';
import { flatten, equals } from 'vs/base/common/arrays';
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
@@ -21,13 +21,17 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { localize } from 'vs/nls';
import { canceled, isPromiseCanceledError } from 'vs/base/common/errors';
import { canceled } from 'vs/base/common/errors';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { Action } from 'vs/base/common/actions';
import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views';
import { IDecorationsProvider, IDecorationData, IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations';
type UserAccountClassification = {
id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
@@ -41,6 +45,8 @@ type UserAccountEvent = {
id: string;
};
type FirstTimeSyncAction = 'pull' | 'push' | 'merge' | 'manual';
type AccountQuickPickItem = { label: string, authenticationProvider: IAuthenticationProvider, account?: UserDataSyncAccount, description?: string };
class UserDataSyncAccount implements IUserDataSyncAccount {
@@ -75,6 +81,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
private readonly syncEnablementContext: IContextKey<boolean>;
private readonly syncStatusContext: IContextKey<string>;
private readonly accountStatusContext: IContextKey<string>;
private readonly showManualSyncViewContext: IContextKey<boolean>;
readonly userDataSyncPreview: UserDataSyncPreview = this._register(new UserDataSyncPreview(this.userDataSyncService));
constructor(
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@@ -94,12 +103,18 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
@IDialogService private readonly dialogService: IDialogService,
@ICommandService private readonly commandService: ICommandService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewsService private readonly viewsService: IViewsService,
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
@IDecorationsService decorationsService: IDecorationsService,
) {
super();
this.authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || [];
this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService);
this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService);
this.showManualSyncViewContext = CONTEXT_SHOW_MANUAL_SYNC_VIEW.bindTo(contextKeyService);
decorationsService.registerDecorationsProvider(this.userDataSyncPreview);
if (this.authenticationProviders.length) {
@@ -222,12 +237,8 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
const preferencesSyncTitle = localize('preferences sync', "Preferences Sync");
const title = `${preferencesSyncTitle} [(${localize('details', "details")})](command:${SHOW_SYNC_LOG_COMMAND_ID})`;
await this.progressService.withProgress({
location: ProgressLocation.Notification,
title,
delay: 500,
}, (progress) => this.turnOnWithProgress(progress));
await this.syncBeforeTurningOn(title);
await this.userDataAutoSyncService.turnOn();
this.notificationService.info(localize('sync turned on', "{0} is turned on", title));
}
@@ -235,53 +246,69 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
return this.userDataAutoSyncService.turnOff(everywhere);
}
private async turnOnWithProgress(progress: IProgress<IProgressStep>): Promise<void> {
progress.report({ message: localize('turning on', "Turning on...") });
private async syncBeforeTurningOn(title: string): Promise<void> {
/* Make sure sync started on clean local state */
await this.userDataSyncService.resetLocal();
const manualSyncTask = await this.userDataSyncService.createManualSyncTask();
const preview = await manualSyncTask.preview();
const hasRemoteData = manualSyncTask.manifest !== null;
const hasLocalData = await this.userDataSyncService.hasLocalData();
const isLastSyncFromCurrentMachine = preview.every(([, { isLastSyncFromCurrentMachine }]) => isLastSyncFromCurrentMachine);
const hasChanges = preview.some(([, { resourcePreviews }]) => resourcePreviews.some(r => r.localChange !== Change.None || r.remoteChange !== Change.None));
const progressDisposable = manualSyncTask.onSynchronizeResources(synchronizingResources =>
synchronizingResources.length ? progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(synchronizingResources[0][0])) }) : undefined);
try {
if (!hasLocalData /* no data on local */
|| !hasRemoteData /* no data on remote */
|| !hasChanges /* no changes */
|| isLastSyncFromCurrentMachine /* has changes but last sync is from current machine */
) {
await manualSyncTask.merge();
} else {
const pull = await this.askForPullOrMerge();
if (pull) {
await manualSyncTask.pull();
} else {
await manualSyncTask.merge();
let action: FirstTimeSyncAction = 'manual';
let preview: [SyncResource, ISyncResourcePreview][] = [];
await this.progressService.withProgress({
location: ProgressLocation.Notification,
title,
delay: 500,
}, async progress => {
progress.report({ message: localize('turning on', "Turning on...") });
preview = await manualSyncTask.preview();
const hasRemoteData = manualSyncTask.manifest !== null;
const hasLocalData = await this.userDataSyncService.hasLocalData();
const hasChanges = preview.some(([, { resourcePreviews }]) => resourcePreviews.some(r => r.localChange !== Change.None || r.remoteChange !== Change.None));
const isLastSyncFromCurrentMachine = preview.every(([, { isLastSyncFromCurrentMachine }]) => isLastSyncFromCurrentMachine);
action = await this.getFirstTimeSyncAction(hasRemoteData, hasLocalData, hasChanges, isLastSyncFromCurrentMachine);
const progressDisposable = manualSyncTask.onSynchronizeResources(synchronizingResources =>
synchronizingResources.length ? progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(synchronizingResources[0][0])) }) : undefined);
try {
switch (action) {
case 'merge': return await manualSyncTask.merge();
case 'pull': return await manualSyncTask.pull();
case 'push': return await manualSyncTask.push();
case 'manual': return;
}
} finally {
progressDisposable.dispose();
}
});
if (action === 'manual') {
await this.syncManually(manualSyncTask, preview);
}
await this.userDataAutoSyncService.turnOn();
} catch (error) {
if (isPromiseCanceledError(error)) {
await manualSyncTask.stop();
}
await manualSyncTask.stop();
throw error;
} finally {
manualSyncTask.dispose();
progressDisposable.dispose();
}
}
private async askForPullOrMerge(): Promise<boolean> {
private async getFirstTimeSyncAction(hasRemoteData: boolean, hasLocalData: boolean, hasChanges: boolean, isLastSyncFromCurrentMachine: boolean): Promise<FirstTimeSyncAction> {
if (!hasLocalData /* no data on local */
|| !hasRemoteData /* no data on remote */
|| !hasChanges /* no changes */
|| isLastSyncFromCurrentMachine /* has changes but last sync is from current machine */
) {
return 'merge';
}
const result = await this.dialogService.show(
Severity.Info,
localize('Replace or Merge', "Replace or Merge"),
[
localize('show synced data', "Show Synced Data"),
localize('sync manually', "Sync Manually"),
localize('merge', "Merge"),
localize('replace local', "Replace Local"),
localize('cancel', "Cancel"),
@@ -293,20 +320,41 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
);
switch (result.choice) {
case 0:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'showSyncedData' });
await this.commandService.executeCommand(SHOW_SYNCED_DATA_COMMAND_ID);
throw canceled();
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'manual' });
return 'manual';
case 1:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'merge' });
return false;
return 'merge';
case 2:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'replace-local' });
return true;
case 3:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'cancelled' });
throw canceled();
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'pull' });
return 'pull';
}
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'cancelled' });
throw canceled();
}
private async syncManually(task: IManualSyncTask, preview: [SyncResource, ISyncResourcePreview][]): Promise<void> {
const visibleViewContainer = this.viewsService.getVisibleViewContainer(ViewContainerLocation.Sidebar);
this.userDataSyncPreview.setManualSyncPreview(task, preview);
this.showManualSyncViewContext.set(true);
await this.commandService.executeCommand(SHOW_SYNCED_DATA_COMMAND_ID);
await this.viewsService.openView(MANUAL_SYNC_VIEW_ID);
await Event.toPromise(Event.filter(this.userDataSyncPreview.onDidChangeChanges, e => e.length === 0));
if (this.userDataSyncPreview.conflicts.length) {
await Event.toPromise(Event.filter(this.userDataSyncPreview.onDidChangeConflicts, e => e.length === 0));
}
/* Merge to sync globalState changes */
await task.merge();
if (visibleViewContainer) {
this.viewsService.openViewContainer(visibleViewContainer.id);
} else {
const viewContainer = this.viewDescriptorService.getViewContainerByViewId(MANUAL_SYNC_VIEW_ID);
this.viewsService.closeViewContainer(viewContainer!.id);
}
return false;
}
private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean {
@@ -483,4 +531,137 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
}
class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview, IDecorationsProvider {
readonly label: string = localize('label', "UserDataSyncResources");
private readonly _onDidChange = this._register(new Emitter<URI[]>());
readonly onDidChange = this._onDidChange.event;
private _onDidChangeChanges = this._register(new Emitter<ReadonlyArray<IUserDataSyncResourceGroup>>());
readonly onDidChangeChanges = this._onDidChangeChanges.event;
private _onDidChangeConflicts = this._register(new Emitter<ReadonlyArray<IUserDataSyncResourceGroup>>());
readonly onDidChangeConflicts = this._onDidChangeConflicts.event;
private _changes: ReadonlyArray<IUserDataSyncResourceGroup> = [];
get changes() { return Object.freeze(this._changes); }
private _conflicts: ReadonlyArray<IUserDataSyncResourceGroup> = [];
get conflicts() { return Object.freeze(this._conflicts); }
private manualSync: { preview: [SyncResource, ISyncResourcePreview][], task: IManualSyncTask } | undefined;
constructor(
private readonly userDataSyncService: IUserDataSyncService
) {
super();
this.updateConflicts(userDataSyncService.conflicts);
this._register(userDataSyncService.onDidChangeConflicts(conflicts => this.updateConflicts(conflicts)));
}
setManualSyncPreview(task: IManualSyncTask, preview: [SyncResource, ISyncResourcePreview][]): void {
this.manualSync = { task, preview };
this.updateChanges();
}
async accept(syncResource: SyncResource, resource: URI, content: string): Promise<void> {
if (this.manualSync) {
const syncPreview = await this.manualSync.task.accept(resource, content);
this.updatePreview(syncPreview);
} else {
await this.userDataSyncService.acceptPreviewContent(syncResource, resource, content);
}
}
async merge(resource?: URI): Promise<void> {
if (!this.manualSync) {
throw new Error('Can merge only while syncing manually');
}
const syncPreview = await this.manualSync.task.merge(resource);
this.updatePreview(syncPreview);
}
async pull(): Promise<void> {
if (!this.manualSync) {
throw new Error('Can pull only while syncing manually');
}
await this.manualSync.task.pull();
this.updatePreview([]);
}
async push(): Promise<void> {
if (!this.manualSync) {
throw new Error('Can push only while syncing manually');
}
await this.manualSync.task.push();
this.updatePreview([]);
}
provideDecorations(resource: URI): IDecorationData | undefined {
const changeResource = this.changes.find(c => isEqual(c.remote, resource)) || this.conflicts.find(c => isEqual(c.remote, resource));
if (changeResource) {
if (changeResource.localChange === Change.Modified || changeResource.remoteChange === Change.Modified) {
return {
letter: 'M',
};
}
if (changeResource.localChange === Change.Added
|| changeResource.localChange === Change.Deleted
|| changeResource.remoteChange === Change.Added
|| changeResource.remoteChange === Change.Deleted) {
return {
letter: 'A',
};
}
}
return undefined;
}
private updatePreview(preview: [SyncResource, ISyncResourcePreview][]) {
if (this.manualSync) {
this.manualSync.preview = preview;
this.updateChanges();
}
}
private updateConflicts(conflicts: [SyncResource, IResourcePreview[]][]): void {
const newConflicts = this.toUserDataSyncResourceGroups(conflicts);
if (!equals(newConflicts, this._conflicts, (a, b) => isEqual(a.local, b.local))) {
this._conflicts = newConflicts;
this._onDidChangeConflicts.fire(this.conflicts);
}
this.updateChanges();
}
private updateChanges(): void {
const newChanges = this.toUserDataSyncResourceGroups(
(this.manualSync?.preview || [])
.filter(([syncResource]) => syncResource !== SyncResource.GlobalState) /* Filter Global State Changes */
.map(([syncResource, syncResourcePreview]) =>
([
syncResource,
/* remove merged previews and conflicts and with no changes and conflicts */
syncResourcePreview.resourcePreviews.filter(r =>
!r.merged
&& (r.localChange !== Change.None || r.remoteChange !== Change.None)
&& !this._conflicts.some(c => c.syncResource === syncResource && isEqual(c.local, r.localResource)))
]))
);
if (!equals(newChanges, this._changes, (a, b) => isEqual(a.local, b.local))) {
this._changes = newChanges;
this._onDidChangeChanges.fire(this.changes);
}
}
private toUserDataSyncResourceGroups(syncResourcePreviews: [SyncResource, IResourcePreview[]][]): IUserDataSyncResourceGroup[] {
return flatten(
syncResourcePreviews.map(([syncResource, resourcePreviews]) =>
resourcePreviews.map<IUserDataSyncResourceGroup>(({ localResource, remoteResource, previewResource, localChange, remoteChange }) =>
({ syncResource, local: localResource, remote: remoteResource, preview: previewResource, localChange, remoteChange })))
);
}
}
registerSingleton(IUserDataSyncWorkbenchService, UserDataSyncWorkbenchService);

View File

@@ -4,10 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IAuthenticationProvider, SyncStatus, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { IAuthenticationProvider, SyncStatus, SyncResource, Change } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { localize } from 'vs/nls';
import { URI } from 'vs/base/common/uri';
export interface IUserDataSyncAccount {
readonly authenticationProviderId: string;
@@ -15,6 +16,28 @@ export interface IUserDataSyncAccount {
readonly accountId: string;
}
export interface IUserDataSyncPreview {
readonly onDidChangeChanges: Event<ReadonlyArray<IUserDataSyncResourceGroup>>;
readonly changes: ReadonlyArray<IUserDataSyncResourceGroup>;
onDidChangeConflicts: Event<ReadonlyArray<IUserDataSyncResourceGroup>>;
readonly conflicts: ReadonlyArray<IUserDataSyncResourceGroup>;
accept(syncResource: SyncResource, resource: URI, content: string): Promise<void>;
merge(resource?: URI): Promise<void>;
pull(): Promise<void>;
push(): Promise<void>;
}
export interface IUserDataSyncResourceGroup {
readonly syncResource: SyncResource;
readonly local: URI;
readonly remote: URI;
readonly preview: URI;
readonly localChange: Change;
readonly remoteChange: Change;
}
export const IUserDataSyncWorkbenchService = createDecorator<IUserDataSyncWorkbenchService>('IUserDataSyncWorkbenchService');
export interface IUserDataSyncWorkbenchService {
_serviceBrand: any;
@@ -26,6 +49,8 @@ export interface IUserDataSyncWorkbenchService {
readonly accountStatus: AccountStatus;
readonly onDidChangeAccountStatus: Event<AccountStatus>;
readonly userDataSyncPreview: IUserDataSyncPreview;
turnOn(): Promise<void>;
turnoff(everyWhere: boolean): Promise<void>;
signIn(): Promise<void>;
@@ -52,9 +77,12 @@ export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncSt
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
export const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Uninitialized);
export const CONTEXT_ENABLE_VIEWS = new RawContextKey<boolean>(`showUserDataSyncViews`, false);
export const CONTEXT_SHOW_MANUAL_SYNC_VIEW = new RawContextKey<boolean>(`showManualSyncView`, false);
// Commands
export const ENABLE_SYNC_VIEWS_COMMAND_ID = 'workbench.userDataSync.actions.enableViews';
export const CONFIGURE_SYNC_COMMAND_ID = 'workbench.userDataSync.actions.configure';
export const SHOW_SYNC_LOG_COMMAND_ID = 'workbench.userDataSync.actions.showLog';
export const SHOW_SYNCED_DATA_COMMAND_ID = 'workbench.userDataSync.actions.showSyncedData';
// VIEWS
export const MANUAL_SYNC_VIEW_ID = 'workbench.views.manualSyncView';