Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/inlineEdits/test/node/edits.spec.ts
13405 views
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 from 'assert';
7
import { expect, suite, test } from 'vitest';
8
import { range } from '../../../../util/vs/base/common/arrays';
9
import { splitLines } from '../../../../util/vs/base/common/strings';
10
import { LineEdit, LineReplacement } from '../../../../util/vs/editor/common/core/edits/lineEdit';
11
import { StringEdit, StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';
12
import { TextReplacement } from '../../../../util/vs/editor/common/core/edits/textEdit';
13
import { LineRange } from '../../../../util/vs/editor/common/core/ranges/lineRange';
14
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
15
import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';
16
import { RootedEdit } from '../../common/dataTypes/edit';
17
import { decomposeStringEdit } from '../../common/dataTypes/editUtils';
18
import { Permutation } from '../../common/dataTypes/permutation';
19
import { RootedLineEdit } from '../../common/dataTypes/rootedLineEdit';
20
import { Random, sequenceGenerator } from './random';
21
22
suite('Edit <-> LineEdit equivalence', () => {
23
for (let i = 0; i < 100; i++) {
24
test('case' + i, () => {
25
testWithSeed(i);
26
});
27
}
28
29
test.skip('fuzz', () => {
30
for (let i = 0; i < 1_000_000; i++) {
31
testWithSeed(i);
32
}
33
});
34
35
function testWithSeed(seed: number) {
36
const rand = Random.create(seed);
37
const lineCount = rand.nextIntRange(1, 4);
38
const str = rand.nextMultiLineString(lineCount, new OffsetRange(0, 5));
39
const editCount = rand.nextIntRange(1, 4);
40
const randomOffsetEdit = rand.nextOffsetEdit(str, editCount);
41
const randomEdit = randomOffsetEdit;
42
43
const rootedEdit = new RootedEdit(new StringText(str), randomEdit);
44
const editApplied = rootedEdit.getEditedState().value;
45
46
const rootedLineEdit = RootedLineEdit.fromEdit(rootedEdit);
47
const lineEditApplied = rootedLineEdit.getEditedState().join('\n');
48
const editFromLineEditApplied = rootedLineEdit.toRootedEdit().getEditedState().value;
49
50
assert.deepStrictEqual(lineEditApplied, editApplied);
51
assert.deepStrictEqual(editFromLineEditApplied, editApplied);
52
}
53
});
54
55
suite('Edit.compose', () => {
56
for (let i = 0; i < 1000; i++) {
57
test('case' + i, () => {
58
runTest(i);
59
});
60
}
61
62
test.skip('fuzz', () => {
63
for (let i = 0; i < 1_000_000; i++) {
64
runTest(i);
65
}
66
});
67
68
function runTest(seed: number) {
69
const rng = Random.create(seed);
70
71
const s0 = 'abcde\nfghij\nklmno\npqrst\n';
72
73
const edits1 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng);
74
const s1 = edits1.apply(s0);
75
76
const edits2 = getRandomEdit(s1, rng.nextIntRange(1, 4), rng);
77
const s2 = edits2.apply(s1);
78
79
const combinedEdits = edits1.compose(edits2);
80
const s2C = combinedEdits.apply(s0);
81
82
assert.strictEqual(s2C, s2);
83
}
84
});
85
86
87
function getRandomEdit(str: string, count: number, rng: Random): StringEdit {
88
const edits: StringReplacement[] = [];
89
let i = 0;
90
for (let j = 0; j < count; j++) {
91
if (i >= str.length) {
92
break;
93
}
94
edits.push(getRandomSingleEdit(str, i, rng));
95
i = edits[j].replaceRange.endExclusive + 1;
96
}
97
return StringEdit.create(edits);
98
}
99
100
function getRandomSingleEdit(str: string, rangeOffsetStart: number, rng: Random): StringReplacement {
101
const offsetStart = rng.nextIntRange(rangeOffsetStart, str.length);
102
const offsetEnd = rng.nextIntRange(offsetStart, str.length);
103
104
const textStart = rng.nextIntRange(0, str.length);
105
const textLen = rng.nextIntRange(0, Math.min(7, str.length - textStart));
106
107
return StringReplacement.replace(
108
new OffsetRange(offsetStart, offsetEnd),
109
str.substring(textStart, textStart + textLen)
110
);
111
}
112
113
suite('LineEdit', () => {
114
suite('fromSingleTextEdit', () => {
115
for (let i = 0; i < 100; i++) {
116
test('case' + i, () => {
117
testWithSeed(i);
118
});
119
}
120
121
test.skip('fuzz', () => {
122
for (let i = 0; i < 1_000_000; i++) {
123
testWithSeed(i);
124
}
125
});
126
127
function testWithSeed(seed: number) {
128
const rand = Random.create(seed);
129
const lineCount = rand.nextIntRange(1, 4);
130
// Use unique letters to such that .shrink can be tested
131
const str = rand.nextMultiLineString(lineCount, new OffsetRange(0, 5), sequenceGenerator([...Random.alphabetUppercase]));
132
133
let randomOffsetEdit = rand.nextSingleOffsetEdit(str, Random.alphabetSmallLowercase + '\n');
134
randomOffsetEdit = randomOffsetEdit.removeCommonSuffixPrefix(str);
135
const randomEdit = randomOffsetEdit;
136
137
const strVal = new StringText(str);
138
139
const singleTextEdit = TextReplacement.fromStringReplacement(randomEdit, strVal);
140
const singleLineEdit1 = LineReplacement.fromSingleTextEdit(singleTextEdit, strVal);
141
142
const extendedEdit = singleTextEdit.extendToFullLine(strVal);
143
const singleLineEdit2Full = new LineReplacement(
144
new LineRange(extendedEdit.range.startLineNumber, extendedEdit.range.endLineNumber + 1),
145
splitLines(extendedEdit.text)
146
);
147
const singleLineEdit2 = singleLineEdit2Full.removeCommonSuffixPrefixLines(strVal);
148
149
if (singleLineEdit1.lineRange.isEmpty && singleLineEdit2.lineRange.isEmpty
150
&& singleLineEdit1.newLines.length === 0 && singleLineEdit2.newLines.length === 0) {
151
return;
152
}
153
154
assert.deepStrictEqual(singleLineEdit1, singleLineEdit2);
155
}
156
});
157
158
suite('RootedLineEdit.toString', () => {
159
test('format normal edit 1', () => {
160
const lineEdit = new RootedLineEdit(
161
new StringText('abc\ndef\nghi'),
162
new LineEdit([new LineReplacement(new LineRange(2, 3), ['xyz'])])
163
);
164
expect(lineEdit.toString()).toMatchInlineSnapshot(`
165
" 1 1 abc
166
- 2 def
167
+ 2 xyz
168
3 3 ghi"
169
`);
170
});
171
172
test('format normal edit 2', () => {
173
const lineEdit = new RootedLineEdit(
174
new StringText('abc\ndef\nghi'),
175
new LineEdit([new LineReplacement(new LineRange(3, 4), ['xyz'])])
176
);
177
expect(lineEdit.toString()).toMatchInlineSnapshot(`
178
" 1 1 abc
179
2 2 def
180
- 3 ghi
181
+ 3 xyz"
182
`);
183
});
184
185
test('format invalid edit', () => {
186
const lineEdit = new RootedLineEdit(
187
new StringText('abc\ndef\nghi'),
188
new LineEdit([new LineReplacement(new LineRange(4, 5), ['xyz'])])
189
);
190
expect(lineEdit.toString()).toMatchInlineSnapshot(`
191
" 2 2 def
192
3 3 ghi
193
- 4 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]
194
+ 4 xyz"
195
`);
196
});
197
198
test('format invalid edit', () => {
199
const lineEdit = new RootedLineEdit(
200
new StringText('abc\ndef\nghi'),
201
new LineEdit([new LineReplacement(new LineRange(6, 7), ['xyz'])])
202
);
203
expect(lineEdit.toString()).toMatchInlineSnapshot(`
204
" 4 4 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]
205
5 5 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]
206
- 6 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]
207
+ 6 xyz"
208
`);
209
});
210
});
211
});
212
213
suite('Edit#decompose', () => {
214
test('', () => {
215
const edit = StringEdit.create([
216
StringReplacement.replace(new OffsetRange(0, 5), '12345'),
217
StringReplacement.replace(new OffsetRange(10, 12), ''),
218
]);
219
220
expect(decomposeStringEdit(edit).edits.toString()).toMatchInlineSnapshot(`"[0, 5) -> "12345",[10, 12) -> """`);
221
});
222
223
test('1', () => {
224
const edit = StringEdit.create([
225
StringReplacement.replace(new OffsetRange(0, 5), '12345'),
226
StringReplacement.replace(new OffsetRange(10, 12), ''),
227
]);
228
229
expect(decomposeStringEdit(edit, new Permutation([1, 0])).edits.toString()).toMatchInlineSnapshot(`"[10, 12) -> "",[0, 5) -> "12345""`);
230
});
231
232
test('2', () => {
233
const edit = StringEdit.create([
234
StringReplacement.replace(new OffsetRange(0, 5), '12345'),
235
StringReplacement.replace(new OffsetRange(10, 22), ''),
236
StringReplacement.replace(new OffsetRange(23, 24), ''),
237
]);
238
239
const decomposedEdits = decomposeStringEdit(edit, new Permutation([1, 0, 2]));
240
241
const recomposedEdits = decomposedEdits.compose();
242
243
expect(decomposedEdits.edits.toString()).toMatchInlineSnapshot(`"[10, 22) -> "",[0, 5) -> "12345",[11, 12) -> """`);
244
expect(edit.toString()).toStrictEqual(recomposedEdits.toString());
245
});
246
247
test.each(range(100))('fuzzing %i', (i) => {
248
const rand = Random.create(i);
249
const strLength = rand.nextIntRange(1, 100);
250
const str = rand.nextString(strLength);
251
const editCount = rand.nextIntRange(1, 10);
252
const randomOffsetEdit = rand.nextOffsetEdit(str, editCount);
253
const randomEdit = randomOffsetEdit;
254
255
const shuffledEdits = shuffle(range(randomEdit.replacements.length), i);
256
const decomposedEdits = decomposeStringEdit(randomEdit, shuffledEdits);
257
const recomposedEdits = decomposedEdits.compose();
258
259
expect(randomEdit.toString()).toStrictEqual(recomposedEdits.toString());
260
});
261
});
262
263
export function shuffle<T>(array: T[], _seed?: number): Permutation {
264
let rand: () => number;
265
const indexMap = array.map((_, i) => i); // Create an index map that will be shuffled
266
267
if (typeof _seed === 'number') {
268
let seed = _seed;
269
// Seeded random number generator in JS
270
rand = () => {
271
const x = Math.sin(seed++) * 179426549;
272
return x - Math.floor(x);
273
};
274
} else {
275
rand = Math.random;
276
}
277
278
for (let i = indexMap.length - 1; i > 0; i -= 1) {
279
const j = Math.floor(rand() * (i + 1));
280
281
// Swap elements in the index map
282
[indexMap[i], indexMap[j]] = [indexMap[j], indexMap[i]];
283
}
284
285
// Return a new Permutation instance based on the shuffled index map
286
return new Permutation(indexMap);
287
}
288
289