Skip to content

Commit 44eb607

Browse files
committed
extract settings merge as a service
1 parent 4e0de7c commit 44eb607

4 files changed

Lines changed: 238 additions & 203 deletions

File tree

src/vs/platform/userDataSync/common/userDataSync.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,13 @@ export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUser
9292
export interface IUserDataSyncService extends ISynchroniser {
9393
_serviceBrand: any;
9494
}
95+
96+
export const ISettingsMergeService = createDecorator<ISettingsMergeService>('ISettingsMergeService');
97+
98+
export interface ISettingsMergeService {
99+
100+
_serviceBrand: undefined;
101+
102+
merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<string>;
103+
104+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as objects from 'vs/base/common/objects';
7+
import { values } from 'vs/base/common/map';
8+
import { parse, findNodeAtLocation, parseTree } from 'vs/base/common/json';
9+
import { EditOperation } from 'vs/editor/common/core/editOperation';
10+
import { IModeService } from 'vs/editor/common/services/modeService';
11+
import { ITextModel } from 'vs/editor/common/model';
12+
import { setProperty } from 'vs/base/common/jsonEdit';
13+
import { Range } from 'vs/editor/common/core/range';
14+
import { Selection } from 'vs/editor/common/core/selection';
15+
import { IModelService } from 'vs/editor/common/services/modelService';
16+
import { Position } from 'vs/editor/common/core/position';
17+
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
18+
import { ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync';
19+
20+
class SettingsMergeService implements ISettingsMergeService {
21+
22+
_serviceBrand: undefined;
23+
24+
constructor(
25+
@IModelService private readonly modelService: IModelService,
26+
@IModeService private readonly modeService: IModeService
27+
) { }
28+
29+
async merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<string> {
30+
const local = parse(localContent);
31+
const remote = parse(remoteContent);
32+
const base = baseContent ? parse(baseContent) : null;
33+
const { changes, conflicts } = this.getChanges(local, remote, base);
34+
35+
if (!changes.length && !conflicts.length) {
36+
return localContent;
37+
}
38+
39+
const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc'));
40+
for (const change of changes) {
41+
this.editSetting(settingsPreviewModel, change.key, change.value);
42+
}
43+
for (const key of conflicts) {
44+
const tree = parseTree(settingsPreviewModel.getValue());
45+
const valueNode = findNodeAtLocation(tree, [key]);
46+
const eol = settingsPreviewModel.getEOL();
47+
const remoteEdit = setProperty(`{${eol}\t${eol}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: eol })[0];
48+
const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${eol}` : '';
49+
if (valueNode) {
50+
// Updated in Local and Remote with different value
51+
const keyPosition = settingsPreviewModel.getPositionAt(valueNode.parent!.offset);
52+
const valuePosition = settingsPreviewModel.getPositionAt(valueNode.offset + valueNode.length);
53+
const editOperations = [
54+
EditOperation.insert(new Position(keyPosition.lineNumber - 1, settingsPreviewModel.getLineMaxColumn(keyPosition.lineNumber - 1)), `${eol}<<<<<<< local`),
55+
EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${eol}=======${eol}${remoteContent}>>>>>>> remote`)
56+
];
57+
settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []);
58+
} else {
59+
// Removed in Local, but updated in Remote
60+
const position = new Position(settingsPreviewModel.getLineCount() - 1, settingsPreviewModel.getLineMaxColumn(settingsPreviewModel.getLineCount() - 1));
61+
const editOperations = [
62+
EditOperation.insert(position, `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote`)
63+
];
64+
settingsPreviewModel.pushEditOperations([new Selection(position.lineNumber, position.column, position.lineNumber, position.column)], editOperations, () => []);
65+
}
66+
}
67+
return settingsPreviewModel.getValue();
68+
}
69+
70+
private editSetting(model: ITextModel, key: string, value: any | undefined): void {
71+
const insertSpaces = false;
72+
const tabSize = 4;
73+
const eol = model.getEOL();
74+
const edit = setProperty(model.getValue(), [key], value, { tabSize, insertSpaces, eol })[0];
75+
if (edit) {
76+
const startPosition = model.getPositionAt(edit.offset);
77+
const endPosition = model.getPositionAt(edit.offset + edit.length);
78+
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
79+
let currentText = model.getValueInRange(range);
80+
if (edit.content !== currentText) {
81+
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
82+
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
83+
}
84+
}
85+
}
86+
87+
private getChanges(local: { [key: string]: any }, remote: { [key: string]: any }, base: { [key: string]: any } | null): { changes: { key: string; value: any | undefined; }[], conflicts: string[] } {
88+
const localToRemote = this.compare(local, remote);
89+
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
90+
// No changes found between local and remote.
91+
return { changes: [], conflicts: [] };
92+
}
93+
94+
const changes: { key: string, value: any | undefined }[] = [];
95+
const conflicts: Set<string> = new Set<string>();
96+
const baseToLocal = base ? this.compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
97+
const baseToRemote = base ? this.compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
98+
99+
// Removed settings in Local
100+
for (const key of baseToLocal.removed.keys()) {
101+
// Got updated in remote
102+
if (baseToRemote.updated.has(key)) {
103+
conflicts.add(key);
104+
}
105+
}
106+
107+
// Removed settings in Remote
108+
for (const key of baseToRemote.removed.keys()) {
109+
if (conflicts.has(key)) {
110+
continue;
111+
}
112+
// Got updated in local
113+
if (baseToLocal.updated.has(key)) {
114+
conflicts.add(key);
115+
} else {
116+
changes.push({ key, value: undefined });
117+
}
118+
}
119+
120+
// Added settings in Local
121+
for (const key of baseToLocal.added.keys()) {
122+
if (conflicts.has(key)) {
123+
continue;
124+
}
125+
// Got added in remote
126+
if (baseToRemote.added.has(key)) {
127+
// Has different value
128+
if (localToRemote.updated.has(key)) {
129+
conflicts.add(key);
130+
}
131+
}
132+
}
133+
134+
// Added settings in remote
135+
for (const key of baseToRemote.added.keys()) {
136+
if (conflicts.has(key)) {
137+
continue;
138+
}
139+
// Got added in local
140+
if (baseToLocal.added.has(key)) {
141+
// Has different value
142+
if (localToRemote.updated.has(key)) {
143+
conflicts.add(key);
144+
}
145+
} else {
146+
changes.push({ key, value: remote[key] });
147+
}
148+
}
149+
150+
// Updated settings in Local
151+
for (const key of baseToLocal.updated.keys()) {
152+
if (conflicts.has(key)) {
153+
continue;
154+
}
155+
// Got updated in remote
156+
if (baseToRemote.updated.has(key)) {
157+
// Has different value
158+
if (localToRemote.updated.has(key)) {
159+
conflicts.add(key);
160+
}
161+
}
162+
}
163+
164+
// Updated settings in Remote
165+
for (const key of baseToRemote.updated.keys()) {
166+
if (conflicts.has(key)) {
167+
continue;
168+
}
169+
// Got updated in local
170+
if (baseToLocal.updated.has(key)) {
171+
// Has different value
172+
if (localToRemote.updated.has(key)) {
173+
conflicts.add(key);
174+
}
175+
} else {
176+
changes.push({ key, value: remote[key] });
177+
}
178+
}
179+
180+
return { changes, conflicts: values(conflicts) };
181+
}
182+
183+
private compare(from: { [key: string]: any }, to: { [key: string]: any }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
184+
const fromKeys = Object.keys(from);
185+
const toKeys = Object.keys(to);
186+
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
187+
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
188+
const updated: Set<string> = new Set<string>();
189+
190+
for (const key of fromKeys) {
191+
if (removed.has(key)) {
192+
continue;
193+
}
194+
const value1 = from[key];
195+
const value2 = to[key];
196+
if (!objects.equals(value1, value2)) {
197+
updated.add(key);
198+
}
199+
}
200+
201+
return { added, removed, updated };
202+
}
203+
204+
}
205+
206+
registerSingleton(ISettingsMergeService, SettingsMergeService);

0 commit comments

Comments
 (0)