Path: blob/main/extensions/copilot/src/platform/inlineEdits/test/common/statelessNextEditProviers.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 { describe, expect, it } from 'vitest';6import { LineEdit, LineReplacement } from '../../../../util/vs/editor/common/core/edits/lineEdit';7import { StringEdit, StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';8import { LineRange } from '../../../../util/vs/editor/common/core/ranges/lineRange';9import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';10import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';11import { Edits } from '../../common/dataTypes/edit';12import { StatelessNextEditDocument } from '../../common/statelessNextEditProvider';13import { editWouldDeleteWhatWasJustInserted2, IgnoreWhitespaceOnlyChanges } from '../../common/statelessNextEditProviders';1415describe('IgnoreFormattingChangesAspect', () => {16// Helper to create test cases with less boilerplate17function createEdit(baseLines: string[], newLines: string[]): LineReplacement {18return new LineReplacement(new LineRange(1, baseLines.length + 1), newLines);19}2021function isFormattingOnly(base: string[], edited: string[]): boolean {22return IgnoreWhitespaceOnlyChanges._isFormattingOnlyChange(base, createEdit(base, edited));23}2425// Test the core algorithm: formatting-only changes preserve content after whitespace removal26it('identifies formatting vs content changes correctly', () => {27// Formatting-only: content identical after removing whitespace28expect(isFormattingOnly(['x=1;'], ['x = 1;'])).toBe(true);29expect(isFormattingOnly([' x'], ['x'])).toBe(true);30expect(isFormattingOnly(['a', 'b'], ['a b'])).toBe(true);3132// Content changes: content differs after removing whitespace33expect(isFormattingOnly(['x=1;'], ['x=2;'])).toBe(false);34expect(isFormattingOnly(['x'], ['x+1'])).toBe(false);35expect(isFormattingOnly(['a'], ['a', 'b'])).toBe(false);36});3738// Representative examples of common scenarios39describe('common scenarios', () => {40const testCases = [41// Formatting-only changes42{ name: 'indentation', base: [' code'], edited: [' code'], expected: true },43{ name: 'space normalization', base: ['a b'], edited: ['a b'], expected: true },44{ name: 'line breaks', base: ['a;', 'b;'], edited: ['a; b;'], expected: true },45{ name: 'empty lines', base: [' '], edited: ['\t'], expected: true },4647// Content changes48{ name: 'value change', base: ['x=1'], edited: ['x=2'], expected: false },49{ name: 'added code', base: ['f()'], edited: ['f()', 'g()'], expected: false },50{ name: 'removed code', base: ['a', 'b'], edited: ['a'], expected: false },51];5253it.each(testCases)('$name', ({ base, edited, expected }) => {54expect(isFormattingOnly(base, edited)).toBe(expected);55});56});5758// Edge cases that could break the algorithm59describe('edge cases', () => {60it('handles empty content correctly', () => {61expect(isFormattingOnly([''], [''])).toBe(true);62expect(isFormattingOnly([''], [' '])).toBe(true);63expect(isFormattingOnly([' '], [''])).toBe(true);64});6566it('handles single character changes', () => {67expect(isFormattingOnly(['a'], ['a '])).toBe(true);68expect(isFormattingOnly(['a'], ['b'])).toBe(false);69});70});71});7273describe('editWouldDeleteWhatWasJustInserted', () => {7475it('does not incorrectly flag multi-line removals', async () => {76const file =77`const modifiedTimes: Map<string, number> = new Map()7879export async function getForceFreshForDir(80cacheEntry:81| CacheEntry82| null83| undefined84| Promise<CacheEntry | null | undefined>,85...dirs: Array<string | undefined | null>86) {87const truthyDirs = dirs.filter(Boolean)88for (const d of truthyDirs) {89if (!path.isAbsolute(d)) {90throw new Error(\`Trying to get force fresh for non-absolute path: \${d}\`)91}92}9394const resolvedCacheEntry = await cacheEntry95if (!resolvedCacheEntry) return true96const latestModifiedTime = truthyDirs.reduce((latest, dir) => {97const modifiedTime = modifiedTimes.get(dir)98return modifiedTime && modifiedTime > latest ? modifiedTime : latest99}, 0)100if (!latestModifiedTime) return undefined101return latestModifiedTime > resolvedCacheEntry.metadata.createdTime102? true103: undefined104return latestModifiedTime > resolvedCacheEntry.metadata.createdTime105? true106: undefined107}108`;109110const lineEdit = new LineEdit([new LineReplacement(new LineRange(28, 31), [])]); //[28,31)->[])111112const recentEdits = Edits.single(new StringEdit([113new StringReplacement(new OffsetRange(740, 746), 'return '),114new StringReplacement(new OffsetRange(806, 808), ''),115new StringReplacement(new OffsetRange(811, 875), '? true\\n\\t\\t: undefined')116]));117118const r = editWouldDeleteWhatWasJustInserted2({ documentAfterEdits: new StringText(file), recentEdits } as StatelessNextEditDocument, lineEdit);119120expect(r).toMatchInlineSnapshot(`false`);121});122123});124125126