Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts
5263 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
6
import { timeout } from '../../../../../base/common/async.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { Disposable, DisposableStore, IReference } from '../../../../../base/common/lifecycle.js';
9
import { CoreEditingCommands, CoreNavigationCommands } from '../../../../browser/coreCommands.js';
10
import { Position } from '../../../../common/core/position.js';
11
import { ITextModel } from '../../../../common/model.js';
12
import { IInlineCompletionChangeHint, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js';
13
import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
14
import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js';
15
import { autorun, derived } from '../../../../../base/common/observable.js';
16
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
17
import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
18
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
19
import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';
20
import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';
21
import { ViewModel } from '../../../../common/viewModel/viewModelImpl.js';
22
import { InlineCompletionsController } from '../../browser/controller/inlineCompletionsController.js';
23
import { Range } from '../../../../common/core/range.js';
24
import { TextEdit } from '../../../../common/core/edits/textEdit.js';
25
import { BugIndicatingError } from '../../../../../base/common/errors.js';
26
import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js';
27
import { InlineSuggestionsView } from '../../browser/view/inlineSuggestionsView.js';
28
import { IBulkEditService } from '../../../../browser/services/bulkEditService.js';
29
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
30
import { Emitter, Event } from '../../../../../base/common/event.js';
31
import { IRenameSymbolTrackerService, NullRenameSymbolTrackerService } from '../../../../browser/services/renameSymbolTrackerService.js';
32
import { ITextModelService, IResolvedTextEditorModel } from '../../../../common/services/resolverService.js';
33
import { IModelService } from '../../../../common/services/model.js';
34
import { URI } from '../../../../../base/common/uri.js';
35
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
36
37
export class MockInlineCompletionsProvider implements InlineCompletionsProvider {
38
private returnValue: InlineCompletion[] = [];
39
private delayMs: number = 0;
40
41
private callHistory = new Array<unknown>();
42
private calledTwiceIn50Ms = false;
43
44
private readonly _onDidChangeEmitter = new Emitter<IInlineCompletionChangeHint | void>();
45
public readonly onDidChangeInlineCompletions: Event<IInlineCompletionChangeHint | void> = this._onDidChangeEmitter.event;
46
47
constructor(
48
public readonly enableForwardStability = false,
49
) { }
50
51
public setReturnValue(value: InlineCompletion | undefined, delayMs: number = 0): void {
52
this.returnValue = value ? [value] : [];
53
this.delayMs = delayMs;
54
}
55
56
public setReturnValues(values: InlineCompletion[], delayMs: number = 0): void {
57
this.returnValue = values;
58
this.delayMs = delayMs;
59
}
60
61
public getAndClearCallHistory() {
62
const history = [...this.callHistory];
63
this.callHistory = [];
64
return history;
65
}
66
67
public assertNotCalledTwiceWithin50ms() {
68
if (this.calledTwiceIn50Ms) {
69
throw new Error('provideInlineCompletions has been called at least twice within 50ms. This should not happen.');
70
}
71
}
72
73
/**
74
* Fire an onDidChange event with an optional change hint.
75
*/
76
public fireOnDidChange(changeHint?: IInlineCompletionChangeHint): void {
77
this._onDidChangeEmitter.fire(changeHint);
78
}
79
80
private lastTimeMs: number | undefined = undefined;
81
82
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {
83
const currentTimeMs = new Date().getTime();
84
if (this.lastTimeMs && currentTimeMs - this.lastTimeMs < 50) {
85
this.calledTwiceIn50Ms = true;
86
}
87
this.lastTimeMs = currentTimeMs;
88
89
this.callHistory.push({
90
position: position.toString(),
91
triggerKind: context.triggerKind,
92
text: model.getValue(),
93
...(context.changeHint !== undefined ? { changeHint: context.changeHint } : {}),
94
});
95
const result = new Array<InlineCompletion>();
96
for (const v of this.returnValue) {
97
const x = { ...v };
98
if (!x.range) {
99
x.range = model.getFullModelRange();
100
}
101
result.push(x);
102
}
103
104
if (this.delayMs > 0) {
105
await timeout(this.delayMs);
106
}
107
108
return { items: result, enableForwardStability: this.enableForwardStability };
109
}
110
disposeInlineCompletions() { }
111
handleItemDidShow() { }
112
}
113
114
export class MockSearchReplaceCompletionsProvider implements InlineCompletionsProvider {
115
private _map = new Map<string, string>();
116
117
public add(search: string, replace: string): void {
118
this._map.set(search, replace);
119
}
120
121
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {
122
const text = model.getValue();
123
for (const [search, replace] of this._map) {
124
const idx = text.indexOf(search);
125
// replace idx...idx+text.length with replace
126
if (idx !== -1) {
127
const range = Range.fromPositions(model.getPositionAt(idx), model.getPositionAt(idx + search.length));
128
return {
129
items: [
130
{ range, insertText: replace, isInlineEdit: true }
131
]
132
};
133
}
134
}
135
return { items: [] };
136
}
137
disposeInlineCompletions() { }
138
handleItemDidShow() { }
139
}
140
141
export class InlineEditContext extends Disposable {
142
public readonly prettyViewStates = new Array<string | undefined>();
143
144
constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {
145
super();
146
147
const edit = derived(reader => {
148
const state = model.state.read(reader);
149
return state ? new TextEdit(state.edits) : undefined;
150
});
151
152
this._register(autorun(reader => {
153
/** @description update */
154
const e = edit.read(reader);
155
let view: string | undefined;
156
157
if (e) {
158
view = e.toString(this.editor.getValue());
159
} else {
160
view = undefined;
161
}
162
163
this.prettyViewStates.push(view);
164
}));
165
}
166
167
public getAndClearViewStates(): (string | undefined)[] {
168
const arr = [...this.prettyViewStates];
169
this.prettyViewStates.length = 0;
170
return arr;
171
}
172
}
173
174
export class GhostTextContext extends Disposable {
175
public readonly prettyViewStates = new Array<string | undefined>();
176
private _currentPrettyViewState: string | undefined;
177
public get currentPrettyViewState() {
178
return this._currentPrettyViewState;
179
}
180
181
constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {
182
super();
183
184
this._register(autorun(reader => {
185
/** @description update */
186
const ghostText = model.primaryGhostText.read(reader);
187
let view: string | undefined;
188
if (ghostText) {
189
view = ghostText.render(this.editor.getValue(), true);
190
} else {
191
view = this.editor.getValue();
192
}
193
194
if (this._currentPrettyViewState !== view) {
195
this.prettyViewStates.push(view);
196
}
197
this._currentPrettyViewState = view;
198
}));
199
}
200
201
public getAndClearViewStates(): (string | undefined)[] {
202
const arr = [...this.prettyViewStates];
203
this.prettyViewStates.length = 0;
204
return arr;
205
}
206
207
public keyboardType(text: string): void {
208
this.editor.trigger('keyboard', 'type', { text });
209
}
210
211
public cursorUp(): void {
212
this.editor.runCommand(CoreNavigationCommands.CursorUp, null);
213
}
214
215
public cursorRight(): void {
216
this.editor.runCommand(CoreNavigationCommands.CursorRight, null);
217
}
218
219
public cursorLeft(): void {
220
this.editor.runCommand(CoreNavigationCommands.CursorLeft, null);
221
}
222
223
public cursorDown(): void {
224
this.editor.runCommand(CoreNavigationCommands.CursorDown, null);
225
}
226
227
public cursorLineEnd(): void {
228
this.editor.runCommand(CoreNavigationCommands.CursorLineEnd, null);
229
}
230
231
public leftDelete(): void {
232
this.editor.runCommand(CoreEditingCommands.DeleteLeft, null);
233
}
234
}
235
236
export interface IWithAsyncTestCodeEditorAndInlineCompletionsModel {
237
editor: ITestCodeEditor;
238
editorViewModel: ViewModel;
239
model: InlineCompletionsModel;
240
context: GhostTextContext;
241
store: DisposableStore;
242
}
243
244
export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
245
text: string,
246
options: TestCodeEditorInstantiationOptions & { provider?: InlineCompletionsProvider; fakeClock?: boolean },
247
callback: (args: IWithAsyncTestCodeEditorAndInlineCompletionsModel) => Promise<T>): Promise<T> {
248
return await runWithFakedTimers({
249
useFakeTimers: options.fakeClock,
250
}, async () => {
251
const disposableStore = new DisposableStore();
252
253
try {
254
if (options.provider) {
255
const languageFeaturesService = new LanguageFeaturesService();
256
if (!options.serviceCollection) {
257
options.serviceCollection = new ServiceCollection();
258
}
259
options.serviceCollection.set(ILanguageFeaturesService, languageFeaturesService);
260
// eslint-disable-next-line local/code-no-any-casts
261
options.serviceCollection.set(IAccessibilitySignalService, {
262
playSignal: async () => { },
263
isSoundEnabled(signal: unknown) { return false; },
264
} as any);
265
options.serviceCollection.set(IBulkEditService, {
266
apply: async () => { throw new Error('IBulkEditService.apply not implemented'); },
267
hasPreviewHandler: () => { throw new Error('IBulkEditService.hasPreviewHandler not implemented'); },
268
setPreviewHandler: () => { throw new Error('IBulkEditService.setPreviewHandler not implemented'); },
269
_serviceBrand: undefined,
270
});
271
options.serviceCollection.set(ITextModelService, new SyncDescriptor(MockTextModelService));
272
options.serviceCollection.set(IDefaultAccountService, {
273
_serviceBrand: undefined,
274
onDidChangeDefaultAccount: Event.None,
275
onDidChangePolicyData: Event.None,
276
policyData: null,
277
getDefaultAccount: async () => null,
278
setDefaultAccountProvider: () => { },
279
getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; },
280
refresh: async () => { return null; },
281
signIn: async () => { return null; },
282
});
283
options.serviceCollection.set(IRenameSymbolTrackerService, new NullRenameSymbolTrackerService());
284
285
const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider);
286
disposableStore.add(d);
287
}
288
289
let result: T;
290
await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => {
291
instantiationService.stubInstance(InlineSuggestionsView, {
292
shouldShowHoverAtViewZone: () => false,
293
dispose: () => { },
294
});
295
const controller = instantiationService.createInstance(InlineCompletionsController, editor);
296
const model = controller.model.get()!;
297
const context = new GhostTextContext(model, editor);
298
try {
299
result = await callback({ editor, editorViewModel, model, context, store: disposableStore });
300
} finally {
301
context.dispose();
302
model.dispose();
303
controller.dispose();
304
}
305
});
306
307
if (options.provider instanceof MockInlineCompletionsProvider) {
308
options.provider.assertNotCalledTwiceWithin50ms();
309
}
310
311
return result!;
312
} finally {
313
disposableStore.dispose();
314
}
315
});
316
}
317
318
export class AnnotatedString {
319
public readonly value: string;
320
public readonly markers: { mark: string; idx: number }[];
321
322
constructor(src: string, annotations: string[] = ['↓']) {
323
const markers = findMarkers(src, annotations);
324
this.value = markers.textWithoutMarkers;
325
this.markers = markers.results;
326
}
327
328
getMarkerOffset(markerIdx = 0): number {
329
if (markerIdx >= this.markers.length) {
330
throw new BugIndicatingError(`Marker index ${markerIdx} out of bounds`);
331
}
332
return this.markers[markerIdx].idx;
333
}
334
}
335
336
function findMarkers(text: string, markers: string[]): {
337
results: { mark: string; idx: number }[];
338
textWithoutMarkers: string;
339
} {
340
const results: { mark: string; idx: number }[] = [];
341
let textWithoutMarkers = '';
342
343
markers.sort((a, b) => b.length - a.length);
344
345
let pos = 0;
346
for (let i = 0; i < text.length;) {
347
let foundMarker = false;
348
for (const marker of markers) {
349
if (text.startsWith(marker, i)) {
350
results.push({ mark: marker, idx: pos });
351
i += marker.length;
352
foundMarker = true;
353
break;
354
}
355
}
356
if (!foundMarker) {
357
textWithoutMarkers += text[i];
358
pos++;
359
i++;
360
}
361
}
362
363
return { results, textWithoutMarkers };
364
}
365
366
export class AnnotatedText extends AnnotatedString {
367
private readonly _transformer = new PositionOffsetTransformer(this.value);
368
369
getMarkerPosition(markerIdx = 0): Position {
370
return this._transformer.getPosition(this.getMarkerOffset(markerIdx));
371
}
372
}
373
374
class MockTextModelService implements ITextModelService {
375
readonly _serviceBrand: undefined;
376
377
constructor(
378
@IModelService private readonly _modelService: IModelService,
379
) { }
380
381
async createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
382
const model = this._modelService.getModel(resource);
383
if (!model) {
384
throw new Error(`MockTextModelService: Model not found for ${resource.toString()}`);
385
}
386
return {
387
object: {
388
textEditorModel: model,
389
getLanguageId: () => model.getLanguageId(),
390
isReadonly: () => false,
391
isDisposed: () => model.isDisposed(),
392
isResolved: () => true,
393
onWillDispose: model.onWillDispose,
394
resolve: async () => { },
395
createSnapshot: () => model.createSnapshot(),
396
dispose: () => { },
397
},
398
dispose: () => { },
399
};
400
}
401
402
registerTextModelContentProvider(): never {
403
throw new Error('MockTextModelService.registerTextModelContentProvider not implemented');
404
}
405
406
canHandleResource(): boolean {
407
return false;
408
}
409
}
410
411