Skip to content

Commit 3dd0aea

Browse files
authored
Merge pull request microsoft#165147 from microsoft/hediet/integrated-mule
Implements diffing fixture tests. Fixes microsoft#165041
2 parents f176ad8 + 3c4fc51 commit 3dd0aea

File tree

120 files changed

+5311
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+5311
-6
lines changed

.vscode/settings.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
}
1313
},
1414
"files.associations": {
15-
"cglicenses.json": "jsonc"
15+
"cglicenses.json": "jsonc",
16+
"*.tst": "typescript"
1617
},
1718
"search.exclude": {
1819
"**/node_modules": true,
@@ -27,7 +28,8 @@
2728
"test/automation/out/**": true,
2829
"test/integration/browser/out/**": true,
2930
"src/vs/base/test/node/uri.test.data.txt": true,
30-
"src/vs/workbench/api/test/browser/extHostDocumentData.test.perf-data.ts": true
31+
"src/vs/workbench/api/test/browser/extHostDocumentData.test.perf-data.ts": true,
32+
"src/vs/editor/test/node/diffing/fixtures/**": true,
3133
},
3234
"lcov.path": [
3335
"./.build/coverage/lcov.info",

src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ export function shiftSequenceDiffs(sequence1: ISequence, sequence2: ISequence, s
8585
const diff = sequenceDiffs[i];
8686
if (diff.seq1Range.isEmpty) {
8787
const seq2PrevEndExclusive = (i > 0 ? sequenceDiffs[i - 1].seq2Range.endExclusive : -1);
88-
const seq2NextStart = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1].seq2Range.start : sequence2.length + 1);
88+
const seq2NextStart = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1].seq2Range.start : sequence2.length);
8989
sequenceDiffs[i] = shiftDiffToBetterPosition(diff, sequence1, sequence2, seq2NextStart, seq2PrevEndExclusive);
9090
} else if (diff.seq2Range.isEmpty) {
9191
const seq1PrevEndExclusive = (i > 0 ? sequenceDiffs[i - 1].seq1Range.endExclusive : -1);
92-
const seq1NextStart = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1].seq1Range.start : sequence1.length + 1);
92+
const seq1NextStart = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1].seq1Range.start : sequence1.length);
9393
sequenceDiffs[i] = shiftDiffToBetterPosition(diff.reverse(), sequence2, sequence1, seq1NextStart, seq1PrevEndExclusive).reverse();
9494
}
9595
}
@@ -102,15 +102,15 @@ function shiftDiffToBetterPosition(diff: SequenceDiff, sequence1: ISequence, seq
102102

103103
// don't touch previous or next!
104104
let deltaBefore = 1;
105-
while (diff.seq1Range.start - deltaBefore > seq2PrevEndExclusive &&
105+
while (diff.seq2Range.start - deltaBefore > seq2PrevEndExclusive &&
106106
sequence2.getElement(diff.seq2Range.start - deltaBefore) ===
107107
sequence2.getElement(diff.seq2Range.endExclusive - deltaBefore) && deltaBefore < maxShiftLimit) {
108108
deltaBefore++;
109109
}
110110
deltaBefore--;
111111

112112
let deltaAfter = 1;
113-
while (diff.seq1Range.start + deltaAfter < seq2NextStart &&
113+
while (diff.seq2Range.start + deltaAfter < seq2NextStart &&
114114
sequence2.getElement(diff.seq2Range.start + deltaAfter) ===
115115
sequence2.getElement(diff.seq2Range.endExclusive + deltaAfter) && deltaAfter < maxShiftLimit) {
116116
deltaAfter++;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Diffing Fixture Tests
2+
3+
Every folder in `fixtures` represents a test.
4+
The file that starts with `1.` is diffed against the file that starts with `2.`. Use `tst` instead of `ts` to avoid compiler/linter errors for typescript diff files.
5+
6+
* Missing `*.expected.diff.json` are created automatically (as well as an `*.invalid.diff.json` file).
7+
* If the actual diff does not equal the expected diff, the expected file is updated automatically. The previous value of the expected file is written to `*.invalid.diff.json`.
8+
* The test will fail if there are any `*.invalid.diff.json` files. This makes sure that the test keeps failing even if it is run a second time.
9+
10+
When changing the diffing algorithm, run the fixture tests, review the diff of the `*.expected.diff.json` files and delete all `*.invalid.diff.json` files.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 { readdirSync, readFileSync, existsSync, writeFileSync } from 'fs';
8+
import { join, resolve } from 'path';
9+
import { FileAccess } from 'vs/base/common/network';
10+
import { SmartLinesDiffComputer } from 'vs/editor/common/diff/smartLinesDiffComputer';
11+
import { StandardLinesDiffComputer } from 'vs/editor/common/diff/standardLinesDiffComputer';
12+
13+
suite('diff fixtures', () => {
14+
const fixturesOutDir = FileAccess.asFileUri('vs/editor/test/node/diffing/fixtures').fsPath;
15+
// We want the dir in src, so we can directly update the source files if they disagree and create invalid files to capture the previous state.
16+
// This makes it very easy to update the fixtures.
17+
const fixturesSrcDir = resolve(fixturesOutDir).replaceAll('\\', '/').replace('/out/vs/editor/', '/src/vs/editor/');
18+
const folders = readdirSync(fixturesSrcDir);
19+
20+
for (const folder of folders) {
21+
for (const diffingAlgoName of ['smart', 'experimental']) {
22+
test(`${folder}-${diffingAlgoName}`, () => {
23+
const folderPath = join(fixturesSrcDir, folder);
24+
const files = readdirSync(folderPath);
25+
26+
const firstFileName = files.find(f => f.startsWith('1.'))!;
27+
const secondFileName = files.find(f => f.startsWith('2.'))!;
28+
29+
const firstContentLines = readFileSync(join(folderPath, firstFileName), 'utf8').split(/\r\n|\r|\n/);
30+
const secondContentLines = readFileSync(join(folderPath, secondFileName), 'utf8').split(/\r\n|\r|\n/);
31+
32+
const diffingAlgo = diffingAlgoName === 'smart' ? new SmartLinesDiffComputer() : new StandardLinesDiffComputer();
33+
34+
const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace: false, maxComputationTime: Number.MAX_SAFE_INTEGER });
35+
36+
const actualDiffingResult: DiffingResult = {
37+
originalFileName: `./${firstFileName}`,
38+
modifiedFileName: `./${secondFileName}`,
39+
diffs: diff.changes.map<IDetailedDiff>(c => ({
40+
originalRange: c.originalRange.toString(),
41+
modifiedRange: c.modifiedRange.toString(),
42+
innerChanges: c.innerChanges?.map<IDiff>(c => ({
43+
originalRange: c.originalRange.toString(),
44+
modifiedRange: c.modifiedRange.toString(),
45+
})) || null
46+
}))
47+
};
48+
49+
const expectedFilePath = join(folderPath, `${diffingAlgoName}.expected.diff.json`);
50+
const invalidFilePath = join(folderPath, `${diffingAlgoName}.invalid.diff.json`);
51+
52+
const expectedFileContentFromActual = JSON.stringify(actualDiffingResult, null, '\t');
53+
54+
const invalidExists = existsSync(invalidFilePath);
55+
56+
if (!existsSync(expectedFilePath)) {
57+
writeFileSync(expectedFilePath, expectedFileContentFromActual);
58+
writeFileSync(invalidFilePath, '');
59+
throw new Error('No expected file! Expected and invalid files were written. Delete the invalid file to make the test pass.');
60+
} else {
61+
const expectedFileContent = readFileSync(invalidExists ? invalidFilePath : expectedFilePath, 'utf8');
62+
const expectedFileDiffResult: DiffingResult = JSON.parse(expectedFileContent);
63+
64+
try {
65+
assert.deepStrictEqual(actualDiffingResult, expectedFileDiffResult);
66+
} catch (e) {
67+
if (!invalidExists) {
68+
writeFileSync(invalidFilePath, expectedFileContent);
69+
}
70+
writeFileSync(expectedFilePath, expectedFileContentFromActual);
71+
throw e;
72+
}
73+
}
74+
75+
if (invalidExists) {
76+
throw new Error('Invalid file exists and agrees with expected file! Delete the invalid file to make the test pass.');
77+
}
78+
});
79+
}
80+
}
81+
});
82+
83+
interface DiffingResult {
84+
originalFileName: string;
85+
modifiedFileName: string;
86+
87+
diffs: IDetailedDiff[];
88+
}
89+
90+
interface IDetailedDiff {
91+
originalRange: string; // [startLineNumber, endLineNumberExclusive)
92+
modifiedRange: string; // [startLineNumber, endLineNumberExclusive)
93+
innerChanges: IDiff[] | null;
94+
}
95+
96+
interface IDiff {
97+
originalRange: string; // [1,18 -> 1,19]
98+
modifiedRange: string; // [1,18 -> 1,19]
99+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 { CompareResult } from 'vs/base/common/arrays';
7+
import { autorun, derived } from 'vs/base/common/observable';
8+
import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model';
9+
import { localize } from 'vs/nls';
10+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
11+
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
12+
import { applyObservableDecorations, join } from 'vs/workbench/contrib/mergeEditor/browser/utils';
13+
import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors';
14+
import { CodeEditorView } from './codeEditorView';
15+
16+
export class ResultCodeEditorView extends CodeEditorView {
17+
private readonly decorations = derived('result.decorations', reader => {
18+
const viewModel = this.viewModel.read(reader);
19+
if (!viewModel) {
20+
return [];
21+
}
22+
const model = viewModel.model;
23+
const result = new Array<IModelDeltaDecoration>();
24+
25+
const baseRangeWithStoreAndTouchingDiffs = join(
26+
model.modifiedBaseRanges.read(reader),
27+
model.resultDiffs.read(reader),
28+
(baseRange, diff) => baseRange.baseRange.touches(diff.inputRange)
29+
? CompareResult.neitherLessOrGreaterThan
30+
: LineRange.compareByStart(
31+
baseRange.baseRange,
32+
diff.inputRange
33+
)
34+
);
35+
36+
const activeModifiedBaseRange = viewModel.activeModifiedBaseRange.read(reader);
37+
38+
for (const m of baseRangeWithStoreAndTouchingDiffs) {
39+
const modifiedBaseRange = m.left;
40+
41+
if (modifiedBaseRange) {
42+
const range = model.getRangeInResult(modifiedBaseRange.baseRange, reader).toInclusiveRange();
43+
if (range) {
44+
const blockClassNames = ['merge-editor-block'];
45+
const isHandled = model.isHandled(modifiedBaseRange).read(reader);
46+
if (isHandled) {
47+
blockClassNames.push('handled');
48+
}
49+
if (modifiedBaseRange === activeModifiedBaseRange) {
50+
blockClassNames.push('focused');
51+
}
52+
blockClassNames.push('result');
53+
54+
result.push({
55+
range,
56+
options: {
57+
isWholeLine: true,
58+
blockClassName: blockClassNames.join(' '),
59+
description: 'Result Diff',
60+
minimap: {
61+
position: MinimapPosition.Gutter,
62+
color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor },
63+
},
64+
overviewRuler: {
65+
position: OverviewRulerLane.Center,
66+
color: { id: isHandled ? handledConflictMinimapOverViewRulerColor : unhandledConflictMinimapOverViewRulerColor },
67+
}
68+
}
69+
});
70+
}
71+
}
72+
73+
for (const diff of m.rights) {
74+
const range = diff.outputRange.toInclusiveRange();
75+
if (range) {
76+
result.push({
77+
range,
78+
options: {
79+
className: `merge-editor-diff result`,
80+
description: 'Merge Editor',
81+
isWholeLine: true,
82+
}
83+
});
84+
}
85+
86+
if (diff.rangeMappings) {
87+
for (const d of diff.rangeMappings) {
88+
result.push({
89+
range: d.outputRange,
90+
options: {
91+
className: `merge-editor-diff-word result`,
92+
description: 'Merge Editor'
93+
}
94+
});
95+
}
96+
}
97+
}
98+
}
99+
return result;
100+
});
101+
102+
constructor(
103+
@IInstantiationService instantiationService: IInstantiationService
104+
) {
105+
super(instantiationService);
106+
107+
this._register(applyObservableDecorations(this.editor, this.decorations));
108+
109+
110+
this._register(autorun('update remainingConflicts label', reader => {
111+
const model = this.model.read(reader);
112+
if (!model) {
113+
return;
114+
}
115+
const count = model.unhandledConflictsCount.read(reader);
116+
117+
this.htmlElements.detail.innerText = count === 1
118+
? localize(
119+
'mergeEditor.remainingConflicts',
120+
'{0} Conflict Remaining',
121+
count
122+
)
123+
: localize(
124+
'mergeEditor.remainingConflict',
125+
'{0} Conflicts Remaining ',
126+
count
127+
);
128+
129+
}));
130+
}
131+
}

0 commit comments

Comments
 (0)