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
4798 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 } 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 { 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 { Event } from '../../../../../base/common/event.js';
31
32
export class MockInlineCompletionsProvider implements InlineCompletionsProvider {
33
private returnValue: InlineCompletion[] = [];
34
private delayMs: number = 0;
35
36
private callHistory = new Array<unknown>();
37
private calledTwiceIn50Ms = false;
38
39
constructor(
40
public readonly enableForwardStability = false,
41
) { }
42
43
public setReturnValue(value: InlineCompletion | undefined, delayMs: number = 0): void {
44
this.returnValue = value ? [value] : [];
45
this.delayMs = delayMs;
46
}
47
48
public setReturnValues(values: InlineCompletion[], delayMs: number = 0): void {
49
this.returnValue = values;
50
this.delayMs = delayMs;
51
}
52
53
public getAndClearCallHistory() {
54
const history = [...this.callHistory];
55
this.callHistory = [];
56
return history;
57
}
58
59
public assertNotCalledTwiceWithin50ms() {
60
if (this.calledTwiceIn50Ms) {
61
throw new Error('provideInlineCompletions has been called at least twice within 50ms. This should not happen.');
62
}
63
}
64
65
private lastTimeMs: number | undefined = undefined;
66
67
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {
68
const currentTimeMs = new Date().getTime();
69
if (this.lastTimeMs && currentTimeMs - this.lastTimeMs < 50) {
70
this.calledTwiceIn50Ms = true;
71
}
72
this.lastTimeMs = currentTimeMs;
73
74
this.callHistory.push({
75
position: position.toString(),
76
triggerKind: context.triggerKind,
77
text: model.getValue()
78
});
79
const result = new Array<InlineCompletion>();
80
for (const v of this.returnValue) {
81
const x = { ...v };
82
if (!x.range) {
83
x.range = model.getFullModelRange();
84
}
85
result.push(x);
86
}
87
88
if (this.delayMs > 0) {
89
await timeout(this.delayMs);
90
}
91
92
return { items: result, enableForwardStability: this.enableForwardStability };
93
}
94
disposeInlineCompletions() { }
95
handleItemDidShow() { }
96
}
97
98
export class MockSearchReplaceCompletionsProvider implements InlineCompletionsProvider {
99
private _map = new Map<string, string>();
100
101
public add(search: string, replace: string): void {
102
this._map.set(search, replace);
103
}
104
105
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {
106
const text = model.getValue();
107
for (const [search, replace] of this._map) {
108
const idx = text.indexOf(search);
109
// replace idx...idx+text.length with replace
110
if (idx !== -1) {
111
const range = Range.fromPositions(model.getPositionAt(idx), model.getPositionAt(idx + search.length));
112
return {
113
items: [
114
{ range, insertText: replace, isInlineEdit: true }
115
]
116
};
117
}
118
}
119
return { items: [] };
120
}
121
disposeInlineCompletions() { }
122
handleItemDidShow() { }
123
}
124
125
export class InlineEditContext extends Disposable {
126
public readonly prettyViewStates = new Array<string | undefined>();
127
128
constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {
129
super();
130
131
const edit = derived(reader => {
132
const state = model.state.read(reader);
133
return state ? new TextEdit(state.edits) : undefined;
134
});
135
136
this._register(autorun(reader => {
137
/** @description update */
138
const e = edit.read(reader);
139
let view: string | undefined;
140
141
if (e) {
142
view = e.toString(this.editor.getValue());
143
} else {
144
view = undefined;
145
}
146
147
this.prettyViewStates.push(view);
148
}));
149
}
150
151
public getAndClearViewStates(): (string | undefined)[] {
152
const arr = [...this.prettyViewStates];
153
this.prettyViewStates.length = 0;
154
return arr;
155
}
156
}
157
158
export class GhostTextContext extends Disposable {
159
public readonly prettyViewStates = new Array<string | undefined>();
160
private _currentPrettyViewState: string | undefined;
161
public get currentPrettyViewState() {
162
return this._currentPrettyViewState;
163
}
164
165
constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {
166
super();
167
168
this._register(autorun(reader => {
169
/** @description update */
170
const ghostText = model.primaryGhostText.read(reader);
171
let view: string | undefined;
172
if (ghostText) {
173
view = ghostText.render(this.editor.getValue(), true);
174
} else {
175
view = this.editor.getValue();
176
}
177
178
if (this._currentPrettyViewState !== view) {
179
this.prettyViewStates.push(view);
180
}
181
this._currentPrettyViewState = view;
182
}));
183
}
184
185
public getAndClearViewStates(): (string | undefined)[] {
186
const arr = [...this.prettyViewStates];
187
this.prettyViewStates.length = 0;
188
return arr;
189
}
190
191
public keyboardType(text: string): void {
192
this.editor.trigger('keyboard', 'type', { text });
193
}
194
195
public cursorUp(): void {
196
this.editor.runCommand(CoreNavigationCommands.CursorUp, null);
197
}
198
199
public cursorRight(): void {
200
this.editor.runCommand(CoreNavigationCommands.CursorRight, null);
201
}
202
203
public cursorLeft(): void {
204
this.editor.runCommand(CoreNavigationCommands.CursorLeft, null);
205
}
206
207
public cursorDown(): void {
208
this.editor.runCommand(CoreNavigationCommands.CursorDown, null);
209
}
210
211
public cursorLineEnd(): void {
212
this.editor.runCommand(CoreNavigationCommands.CursorLineEnd, null);
213
}
214
215
public leftDelete(): void {
216
this.editor.runCommand(CoreEditingCommands.DeleteLeft, null);
217
}
218
}
219
220
export interface IWithAsyncTestCodeEditorAndInlineCompletionsModel {
221
editor: ITestCodeEditor;
222
editorViewModel: ViewModel;
223
model: InlineCompletionsModel;
224
context: GhostTextContext;
225
store: DisposableStore;
226
}
227
228
export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
229
text: string,
230
options: TestCodeEditorInstantiationOptions & { provider?: InlineCompletionsProvider; fakeClock?: boolean },
231
callback: (args: IWithAsyncTestCodeEditorAndInlineCompletionsModel) => Promise<T>): Promise<T> {
232
return await runWithFakedTimers({
233
useFakeTimers: options.fakeClock,
234
}, async () => {
235
const disposableStore = new DisposableStore();
236
237
try {
238
if (options.provider) {
239
const languageFeaturesService = new LanguageFeaturesService();
240
if (!options.serviceCollection) {
241
options.serviceCollection = new ServiceCollection();
242
}
243
options.serviceCollection.set(ILanguageFeaturesService, languageFeaturesService);
244
// eslint-disable-next-line local/code-no-any-casts
245
options.serviceCollection.set(IAccessibilitySignalService, {
246
playSignal: async () => { },
247
isSoundEnabled(signal: unknown) { return false; },
248
} as any);
249
options.serviceCollection.set(IBulkEditService, {
250
apply: async () => { throw new Error('IBulkEditService.apply not implemented'); },
251
hasPreviewHandler: () => { throw new Error('IBulkEditService.hasPreviewHandler not implemented'); },
252
setPreviewHandler: () => { throw new Error('IBulkEditService.setPreviewHandler not implemented'); },
253
_serviceBrand: undefined,
254
});
255
options.serviceCollection.set(IDefaultAccountService, {
256
_serviceBrand: undefined,
257
onDidChangeDefaultAccount: Event.None,
258
getDefaultAccount: async () => null,
259
setDefaultAccount: () => { },
260
});
261
262
const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider);
263
disposableStore.add(d);
264
}
265
266
let result: T;
267
await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => {
268
instantiationService.stubInstance(InlineSuggestionsView, {
269
shouldShowHoverAtViewZone: () => false,
270
dispose: () => { },
271
});
272
const controller = instantiationService.createInstance(InlineCompletionsController, editor);
273
const model = controller.model.get()!;
274
const context = new GhostTextContext(model, editor);
275
try {
276
result = await callback({ editor, editorViewModel, model, context, store: disposableStore });
277
} finally {
278
context.dispose();
279
model.dispose();
280
controller.dispose();
281
}
282
});
283
284
if (options.provider instanceof MockInlineCompletionsProvider) {
285
options.provider.assertNotCalledTwiceWithin50ms();
286
}
287
288
return result!;
289
} finally {
290
disposableStore.dispose();
291
}
292
});
293
}
294
295
export class AnnotatedString {
296
public readonly value: string;
297
public readonly markers: { mark: string; idx: number }[];
298
299
constructor(src: string, annotations: string[] = ['↓']) {
300
const markers = findMarkers(src, annotations);
301
this.value = markers.textWithoutMarkers;
302
this.markers = markers.results;
303
}
304
305
getMarkerOffset(markerIdx = 0): number {
306
if (markerIdx >= this.markers.length) {
307
throw new BugIndicatingError(`Marker index ${markerIdx} out of bounds`);
308
}
309
return this.markers[markerIdx].idx;
310
}
311
}
312
313
function findMarkers(text: string, markers: string[]): {
314
results: { mark: string; idx: number }[];
315
textWithoutMarkers: string;
316
} {
317
const results: { mark: string; idx: number }[] = [];
318
let textWithoutMarkers = '';
319
320
markers.sort((a, b) => b.length - a.length);
321
322
let pos = 0;
323
for (let i = 0; i < text.length;) {
324
let foundMarker = false;
325
for (const marker of markers) {
326
if (text.startsWith(marker, i)) {
327
results.push({ mark: marker, idx: pos });
328
i += marker.length;
329
foundMarker = true;
330
break;
331
}
332
}
333
if (!foundMarker) {
334
textWithoutMarkers += text[i];
335
pos++;
336
i++;
337
}
338
}
339
340
return { results, textWithoutMarkers };
341
}
342
343
export class AnnotatedText extends AnnotatedString {
344
private readonly _transformer = new PositionOffsetTransformer(this.value);
345
346
getMarkerPosition(markerIdx = 0): Position {
347
return this._transformer.getPosition(this.getMarkerOffset(markerIdx));
348
}
349
}
350
351