diff --git a/src/vs/workbench/contrib/userData/common/userData.contribution.ts b/src/vs/workbench/contrib/userData/common/userData.contribution.ts index 9e5ad6a79a97e..aa60078099bca 100644 --- a/src/vs/workbench/contrib/userData/common/userData.contribution.ts +++ b/src/vs/workbench/contrib/userData/common/userData.contribution.ts @@ -4,21 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IUserDataSyncService, IRemoteUserDataService } from 'vs/workbench/services/userData/common/userData'; +import { IUserDataSyncService, SyncStatus } from 'vs/workbench/services/userData/common/userData'; import { localize } from 'vs/nls'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Action } from 'vs/base/common/actions'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Disposable, MutableDisposable } 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'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { FalseContext } from 'vs/platform/contextkey/common/contextkeys'; +import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; +import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; +import { timeout } from 'vs/base/common/async'; -const CONTEXT_SYNC_ENABLED = new RawContextKey('syncEnabled', false); +const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); Registry.as(ConfigurationExtensions.Configuration) .registerConfiguration({ @@ -29,106 +30,132 @@ Registry.as(ConfigurationExtensions.Configuration) properties: { 'userData.autoSync': { type: 'boolean', - description: localize('userData.autoSync', "When enabled, automatically gets user data. User data is fetched from the installed user data sync extension."), + description: localize('userData.autoSync', "When enabled, automatically synchronises user configuration - Settings, Keybindings, Extensions & Snippets."), default: false, scope: ConfigurationScope.APPLICATION } } }); -class AutoSyncUserDataContribution extends Disposable implements IWorkbenchContribution { +class AutoSyncUserData extends Disposable implements IWorkbenchContribution { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, ) { super(); - this.autoSync(); } - private async autoSync(): Promise { - if (this.configurationService.getValue('userData.autoSync')) { - this.userDataSyncService.synchronise(); + private loopAutoSync(): void { + this.autoSync() + .then(() => timeout(1000 * 60 * 5)) // every five minutes + .then(() => this.loopAutoSync()); + } + + private autoSync(): Promise { + if (this.userDataSyncService.status === SyncStatus.Idle && this.configurationService.getValue('userData.autoSync')) { + return this.userDataSyncService.sync(); } + return Promise.resolve(); } + } -class UserDataSyncContextUpdateContribution extends Disposable implements IWorkbenchContribution { +class SyncContribution extends Disposable implements IWorkbenchContribution { - private syncEnablementContext: IContextKey; + private readonly syncEnablementContext: IContextKey; + private readonly badgeDisposable = this._register(new MutableDisposable()); constructor( - @IRemoteUserDataService remoteUserDataService: IRemoteUserDataService, - @IContextKeyService contextKeyService: IContextKeyService + @IUserDataSyncService userDataSyncService: IUserDataSyncService, + @IContextKeyService contextKeyService: IContextKeyService, + @IActivityService private readonly activityService: IActivityService ) { super(); - this.syncEnablementContext = CONTEXT_SYNC_ENABLED.bindTo(contextKeyService); - this.syncEnablementContext.set(remoteUserDataService.isEnabled()); - this._register(remoteUserDataService.onDidChangeEnablement(enabled => this.syncEnablementContext.set(enabled))); + this.syncEnablementContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); + this.onDidChangeStatus(userDataSyncService.status); + this._register(userDataSyncService.onDidChangeStatus(status => this.onDidChangeStatus(status))); + this.registerGlobalActivityActions(); } -} + private onDidChangeStatus(status: SyncStatus) { + this.syncEnablementContext.set(status); -const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchRegistry.registerWorkbenchContribution(UserDataSyncContextUpdateContribution, LifecyclePhase.Starting); -workbenchRegistry.registerWorkbenchContribution(AutoSyncUserDataContribution, LifecyclePhase.Ready); - -class ShowUserDataSyncActions extends Action { + let badge: IBadge | undefined = undefined; + let clazz: string | undefined; - public static ID: string = 'workbench.userData.actions.showUserDataSyncActions'; - public static LABEL: string = localize('workbench.userData.actions.showUserDataSyncActions.label', "Show User Data Sync Actions"); + if (status === SyncStatus.HasConflicts) { + badge = new NumberBadge(1, () => localize('resolve conflicts', "Resolve Conflicts")); + } else if (status === SyncStatus.Syncing) { + badge = new ProgressBadge(() => localize('syncing', "Synchronising User Configuration...")); + clazz = 'progress-badge'; + } - constructor( - @IQuickInputService private readonly quickInputService: IQuickInputService, - @ICommandService private commandService: ICommandService, - @IConfigurationService private configurationService: IConfigurationService, - ) { - super(ShowUserDataSyncActions.ID, ShowUserDataSyncActions.LABEL); - } + this.badgeDisposable.clear(); - async run(): Promise { - return this.showSyncActions(); + if (badge) { + this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz); + } } - private async showSyncActions(): Promise { - const autoSync = this.configurationService.getValue('userData.autoSync'); - const picks = []; - if (autoSync) { - picks.push({ label: localize('turn off sync', "Sync: Turn Off Sync"), id: 'workbench.userData.actions.stopAutoSync' }); - } else { - picks.push({ label: localize('sync', "Sync: Start Sync"), id: 'workbench.userData.actions.startSync' }); - picks.push({ label: localize('turn on sync', "Sync: Turn On Auto Sync"), id: 'workbench.userData.actions.startAutoSync' }); - } - picks.push({ label: localize('customise', "Sync: Settings"), id: 'workbench.userData.actions.syncSettings' }); - const result = await this.quickInputService.pick(picks, { canPickMany: false }); - if (result && result.id) { - return this.commandService.executeCommand(result.id); - } + private registerGlobalActivityActions(): void { + CommandsRegistry.registerCommand('workbench.userData.actions.startSync', serviceAccessor => serviceAccessor.get(IUserDataSyncService).sync()); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: 'workbench.userData.actions.startSync', + title: localize('start sync', "Sync: Start") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.Idle), ContextKeyExpr.not('config.userData.autoSync')), + order: 1 + }); + + CommandsRegistry.registerCommand('workbench.userData.actions.turnOnAutoSync', serviceAccessor => serviceAccessor.get(IConfigurationService).updateValue('userData.autoSync', true)); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: 'workbench.userData.actions.turnOnAutoSync', + title: localize('turn on auto sync', "Turn On Auto Sync") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.not('config.userData.autoSync')), + order: 1 + }); + + CommandsRegistry.registerCommand('workbench.userData.actions.turnOffAutoSync', serviceAccessor => serviceAccessor.get(IConfigurationService).updateValue('userData.autoSync', false)); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: 'workbench.userData.actions.turnOffAutoSync', + title: localize('turn off auto sync', "Turn Off Atuo Sync") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has('config.userData.autoSync')), + order: 1 + }); + + CommandsRegistry.registerCommand('sync.resolveConflicts', serviceAccessor => serviceAccessor.get(IUserDataSyncService).resolveConflicts()); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: 'sync.resolveConflicts', + title: localize('resolveConflicts', "Sync: Resolve Conflicts"), + }, + when: CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), + }); + + CommandsRegistry.registerCommand('sync.synchronising', () => { }); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: 'sync.synchronising', + title: localize('Synchronising', "Synchronising..."), + precondition: FalseContext + }, + when: CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.Syncing) + }); } } -CommandsRegistry.registerCommand(ShowUserDataSyncActions.ID, (serviceAccessor) => { - const instantiationService = serviceAccessor.get(IInstantiationService); - return instantiationService.createInstance(ShowUserDataSyncActions).run(); -}); - -CommandsRegistry.registerCommand('workbench.userData.actions.stopAutoSync', (serviceAccessor) => { - const configurationService = serviceAccessor.get(IConfigurationService); - return configurationService.updateValue('userData.autoSync', false); -}); - -CommandsRegistry.registerCommand('workbench.userData.actions.startAutoSync', (serviceAccessor) => { - const configurationService = serviceAccessor.get(IConfigurationService); - return configurationService.updateValue('userData.autoSync', true); -}); - -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '5_sync', - command: { - id: ShowUserDataSyncActions.ID, - title: localize('synchronise user data', "Sync...") - }, - when: CONTEXT_SYNC_ENABLED, - order: 1 -}); +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(SyncContribution, LifecyclePhase.Starting); +workbenchRegistry.registerWorkbenchContribution(AutoSyncUserData, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/services/userData/common/settingsSync.ts b/src/vs/workbench/services/userData/common/settingsSync.ts index 5a9cb0556d43b..680f3ed674bf6 100644 --- a/src/vs/workbench/services/userData/common/settingsSync.ts +++ b/src/vs/workbench/services/userData/common/settingsSync.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IRemoteUserDataService, IUserData, RemoteUserDataError, RemoteUserDataErrorCode } from 'vs/workbench/services/userData/common/userData'; +import { IRemoteUserDataService, IUserData, RemoteUserDataError, RemoteUserDataErrorCode, ISynchroniser, SyncStatus } from 'vs/workbench/services/userData/common/userData'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse, JSONPath } from 'vs/base/common/json'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; @@ -25,15 +25,7 @@ import { setProperty } from 'vs/base/common/jsonEdit'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export const ISettingsSyncService = createDecorator('ISettingsSyncService'); - -export interface ISettingsSyncService { - _serviceBrand: undefined; - sync(): Promise; -} +import { Emitter, Event } from 'vs/base/common/event'; interface ISyncPreviewResult { readonly fileContent: IFileContent | null; @@ -41,9 +33,10 @@ interface ISyncPreviewResult { readonly localSettingsPreviewModel: ITextModel; readonly remoteSettingsPreviewModel: ITextModel; readonly hasChanges: boolean; + readonly hasConflicts: boolean; } -export class SettingsSyncService extends Disposable implements ISettingsSyncService, ITextModelContentProvider { +export class SettingsSyncService extends Disposable implements ISynchroniser, ITextModelContentProvider { _serviceBrand: undefined; private static LAST_SYNC_SETTINGS_STORAGE_KEY: string = 'LAST_SYNC_SETTINGS_CONTENTS'; @@ -54,6 +47,11 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ private syncPreviewResultPromise: Promise | null = null; + private _status: SyncStatus = SyncStatus.Idle; + get status(): SyncStatus { return this._status; } + private _onDidChangStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + constructor( @IFileService private readonly fileService: IFileService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @@ -82,36 +80,63 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ return null; } - async sync(): Promise { + private setStatus(status: SyncStatus): void { + if (this._status !== status) { + this._status = status; + this._onDidChangStatus.fire(status); + } + } + + async sync(): Promise { + + if (this.status !== SyncStatus.Idle) { + return false; + } + + this.setStatus(SyncStatus.Syncing); const result = await this.getPreview(); - if (result.localSettingsPreviewModel.getValue() === result.remoteSettingsPreviewModel.getValue()) { - // Ask to show preview? - await this.applySyncPreview(result); - this.syncPreviewResultPromise = null; - return; + if (result.hasConflicts) { + this.setStatus(SyncStatus.HasConflicts); + return false; } - await this.editorService.openEditor({ - leftResource: this.remoteSettingsPreviewResource, - rightResource: this.localSettingsPreviewResource, - label: localize('fileReplaceChanges', "Remote Settings ↔ Local Settings (Settings Preview)"), - options: { - preserveFocus: false, - pinned: true, - revealIfVisible: true - } - }); + await this.apply(); + return true; + } + resolveConflicts(): void { + if (this.status === SyncStatus.HasConflicts) { + this.editorService.openEditor({ + leftResource: this.remoteSettingsPreviewResource, + rightResource: this.localSettingsPreviewResource, + label: localize('fileReplaceChanges', "Remote Settings ↔ Local Settings (Settings Preview)"), + options: { + preserveFocus: false, + pinned: true, + revealIfVisible: true + } + }); + } } - private async applySyncPreview({ fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel }: ISyncPreviewResult): Promise { - const syncedRemoteUserData = remoteUserData && remoteUserData.content === remoteSettingsPreviewModel.getValue() ? remoteUserData : { content: remoteSettingsPreviewModel.getValue(), version: remoteUserData ? remoteUserData.version + 1 : 1 }; - if (!(remoteUserData && remoteUserData.version === syncedRemoteUserData.version)) { - await this.writeToRemote(syncedRemoteUserData); + async apply(): Promise { + if (this.syncPreviewResultPromise) { + const { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges } = await this.syncPreviewResultPromise; + if (hasChanges) { + const syncedRemoteUserData = remoteUserData && remoteUserData.content === remoteSettingsPreviewModel.getValue() ? remoteUserData : { content: remoteSettingsPreviewModel.getValue(), version: remoteUserData ? remoteUserData.version + 1 : 1 }; + if (!(remoteUserData && remoteUserData.version === syncedRemoteUserData.version)) { + await this.writeToRemote(syncedRemoteUserData); + } + await this.writeToLocal(localSettingsPreviewModel.getValue(), fileContent, syncedRemoteUserData); + } + if (remoteUserData) { + this.updateLastSyncValue(remoteUserData); + } } - await this.writeToLocal(localSettingsPreviewModel.getValue(), fileContent, syncedRemoteUserData); + this.syncPreviewResultPromise = null; + this.setStatus(SyncStatus.Idle); } private getPreview(): Promise { @@ -122,18 +147,18 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ } private async generatePreview(): Promise { - const fileContent = await this.getLocalFileContent(); const remoteUserData = await this.remoteUserDataService.read(SettingsSyncService.EXTERNAL_USER_DATA_SETTINGS_KEY); + const fileContent = await this.getLocalFileContent(); - const remoteSettingsPreviewModel = this.modelService.getModel(this.remoteSettingsPreviewResource) || this.modelService.createModel('', this.modeService.create('jsonc'), this.remoteSettingsPreviewResource); const localSettingsPreviewModel = this.modelService.getModel(this.localSettingsPreviewResource) || this.modelService.createModel('', this.modeService.create('jsonc'), this.localSettingsPreviewResource); + const remoteSettingsPreviewModel = this.modelService.getModel(this.remoteSettingsPreviewResource) || this.modelService.createModel('', this.modeService.create('jsonc'), this.remoteSettingsPreviewResource); if (fileContent && !remoteUserData) { // Remote does not exist, so sync with local. const localContent = fileContent.value.toString(); localSettingsPreviewModel.setValue(localContent); remoteSettingsPreviewModel.setValue(localContent); - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true }; + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true, hasConflicts: false }; } if (remoteUserData && !fileContent) { @@ -141,7 +166,7 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ const remoteContent = remoteUserData.content; localSettingsPreviewModel.setValue(remoteContent); remoteSettingsPreviewModel.setValue(remoteContent); - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true }; + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true, hasConflicts: false }; } if (fileContent && remoteUserData) { @@ -154,15 +179,15 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ if (localContent === remoteUserData.content) { localSettingsPreviewModel.setValue(localContent); remoteSettingsPreviewModel.setValue(remoteContent); - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false }; + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false, hasConflicts: false }; } // Not synced till now if (!lastSyncData) { localSettingsPreviewModel.setValue(localContent); remoteSettingsPreviewModel.setValue(remoteContent); - await this.mergeContents(localSettingsPreviewModel, remoteSettingsPreviewModel, null); - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true }; + const hasConflicts = await this.mergeContents(localSettingsPreviewModel, remoteSettingsPreviewModel, null); + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true, hasConflicts }; } // Remote data is newer than last synced data @@ -172,14 +197,14 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ if (lastSyncData.content === localContent) { localSettingsPreviewModel.setValue(remoteContent); remoteSettingsPreviewModel.setValue(remoteContent); - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true }; + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true, hasConflicts: false }; } // Local content is diverged from last synced. Required merge and sync. localSettingsPreviewModel.setValue(localContent); remoteSettingsPreviewModel.setValue(remoteContent); - await this.mergeContents(localSettingsPreviewModel, remoteSettingsPreviewModel, null); - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true }; + const hasConflicts = await this.mergeContents(localSettingsPreviewModel, remoteSettingsPreviewModel, lastSyncData.content); + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true, hasConflicts }; } // Remote data is same as last synced data @@ -187,18 +212,18 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ // Local contents are same as last synced data. No op. if (lastSyncData.content === localContent) { - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false }; + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false, hasConflicts: false }; } // New local contents. Sync with Local. localSettingsPreviewModel.setValue(localContent); remoteSettingsPreviewModel.setValue(localContent); - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true }; + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: true, hasConflicts: false }; } } - return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false }; + return { fileContent, remoteUserData, localSettingsPreviewModel, remoteSettingsPreviewModel, hasChanges: false, hasConflicts: false }; } @@ -221,7 +246,7 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ } } - private async mergeContents(localSettingsPreviewModel: ITextModel, remoteSettingsPreviewModel: ITextModel, lastSyncedContent: string | null): Promise { + private async mergeContents(localSettingsPreviewModel: ITextModel, remoteSettingsPreviewModel: ITextModel, lastSyncedContent: string | null): Promise { const local = parse(localSettingsPreviewModel.getValue()); const remote = parse(remoteSettingsPreviewModel.getValue()); const base = lastSyncedContent ? parse(lastSyncedContent) : null; @@ -315,6 +340,8 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ } } } + + return conflicts.size > 0; } private compare(from: { [key: string]: any }, to: { [key: string]: any }): { added: Set, removed: Set, updated: Set } { @@ -325,6 +352,9 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ const updated: Set = new Set(); for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } const value1 = from[key]; const value2 = to[key]; if (!objects.equals(value1, value2)) { @@ -354,7 +384,8 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ } private getEdit(model: ITextModel, jsonPath: JSONPath, value: any): Edit | undefined { - const { tabSize, insertSpaces } = model.getOptions(); + const insertSpaces = false; + const tabSize = 4; const eol = model.getEOL(); // Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify @@ -376,7 +407,7 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ } catch (e) { if (e instanceof RemoteUserDataError && e.code === RemoteUserDataErrorCode.VersionExists) { // Rejected as there is a new version. Sync again - return this.sync(); + await this.sync(); } // An unknown error throw e; @@ -398,15 +429,16 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ await this.fileService.createFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false }); } catch (error) { if (error instanceof FileSystemProviderError && error.code === FileSystemProviderErrorCode.FileExists) { - return this.sync(); + await this.sync(); } throw error; } } + } + + private updateLastSyncValue(remoteUserData: IUserData): void { this.storageService.store(SettingsSyncService.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL); } } - -registerSingleton(ISettingsSyncService, SettingsSyncService); diff --git a/src/vs/workbench/services/userData/common/userData.ts b/src/vs/workbench/services/userData/common/userData.ts index 62babe33d350e..29175722d3be9 100644 --- a/src/vs/workbench/services/userData/common/userData.ts +++ b/src/vs/workbench/services/userData/common/userData.ts @@ -87,20 +87,26 @@ export interface IRemoteUserDataService { } export enum SyncStatus { - Syncing = 1, - SyncDone + Uninitialized = 'uninitialized', + Idle = 'idle', + Syncing = 'syncing', + HasConflicts = 'hasConflicts', +} + +export interface ISynchroniser { + readonly status: SyncStatus; + readonly onDidChangeStatus: Event; + sync(): Promise; + resolveConflicts(): void; + apply(): void; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); export interface IUserDataSyncService { - _serviceBrand: any; - - readonly syncStatus: SyncStatus; - - readonly onDidChangeSyncStatus: Event; - - synchronise(): Promise; - + readonly status: SyncStatus; + readonly onDidChangeStatus: Event; + sync(): Promise; + resolveConflicts(): void; } diff --git a/src/vs/workbench/services/userData/common/userDataSyncService.ts b/src/vs/workbench/services/userData/common/userDataSyncService.ts index f2fe2ecfcb9a1..9a19cac1d241c 100644 --- a/src/vs/workbench/services/userData/common/userDataSyncService.ts +++ b/src/vs/workbench/services/userData/common/userDataSyncService.ts @@ -3,47 +3,77 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IRemoteUserDataService } from 'vs/workbench/services/userData/common/userData'; +import { IUserDataSyncService, SyncStatus, IRemoteUserDataService, ISynchroniser } from 'vs/workbench/services/userData/common/userData'; import { Disposable } from 'vs/base/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { SettingsSyncService as SettingsSynchroniser } from 'vs/workbench/services/userData/common/settingsSync'; import { Emitter, Event } from 'vs/base/common/event'; -import { ISettingsSyncService } from 'vs/workbench/services/userData/common/settingsSync'; export class UserDataSyncService extends Disposable implements IUserDataSyncService { _serviceBrand: any; - private _onDidChangeSyncStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeSyncStatus: Event = this._onDidChangeSyncStatus.event; + private readonly synchronisers: ISynchroniser[]; - private _syncStatus: SyncStatus = SyncStatus.SyncDone; - get syncStatus(): SyncStatus { - return this._syncStatus; - } - set syncStatus(status: SyncStatus) { - if (this._syncStatus !== status) { - this._syncStatus = status; - this._onDidChangeSyncStatus.fire(status); - } - } + private _status: SyncStatus = SyncStatus.Uninitialized; + get status(): SyncStatus { return this._status; } + private _onDidChangStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangStatus.event; constructor( @IRemoteUserDataService private readonly remoteUserDataService: IRemoteUserDataService, - @ISettingsSyncService private readonly settingsSyncService: ISettingsSyncService + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); + this.synchronisers = [ + this.instantiationService.createInstance(SettingsSynchroniser) + ]; + this.updateStatus(); + this._register(Event.any(this.remoteUserDataService.onDidChangeEnablement, ...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); } - - async synchronise(): Promise { + async sync(): Promise { if (!this.remoteUserDataService.isEnabled()) { throw new Error('Not enabled'); } - this.syncStatus = SyncStatus.Syncing; - await this.settingsSyncService.sync(); - this.syncStatus = SyncStatus.SyncDone; + for (const synchroniser of this.synchronisers) { + if (!await synchroniser.sync()) { + return; + } + } + } + + resolveConflicts(): void { + const synchroniserWithConflicts = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0]; + if (synchroniserWithConflicts) { + synchroniserWithConflicts.resolveConflicts(); + } + } + + private updateStatus(): void { + this.setStatus(this.computeStatus()); + } + + private setStatus(status: SyncStatus): void { + if (this._status !== status) { + this._status = status; + this._onDidChangStatus.fire(status); + } } + private computeStatus(): SyncStatus { + if (!this.remoteUserDataService.isEnabled()) { + return SyncStatus.Uninitialized; + } + if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) { + return SyncStatus.HasConflicts; + } + if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) { + return SyncStatus.Syncing; + } + return SyncStatus.Idle; + } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 97b56ca08ca4d..c0601847c29a5 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -77,7 +77,6 @@ import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/extensions/common/staticExtensions'; import 'vs/workbench/services/userData/common/remoteUserDataService'; import 'vs/workbench/services/userData/common/userDataSyncService'; -import 'vs/workbench/services/userData/common/settingsSync'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';