Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/test/node/utils.spec.ts
13399 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 * as assert from 'assert';
7
import { expect, suite, test } from 'vitest';
8
import { EditSurvivalTracker, applyEditsToRanges, compute4GramTextSimilarity } from '../../../platform/editSurvivalTracking/common/editSurvivalTracker';
9
import { ISerializedStringEdit, StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';
10
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
11
12
suite('OffsetEdit.join', () => {
13
for (let seed = 0; seed < 50; seed++) {
14
test('test' + seed, () => {
15
runTest(seed);
16
});
17
}
18
});
19
20
function runTest(seed: number) {
21
const rng = new MersenneTwister(seed);
22
23
const s0 = 'abcde\nfghij\nklmno\npqrst\n';
24
25
const edits1 = getRandomEdits(s0, rng.nextIntRange(1, 4), rng);
26
const s1 = edits1.apply(s0);
27
28
const edits2 = getRandomEdits(s1, rng.nextIntRange(1, 4), rng);
29
const s2 = edits2.apply(s1);
30
31
const combinedEdits = edits1.compose(edits2);
32
const s2C = combinedEdits.apply(s0);
33
34
assert.strictEqual(s2C, s2);
35
}
36
37
function getRandomEdits(str: string, count: number, rng: MersenneTwister): StringEdit {
38
const edits: StringReplacement[] = [];
39
let i = 0;
40
for (let j = 0; j < count; j++) {
41
if (i >= str.length) {
42
break;
43
}
44
edits.push(getRandomEdit(str, i, rng));
45
i = edits[j].replaceRange.endExclusive + 1;
46
}
47
return new StringEdit(edits);
48
}
49
50
function getRandomEdit(str: string, rangeOffsetStart: number, rng: MersenneTwister): StringReplacement {
51
const offsetStart = rng.nextIntRange(rangeOffsetStart, str.length);
52
const offsetEnd = rng.nextIntRange(offsetStart, str.length);
53
54
const textStart = rng.nextIntRange(0, str.length);
55
const textLen = rng.nextIntRange(0, Math.min(7, str.length - textStart));
56
57
return new StringReplacement(
58
new OffsetRange(offsetStart, offsetEnd),
59
str.substring(textStart, textStart + textLen)
60
);
61
}
62
63
// Generated by copilot
64
class MersenneTwister {
65
private readonly mt = new Array(624);
66
private index = 0;
67
68
constructor(seed: number) {
69
this.mt[0] = seed >>> 0;
70
for (let i = 1; i < 624; i++) {
71
const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
72
this.mt[i] = (((((s & 0xffff0000) >>> 16) * 0x6c078965) << 16) + (s & 0x0000ffff) * 0x6c078965 + i) >>> 0;
73
}
74
}
75
76
public nextInt() {
77
if (this.index === 0) {
78
this.generateNumbers();
79
}
80
81
let y = this.mt[this.index];
82
y = y ^ (y >>> 11);
83
y = y ^ ((y << 7) & 0x9d2c5680);
84
y = y ^ ((y << 15) & 0xefc60000);
85
y = y ^ (y >>> 18);
86
87
this.index = (this.index + 1) % 624;
88
89
return y >>> 0;
90
}
91
92
public nextIntRange(start: number, endExclusive: number) {
93
const range = endExclusive - start;
94
return Math.floor(this.nextInt() / (0x100000000 / range)) + start;
95
}
96
97
private generateNumbers() {
98
for (let i = 0; i < 624; i++) {
99
const y = (this.mt[i] & 0x80000000) + (this.mt[(i + 1) % 624] & 0x7fffffff);
100
this.mt[i] = this.mt[(i + 397) % 624] ^ (y >>> 1);
101
if ((y % 2) !== 0) {
102
this.mt[i] = this.mt[i] ^ 0x9908b0df;
103
}
104
}
105
}
106
}
107
108
const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi.`;
109
110
function getRandomString(rng: MersenneTwister): string {
111
let result = '';
112
for (let i = 0; i < 4; i++) {
113
const start = rng.nextIntRange(0, loremIpsum.length);
114
const end = rng.nextIntRange(start, loremIpsum.length);
115
result += loremIpsum.substring(start, end);
116
}
117
return result;
118
}
119
120
suite('applyEditsToRanges', () => {
121
test('edit after ranges', () => {
122
const ranges = [
123
new OffsetRange(10, 20),
124
new OffsetRange(30, 40),
125
new OffsetRange(50, 60),
126
];
127
128
const edits = new StringEdit([
129
new StringReplacement(new OffsetRange(100, 110), 'abc'),
130
]);
131
132
const newRanges = applyEditsToRanges(ranges, edits);
133
assert.deepStrictEqual(newRanges.map(r => r.toString()), [
134
'[10, 20)',
135
'[30, 40)',
136
'[50, 60)',
137
]);
138
});
139
140
test('edit before ranges', () => {
141
const ranges = [
142
new OffsetRange(10, 20),
143
new OffsetRange(30, 40),
144
new OffsetRange(50, 60),
145
];
146
147
const edits = new StringEdit([
148
new StringReplacement(new OffsetRange(5, 6), 'abc'),
149
]);
150
151
const newRanges = applyEditsToRanges(ranges, edits);
152
assert.deepStrictEqual(newRanges.map(r => r.toString()), [
153
'[12, 22)',
154
'[32, 42)',
155
'[52, 62)',
156
]);
157
});
158
159
test('edit in range', () => {
160
const ranges = [
161
new OffsetRange(10, 20),
162
new OffsetRange(30, 40),
163
new OffsetRange(50, 60),
164
];
165
166
const edits = new StringEdit([
167
new StringReplacement(new OffsetRange(11, 19), 'x'),
168
]);
169
170
const newRanges = applyEditsToRanges(ranges, edits);
171
assert.deepStrictEqual(newRanges.map(r => r.toString()), [
172
'[10, 13)',
173
'[23, 33)',
174
'[43, 53)',
175
]);
176
});
177
178
test('edit in multiple ranges', () => {
179
const ranges = [
180
new OffsetRange(10, 20),
181
new OffsetRange(30, 40),
182
new OffsetRange(50, 60),
183
];
184
185
const edits = new StringEdit([
186
new StringReplacement(new OffsetRange(15, 55), 'x'),
187
]);
188
189
const newRanges = applyEditsToRanges(ranges, edits);
190
assert.deepStrictEqual(newRanges.map(r => r.toString()), [
191
'[10, 16)',
192
'[16, 16)',
193
'[16, 21)',
194
]);
195
});
196
197
test('edit in multiple ranges 2', () => {
198
const ranges = [
199
new OffsetRange(10, 20),
200
new OffsetRange(30, 40),
201
new OffsetRange(50, 60),
202
];
203
204
const edits = new StringEdit([
205
new StringReplacement(new OffsetRange(15, 55), 'x'),
206
new StringReplacement(new OffsetRange(58, 59), 'yy'),
207
]);
208
209
const newRanges = applyEditsToRanges(ranges, edits);
210
assert.deepStrictEqual(newRanges.map(r => r.toString()), [
211
'[10, 16)',
212
'[16, 16)',
213
'[16, 22)',
214
]);
215
});
216
217
test('touching edit', () => {
218
const ranges = [
219
new OffsetRange(10, 20),
220
new OffsetRange(30, 40),
221
new OffsetRange(50, 60),
222
];
223
224
const edits = new StringEdit([
225
new StringReplacement(new OffsetRange(40, 40), 'x'),
226
new StringReplacement(new OffsetRange(50, 50), 'x'),
227
]);
228
229
const newRanges = applyEditsToRanges(ranges, edits);
230
assert.deepStrictEqual(newRanges.map(r => r.toString()), [
231
'[10, 20)',
232
'[30, 41)',
233
'[51, 62)'
234
]);
235
});
236
});
237
238
239
function projectableValue_editable<T>(arg: T): T {
240
return arg;
241
}
242
243
244
suite('compute4GramTextSimilarity', () => {
245
for (let seed = 0; seed < 50; seed++) {
246
test('test' + seed, () => {
247
runTest(seed);
248
});
249
}
250
251
function runTest(seed: number) {
252
const rng = new MersenneTwister(seed);
253
254
const s1 = getRandomString(rng);
255
const s2 = getRandomString(rng);
256
257
const similarity = compute4GramTextSimilarity(s1, s2);
258
259
assert.ok(similarity >= 0 && similarity <= 1, `similarity should be between 0 and 1, but was ${similarity}`);
260
}
261
});
262
263
suite('EditSurvivalTracker', () => {
264
function renameProps<T extends object>(obj: T, map: { [K in keyof T]?: string }): any {
265
const result: any = {};
266
for (const key of Object.keys(obj) as (keyof T)[]) {
267
const newKey = map[key] || key;
268
result[newKey] = obj[key];
269
}
270
return result;
271
}
272
273
function getScore(input: { text: string; edits: ISerializedStringEdit[] }): unknown {
274
const originalText = input.text;
275
const t = new EditSurvivalTracker(originalText, StringEdit.fromJson(input.edits[0]));
276
t.handleEdits(StringEdit.fromJson(input.edits[1]));
277
const score = t.computeTrackedEditsSurvivalScore();
278
return renameProps(score, {
279
textBeforeAiEdits: 'text1BeforeAiEdits',
280
textAfterAiEdits: 'text2AfterAiEdits',
281
textAfterUserEdits: 'text3AfterUserEdits',
282
});
283
}
284
285
test('simple', async () => {
286
expect(
287
getScore(projectableValue_editable({
288
'text': 'console.log(123456);',
289
'edits': [
290
[
291
{
292
'pos': 12,
293
'len': 6,
294
'txt': `'hello'`
295
}
296
],
297
[
298
{
299
'pos': 12,
300
'len': 7,
301
'txt': `'Hello'`
302
}
303
]
304
],
305
'x-editor': 'edit-editor'
306
})),
307
).toMatchInlineSnapshot(`
308
{
309
"fourGram": 0.5,
310
"noRevert": 1,
311
"text1BeforeAiEdits": [
312
"123456",
313
],
314
"text2AfterAiEdits": [
315
"'hello'",
316
],
317
"text3AfterUserEdits": [
318
"'Hello'",
319
],
320
}
321
`);
322
});
323
324
test('multi edit', async () => {
325
expect(
326
getScore(projectableValue_editable({
327
'text': 'console.log(123456);',
328
'edits': [
329
[
330
{
331
'pos': 0,
332
'len': 0,
333
'txt': '// comment\\n'
334
},
335
{
336
'pos': 12,
337
'len': 6,
338
'txt': `'hello'`
339
}
340
],
341
[
342
{
343
'pos': 0,
344
'len': 2,
345
'txt': '/*'
346
},
347
{
348
'pos': 10,
349
'len': 2,
350
'txt': ' */'
351
},
352
{
353
'pos': 25,
354
'len': 7,
355
'txt': 'Hello'
356
}
357
]
358
],
359
'x-editor': 'edit-editor'
360
})),
361
).toMatchInlineSnapshot(`
362
{
363
"fourGram": 0.4376731301939058,
364
"noRevert": 1,
365
"text1BeforeAiEdits": [
366
"",
367
"123456",
368
],
369
"text2AfterAiEdits": [
370
"// comment\\n",
371
"'hello'",
372
],
373
"text3AfterUserEdits": [
374
"/* comment */",
375
"'Hello",
376
],
377
}
378
`);
379
});
380
381
test('realistic example', async () => {
382
expect(
383
getScore(projectableValue_editable({
384
'text': `import {\r\n\tTextDocument,\r\n\tWebviewPanel,\r\n\tCancellationToken,\r\n\tworkspace,\r\n\tWorkspaceEdit,\r\n\tRange,\r\n\tCustomTextEditorProvider,\r\n} from "vscode";\r\nimport { WebviewInitializer } from "./WebviewInitializer";\r\n\r\ninterface EditableDocument {\r\n\t"x-editable"?: {\r\n\t\tkind: string;\r\n\t\tdefaultUrl: string;\r\n\t};\r\n}\r\n\r\nexport class TextEditorProvider implements CustomTextEditorProvider {\r\n\tconstructor(private readonly webviewInitializer: WebviewInitializer) {}\r\n\r\n\tpublic async resolveCustomTextEditor(\r\n\t\tdocument: TextDocument,\r\n\t\twebviewPanel: WebviewPanel,\r\n\t\ttoken: CancellationToken\r\n\t): Promise<void> {\r\n\t\tlet isThisEditorSaving = false;\r\n\r\n\t\tconst text = document.getText();\r\n\t\tconst doc = JSON.parse(text) as EditableDocument;\r\n\t\tconst args = doc["x-editable"];\r\n\r\n\r\n\t\tconst bridge = this.webviewInitializer.setupWebview(\r\n\t\t\t{ editorUrl: args.defaultUrl },\r\n\t\t\twebviewPanel.webview\r\n\t\t);\r\n\r\n\t\tconst setContentFromDocument = () => {\r\n\t\t\tconst newText = document.getText();\r\n\t\t\tconst content = JSON.parse(newText);\r\n\t\t\tbridge.setContent(content);\r\n\t\t};\r\n\r\n\t\tworkspace.onDidChangeTextDocument(async (evt) => {\r\n\t\t\tif (evt.document !== document) {\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tif (isThisEditorSaving) {\r\n\t\t\t\t// We don't want to integrate our own changes\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tif (evt.contentChanges.length === 0) {\r\n\t\t\t\t// Sometimes VS Code reports a document change without a change.\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tsetContentFromDocument();\r\n\t\t});\r\n\r\n\t\tbridge.onChange.sub(async ({ newContent }) => {\r\n\t\t\tconst workspaceEdit = new WorkspaceEdit();\r\n\t\t\tconst data = newContent as EditableDocument;\r\n\t\t\tif (!data['x-editable']) {\r\n\t\t\t\tdata['x-editable'] = args;\r\n\t\t\t}\r\n\t\t\tconst output = JSON.stringify(newContent, undefined, 4);\r\n\t\t\tworkspaceEdit.replace(\r\n\t\t\t\tdocument.uri,\r\n\t\t\t\tnew Range(0, 0, document.lineCount, 0),\r\n\t\t\t\toutput\r\n\t\t\t);\r\n\r\n\t\t\tisThisEditorSaving = true;\r\n\t\t\ttry {\r\n\t\t\t\tawait workspace.applyEdit(workspaceEdit);\r\n\t\t\t} finally {\r\n\t\t\t\tisThisEditorSaving = false;\r\n\t\t\t}\r\n\t\t});\r\n\r\n\t\tbridge.onInit.sub(() => {\r\n\t\t\tsetContentFromDocument();\r\n\t\t});\r\n\t}\r\n}\r\n`,
385
'edits': [
386
[
387
{
388
'pos': 762,
389
'len': 2,
390
'txt': '\r\n\r\n\t\tif (!args) {\r\n\t\t\tthrow new Error("invalid json document!");\r\n\t\t}'
391
}
392
],
393
[
394
{
395
'pos': 801,
396
'len': 24,
397
'txt': '""'
398
}
399
]
400
],
401
'x-editor': 'edit-editor'
402
}))).toMatchInlineSnapshot(
403
`
404
{
405
"fourGram": 0.75,
406
"noRevert": 1,
407
"text1BeforeAiEdits": [
408
"
409
",
410
],
411
"text2AfterAiEdits": [
412
"
413
414
if (!args) {
415
throw new Error("invalid json document!");
416
}",
417
],
418
"text3AfterUserEdits": [
419
"
420
421
if (!args) {
422
throw new Error("");
423
}",
424
],
425
}
426
`);
427
});
428
});
429
430
suite('OffsetEdits', () => {
431
suite('removeCommonSuffixPrefix', () => {
432
test('simple', () => {
433
const str = 'abcde';
434
435
const e = new StringEdit([
436
new StringReplacement(new OffsetRange(0, 2), 'ax'),
437
new StringReplacement(new OffsetRange(2, 5), 'cye'),
438
]);
439
440
const e2 = e.removeCommonSuffixPrefix(str);
441
442
assert.deepStrictEqual(e2.apply(str), e.apply(str));
443
assert.deepStrictEqual(e2.toString(), '[[1, 2) -> "x", [3, 4) -> "y"]');
444
});
445
446
for (let seed = 0; seed < 50; seed++) {
447
test('test' + seed, () => {
448
const rng = new MersenneTwister(seed);
449
450
const s0 = loremIpsum;
451
452
const edits = getRandomEdits(s0, rng.nextIntRange(1, 4), rng);
453
const edits2 = edits.removeCommonSuffixPrefix(s0);
454
455
assert.deepStrictEqual(edits2.apply(s0), edits.apply(s0));
456
});
457
}
458
});
459
});
460
461