diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index adf5e433633..ee7fab8ba62 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -50,10 +50,10 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -63,6 +63,7 @@ import { AuthTokenChannel } from 'vs/platform/auth/common/authTokenIpc'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; import { UserDataAutoSync } from 'vs/platform/userDataSync/electron-browser/userDataAutoSync'; +import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -186,6 +187,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', activeWindowRouter))); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); + services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); registerConfiguration(); @@ -209,6 +211,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const authTokenChannel = new AuthTokenChannel(authTokenService); server.registerChannel('authToken', authTokenChannel); + const settingsSyncService = accessor.get(ISettingsSyncService); + const settingsSyncChannel = new SettingsSyncChannel(settingsSyncService); + server.registerChannel('settingsSync', settingsSyncChannel); + const userDataSyncService = accessor.get(IUserDataSyncService); const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); server.registerChannel('userDataSync', userDataSyncChannel); diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index d7224e9f3a0..1b710e133bd 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -10,6 +10,7 @@ import { values } from 'vs/base/common/map'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import * as contentUtil from 'vs/platform/userDataSync/common/content'; +import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; export function computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string { if (ignoredSettings.length) { @@ -24,7 +25,7 @@ export function computeRemoteContent(localContent: string, remoteContent: string return localContent; } -export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, hasConflicts: boolean } { +export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, conflicts: IConflictSetting[] } { const local = parse(localContent); const remote = parse(remoteContent); const base = baseContent ? parse(baseContent) : null; @@ -33,30 +34,41 @@ export function merge(localContent: string, remoteContent: string, baseContent: const localToRemote = compare(local, remote, ignored); if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { // No changes found between local and remote. - return { mergeContent: localContent, hasChanges: false, hasConflicts: false }; + return { mergeContent: localContent, hasChanges: false, conflicts: [] }; } - const conflicts: Set = new Set(); + const conflicts: Map = new Map(); + const handledConflicts: Set = new Set(); const baseToLocal = base ? compare(base, local, ignored) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; const baseToRemote = base ? compare(base, remote, ignored) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; let mergeContent = localContent; + const handleConflict = (conflictKey: string): void => { + handledConflicts.add(conflictKey); + const resolvedConflict = resolvedConflicts.filter(({ key }) => key === conflictKey)[0]; + if (resolvedConflict) { + mergeContent = contentUtil.edit(mergeContent, [conflictKey], resolvedConflict.value, formattingOptions); + } else { + conflicts.set(conflictKey, { key: conflictKey, localValue: local[conflictKey], remoteValue: remote[conflictKey] }); + } + }; + // Removed settings in Local for (const key of values(baseToLocal.removed)) { // Got updated in remote if (baseToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } // Removed settings in Remote for (const key of values(baseToRemote.removed)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in local if (baseToLocal.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } else { mergeContent = contentUtil.edit(mergeContent, [key], undefined, formattingOptions); } @@ -64,28 +76,28 @@ export function merge(localContent: string, remoteContent: string, baseContent: // Added settings in Local for (const key of values(baseToLocal.added)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got added in remote if (baseToRemote.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } } // Added settings in remote for (const key of values(baseToRemote.added)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got added in local if (baseToLocal.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else { mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions); @@ -94,28 +106,28 @@ export function merge(localContent: string, remoteContent: string, baseContent: // Updated settings in Local for (const key of values(baseToLocal.updated)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in remote if (baseToRemote.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } } // Updated settings in Remote for (const key of values(baseToRemote.updated)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in local if (baseToLocal.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else { mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions); @@ -126,7 +138,7 @@ export function merge(localContent: string, remoteContent: string, baseContent: const conflictNodes: { key: string, node: Node | undefined }[] = []; const tree = parseTree(mergeContent); const eol = formattingOptions.eol!; - for (const key of values(conflicts)) { + for (const { key } of values(conflicts)) { const node = findNodeAtLocation(tree, [key]); conflictNodes.push({ key, node }); } @@ -166,7 +178,7 @@ export function merge(localContent: string, remoteContent: string, baseContent: } } - return { mergeContent, hasChanges: true, hasConflicts: conflicts.size > 0 }; + return { mergeContent, hasChanges: true, conflicts: values(conflicts) }; } function compare(from: IStringDictionary, to: IStringDictionary, ignored: Set): { added: Set, removed: Set, updated: Set } { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index d65b456a539..835e4d242f5 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files'; -import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse, ParseError } from 'vs/base/common/json'; import { localize } from 'vs/nls'; @@ -25,10 +25,12 @@ interface ISyncPreviewResult { readonly remoteUserData: IUserData; readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; - readonly hasConflicts: boolean; + readonly conflicts: IConflictSetting[]; } -export class SettingsSynchroniser extends Disposable implements ISynchroniser { +export class SettingsSynchroniser extends Disposable implements ISettingsSyncService { + + _serviceBrand: any; private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings'; @@ -81,12 +83,44 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { return false; } + return this.doSync([]); + } + + stop(): void { + if (this.syncPreviewResultPromise) { + this.syncPreviewResultPromise.cancel(); + this.syncPreviewResultPromise = null; + this.logService.info('Settings: Stopped synchronizing settings.'); + } + this.fileService.del(this.environmentService.settingsSyncPreviewResource); + this.setStatus(SyncStatus.Idle); + } + + async getConflicts(): Promise { + if (!this.syncPreviewResultPromise) { + return []; + } + if (this.status !== SyncStatus.HasConflicts) { + return []; + } + const preview = await this.syncPreviewResultPromise; + return preview.conflicts; + } + + async resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise { + if (this.status === SyncStatus.HasConflicts) { + this.stop(); + await this.doSync(resolvedConflicts); + } + } + + private async doSync(resolvedConflicts: { key: string, value: any | undefined }[]): Promise { this.logService.trace('Settings: Started synchronizing settings...'); this.setStatus(SyncStatus.Syncing); try { - const result = await this.getPreview(); - if (result.hasConflicts) { + const result = await this.getPreview(resolvedConflicts); + if (result.conflicts.length) { this.logService.info('Settings: Detected conflicts while synchronizing settings.'); this.setStatus(SyncStatus.HasConflicts); return false; @@ -110,16 +144,6 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } } - stop(): void { - if (this.syncPreviewResultPromise) { - this.syncPreviewResultPromise.cancel(); - this.syncPreviewResultPromise = null; - this.logService.info('Settings: Stopped synchronizing settings.'); - } - this.fileService.del(this.environmentService.settingsSyncPreviewResource); - this.setStatus(SyncStatus.Idle); - } - private async continueSync(): Promise { if (this.status === SyncStatus.HasConflicts) { await this.apply(); @@ -178,14 +202,14 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { return parseErrors.length > 0; } - private getPreview(): Promise { + private getPreview(resolvedConflicts: { key: string, value: any }[]): Promise { if (!this.syncPreviewResultPromise) { - this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(token)); + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(resolvedConflicts, token)); } return this.syncPreviewResultPromise; } - private async generatePreview(token: CancellationToken): Promise { + private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise { const lastSyncData = await this.getLastSyncUserData(); const remoteUserData = await this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData); const remoteContent: string | null = remoteUserData.content; @@ -193,14 +217,14 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { const fileContent = await this.getLocalFileContent(); let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; - let hasConflicts: boolean = false; + let conflicts: IConflictSetting[] = []; let previewContent = null; if (remoteContent) { const localContent: string = fileContent ? fileContent.value.toString() : '{}'; if (this.hasErrors(localContent)) { this.logService.error('Settings: Unable to sync settings as there are errors/warning in settings file.'); - return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, conflicts }; } if (!lastSyncData // First time sync @@ -209,12 +233,12 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { ) { this.logService.trace('Settings: Merging remote settings with local settings...'); const formatUtils = await this.getFormattingOptions(); - const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings(), formatUtils); + const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings(), resolvedConflicts, formatUtils); // Sync only if there are changes if (result.hasChanges) { hasLocalChanged = result.mergeContent !== localContent; hasRemoteChanged = result.mergeContent !== remoteContent; - hasConflicts = result.hasConflicts; + conflicts = result.conflicts; previewContent = result.mergeContent; } } @@ -231,7 +255,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent)); } - return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, conflicts }; } private _formattingOptions: Promise | undefined = undefined; diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index e273fdde569..40ae0d325cb 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -135,12 +135,9 @@ export function getUserDataSyncStore(configurationService: IConfigurationService } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); - export interface IUserDataSyncStoreService { _serviceBrand: undefined; - readonly userDataSyncStore: IUserDataSyncStore | undefined; - read(key: string, oldValue: IUserData | null): Promise; write(key: string, content: string, ref: string | null): Promise; } @@ -170,40 +167,41 @@ export const enum SyncStatus { } export interface ISynchroniser { - readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly onDidChangeLocal: Event; - sync(_continue?: boolean): Promise; stop(): void; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); - export interface IUserDataSyncService extends ISynchroniser { _serviceBrand: any; readonly conflictsSource: SyncSource | null; - removeExtension(identifier: IExtensionIdentifier): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); - export interface IUserDataSyncUtilService { - _serviceBrand: undefined; - resolveUserBindings(userbindings: string[]): Promise>; - resolveFormattingOptions(resource: URI): Promise; - } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); +export interface IUserDataSyncLogService extends ILogService { } -export interface IUserDataSyncLogService extends ILogService { +export interface IConflictSetting { + key: string; + localValue: any | undefined; + remoteValue: any | undefined; +} +export const ISettingsSyncService = createDecorator('ISettingsSyncService'); +export interface ISettingsSyncService extends ISynchroniser { + _serviceBrand: any; + getConflicts(): Promise; + resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise; } export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index b910d3f3b85..3841e850cb4 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -34,6 +34,30 @@ export class UserDataSyncChannel implements IServerChannel { } } +export class SettingsSyncChannel implements IServerChannel { + + constructor(private readonly service: ISettingsSyncService) { } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChangeStatus': return this.service.onDidChangeStatus; + case 'onDidChangeLocal': return this.service.onDidChangeLocal; + } + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'sync': return this.service.sync(args[0]); + case '_getInitialStatus': return Promise.resolve(this.service.status); + case 'stop': this.service.stop(); return Promise.resolve(); + case 'getConflicts': return this.service.getConflicts(); + case 'resolveConflicts': return this.service.resolveConflicts(args[0]); + } + throw new Error('Invalid call'); + } +} + export class UserDataSycnUtilServiceChannel implements IServerChannel { constructor(private readonly service: IUserDataSyncUtilService) { } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 7619d616ddb..1bf9f49040d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; @@ -30,7 +30,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _conflictsSource: SyncSource | null = null; get conflictsSource(): SyncSource | null { return this._conflictsSource; } - private readonly settingsSynchroniser: SettingsSynchroniser; private readonly keybindingsSynchroniser: KeybindingsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser; private readonly globalStateSynchroniser: GlobalStateSynchroniser; @@ -39,9 +38,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService, ) { super(); - this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser)); this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index 911e11c97b7..343c99b38d6 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { merge, computeRemoteContent } from 'vs/platform/userDataSync/common/settingsMerge'; +import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; const formattingOptions = { eol: '\n', insertSpaces: false, tabSize: 4 }; @@ -13,9 +14,9 @@ suite('SettingsMerge - No Conflicts', () => { test('merge when local and remote are same with one entry', async () => { const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({ 'a': 1 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -28,9 +29,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -43,9 +44,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -62,9 +63,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -76,9 +77,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -96,9 +97,9 @@ suite('SettingsMerge - No Conflicts', () => { 'b': 2, 'c': 3, }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, expected); }); @@ -116,9 +117,9 @@ suite('SettingsMerge - No Conflicts', () => { 'b': 2, 'c': 3, }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, expected); }); @@ -130,9 +131,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -141,9 +142,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, }); const remoteContent = stringify({}); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.deepEqual(JSON.parse(actual.mergeContent), {}); }); @@ -154,9 +155,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -170,9 +171,9 @@ suite('SettingsMerge - No Conflicts', () => { 'c': 3, 'd': 4, }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -186,9 +187,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -202,9 +203,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -219,9 +220,9 @@ suite('SettingsMerge - No Conflicts', () => { 'c': 3, 'd': 4, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -234,9 +235,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 2, 'c': 2, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -250,9 +251,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -267,9 +268,10 @@ suite('SettingsMerge - Conflicts', () => { const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [{ key: 'a', localValue: 1, remoteValue: 2 }]; + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -290,9 +292,10 @@ suite('SettingsMerge - Conflicts', () => { const remoteContent = stringify({ 'b': 2 }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [{ key: 'a', localValue: 2, remoteValue: undefined }]; + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -311,9 +314,10 @@ suite('SettingsMerge - Conflicts', () => { const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [{ key: 'a', localValue: undefined, remoteValue: 2 }]; + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -343,9 +347,15 @@ suite('SettingsMerge - Conflicts', () => { 'd': 6, 'e': 5, }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [ + { key: 'b', localValue: undefined, remoteValue: 3 }, + { key: 'a', localValue: 2, remoteValue: undefined }, + { key: 'e', localValue: 4, remoteValue: 5 }, + { key: 'd', localValue: 5, remoteValue: 6 }, + ]; + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -371,6 +381,46 @@ suite('SettingsMerge - Conflicts', () => { }`); }); + test('resolve when local and remote has moved forwareded with conflicts', async () => { + const baseContent = stringify({ + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }); + const localContent = stringify({ + 'a': 2, + 'c': 3, + 'd': 5, + 'e': 4, + 'f': 1, + }); + const remoteContent = stringify({ + 'b': 3, + 'c': 3, + 'd': 6, + 'e': 5, + }); + const expectedConflicts: IConflictSetting[] = [ + { key: 'd', localValue: 5, remoteValue: 6 }, + ]; + const actual = merge(localContent, remoteContent, baseContent, [], [{ key: 'a', value: 2 }, { key: 'b', value: undefined }, { key: 'e', value: 5 }], formattingOptions); + assert.ok(actual.hasChanges); + assert.deepEqual(actual.conflicts, expectedConflicts); + assert.equal(actual.mergeContent, + `{ + "a": 2, + "c": 3, +<<<<<<< local + "d": 5, +======= + "d": 6, +>>>>>>> remote + "e": 5, + "f": 1 +}`); + }); + }); suite('SettingsMerge - Ignored Settings', () => { @@ -378,9 +428,9 @@ suite('SettingsMerge - Ignored Settings', () => { test('ignored setting is not merged when changed in local and remote', async () => { const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, null, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -388,45 +438,45 @@ suite('SettingsMerge - Ignored Settings', () => { const baseContent = stringify({ 'a': 0 }); const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, baseContent, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, baseContent, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when added in remote', async () => { const localContent = stringify({}); const remoteContent = stringify({ 'a': 1 }); - const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, null, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when added in remote from base', async () => { const localContent = stringify({ 'b': 2 }); const remoteContent = stringify({ 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, localContent, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when removed in remote', async () => { const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({}); - const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, null, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when removed in remote from base', async () => { const localContent = stringify({ 'a': 2 }); const remoteContent = stringify({}); - const actual = merge(localContent, remoteContent, localContent, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -453,9 +503,9 @@ suite('SettingsMerge - Ignored Settings', () => { 'a': 1, 'b': 3, }); - const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], formattingOptions); + const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, expectedContent); }); @@ -478,12 +528,14 @@ suite('SettingsMerge - Ignored Settings', () => { 'b': 3, 'e': 6, }); - const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], formattingOptions); - //'{\n\t"a": 1,\n\n<<<<<<< local\t"b": 4,\n=======\n\t"b": 3,\n>>>>>>> remote' - //'{\n\t"a": 1,\n<<<<<<< local\n\t"b": 4,\n=======\n\t"b": 3,\n>>>>>>> remote\n<<<<<<< local\n\t"d": 5\n=======\n>>>>>>> remote\n}' + const expectedConflicts: IConflictSetting[] = [ + { key: 'd', localValue: 5, remoteValue: undefined }, + { key: 'b', localValue: 4, remoteValue: 3 }, + ]; + const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], [], formattingOptions); assert.ok(actual.hasChanges); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ "a": 1, diff --git a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts new file mode 100644 index 00000000000..58f65eee3f4 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncStatus, ISettingsSyncService, IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class SettingsSyncService extends Disposable implements ISettingsSyncService { + + _serviceBrand: undefined; + + private readonly channel: IChannel; + + private _status: SyncStatus = SyncStatus.Uninitialized; + get status(): SyncStatus { return this._status; } + private _onDidChangeStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; + + get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService + ) { + super(); + this.channel = sharedProcessService.getChannel('settingsSync'); + this.channel.call('_getInitialStatus').then(status => { + this.updateStatus(status); + this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); + }); + } + + sync(_continue?: boolean): Promise { + return this.channel.call('sync', [_continue]); + } + + stop(): void { + this.channel.call('stop'); + } + + getConflicts(): Promise { + return this.channel.call('getConflicts'); + } + + resolveConflicts(conflicts: { key: string, value: any | undefined }[]): Promise { + return this.channel.call('resolveConflicts', [conflicts]); + } + + private async updateStatus(status: SyncStatus): Promise { + this._status = status; + this._onDidChangeStatus.fire(status); + } + +} + +registerSingleton(ISettingsSyncService, SettingsSyncService); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 7969bc16681..f303692242f 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -50,6 +50,7 @@ import 'vs/workbench/services/url/electron-browser/urlService'; import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; +import 'vs/workbench/services/userDataSync/electron-browser/settingsSyncService'; import 'vs/workbench/services/authToken/electron-browser/authTokenService'; import 'vs/workbench/services/host/electron-browser/desktopHostService'; import 'vs/workbench/services/request/electron-browser/requestService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index f424c87d927..65b74060fa1 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -65,10 +65,11 @@ import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; import { IAuthTokenService } from 'vs/platform/auth/common/auth'; import { AuthTokenService } from 'vs/workbench/services/authToken/browser/authTokenService'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, ISettingsSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; registerSingleton(IExtensionManagementService, ExtensionManagementService); registerSingleton(IBackupFileService, BackupFileService); @@ -79,6 +80,7 @@ registerSingleton(ILoggerService, FileLoggerService); registerSingleton(IAuthTokenService, AuthTokenService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); +registerSingleton(ISettingsSyncService, SettingsSynchroniser); registerSingleton(IUserDataSyncService, UserDataSyncService); //#endregion