Skip to content

Commit 258ae82

Browse files
committed
Adds some merge editor tests & diffing bug fix.
1 parent 1df4b3c commit 258ae82

File tree

5 files changed

+290
-17
lines changed

5 files changed

+290
-17
lines changed

src/vs/editor/common/services/editorSimpleWorker.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IRequestHandler } from 'vs/base/common/worker/simpleWorker';
1111
import { IPosition, Position } from 'vs/editor/common/core/position';
1212
import { IRange, Range } from 'vs/editor/common/core/range';
1313
import { DiffComputer, IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer';
14-
import { EndOfLineSequence } from 'vs/editor/common/model';
14+
import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model';
1515
import { IMirrorTextModel, IModelChangedEvent, MirrorTextModel as BaseMirrorModel } from 'vs/editor/common/model/mirrorTextModel';
1616
import { ensureValidWordDefinition, getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper';
1717
import { IInplaceReplaceSupportResult, ILink, TextEdit } from 'vs/editor/common/languages';
@@ -388,8 +388,12 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
388388
return null;
389389
}
390390

391-
const originalLines = original.getLinesContent();
392-
const modifiedLines = modified.getLinesContent();
391+
return EditorSimpleWorker.computeDiff(original, modified, ignoreTrimWhitespace, maxComputationTime);
392+
}
393+
394+
public static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, ignoreTrimWhitespace: boolean, maxComputationTime: number): IDiffComputationResult | null {
395+
const originalLines = originalTextModel.getLinesContent();
396+
const modifiedLines = modifiedTextModel.getLinesContent();
393397
const diffComputer = new DiffComputer(originalLines, modifiedLines, {
394398
shouldComputeCharChanges: true,
395399
shouldPostProcessCharChanges: true,
@@ -399,15 +403,15 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
399403
});
400404

401405
const diffResult = diffComputer.computeDiff();
402-
const identical = (diffResult.changes.length > 0 ? false : this._modelsAreIdentical(original, modified));
406+
const identical = (diffResult.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));
403407
return {
404408
quitEarly: diffResult.quitEarly,
405409
identical: identical,
406410
changes: diffResult.changes
407411
};
408412
}
409413

