Path: blob/main/extensions/copilot/src/platform/inlineEdits/test/node/edits.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { expect, suite, test } from 'vitest';7import { range } from '../../../../util/vs/base/common/arrays';8import { splitLines } from '../../../../util/vs/base/common/strings';9import { LineEdit, LineReplacement } from '../../../../util/vs/editor/common/core/edits/lineEdit';10import { StringEdit, StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';11import { TextReplacement } from '../../../../util/vs/editor/common/core/edits/textEdit';12import { LineRange } from '../../../../util/vs/editor/common/core/ranges/lineRange';13import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';14import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';15import { RootedEdit } from '../../common/dataTypes/edit';16import { decomposeStringEdit } from '../../common/dataTypes/editUtils';17import { Permutation } from '../../common/dataTypes/permutation';18import { RootedLineEdit } from '../../common/dataTypes/rootedLineEdit';19import { Random, sequenceGenerator } from './random';2021suite('Edit <-> LineEdit equivalence', () => {22for (let i = 0; i < 100; i++) {23test('case' + i, () => {24testWithSeed(i);25});26}2728test.skip('fuzz', () => {29for (let i = 0; i < 1_000_000; i++) {30testWithSeed(i);31}32});3334function testWithSeed(seed: number) {35const rand = Random.create(seed);36const lineCount = rand.nextIntRange(1, 4);37const str = rand.nextMultiLineString(lineCount, new OffsetRange(0, 5));38const editCount = rand.nextIntRange(1, 4);39const randomOffsetEdit = rand.nextOffsetEdit(str, editCount);40const randomEdit = randomOffsetEdit;4142const rootedEdit = new RootedEdit(new StringText(str), randomEdit);43const editApplied = rootedEdit.getEditedState().value;4445const rootedLineEdit = RootedLineEdit.fromEdit(rootedEdit);46const lineEditApplied = rootedLineEdit.getEditedState().join('\n');47const editFromLineEditApplied = rootedLineEdit.toRootedEdit().getEditedState().value;4849assert.deepStrictEqual(lineEditApplied, editApplied);50assert.deepStrictEqual(editFromLineEditApplied, editApplied);51}52});5354suite('Edit.compose', () => {55for (let i = 0; i < 1000; i++) {56test('case' + i, () => {57runTest(i);58});59}6061test.skip('fuzz', () => {62for (let i = 0; i < 1_000_000; i++) {63runTest(i);64}65});6667function runTest(seed: number) {68const rng = Random.create(seed);6970const s0 = 'abcde\nfghij\nklmno\npqrst\n';7172const edits1 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng);73const s1 = edits1.apply(s0);7475const edits2 = getRandomEdit(s1, rng.nextIntRange(1, 4), rng);76const s2 = edits2.apply(s1);7778const combinedEdits = edits1.compose(edits2);79const s2C = combinedEdits.apply(s0);8081assert.strictEqual(s2C, s2);82}83});848586function getRandomEdit(str: string, count: number, rng: Random): StringEdit {87const edits: StringReplacement[] = [];88let i = 0;89for (let j = 0; j < count; j++) {90if (i >= str.length) {91break;92}93edits.push(getRandomSingleEdit(str, i, rng));94i = edits[j].replaceRange.endExclusive + 1;95}96return StringEdit.create(edits);97}9899function getRandomSingleEdit(str: string, rangeOffsetStart: number, rng: Random): StringReplacement {100const offsetStart = rng.nextIntRange(rangeOffsetStart, str.length);101const offsetEnd = rng.nextIntRange(offsetStart, str.length);102103const textStart = rng.nextIntRange(0, str.length);104const textLen = rng.nextIntRange(0, Math.min(7, str.length - textStart));105106return StringReplacement.replace(107new OffsetRange(offsetStart, offsetEnd),108str.substring(textStart, textStart + textLen)109);110}111112suite('LineEdit', () => {113suite('fromSingleTextEdit', () => {114for (let i = 0; i < 100; i++) {115test('case' + i, () => {116testWithSeed(i);117});118}119120test.skip('fuzz', () => {121for (let i = 0; i < 1_000_000; i++) {122testWithSeed(i);123}124});125126function testWithSeed(seed: number) {127const rand = Random.create(seed);128const lineCount = rand.nextIntRange(1, 4);129// Use unique letters to such that .shrink can be tested130const str = rand.nextMultiLineString(lineCount, new OffsetRange(0, 5), sequenceGenerator([...Random.alphabetUppercase]));131132let randomOffsetEdit = rand.nextSingleOffsetEdit(str, Random.alphabetSmallLowercase + '\n');133randomOffsetEdit = randomOffsetEdit.removeCommonSuffixPrefix(str);134const randomEdit = randomOffsetEdit;135136const strVal = new StringText(str);137138const singleTextEdit = TextReplacement.fromStringReplacement(randomEdit, strVal);139const singleLineEdit1 = LineReplacement.fromSingleTextEdit(singleTextEdit, strVal);140141const extendedEdit = singleTextEdit.extendToFullLine(strVal);142const singleLineEdit2Full = new LineReplacement(143new LineRange(extendedEdit.range.startLineNumber, extendedEdit.range.endLineNumber + 1),144splitLines(extendedEdit.text)145);146const singleLineEdit2 = singleLineEdit2Full.removeCommonSuffixPrefixLines(strVal);147148if (singleLineEdit1.lineRange.isEmpty && singleLineEdit2.lineRange.isEmpty149&& singleLineEdit1.newLines.length === 0 && singleLineEdit2.newLines.length === 0) {150return;151}152153assert.deepStrictEqual(singleLineEdit1, singleLineEdit2);154}155});156157suite('RootedLineEdit.toString', () => {158test('format normal edit 1', () => {159const lineEdit = new RootedLineEdit(160new StringText('abc\ndef\nghi'),161new LineEdit([new LineReplacement(new LineRange(2, 3), ['xyz'])])162);163expect(lineEdit.toString()).toMatchInlineSnapshot(`164" 1 1 abc165- 2 def166+ 2 xyz1673 3 ghi"168`);169});170171test('format normal edit 2', () => {172const lineEdit = new RootedLineEdit(173new StringText('abc\ndef\nghi'),174new LineEdit([new LineReplacement(new LineRange(3, 4), ['xyz'])])175);176expect(lineEdit.toString()).toMatchInlineSnapshot(`177" 1 1 abc1782 2 def179- 3 ghi180+ 3 xyz"181`);182});183184test('format invalid edit', () => {185const lineEdit = new RootedLineEdit(186new StringText('abc\ndef\nghi'),187new LineEdit([new LineReplacement(new LineRange(4, 5), ['xyz'])])188);189expect(lineEdit.toString()).toMatchInlineSnapshot(`190" 2 2 def1913 3 ghi192- 4 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]193+ 4 xyz"194`);195});196197test('format invalid edit', () => {198const lineEdit = new RootedLineEdit(199new StringText('abc\ndef\nghi'),200new LineEdit([new LineReplacement(new LineRange(6, 7), ['xyz'])])201);202expect(lineEdit.toString()).toMatchInlineSnapshot(`203" 4 4 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]2045 5 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]205- 6 [[[[[ WARNING: LINE DOES NOT EXIST ]]]]]206+ 6 xyz"207`);208});209});210});211212suite('Edit#decompose', () => {213test('', () => {214const edit = StringEdit.create([215StringReplacement.replace(new OffsetRange(0, 5), '12345'),216StringReplacement.replace(new OffsetRange(10, 12), ''),217]);218219expect(decomposeStringEdit(edit).edits.toString()).toMatchInlineSnapshot(`"[0, 5) -> "12345",[10, 12) -> """`);220});221222test('1', () => {223const edit = StringEdit.create([224StringReplacement.replace(new OffsetRange(0, 5), '12345'),225StringReplacement.replace(new OffsetRange(10, 12), ''),226]);227228expect(decomposeStringEdit(edit, new Permutation([1, 0])).edits.toString()).toMatchInlineSnapshot(`"[10, 12) -> "",[0, 5) -> "12345""`);229});230231test('2', () => {232const edit = StringEdit.create([233StringReplacement.replace(new OffsetRange(0, 5), '12345'),234StringReplacement.replace(new OffsetRange(10, 22), ''),235StringReplacement.replace(new OffsetRange(23, 24), ''),236]);237238const decomposedEdits = decomposeStringEdit(edit, new Permutation([1, 0, 2]));239240const recomposedEdits = decomposedEdits.compose();241242expect(decomposedEdits.edits.toString()).toMatchInlineSnapshot(`"[10, 22) -> "",[0, 5) -> "12345",[11, 12) -> """`);243expect(edit.toString()).toStrictEqual(recomposedEdits.toString());244});245246test.each(range(100))('fuzzing %i', (i) => {247const rand = Random.create(i);248const strLength = rand.nextIntRange(1, 100);249const str = rand.nextString(strLength);250const editCount = rand.nextIntRange(1, 10);251const randomOffsetEdit = rand.nextOffsetEdit(str, editCount);252const randomEdit = randomOffsetEdit;253254const shuffledEdits = shuffle(range(randomEdit.replacements.length), i);255const decomposedEdits = decomposeStringEdit(randomEdit, shuffledEdits);256const recomposedEdits = decomposedEdits.compose();257258expect(randomEdit.toString()).toStrictEqual(recomposedEdits.toString());259});260});261262export function shuffle<T>(array: T[], _seed?: number): Permutation {263let rand: () => number;264const indexMap = array.map((_, i) => i); // Create an index map that will be shuffled265266if (typeof _seed === 'number') {267let seed = _seed;268// Seeded random number generator in JS269rand = () => {270const x = Math.sin(seed++) * 179426549;271return x - Math.floor(x);272};273} else {274rand = Math.random;275}276277for (let i = indexMap.length - 1; i > 0; i -= 1) {278const j = Math.floor(rand() * (i + 1));279280// Swap elements in the index map281[indexMap[i], indexMap[j]] = [indexMap[j], indexMap[i]];282}283284// Return a new Permutation instance based on the shuffled index map285return new Permutation(indexMap);286}287288289