Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/test/node/nextEditCacheRebase.spec.ts
13405 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
import { assert, beforeEach, describe, it } from 'vitest';
6
import { ConfigKey } from '../../../../platform/configuration/common/configurationService';
7
import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService';
8
import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService';
9
import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';
10
import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext';
11
import { MutableObservableWorkspace } from '../../../../platform/inlineEdits/common/observableWorkspace';
12
import { LogServiceImpl } from '../../../../platform/log/common/logService';
13
import { NullExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
14
import { URI } from '../../../../util/vs/base/common/uri';
15
import { generateUuid } from '../../../../util/vs/base/common/uuid';
16
import { StringEdit, StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';
17
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
18
import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';
19
import { NextEditCache } from '../../node/nextEditCache';
20
import { NextEditFetchRequest } from '../../node/nextEditProvider';
21
22
/**
23
* Regression test from a real scenario:
24
*
25
* User typed `class Fibonacci {\n\t` character by character. Two NES requests
26
* were made at different points during typing:
27
*
28
* - Request #6 (early): doc ended with `class `, model predicted `class FibonacciCalculator {`
29
* - Request #18 (later): doc ended with `class Fibonacci `, model predicted `class Fibonacci {`
30
*
31
* When `lookupNextEdit` runs, it should find and rebase the compatible cached edit
32
* from request #18 (whose prediction matches the user's typing).
33
*/
34
describe('NextEditCache rebase — Fibonacci scenario', () => {
35
36
let configService: InMemoryConfigurationService;
37
let obsWorkspace: MutableObservableWorkspace;
38
let logService: LogServiceImpl;
39
let expService: NullExperimentationService;
40
let cache: NextEditCache;
41
let docId: DocumentId;
42
43
// Common prefix of all document states — everything before the class declaration
44
const docPrefix =
45
'import * as vscode from \'vscode\';\n' +
46
'import { ASTNodeWithOffset } from \'./nodeTypes\';\n' +
47
'import { NodeTypesIndex } from \'./nodeTypesIndex\';\n' +
48
'import { Result } from \'./util/common/result\';\n' +
49
'import { LRUCache } from \'./util/vs/base/common/map\';\n' +
50
'\n' +
51
'export class NodeTypesDefinitionProvider implements vscode.DefinitionProvider {\n' +
52
'\n' +
53
'\tprivate _cache: LRUCache<ASTNodeWithOffset[], true>;\n' +
54
'\tprivate _definitions: Map<string, ASTNodeWithOffset>;\n' +
55
'\n' +
56
'\tconstructor() {\n' +
57
'\t\tthis._definitions = new Map();\n' +
58
'\t\tthis._cache = new LRUCache<ASTNodeWithOffset[], true>(10);\n' +
59
'\t}\n' +
60
'\n' +
61
'\tasync provideDefinition(\n' +
62
'\t\tdocument: vscode.TextDocument,\n' +
63
'\t\tposition: vscode.Position,\n' +
64
'\t\ttoken: vscode.CancellationToken\n' +
65
'\t): Promise<vscode.DefinitionLink[] | null> {\n' +
66
'\t\tconst word = NodeTypesDefinitionProvider.positionToSymbol(document, position);\n' +
67
'\t\tif (!word) {\n' +
68
'\t\t\treturn null;\n' +
69
'\t\t}\n' +
70
'\t\tconst def = this.computeDefForSymbol(document, word);\n' +
71
'\t\tif (!def) {\n' +
72
'\t\t\treturn null;\n' +
73
'\t\t}\n' +
74
'\t\treturn [{\n' +
75
'\t\t\ttargetUri: document.uri,\n' +
76
'\t\t\ttargetRange: new vscode.Range(document.positionAt(def.offset), document.positionAt(def.offset + def.length))\n' +
77
'\t\t}];\n' +
78
'\t}\n' +
79
'\n' +
80
'\tprivate computeDefForSymbol(document: vscode.TextDocument, symbol: string) {\n' +
81
'\t\tconst index = new NodeTypesIndex(document);\n' +
82
'\t\tconst astNodes = index.nodes;\n' +
83
'\t\tif (Result.isErr(astNodes)) {\n' +
84
'\t\t\treturn null;\n' +
85
'\t\t}\n' +
86
'\t\tthis.recomputeDefinitions(astNodes.val);\n' +
87
'\t\treturn this._definitions.get(symbol) || null;\n' +
88
'\t}\n' +
89
'\n' +
90
'\tprivate recomputeDefinitions(nodes: ASTNodeWithOffset[]) {\n' +
91
'\t\tif (this._cache.has(nodes)) {\n' +
92
'\t\t\treturn;\n' +
93
'\t\t}\n' +
94
'\t\tfor (const node of nodes) {\n' +
95
'\t\t\tthis._definitions.set(node.type.value, node);\n' +
96
'\t\t}\n' +
97
'\t\tthis._cache.set(nodes, true);\n' +
98
'\t}\n' +
99
'\n' +
100
'\tprivate static positionToSymbol(document: vscode.TextDocument, position: vscode.Position) {\n' +
101
'\t\tconst wordRange = document.getWordRangeAtPosition(position);\n' +
102
'\t\treturn wordRange ? document.getText(wordRange) : null;\n' +
103
'\t}\n' +
104
'}\n' +
105
'\n' +
106
'function fibonacci(n: number): number {\n' +
107
'\tif (n <= 1) {\n' +
108
'\t\treturn n;\n' +
109
'\t}\n' +
110
'\treturn fibonacci(n - 1) + fibonacci(n - 2);\n' +
111
'}\n' +
112
'\n';
113
114
// Document states at different points in time — offsets derived from docPrefix.length
115
const classStart = docPrefix.length; // where "class " begins
116
const docAtRequest18 = docPrefix + 'class Fibonacci '; // "class Fibonacci " ends at classStart + 16
117
const classEndAtRequest18 = classStart + 'class Fibonacci '.length; // = classStart + 16
118
const currentDoc = docPrefix + 'class Fibonacci {\n\t'; // "class Fibonacci {\n\t" ends at classStart + 19
119
const cursorOffset = classStart + 'class Fibonacci {\n\t'.length; // = classStart + 19
120
121
function makeSource(): NextEditFetchRequest {
122
const logContext = new InlineEditRequestLogContext('test', 0, undefined);
123
return new NextEditFetchRequest(generateUuid(), logContext, undefined, false);
124
}
125
126
beforeEach(async () => {
127
configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());
128
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsReverseAgreement, true);
129
obsWorkspace = new MutableObservableWorkspace();
130
logService = new LogServiceImpl([]);
131
expService = new NullExperimentationService();
132
133
docId = DocumentId.create(URI.file('/test/nodeTypesDefinitionProvider.ts').toString());
134
// Initialize workspace doc with the CURRENT document state
135
// (so checkEditConsistency(documentBeforeEdit + userEditSince = currentDoc) passes)
136
obsWorkspace.addDocument({ id: docId, initialValue: currentDoc });
137
138
cache = new NextEditCache(obsWorkspace, logService, configService, expService);
139
});
140
141
it('rebases cached edit when model predicted class Fibonacci { and user typed the same', () => {
142
// Scenario from real usage:
143
// documentBeforeEdit (at cache time): ...class Fibonacci \n (ends at offset 1960)
144
// Model's edit: replace [1944,1960) "class Fibonacci " → "class Fibonacci {"
145
// Model also has a 2nd edit: insert at 1961 → class body
146
// User then typed "{\n\t" → userEditSince: [1944,1960) → "class Fibonacci {\n\t"
147
//
148
// The user's typing is a superset of the model's first edit (model: "{", user: "{\n\t"),
149
// so rebase should succeed and the 2nd edit (class body) should be offered.
150
const cachedEdit = cache.setKthNextEdit(
151
docId,
152
new StringText(docAtRequest18),
153
new OffsetRange(classStart, classEndAtRequest18), // editWindow
154
new StringReplacement(new OffsetRange(classStart, classEndAtRequest18), 'class Fibonacci {'),
155
0,
156
[
157
new StringReplacement(new OffsetRange(classStart, classEndAtRequest18), 'class Fibonacci {'),
158
new 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}'),
159
],
160
StringEdit.single(new StringReplacement(new OffsetRange(classStart, classEndAtRequest18), 'class Fibonacci {\n\t')),
161
makeSource(),
162
{ isFromCursorJump: false, cursorOffset: classEndAtRequest18 },
163
);
164
165
assert(cachedEdit !== undefined, 'setKthNextEdit should return the cached edit');
166
assert(cachedEdit.userEditSince !== undefined, 'userEditSince should be set');
167
168
const rebaseResult = cache.tryRebaseCacheEntry(
169
cachedEdit,
170
new StringText(currentDoc),
171
[new OffsetRange(cursorOffset, cursorOffset)],
172
);
173
174
assert(rebaseResult.edit !== undefined, 'should rebase successfully');
175
assert(rebaseResult.edit.rebasedEdit !== undefined, 'should have a rebased edit for the class body');
176
});
177
});
178
179