Path: blob/main/extensions/copilot/src/extension/inlineEdits/test/node/nextEditCacheCursorDistance.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*--------------------------------------------------------------------------------------------*/4import { assert, beforeEach, describe, it } from 'vitest';5import { ConfigKey } from '../../../../platform/configuration/common/configurationService';6import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService';7import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService';8import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';9import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext';10import { MutableObservableWorkspace } from '../../../../platform/inlineEdits/common/observableWorkspace';11import { LogServiceImpl } from '../../../../platform/log/common/logService';12import { NullExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';13import { URI } from '../../../../util/vs/base/common/uri';14import { generateUuid } from '../../../../util/vs/base/common/uuid';15import { StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';16import { Position } from '../../../../util/vs/editor/common/core/position';17import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';18import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';19import { NextEditCache } from '../../node/nextEditCache';20import { NextEditFetchRequest } from '../../node/nextEditProvider';2122describe('NextEditCache cursor distance check', () => {2324let configService: InMemoryConfigurationService;25let obsWorkspace: MutableObservableWorkspace;26let logService: LogServiceImpl;27let expService: NullExperimentationService;28let cache: NextEditCache;29let docId: DocumentId;3031// A multi-line document:32// Line 1: "line1"33// Line 2: "line2"34// ...35// Line 10: "line10"36const docContent = Array.from({ length: 10 }, (_, i) => `line${i + 1}`).join('\n');37const docText = new StringText(docContent);3839function makeSource(): NextEditFetchRequest {40const logContext = new InlineEditRequestLogContext('test', 0, undefined);41return new NextEditFetchRequest(generateUuid(), logContext, undefined, false);42}4344/** Get the offset of the start of a 1-indexed line in docContent. */45function lineStartOffset(lineNumber: number): number {46return docText.getTransformer().getOffset(new Position(lineNumber, 1));47}4849function cursorAtLine(lineNumber: number): OffsetRange[] {50const offset = lineStartOffset(lineNumber);51return [new OffsetRange(offset, offset)];52}5354beforeEach(() => {55configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());56obsWorkspace = new MutableObservableWorkspace();57logService = new LogServiceImpl([]);58expService = new NullExperimentationService();5960docId = DocumentId.create(URI.file('/test/cursor-distance.ts').toString());61obsWorkspace.addDocument({ id: docId, initialValue: docContent });6263cache = new NextEditCache(obsWorkspace, logService, configService, expService);64});6566// Edit targets line 6 (replaces "line6" with "REPLACED")67const editStartOffset = docContent.indexOf('line6');68const editEndOffset = editStartOffset + 'line6'.length;69const edit = new StringReplacement(new OffsetRange(editStartOffset, editEndOffset), 'REPLACED');7071function cacheEditWithCursorAtLine(cursorLine: number) {72cache.setKthNextEdit(73docId,74docText,75undefined, // editWindow76edit,770, // subsequentN78undefined, // nextEdits79undefined, // userEditSince80makeSource(),81{ isFromCursorJump: false, cursorOffset: lineStartOffset(cursorLine) },82);83}8485describe('when flag is disabled (default)', () => {86it('serves cached edit regardless of cursor distance', () => {87// Cache edit with cursor on line 5 (1 line away from edit on line 6)88cacheEditWithCursorAtLine(5);8990// Move cursor to line 1 (5 lines away — farther)91const result = cache.lookupNextEdit(docId, docText, cursorAtLine(1));92assert(result?.edit, 'should serve cached edit when flag is off');93});94});9596describe('when flag is enabled', () => {97beforeEach(async () => {98await configService.setConfig(ConfigKey.TeamInternal.InlineEditsCacheCursorDistanceCheck, true);99});100101it('serves cached edit when cursor moves closer to the edit', () => {102// Cache edit with cursor on line 4 (2 lines away from edit on line 6)103cacheEditWithCursorAtLine(4);104105// Move cursor to line 5 (1 line away — closer)106const result = cache.lookupNextEdit(docId, docText, cursorAtLine(5));107assert(result?.edit, 'should serve cached edit when cursor is closer');108});109110it('serves cached edit when cursor stays at the same distance', () => {111// Cache edit with cursor on line 4 (2 lines away from edit on line 6)112cacheEditWithCursorAtLine(4);113114// Move cursor to line 8 (also 2 lines away — same distance, other side)115const result = cache.lookupNextEdit(docId, docText, cursorAtLine(8));116assert(result?.edit, 'should serve cached edit at equal distance');117});118119it('rejects cached edit when cursor moves farther from the edit', () => {120// Cache edit with cursor on line 5 (1 line away from edit on line 6)121cacheEditWithCursorAtLine(5);122123// Move cursor to line 1 (5 lines away — farther)124const result = cache.lookupNextEdit(docId, docText, cursorAtLine(1));125assert(result?.rejected === true, 'should return cached edit marked as rejected');126});127128it('marks the cached edit as rejected when cursor moves farther', () => {129// Cache edit with cursor on line 5 (1 line away from edit on line 6)130cacheEditWithCursorAtLine(5);131132// Move cursor to line 1 (5 lines away — farther) — triggers rejection133cache.lookupNextEdit(docId, docText, cursorAtLine(1));134135// Now even looking up from the original close position should show rejected136const result = cache.lookupNextEdit(docId, docText, cursorAtLine(5));137assert(result?.rejected === true, 'cached edit should be marked as rejected');138});139140it('does not apply to subsequent edits (subsequentN > 0)', () => {141// Cache a subsequent edit (subsequentN = 1) with cursor on line 5142cache.setKthNextEdit(143docId,144docText,145undefined,146edit,1471, // subsequentN > 0148undefined,149undefined,150makeSource(),151{ isFromCursorJump: false, cursorOffset: lineStartOffset(5) },152);153154// Move cursor to line 1 (farther) — should still serve because it's subsequent155const result = cache.lookupNextEdit(docId, docText, cursorAtLine(1));156assert(result?.edit, 'subsequent edits should not be filtered by cursor distance');157});158159it('does not apply when cursorOffsetAtCacheTime is not set', () => {160// Cache edit without cursor offset161cache.setKthNextEdit(162docId,163docText,164undefined,165edit,1660,167undefined,168undefined,169makeSource(),170{ isFromCursorJump: false }, // no cursorOffset171);172173// Move cursor to line 1 (farther) — should still serve because no cursor offset recorded174const result = cache.lookupNextEdit(docId, docText, cursorAtLine(1));175assert(result?.edit, 'should serve when no cursor offset was recorded at cache time');176});177178it('serves cached edit when cursor is on the same line as the edit', () => {179// Cache edit with cursor on line 4 (2 lines away from edit on line 6)180cacheEditWithCursorAtLine(4);181182// Move cursor to line 6 (0 lines away — on the edit itself)183const result = cache.lookupNextEdit(docId, docText, cursorAtLine(6));184assert(result?.edit, 'should serve cached edit when cursor is on the edit line');185});186});187});188189190