diff --git a/src/vs/workbench/contrib/userData/browser/userData.contribution.ts b/src/vs/workbench/contrib/userData/browser/userData.contribution.ts index b192380585254bfc7a7272314f3b851af5d35de3..83ce92d1f86d8a499bd928928d733383cd7f9e24 100644 --- a/src/vs/workbench/contrib/userData/browser/userData.contribution.ts +++ b/src/vs/workbench/contrib/userData/browser/userData.contribution.ts @@ -6,7 +6,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IUserDataSyncService, SyncStatus } from 'vs/workbench/services/userData/common/userData'; import { localize } from 'vs/nls'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -20,6 +20,7 @@ import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { timeout } from 'vs/base/common/async'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { AcceptChangesController } from 'vs/workbench/contrib/userData/browser/userDataPreviewEditorContribution'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); @@ -74,11 +75,13 @@ class SyncContribution extends Disposable implements IWorkbenchContribution { private readonly syncEnablementContext: IContextKey; private readonly badgeDisposable = this._register(new MutableDisposable()); + private readonly conflictsWarningDisposable = this._register(new MutableDisposable()); constructor( - @IUserDataSyncService userDataSyncService: IUserDataSyncService, + @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IContextKeyService contextKeyService: IContextKeyService, - @IActivityService private readonly activityService: IActivityService + @IActivityService private readonly activityService: IActivityService, + @INotificationService private readonly notificationService: INotificationService ) { super(); this.syncEnablementContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); @@ -105,6 +108,24 @@ class SyncContribution extends Disposable implements IWorkbenchContribution { if (badge) { this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz); } + + if (status !== SyncStatus.HasConflicts) { + this.conflictsWarningDisposable.clear(); + } + } + + private async sync(): Promise { + await this.userDataSyncService.sync(); + if (this.userDataSyncService.status === SyncStatus.HasConflicts) { + const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts. Please resolve them to continue."), + [ + { + label: localize('resolve', "Resolve Conflicts"), + run: () => this.userDataSyncService.handleConflicts() + } + ]); + this.conflictsWarningDisposable.value = toDisposable(() => handle.close()); + } } private registerActions(): void { @@ -154,7 +175,7 @@ class SyncContribution extends Disposable implements IWorkbenchContribution { // Command Pallette Actions - CommandsRegistry.registerCommand('workbench.userData.actions.startSync', serviceAccessor => serviceAccessor.get(IUserDataSyncService).sync()); + CommandsRegistry.registerCommand('workbench.userData.actions.startSync', () => this.sync()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: 'workbench.userData.actions.startSync', @@ -163,6 +184,15 @@ class SyncContribution extends Disposable implements IWorkbenchContribution { when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.Idle), ContextKeyExpr.not('config.userConfiguration.autoSync')), }); + CommandsRegistry.registerCommand('workbench.userData.actions.stopSync', serviceAccessor => serviceAccessor.get(IUserDataSyncService).stopSync()); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.userData.actions.stopSync', + title: localize('stop sync', "Sync: Stop") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ContextKeyExpr.not('config.userConfiguration.autoSync')), + }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: 'sync.resolveConflicts', diff --git a/src/vs/workbench/services/userData/common/settingsSync.ts b/src/vs/workbench/services/userData/common/settingsSync.ts index 2b977404c007e099c8e94bf9bda5b684e46f4d4f..79603210164dd30e6fe7dbf6c496d7ed004b9d2d 100644 --- a/src/vs/workbench/services/userData/common/settingsSync.ts +++ b/src/vs/workbench/services/userData/common/settingsSync.ts @@ -26,6 +26,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { Position } from 'vs/editor/common/core/position'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { isEqual } from 'vs/base/common/resources'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; interface ISyncPreviewResult { readonly fileContent: IFileContent | null; @@ -40,7 +41,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private static LAST_SYNC_SETTINGS_STORAGE_KEY: string = 'LAST_SYNC_SETTINGS_CONTENTS'; private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings'; - private syncPreviewResultPromise: Promise | null = null; + private syncPreviewResultPromise: CancelablePromise | null = null; private _status: SyncStatus = SyncStatus.Idle; get status(): SyncStatus { return this._status; } @@ -101,6 +102,15 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } } + async stopSync(): Promise { + await this.fileService.del(SETTINGS_PREVIEW_RESOURCE); + if (this.syncPreviewResultPromise) { + this.syncPreviewResultPromise.cancel(); + this.syncPreviewResultPromise = null; + this.setStatus(SyncStatus.Idle); + } + } + handleConflicts(): boolean { if (this.status !== SyncStatus.HasConflicts) { return false; @@ -152,7 +162,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private getPreview(): Promise { if (!this.syncPreviewResultPromise) { - this.syncPreviewResultPromise = this.generatePreview(); + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview()); } return this.syncPreviewResultPromise; } diff --git a/src/vs/workbench/services/userData/common/userData.ts b/src/vs/workbench/services/userData/common/userData.ts index 85ca17f02e77afd79b679fb05c69adc8a6fcad1e..470d2b4386210e6c93bfddf53cb5816911807ea2 100644 --- a/src/vs/workbench/services/userData/common/userData.ts +++ b/src/vs/workbench/services/userData/common/userData.ts @@ -101,6 +101,7 @@ export interface ISynchroniser { readonly status: SyncStatus; readonly onDidChangeStatus: Event; sync(): Promise; + stopSync(): Promise; handleConflicts(): boolean; apply(previewResource: URI): Promise; } diff --git a/src/vs/workbench/services/userData/common/userDataSyncService.ts b/src/vs/workbench/services/userData/common/userDataSyncService.ts index d255a68111f28db03fecf364cb7f743ec6a5827d..8fb5a98fa3ef490294834db9741a511701667f83 100644 --- a/src/vs/workbench/services/userData/common/userDataSyncService.ts +++ b/src/vs/workbench/services/userData/common/userDataSyncService.ts @@ -50,6 +50,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return true; } + async stopSync(): Promise { + if (!this.remoteUserDataService.isEnabled()) { + throw new Error('Not enabled'); + } + for (const synchroniser of this.synchronisers) { + await synchroniser.stopSync(); + } + } + async apply(previewResource: URI): Promise { if (!this.remoteUserDataService.isEnabled()) { throw new Error('Not enabled');