Path: blob/main/extensions/copilot/src/extension/inlineEdits/test/node/nextEditCacheRebase.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 { StringEdit, StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';16import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';17import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';18import { NextEditCache } from '../../node/nextEditCache';19import { NextEditFetchRequest } from '../../node/nextEditProvider';2021/**22* Regression test from a real scenario:23*24* User typed `class Fibonacci {\n\t` character by character. Two NES requests25* were made at different points during typing:26*27* - Request #6 (early): doc ended with `class `, model predicted `class FibonacciCalculator {`28* - Request #18 (later): doc ended with `class Fibonacci `, model predicted `class Fibonacci {`29*30* When `lookupNextEdit` runs, it should find and rebase the compatible cached edit31* from request #18 (whose prediction matches the user's typing).32*/33describe('NextEditCache rebase — Fibonacci scenario', () => {3435let configService: InMemoryConfigurationService;36let obsWorkspace: MutableObservableWorkspace;37let logService: LogServiceImpl;38let expService: NullExperimentationService;39let cache: NextEditCache;40let docId: DocumentId;4142// Common prefix of all document states — everything before the class declaration43const docPrefix =44'import * as vscode from \'vscode\';\n' +45'import { ASTNodeWithOffset } from \'./nodeTypes\';\n' +46'import { NodeTypesIndex } from \'./nodeTypesIndex\';\n' +47'import { Result } from \'./util/common/result\';\n' +48'import { LRUCache } from \'./util/vs/base/common/map\';\n' +49'\n' +50'export class NodeTypesDefinitionProvider implements vscode.DefinitionProvider {\n' +51'\n' +52'\tprivate _cache: LRUCache<ASTNodeWithOffset[], true>;\n' +53'\tprivate _definitions: Map<string, ASTNodeWithOffset>;\n' +54'\n' +55'\tconstructor() {\n' +56'\t\tthis._definitions = new Map();\n' +57'\t\tthis._cache = new LRUCache<ASTNodeWithOffset[], true>(10);\n' +58'\t}\n' +59'\n' +60'\tasync provideDefinition(\n' +61'\t\tdocument: vscode.TextDocument,\n' +62'\t\tposition: vscode.Position,\n' +63'\t\ttoken: vscode.CancellationToken\n' +64'\t): Promise<vscode.DefinitionLink[] | null> {\n' +65'\t\tconst word = NodeTypesDefinitionProvider.positionToSymbol(document, position);\n' +66'\t\tif (!word) {\n' +67'\t\t\treturn null;\n' +68'\t\t}\n' +69'\t\tconst def = this.computeDefForSymbol(document, word);\n' +70'\t\tif (!def) {\n' +71'\t\t\treturn null;\n' +72'\t\t}\n' +73'\t\treturn [{\n' +74'\t\t\ttargetUri: document.uri,\n' +75'\t\t\ttargetRange: new vscode.Range(document.positionAt(def.offset), document.positionAt(def.offset + def.length))\n' +76'\t\t}];\n' +77'\t}\n' +78'\n' +79'\tprivate computeDefForSymbol(document: vscode.TextDocument, symbol: string) {\n' +80'\t\tconst index = new NodeTypesIndex(document);\n' +81'\t\tconst astNodes = index.nodes;\n' +82'\t\tif (Result.isErr(astNodes)) {\n' +83'\t\t\treturn null;\n' +84'\t\t}\n' +85'\t\tthis.recomputeDefinitions(astNodes.val);\n' +86'\t\treturn this._definitions.get(symbol) || null;\n' +87'\t}\n' +88'\n' +89'\tprivate recomputeDefinitions(nodes: ASTNodeWithOffset[]) {\n' +90'\t\tif (this._cache.has(nodes)) {\n' +91'\t\t\treturn;\n' +92'\t\t}\n' +93'\t\tfor (const node of nodes) {\n' +94'\t\t\tthis._definitions.set(node.type.value, node);\n' +95'\t\t}\n' +96'\t\tthis._cache.set(nodes, true);\n' +97'\t}\n' +98'\n' +99'\tprivate static positionToSymbol(document: vscode.TextDocument, position: vscode.Position) {\n' +100'\t\tconst wordRange = document.getWordRangeAtPosition(position);\n' +101'\t\treturn wordRange ? document.getText(wordRange) : null;\n' +102'\t}\n' +103'}\n' +104'\n' +105'function fibonacci(n: number): number {\n' +106'\tif (n <= 1) {\n' +107'\t\treturn n;\n' +108'\t}\n' +109'\treturn fibonacci(n - 1) + fibonacci(n - 2);\n' +110'}\n' +111'\n';112113// Document states at different points in time — offsets derived from docPrefix.length114const classStart = docPrefix.length; // where "class " begins115const docAtRequest18 = docPrefix + 'class Fibonacci '; // "class Fibonacci " ends at classStart + 16116const classEndAtRequest18 = classStart + 'class Fibonacci '.length; // = classStart + 16117const currentDoc = docPrefix + 'class Fibonacci {\n\t'; // "class Fibonacci {\n\t" ends at classStart + 19118const cursorOffset = classStart + 'class Fibonacci {\n\t'.length; // = classStart + 19119120function makeSource(): NextEditFetchRequest {121const logContext = new InlineEditRequestLogContext('test', 0, undefined);122return new NextEditFetchRequest(generateUuid(), logContext, undefined, false);123}124125beforeEach(async () => {126configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());127await configService.setConfig(ConfigKey.TeamInternal.InlineEditsReverseAgreement, true);128obsWorkspace = new MutableObservableWorkspace();129logService = new LogServiceImpl([]);130expService = new NullExperimentationService();131132docId = DocumentId.create(URI.file('/test/nodeTypesDefinitionProvider.ts').toString());133// Initialize workspace doc with the CURRENT document state134// (so checkEditConsistency(documentBeforeEdit + userEditSince = currentDoc) passes)135obsWorkspace.addDocument({ id: docId, initialValue: currentDoc });136137cache = new NextEditCache(obsWorkspace, logService, configService, expService);138});139140it('rebases cached edit when model predicted class Fibonacci { and user typed the same', () => {141// Scenario from real usage:142// documentBeforeEdit (at cache time): ...class Fibonacci \n (ends at offset 1960)143// Model's edit: replace [1944,1960) "class Fibonacci " → "class Fibonacci {"144// Model also has a 2nd edit: insert at 1961 → class body145// User then typed "{\n\t" → userEditSince: [1944,1960) → "class Fibonacci {\n\t"146//147// The user's typing is a superset of the model's first edit (model: "{", user: "{\n\t"),148// so rebase should succeed and the 2nd edit (class body) should be offered.149const cachedEdit = cache.setKthNextEdit(150docId,151new StringText(docAtRequest18),152new OffsetRange(classStart, classEndAtRequest18), // editWindow153new StringReplacement(new OffsetRange(classStart, classEndAtRequest18), 'class Fibonacci {'),1540,155[156new StringReplacement(new OffsetRange(classStart, classEndAtRequest18), 'class Fibonacci {'),157new StringReplacement(OffsetRange.emptyAt(classStart + 'class Fibonacci {'.length), '\n\tprivate memo: Map<number, number>;\n\n\tconstructor() {\n\t\tthis.memo = new Map();\n\t}\n\n\tcalc(n: number): number {\n\t\tif (n <= 1) {\n\t\t\treturn n;\n\t\t}\n\t\tif (this.memo.has(n)) {\n\t\t\treturn this.memo.get(n)!;\n\t\t}\n\t\tconst result = this.calc(n - 1) + this.calc(n - 2);\n\t\tthis.memo.set(n, result);\n\t\treturn result;\n\t}\n}'),158],159StringEdit.single(new StringReplacement(new OffsetRange(classStart, classEndAtRequest18), 'class Fibonacci {\n\t')),160makeSource(),161{ isFromCursorJump: false, cursorOffset: classEndAtRequest18 },162);163164assert(cachedEdit !== undefined, 'setKthNextEdit should return the cached edit');165assert(cachedEdit.userEditSince !== undefined, 'userEditSince should be set');166167const rebaseResult = cache.tryRebaseCacheEntry(168cachedEdit,169new StringText(currentDoc),170[new OffsetRange(cursorOffset, cursorOffset)],171);172173assert(rebaseResult.edit !== undefined, 'should rebase successfully');174assert(rebaseResult.edit.rebasedEdit !== undefined, 'should have a rebased edit for the class body');175});176});177178179