Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderCaching.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 { outdent } from 'outdent';
6
import { afterAll, assert, beforeAll, describe, expect, it } from 'vitest';
7
import { IConfigurationService } from '../../../../platform/configuration/common/configurationService';
8
import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService';
9
import { IGitExtensionService } from '../../../../platform/git/common/gitExtensionService';
10
import { NullGitExtensionService } from '../../../../platform/git/common/nullGitExtensionService';
11
import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';
12
import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext';
13
import { ObservableGit } from '../../../../platform/inlineEdits/common/observableGit';
14
import { MutableObservableWorkspace } from '../../../../platform/inlineEdits/common/observableWorkspace';
15
import { IStatelessNextEditProvider, NoNextEditReason, StatelessNextEditRequest, StatelessNextEditTelemetryBuilder, WithStatelessProviderTelemetry } from '../../../../platform/inlineEdits/common/statelessNextEditProvider';
16
import { NesHistoryContextProvider } from '../../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider';
17
import { NesXtabHistoryTracker } from '../../../../platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker';
18
import { ILogger, ILogService, LogServiceImpl } from '../../../../platform/log/common/logService';
19
import { IRequestLogger } from '../../../../platform/requestLogger/common/requestLogger';
20
import { NullRequestLogger } from '../../../../platform/requestLogger/node/nullRequestLogger';
21
import { ISnippyService, NullSnippyService } from '../../../../platform/snippy/common/snippyService';
22
import { IExperimentationService, NullExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
23
import { mockNotebookService } from '../../../../platform/test/common/testNotebookService';
24
import { TestWorkspaceService } from '../../../../platform/test/node/testWorkspaceService';
25
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
26
import { Result } from '../../../../util/common/result';
27
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
28
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
29
import { URI } from '../../../../util/vs/base/common/uri';
30
import { generateUuid } from '../../../../util/vs/base/common/uuid';
31
import { LineEdit, LineReplacement } from '../../../../util/vs/editor/common/core/edits/lineEdit';
32
import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringEdit';
33
import { LineRange } from '../../../../util/vs/editor/common/core/ranges/lineRange';
34
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
35
import { NESInlineCompletionContext, NextEditProvider } from '../../node/nextEditProvider';
36
import { NextEditProviderTelemetryBuilder } from '../../node/nextEditProviderTelemetry';
37
38
describe('NextEditProvider Caching', () => {
39
40
let configService: IConfigurationService;
41
let snippyService: ISnippyService;
42
let gitExtensionService: IGitExtensionService;
43
let logService: ILogService;
44
let expService: IExperimentationService;
45
let disposableStore: DisposableStore;
46
let workspaceService: IWorkspaceService;
47
let requestLogger: IRequestLogger;
48
beforeAll(() => {
49
disposableStore = new DisposableStore();
50
workspaceService = disposableStore.add(new TestWorkspaceService());
51
configService = new DefaultsOnlyConfigurationService();
52
snippyService = new NullSnippyService();
53
gitExtensionService = new NullGitExtensionService();
54
logService = new LogServiceImpl([]);
55
expService = new NullExperimentationService();
56
requestLogger = new NullRequestLogger();
57
});
58
afterAll(() => {
59
disposableStore.dispose();
60
});
61
function createStatelessNextEditProvider(): IStatelessNextEditProvider {
62
return {
63
ID: 'TestNextEditProvider',
64
provideNextEdit: async function*(request: StatelessNextEditRequest, logger: ILogger, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken) {
65
const telemetryBuilder = new StatelessNextEditTelemetryBuilder(request.headerRequestId);
66
const lineEdit = LineEdit.createFromUnsorted(
67
[
68
new LineReplacement(
69
new LineRange(11, 12),
70
['const myPoint = new Point3D(0, 1, 2);']
71
),
72
new LineReplacement(
73
new LineRange(5, 5),
74
['\t\tprivate readonly z: number,']
75
),
76
new LineReplacement(
77
new LineRange(6, 9),
78
[
79
'\tgetDistance() {',
80
'\t\treturn Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);',
81
'\t}'
82
]
83
)
84
]
85
);
86
for (const edit of lineEdit.replacements) {
87
yield new WithStatelessProviderTelemetry({ targetDocument: request.getActiveDocument().id, edit, isFromCursorJump: false }, telemetryBuilder.build(Result.ok(undefined)));
88
}
89
const noSuggestions = new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, undefined);
90
return new WithStatelessProviderTelemetry(noSuggestions, telemetryBuilder.build(Result.error(noSuggestions)));
91
}
92
};
93
}
94
95
it('caches a response with multiple edits and reuses them correctly with rebasing', async () => {
96
const obsWorkspace = new MutableObservableWorkspace();
97
const obsGit = new ObservableGit(gitExtensionService);
98
const statelessNextEditProvider = createStatelessNextEditProvider();
99
100
const nextEditProvider: NextEditProvider = new NextEditProvider(obsWorkspace, statelessNextEditProvider, new NesHistoryContextProvider(obsWorkspace, obsGit), new NesXtabHistoryTracker(obsWorkspace, undefined, configService, expService), undefined, configService, snippyService, logService, expService, requestLogger);
101
102
const doc = obsWorkspace.addDocument({
103
id: DocumentId.create(URI.file('/test/test.ts').toString()),
104
initialValue: outdent`
105
class Point {
106
constructor(
107
private readonly x: number,
108
private readonly y: number,
109
) { }
110
getDistance() {
111
return Math.sqrt(this.x ** 2 + this.y ** 2);
112
}
113
}
114
115
const myPoint = new Point(0, 1);`.trimStart()
116
});
117
doc.setSelection([new OffsetRange(1, 1)], undefined);
118
119
doc.applyEdit(StringEdit.insert(11, '3D'));
120
121
const context: NESInlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, enforceCacheDelay: false };
122
const logContext = new InlineEditRequestLogContext(doc.id.toString(), 1, context);
123
const cancellationToken = CancellationToken.None;
124
const tb1 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
125
126
let result = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb1.nesBuilder);
127
128
tb1.dispose();
129
130
assert(result.result?.edit);
131
132
doc.applyEdit(result.result.edit.toEdit());
133
134
expect(doc.value.get().value).toMatchInlineSnapshot(`
135
"class Point3D {
136
constructor(
137
private readonly x: number,
138
private readonly y: number,
139
private readonly z: number,
140
) { }
141
getDistance() {
142
return Math.sqrt(this.x ** 2 + this.y ** 2);
143
}
144
}
145
146
const myPoint = new Point(0, 1);"
147
`);
148
149
const tb2 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
150
151
result = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb2.nesBuilder);
152
153
tb2.dispose();
154
155
assert(result.result?.edit);
156
157
doc.applyEdit(result.result.edit.toEdit());
158
159
expect(doc.value.get().value).toMatchInlineSnapshot(`
160
"class Point3D {
161
constructor(
162
private readonly x: number,
163
private readonly y: number,
164
private readonly z: number,
165
) { }
166
getDistance() {
167
return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
168
}
169
}
170
171
const myPoint = new Point(0, 1);"
172
`);
173
174
const tb3 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
175
176
result = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb3.nesBuilder);
177
178
tb3.dispose();
179
180
assert(result.result?.edit);
181
182
doc.applyEdit(result.result.edit.toEdit());
183
184
expect(doc.value.get().value).toMatchInlineSnapshot(`
185
"class Point3D {
186
constructor(
187
private readonly x: number,
188
private readonly y: number,
189
private readonly z: number,
190
) { }
191
getDistance() {
192
return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
193
}
194
}
195
196
const myPoint = new Point3D(0, 1, 2);"
197
`);
198
});
199
200
it('caches a response with multiple edits correctly when document uses CRLF line endings', async () => {
201
const obsWorkspace = new MutableObservableWorkspace();
202
const obsGit = new ObservableGit(gitExtensionService);
203
const statelessNextEditProvider = createStatelessNextEditProvider();
204
205
const nextEditProvider: NextEditProvider = new NextEditProvider(obsWorkspace, statelessNextEditProvider, new NesHistoryContextProvider(obsWorkspace, obsGit), new NesXtabHistoryTracker(obsWorkspace, undefined, configService, expService), undefined, configService, snippyService, logService, expService, requestLogger);
206
207
// Use \r\n line endings to simulate a Windows document
208
const initialValue = [
209
'class Point {',
210
'\tconstructor(',
211
'\t\tprivate readonly x: number,',
212
'\t\tprivate readonly y: number,',
213
'\t) { }',
214
'\tgetDistance() {',
215
'\t\treturn Math.sqrt(this.x ** 2 + this.y ** 2);',
216
'\t}',
217
'}',
218
'',
219
'const myPoint = new Point(0, 1);',
220
].join('\r\n');
221
222
const doc = obsWorkspace.addDocument({
223
id: DocumentId.create(URI.file('/test/test.ts').toString()),
224
initialValue,
225
});
226
doc.setSelection([new OffsetRange(1, 1)], undefined);
227
228
// Insert "3D" after "Point" at offset 11 (same offset, within first line before any line ending)
229
doc.applyEdit(StringEdit.insert(11, '3D'));
230
231
const context: NESInlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, enforceCacheDelay: false };
232
const logContext = new InlineEditRequestLogContext(doc.id.toString(), 1, context);
233
const cancellationToken = CancellationToken.None;
234
const tb1 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
235
236
// First edit: should add z parameter
237
let result = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb1.nesBuilder);
238
tb1.dispose();
239
assert(result.result?.edit);
240
doc.applyEdit(result.result.edit.toEdit());
241
242
// Verify CRLF line endings are preserved
243
expect(doc.value.get().value).toContain('\r\n');
244
expect(doc.value.get().value).not.toMatch(/[^\r]\n/);
245
246
// Second edit: should update getDistance method — this uses a cached edit
247
const tb2 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
248
result = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb2.nesBuilder);
249
tb2.dispose();
250
assert(result.result?.edit, 'second cached edit should be found');
251
doc.applyEdit(result.result.edit.toEdit());
252
253
expect(doc.value.get().value).not.toMatch(/[^\r]\n/);
254
255
// Third edit: should update the variable — also from cache
256
const tb3 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
257
result = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb3.nesBuilder);
258
tb3.dispose();
259
assert(result.result?.edit, 'third cached edit should be found');
260
doc.applyEdit(result.result.edit.toEdit());
261
262
// Final state should match expected content with CRLF throughout
263
const expectedLines = [
264
'class Point3D {',
265
'\tconstructor(',
266
'\t\tprivate readonly x: number,',
267
'\t\tprivate readonly y: number,',
268
'\t\tprivate readonly z: number,',
269
'\t) { }',
270
'\tgetDistance() {',
271
'\t\treturn Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);',
272
'\t}',
273
'}',
274
'',
275
'const myPoint = new Point3D(0, 1, 2);',
276
].join('\r\n');
277
expect(doc.value.get().value).toBe(expectedLines);
278
});
279
280
it('exposes the cache entry on NextEditResult and preserves the wasRenderedAsInlineSuggestion flag across lookups', async () => {
281
const obsWorkspace = new MutableObservableWorkspace();
282
const obsGit = new ObservableGit(gitExtensionService);
283
const statelessNextEditProvider = createStatelessNextEditProvider();
284
285
const nextEditProvider: NextEditProvider = new NextEditProvider(obsWorkspace, statelessNextEditProvider, new NesHistoryContextProvider(obsWorkspace, obsGit), new NesXtabHistoryTracker(obsWorkspace, undefined, configService, expService), undefined, configService, snippyService, logService, expService, requestLogger);
286
287
const doc = obsWorkspace.addDocument({
288
id: DocumentId.create(URI.file('/test/test.ts').toString()),
289
initialValue: outdent`
290
class Point {
291
constructor(
292
private readonly x: number,
293
private readonly y: number,
294
) { }
295
getDistance() {
296
return Math.sqrt(this.x ** 2 + this.y ** 2);
297
}
298
}
299
300
const myPoint = new Point(0, 1);`.trimStart()
301
});
302
doc.setSelection([new OffsetRange(1, 1)], undefined);
303
304
doc.applyEdit(StringEdit.insert(11, '3D'));
305
306
const context: NESInlineCompletionContext = { triggerKind: 1, selectedCompletionInfo: undefined, requestUuid: generateUuid(), requestIssuedDateTime: Date.now(), earliestShownDateTime: Date.now() + 200, enforceCacheDelay: false };
307
const logContext = new InlineEditRequestLogContext(doc.id.toString(), 1, context);
308
const cancellationToken = CancellationToken.None;
309
310
// First call: edit comes fresh from the (mock) provider but is also cached.
311
const tb1 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
312
const first = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb1.nesBuilder);
313
tb1.dispose();
314
assert(first.result?.edit);
315
const firstCacheEntry = first.result.cacheEntry;
316
assert(firstCacheEntry, 'expected a cacheEntry reference on the first (fresh) NextEditResult');
317
expect(firstCacheEntry.wasRenderedAsInlineSuggestion).toBeFalsy();
318
319
// Simulate the inline-completion-provider marking the entry as having been
320
// rendered as an inline (ghost text) suggestion.
321
firstCacheEntry.wasRenderedAsInlineSuggestion = true;
322
323
// Second call (no document changes): we should still get the same cached
324
// edit back, and the flag must have been preserved on the same entry.
325
const tb2 = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, doc);
326
const second = await nextEditProvider.getNextEdit(doc.id, context, logContext, cancellationToken, tb2.nesBuilder);
327
tb2.dispose();
328
assert(second.result?.edit);
329
const secondCacheEntry = second.result.cacheEntry;
330
assert(secondCacheEntry, 'expected a cacheEntry reference on the second (cached) NextEditResult');
331
expect(secondCacheEntry).toBe(firstCacheEntry);
332
expect(secondCacheEntry.wasRenderedAsInlineSuggestion).toBe(true);
333
});
334
});
335
336