410-
private _modelsAreIdentical(original: ICommonModel, modified: ICommonModel): boolean {
414+
private static _modelsAreIdentical(original: ICommonModel | ITextModel, modified: ICommonModel | ITextModel): boolean {
411415
const originalLineCount = original.getLineCount();
412416
const modifiedLineCount = modified.getLineCount();
413417
if (originalLineCount !== modifiedLineCount) {

src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
1616
import { IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor';
1717
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
1818
import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
19+
import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
1920
import { autorun } from 'vs/workbench/contrib/audioCues/browser/observable';
2021
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
2122
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -107,7 +108,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
107108
this.input2.title,
108109
this.input2.detail,
109110
this.input2.description,
110-
result.object.textEditorModel
111+
result.object.textEditorModel,
112+
this._instaService.createInstance(EditorWorkerServiceDiffComputer),
111113
);
112114

113115
await this._model.onInitialized;

src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class EditorWorkerServiceDiffComputer implements IDiffComputer {
2424

2525
async computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<IDiffComputerResult> {
2626
const diffs = await this.editorWorkerService.computeDiff(textModel1.uri, textModel2.uri, false, 1000);
27-
if (!diffs || diffs.quitEarly) {
27+
if (!diffs) {
2828
return { diffs: null };
2929
}
3030
return { diffs: EditorWorkerServiceDiffComputer.fromDiffComputationResult(diffs, textModel1, textModel2) };
@@ -53,7 +53,7 @@ function fromLineChange(lineChange: ILineChange, originalTextModel: ITextModel,
5353
}
5454

5555
let innerDiffs = lineChange.charChanges?.map(c => rangeMappingFromCharChange(c, originalTextModel, modifiedTextModel)).filter(isDefined);
56-
if (!innerDiffs) {
56+
if (!innerDiffs || innerDiffs.length === 0) {
5757
innerDiffs = [rangeMappingFromLineRanges(originalRange, modifiedRange)];
5858
}
5959

src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,24 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { compareBy, CompareResult, tieBreakComparators, equals, numberComparator } from 'vs/base/common/arrays';
6+
import { compareBy, CompareResult, equals, numberComparator, tieBreakComparators } from 'vs/base/common/arrays';
77
import { BugIndicatingError } from 'vs/base/common/errors';
88
import { splitLines } from 'vs/base/common/strings';
99
import { Constants } from 'vs/base/common/uint';
1010
import { Position } from 'vs/editor/common/core/position';
1111
import { Range } from 'vs/editor/common/core/range';
12+
import { ILanguageService } from 'vs/editor/common/languages/language';
1213
import { ITextModel } from 'vs/editor/common/model';
13-
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
14+
import { IModelService } from 'vs/editor/common/services/model';
1415
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
1516
import { autorunHandleChanges, derivedObservable, IObservable, IReader, ITransaction, keepAlive, ObservableValue, transaction, waitForState } from 'vs/workbench/contrib/audioCues/browser/observable';
16-
import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
17-
import { DetailedLineRangeMapping, DocumentMapping, LineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping';
17+
import { IDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
1818
import { LineRangeEdit, RangeEdit } from 'vs/workbench/contrib/mergeEditor/browser/model/editing';
1919
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
20+
import { DetailedLineRangeMapping, DocumentMapping, LineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping';
2021
import { TextModelDiffChangeReason, TextModelDiffs, TextModelDiffState } from 'vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs';
21-
import { concatArrays, leftJoin, elementAtOrUndefined } from 'vs/workbench/contrib/mergeEditor/browser/utils';
22+
import { concatArrays, elementAtOrUndefined, leftJoin } from 'vs/workbench/contrib/mergeEditor/browser/utils';
2223
import { ModifiedBaseRange, ModifiedBaseRangeState } from './modifiedBaseRange';
23-
import { IModelService } from 'vs/editor/common/services/model';
24-
import { ILanguageService } from 'vs/editor/common/languages/language';
2524

2625
export const enum MergeEditorModelState {
2726
initializing = 1,
@@ -30,7 +29,6 @@ export const enum MergeEditorModelState {
3029
}
3130

3231
export class MergeEditorModel extends EditorModel {
33-
private readonly diffComputer = new EditorWorkerServiceDiffComputer(this.editorWorkerService);
3432
private readonly input1TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input1, this.diffComputer));
3533
private readonly input2TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input2, this.diffComputer));
3634
private readonly resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.result, this.diffComputer));
@@ -138,7 +136,7 @@ export class MergeEditorModel extends EditorModel {
138136
readonly input2Detail: string | undefined,
139137
readonly input2Description: string | undefined,
140138
readonly result: ITextModel,
141-
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
139+
private readonly diffComputer: IDiffComputer,
142140
@IModelService private readonly modelService: IModelService,
143141
@ILanguageService private readonly languageService: ILanguageService,
144142
) {
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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 assert = require('assert');
7+
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
8+
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
9+
import { Range } from 'vs/editor/common/core/range';
10+
import { ITextModel } from 'vs/editor/common/model';
11+
import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker';
12+
import { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel';
13+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
14+
import { transaction } from 'vs/workbench/contrib/audioCues/browser/observable';
15+
import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
16+
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
17+
18+
suite('merge editor model', () => {
19+
ensureNoDisposablesAreLeakedInTestSuite();
20+
21+
test('prepend line', async () => {
22+
await testMergeModel(
23+
{
24+
"languageId": "plaintext",
25+
"base": "line1\nline2",
26+
"input1": "0\nline1\nline2",
27+
"input2": "0\nline1\nline2",
28+
"result": ""
29+
},
30+
model => {
31+
assert.deepStrictEqual(model.getProjections(), {
32+
base: '⟦⟧₀line1\nline2',
33+
input1: '⟦0\n⟧₀line1\nline2',
34+
input2: '⟦0\n⟧₀line1\nline2',
35+
});
36+
37+
model.toggleConflict(0, 1);
38+
assert.deepStrictEqual(
39+
{ result: model.getResult() },
40+
{ result: '0\nline1\nline2' }
41+
);
42+
43+
model.toggleConflict(0, 2);
44+
assert.deepStrictEqual(
45+
{ result: model.getResult() },
46+
({ result: "0\n0\nline1\nline2" })
47+
);
48+
}
49+
);
50+
});
51+
52+
test('empty base', async () => {
53+
await testMergeModel(
54+
{
55+
"languageId": "plaintext",
56+
"base": "",
57+
"input1": "input1",
58+
"input2": "input2",
59+
"result": ""
60+
},
61+
model => {
62+
assert.deepStrictEqual(model.getProjections(), ({ base: "⟦⟧₀", input1: "⟦input1⟧₀", input2: "⟦input2⟧₀" }));
63+
64+
model.toggleConflict(0, 1);
65+
assert.deepStrictEqual(
66+
{ result: model.getResult() },
67+
({ result: "input1" })
68+
);
69+
70+
model.toggleConflict(0, 2);
71+
assert.deepStrictEqual(
72+
{ result: model.getResult() },
73+
({ result: "input2" })
74+
);
75+
}
76+
);
77+
});
78+
79+
test('can merge word changes', async () => {
80+
await testMergeModel(
81+
{
82+
"languageId": "plaintext",
83+
"base": "hello",
84+
"input1": "hallo",
85+
"input2": "helloworld",
86+
"result": ""
87+
},
88+
model => {
89+
assert.deepStrictEqual(model.getProjections(), {
90+
base: '⟦hello⟧₀',
91+
input1: '⟦hallo⟧₀',
92+
input2: '⟦helloworld⟧₀',
93+
});
94+
95+
model.toggleConflict(0, 1);
96+
model.toggleConflict(0, 2);
97+
98+
assert.deepStrictEqual(
99+
{ result: model.getResult() },
100+
{ result: 'halloworld' }
101+
);
102+
}
103+
);
104+
105+
});
106+
107+
test('can combine insertions at end of document', async () => {
108+
await testMergeModel(
109+
{
110+
"languageId": "plaintext",
111+
"base": "Zürich\nBern\nBasel\nChur\nGenf\nThun",
112+
"input1": "Zürich\nBern\nChur\nDavos\nGenf\nThun\nfunction f(b:boolean) {}",
113+
"input2": "Zürich\nBern\nBasel (FCB)\nChur\nGenf\nThun\nfunction f(a:number) {}",
114+
"result": "Zürich\nBern\nBasel\nChur\nDavos\nGenf\nThun"
115+
},
116+
model => {
117+
assert.deepStrictEqual(model.getProjections(), {
118+
base: 'Zürich\nBern\n⟦Basel\n⟧₀Chur\n⟦⟧₁Genf\nThun⟦⟧₂',
119+
input1:
120+
'Zürich\nBern\n⟦⟧₀Chur\n⟦Davos\n⟧₁Genf\nThun\n⟦function f(b:boolean) {}⟧₂',
121+
input2:
122+
'Zürich\nBern\n⟦Basel (FCB)\n⟧₀Chur\n⟦⟧₁Genf\nThun\n⟦function f(a:number) {}⟧₂',
123+
});
124+
125+
model.toggleConflict(2, 1);
126+
model.toggleConflict(2, 2);
127+
128+
assert.deepStrictEqual(
129+
{ result: model.getResult() },
130+
{
131+
result:
132+
'Zürich\nBern\nBasel\nChur\nDavos\nGenf\nThun\nfunction f(b:boolean) {}\nfunction f(a:number) {}',
133+
}
134+
);
135+
}
136+
);
137+
});
138+
});
139+
140+
async function testMergeModel(
141+
options: MergeModelOptions,
142+
fn: (model: MergeModelInterface) => void
143+
): Promise<void> {
144+
const disposables = new DisposableStore();
145+
const modelInterface = disposables.add(
146+
new MergeModelInterface(options, createModelServices(disposables))
147+
);
148+
await modelInterface.mergeModel.onInitialized;
149+
await fn(modelInterface);
150+
disposables.dispose();
151+
}
152+
153+
interface MergeModelOptions {
154+
languageId: string;
155+
input1: string;
156+
input2: string;
157+
base: string;
158+
result: string;
159+
}
160+
161+
function toSmallNumbersDec(value: number): string {
162+
const smallNumbers = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
163+
return value.toString().split('').map(c => smallNumbers[parseInt(c)]).join('');
164+
}
165+
166+
class MergeModelInterface extends Disposable {
167+
public readonly mergeModel: MergeEditorModel;
168+
169+
constructor(options: MergeModelOptions, instantiationService: IInstantiationService) {
170+
super();
171+
const input1TextModel = this._register(createTextModel(options.input1, options.languageId));
172+
const input2TextModel = this._register(createTextModel(options.input2, options.languageId));
173+
const baseTextModel = this._register(createTextModel(options.base, options.languageId));
174+
const resultTextModel = this._register(createTextModel(options.result, options.languageId));
175+
this.mergeModel = this._register(instantiationService.createInstance(MergeEditorModel,
176+
baseTextModel,
177+
input1TextModel,
178+
'',
179+
'',
180+
'',
181+
input2TextModel,
182+
'',
183+
'',
184+
'',
185+
resultTextModel,
186+
{
187+
async computeDiff(textModel1, textModel2) {
188+
const result = EditorSimpleWorker.computeDiff(textModel1, textModel2, false, 10000);
189+
if (!result) {
190+
return { diffs: null };
191+
}
192+
return {
193+
diffs: EditorWorkerServiceDiffComputer.fromDiffComputationResult(
194+
result,
195+
textModel1,
196+
textModel2
197+
),
198+
};
199+
},
200+
}
201+
));
202+
}
203+
204+
getProjections(): unknown {
205+
interface LabeledRange {
206+
range: Range;
207+
label: string;
208+
}
209+
function applyRanges(textModel: ITextModel, ranges: LabeledRange[]): void {
210+
textModel.applyEdits(ranges.map(({ range, label }) => ({
211+
range: range,
212+
text: `⟦${textModel.getValueInRange(range)}${label}`,
213+
})));
214+
}
215+
const baseRanges = this.mergeModel.modifiedBaseRanges.get();
216+
217+
const baseTextModel = createTextModel(this.mergeModel.base.getValue());
218+
applyRanges(
219+
baseTextModel,
220+
baseRanges.map<LabeledRange>((r, idx) => ({
221+
range: r.baseRange.toRange(),
222+
label: toSmallNumbersDec(idx),
223+
}))
224+
);
225+
226+
const input1TextModel = createTextModel(this.mergeModel.input1.getValue());
227+
applyRanges(
228+
input1TextModel,
229+
baseRanges.map<LabeledRange>((r, idx) => ({
230+
range: r.input1Range.toRange(),
231+
label: toSmallNumbersDec(idx),
232+
}))
233+
);
234+
235+
const input2TextModel = createTextModel(this.mergeModel.input2.getValue());
236+
applyRanges(
237+
input2TextModel,
238+
baseRanges.map<LabeledRange>((r, idx) => ({
239+
range: r.input2Range.toRange(),
240+
label: toSmallNumbersDec(idx),
241+
}))
242+
);
243+
244+
const result = {
245+
base: baseTextModel.getValue(),
246+
input1: input1TextModel.getValue(),
247+
input2: input2TextModel.getValue(),
248+
};
249+
baseTextModel.dispose();
250+
input1TextModel.dispose();
251+
input2TextModel.dispose();
252+
return result;
253+
}
254+
255+
toggleConflict(conflictIdx: number, inputNumber: 1 | 2): void {
256+
const baseRange = this.mergeModel.modifiedBaseRanges.get()[conflictIdx];
257+
if (!baseRange) {
258+
throw new Error();
259+
}
260+
const state = this.mergeModel.getState(baseRange).get();
261+
transaction(tx => {
262+
this.mergeModel.setState(baseRange, state.toggle(inputNumber), true, tx);
263+
});
264+
}
265+
266+
getResult(): string {
267+
return this.mergeModel.result.getValue();
268+
}
269+
}

0 commit comments

Comments
 (0)