diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index e6ec0ab0145..f408d51a7c6 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -199,13 +199,13 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { || lastSyncData.content !== remoteContent // Remote has moved forwarded ) { this.logService.trace('Settings Sync: Merging remote contents with settings file.'); - const mergeContent = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null); - hasLocalChanged = mergeContent !== localContent; - hasRemoteChanged = mergeContent !== remoteContent; - if (hasLocalChanged || hasRemoteChanged) { - // Sync only if there are changes - hasConflicts = this.hasErrors(mergeContent); - await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(mergeContent)); + const result = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null); + // Sync only if there are changes + if (result.hasChanges) { + hasLocalChanged = result.mergeContent !== localContent; + hasRemoteChanged = result.mergeContent !== remoteContent; + hasConflicts = result.hasConflicts; + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(result.mergeContent)); return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } } diff --git a/src/vs/platform/userDataSync/common/settingsSyncIpc.ts b/src/vs/platform/userDataSync/common/settingsSyncIpc.ts index a8e4cac6ac5..dfee58a6191 100644 --- a/src/vs/platform/userDataSync/common/settingsSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/settingsSyncIpc.ts @@ -30,7 +30,7 @@ export class SettingsMergeChannelClient implements ISettingsMergeService { constructor(private readonly channel: IChannel) { } - merge(localContent: string, remoteContent: string, baseContent: string | null): Promise { + merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> { return this.channel.call('merge', [localContent, remoteContent, baseContent]); } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index a6bf31b22ac..5801cea4ca3 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -74,6 +74,6 @@ export interface ISettingsMergeService { _serviceBrand: undefined; - merge(localContent: string, remoteContent: string, baseContent: string | null): Promise; + merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{mergeContent: string, hasChanges: boolean, hasConflicts: boolean}>; } diff --git a/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts b/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts index 835e1a7179b..cd11a6743df 100644 --- a/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts +++ b/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as objects from 'vs/base/common/objects'; -import { values } from 'vs/base/common/map'; import { parse, findNodeAtLocation, parseTree } from 'vs/base/common/json'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -26,75 +25,21 @@ class SettingsMergeService implements ISettingsMergeService { @IModeService private readonly modeService: IModeService ) { } - async merge(localContent: string, remoteContent: string, baseContent: string | null): Promise { + async merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> { const local = parse(localContent); const remote = parse(remoteContent); const base = baseContent ? parse(baseContent) : null; - const { changes, conflicts } = this.getChanges(local, remote, base); - if (!changes.length && !conflicts.length) { - return localContent; - } - - const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc')); - for (const change of changes) { - this.editSetting(settingsPreviewModel, change.key, change.value); - } - for (const key of conflicts) { - const tree = parseTree(settingsPreviewModel.getValue()); - const valueNode = findNodeAtLocation(tree, [key]); - const eol = settingsPreviewModel.getEOL(); - const remoteEdit = setProperty(`{${eol}\t${eol}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: eol })[0]; - const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${eol}` : ''; - if (valueNode) { - // Updated in Local and Remote with different value - const keyPosition = settingsPreviewModel.getPositionAt(valueNode.parent!.offset); - const valuePosition = settingsPreviewModel.getPositionAt(valueNode.offset + valueNode.length); - const editOperations = [ - EditOperation.insert(new Position(keyPosition.lineNumber - 1, settingsPreviewModel.getLineMaxColumn(keyPosition.lineNumber - 1)), `${eol}<<<<<<< local`), - EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${eol}=======${eol}${remoteContent}>>>>>>> remote`) - ]; - settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []); - } else { - // Removed in Local, but updated in Remote - const position = new Position(settingsPreviewModel.getLineCount() - 1, settingsPreviewModel.getLineMaxColumn(settingsPreviewModel.getLineCount() - 1)); - const editOperations = [ - EditOperation.insert(position, `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote`) - ]; - settingsPreviewModel.pushEditOperations([new Selection(position.lineNumber, position.column, position.lineNumber, position.column)], editOperations, () => []); - } - } - return settingsPreviewModel.getValue(); - } - - private editSetting(model: ITextModel, key: string, value: any | undefined): void { - const insertSpaces = false; - const tabSize = 4; - const eol = model.getEOL(); - const edit = setProperty(model.getValue(), [key], value, { tabSize, insertSpaces, eol })[0]; - if (edit) { - const startPosition = model.getPositionAt(edit.offset); - const endPosition = model.getPositionAt(edit.offset + edit.length); - const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); - let currentText = model.getValueInRange(range); - if (edit.content !== currentText) { - const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content); - model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []); - } - } - } - - private getChanges(local: { [key: string]: any }, remote: { [key: string]: any }, base: { [key: string]: any } | null): { changes: { key: string; value: any | undefined; }[], conflicts: string[] } { const localToRemote = this.compare(local, remote); if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { // No changes found between local and remote. - return { changes: [], conflicts: [] }; + return { mergeContent: localContent, hasChanges: false, hasConflicts: false }; } - const changes: { key: string, value: any | undefined }[] = []; const conflicts: Set = new Set(); const baseToLocal = base ? this.compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; const baseToRemote = base ? this.compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc')); // Removed settings in Local for (const key of baseToLocal.removed.keys()) { @@ -113,7 +58,7 @@ class SettingsMergeService implements ISettingsMergeService { if (baseToLocal.updated.has(key)) { conflicts.add(key); } else { - changes.push({ key, value: undefined }); + this.editSetting(settingsPreviewModel, key, undefined); } } @@ -143,7 +88,7 @@ class SettingsMergeService implements ISettingsMergeService { conflicts.add(key); } } else { - changes.push({ key, value: remote[key] }); + this.editSetting(settingsPreviewModel, key, remote[key]); } } @@ -173,11 +118,52 @@ class SettingsMergeService implements ISettingsMergeService { conflicts.add(key); } } else { - changes.push({ key, value: remote[key] }); + this.editSetting(settingsPreviewModel, key, remote[key]); } } - return { changes, conflicts: values(conflicts) }; + for (const key of conflicts.keys()) { + const tree = parseTree(settingsPreviewModel.getValue()); + const valueNode = findNodeAtLocation(tree, [key]); + const eol = settingsPreviewModel.getEOL(); + const remoteEdit = setProperty(`{${eol}\t${eol}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: eol })[0]; + const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${eol}` : ''; + if (valueNode) { + // Updated in Local and Remote with different value + const keyPosition = settingsPreviewModel.getPositionAt(valueNode.parent!.offset); + const valuePosition = settingsPreviewModel.getPositionAt(valueNode.offset + valueNode.length); + const editOperations = [ + EditOperation.insert(new Position(keyPosition.lineNumber - 1, settingsPreviewModel.getLineMaxColumn(keyPosition.lineNumber - 1)), `${eol}<<<<<<< local`), + EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${eol}=======${eol}${remoteContent}>>>>>>> remote`) + ]; + settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []); + } else { + // Removed in Local, but updated in Remote + const position = new Position(settingsPreviewModel.getLineCount() - 1, settingsPreviewModel.getLineMaxColumn(settingsPreviewModel.getLineCount() - 1)); + const editOperations = [ + EditOperation.insert(position, `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote`) + ]; + settingsPreviewModel.pushEditOperations([new Selection(position.lineNumber, position.column, position.lineNumber, position.column)], editOperations, () => []); + } + } + return { mergeContent: settingsPreviewModel.getValue(), hasChanges: true, hasConflicts: conflicts.size > 0 }; + } + + private editSetting(model: ITextModel, key: string, value: any | undefined): void { + const insertSpaces = false; + const tabSize = 4; + const eol = model.getEOL(); + const edit = setProperty(model.getValue(), [key], value, { tabSize, insertSpaces, eol })[0]; + if (edit) { + const startPosition = model.getPositionAt(edit.offset); + const endPosition = model.getPositionAt(edit.offset + edit.length); + const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column); + let currentText = model.getValueInRange(range); + if (edit.content !== currentText) { + const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content); + model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []); + } + } } private compare(from: { [key: string]: any }, to: { [key: string]: any }): { added: Set, removed: Set, updated: Set } {