Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/renameSuggestionsProvider.stest.ts
13389 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 * as assert from 'assert';
6
import { outdent } from 'outdent';
7
import type * as vscode from 'vscode';
8
import { guessNamingConvention, NamingConvention } from '../../src/extension/renameSuggestions/common/namingConvention';
9
import { RenameSuggestionsProvider } from '../../src/extension/renameSuggestions/node/renameSuggestionsProvider';
10
import { TestingServiceCollection } from '../../src/platform/test/node/services';
11
import { IRelativeFile } from '../../src/platform/test/node/simulationWorkspace';
12
import { deannotateSrc } from '../../src/util/common/test/annotatedSrc';
13
import { CancellationToken } from '../../src/util/vs/base/common/cancellation';
14
import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';
15
import { NewSymbolNameTriggerKind, Range } from '../../src/vscodeTypes';
16
import { ISimulationTestRuntime, ssuite, stest } from '../base/stest';
17
import { setupSimulationWorkspace, teardownSimulationWorkspace } from './inlineChatSimulator';
18
import { INLINE_INITIAL_DOC_TAG } from './shared/sharedTypes';
19
20
type OffsetRange = {
21
startIndex: number;
22
endIndex: number;
23
};
24
25
function offsetRangeToPositionRange(offsetRange: OffsetRange, document: vscode.TextDocument): vscode.Range {
26
const startPos = document.positionAt(offsetRange.startIndex);
27
const endPos = document.positionAt(offsetRange.endIndex);
28
const range = new Range(startPos, endPos);
29
return range;
30
}
31
32
ssuite({ title: 'Rename suggestions', location: 'external' }, () => {
33
34
class AlwaysEnabledNewSymbolNamesProvider extends RenameSuggestionsProvider {
35
override isEnabled() {
36
return true;
37
}
38
}
39
40
/**
41
* Asserts that each newSymbolName includes at least one of the searchStrings.
42
*
43
* @remark lower-cases symbol names for string search but not search-strings
44
*/
45
function assertIncludesLowercased(newSymbolNames: vscode.NewSymbolName[], searchStrings: string | string[]) {
46
searchStrings = Array.isArray(searchStrings) ? searchStrings : [searchStrings];
47
searchStrings = searchStrings.map(s => s.toLowerCase());
48
for (const symbol of newSymbolNames) {
49
const newSymbolNameLowercase = symbol.newSymbolName.toLowerCase();
50
assert.ok(
51
searchStrings.some(searchString => newSymbolNameLowercase.includes(searchString)),
52
`expected to include ${searchStrings.map(s => `'${s}'`).join(' or ')} but received '${newSymbolNameLowercase}'`
53
);
54
}
55
}
56
57
function assertLength(newSymbolNames: vscode.NewSymbolName[]) {
58
assert.ok(newSymbolNames.length > 1,
59
`expected at least ${1} newSymbolNames but received ${newSymbolNames.length}\n${JSON.stringify(newSymbolNames.map(v => v.newSymbolName), null, '\t')}`);
60
}
61
62
function countMatches(newSymbolNames: vscode.NewSymbolName[], searchStrings: string) {
63
const searchStringsLowercased = searchStrings.toLowerCase();
64
return newSymbolNames.filter(symbol => symbol.newSymbolName.toLowerCase().includes(searchStringsLowercased)).length;
65
}
66
67
type IRenameScenarioFile = (IRelativeFile & {
68
isCurrent?: boolean;
69
});
70
71
async function provideNewSymbolNames(testingServiceCollection: TestingServiceCollection, files: IRenameScenarioFile[]) {
72
73
// find current file from files, deannoate it and put it at the end
74
75
const currentFileIx = files.length === 1 ? 0 : files.findIndex(f => f.isCurrent);
76
if (currentFileIx < 0) { throw new Error(`No current file found from files:\n ${JSON.stringify(files, null, '\t')}`); }
77
let currentFile = files[currentFileIx];
78
files.splice(currentFileIx, 1);
79
const { deannotatedSrc, annotatedRange } = deannotateSrc(currentFile.fileContents);
80
currentFile = {
81
...currentFile,
82
fileContents: deannotatedSrc,
83
};
84
files.push(currentFile);
85
86
// set up workspace
87
const workspace = setupSimulationWorkspace(testingServiceCollection, { files });
88
const accessor = testingServiceCollection.createTestingAccessor();
89
try {
90
const document = workspace.getDocument(currentFile.fileName).document;
91
const renameRange = offsetRangeToPositionRange(annotatedRange, document);
92
93
// write initial file contents to disk to be able to view it from swb
94
95
const testRuntime = accessor.get(ISimulationTestRuntime);
96
const workspacePath = workspace.getFilePath(document.uri);
97
await testRuntime.writeFile(workspacePath + '.txt', document.getText(), INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts
98
99
// get rename suggestions
100
101
const provider = accessor.get(IInstantiationService).createInstance(AlwaysEnabledNewSymbolNamesProvider);
102
103
const symbols = await provider.provideNewSymbolNames(document, renameRange, NewSymbolNameTriggerKind.Invoke, CancellationToken.None);
104
105
return symbols;
106
107
} finally {
108
await teardownSimulationWorkspace(accessor, workspace);
109
}
110
111
}
112
113
stest('rename a function at its definition', async (testingServiceCollection) => {
114
const fileContents = outdent`
115
export function <<fibonacci>>(n: number): number {
116
if (n <= 1) {
117
return 1;
118
}
119
return fibonacci(n - 1) + fibonacci(n - 2);
120
}
121
`;
122
123
const file: IRenameScenarioFile = {
124
kind: 'relativeFile',
125
fileName: 'fibonacci.ts',
126
languageId: 'typescript',
127
fileContents,
128
isCurrent: true,
129
};
130
131
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
132
133
assert.ok(symbols, `Expected symbols to be non-null`);
134
assertLength(symbols);
135
assert.ok(countMatches(symbols, 'fib') >= Math.floor(symbols.length * 0.8), 'Expected 80% of symbols to include fib: ' + JSON.stringify(symbols.map(s => s.newSymbolName)));
136
});
137
138
stest('rename follows naming convention _ - rename a function (with underscore) at its definition', async (testingServiceCollection) => {
139
const fileContents = outdent`
140
export function <<_fib>>(n: number): number {
141
if (n <= 1) {
142
return 1;
143
}
144
return _fib(n - 1) + _fib(n - 2);
145
}
146
`;
147
148
const file: IRenameScenarioFile = {
149
kind: 'relativeFile',
150
fileName: 'fibonacci.ts',
151
languageId: 'typescript',
152
fileContents,
153
isCurrent: true,
154
};
155
156
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
157
158
assert.ok(symbols, `Expected symbols to be non-null`);
159
assertLength(symbols);
160
assert.ok(symbols.some(s => s.newSymbolName.startsWith('_')), 'Expected to include symbols with underscore');
161
assertIncludesLowercased(symbols, ['fib', 'sequence']);
162
});
163
164
stest('rename a variable reference within a function', async (testingServiceCollection) => {
165
const fileContents = (outdent`
166
function fromQueryMatches(matches: Parser.QueryMatch[]): InSourceTreeSitterQuery[] {
167
const captures = matches.flatMap(({ captures }) => captures)
168
.sort((a, b) => a.node.startIndex - b.node.startIndex || b.node.endIndex - a.node.endIndex);
169
170
const qs: InSourceTreeSitterQuery[] = [];
171
for (let i = 0; i < captures.length;) {
172
const capture = captures[i];
173
if (capture.name === 'call_expression' && captures[i + 2].name === 'target_language' && captures[i + 3].name === 'query_src_with_quotes') {
174
<<qs>>.push(new InSourceTreeSitterQuery(captures[i + 2].node, captures[i + 3].node));
175
i += 4;
176
} else {
177
i++;
178
}
179
}
180
181
return qs;
182
}
183
`);
184
185
const file: IRenameScenarioFile = {
186
kind: 'relativeFile',
187
fileName: 'queryDiagnosticsProvider.ts',
188
languageId: 'typescript',
189
fileContents,
190
isCurrent: true,
191
};
192
193
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
194
195
assert.ok(symbols, `Expected symbols to be non-null`);
196
assertLength(symbols);
197
assertIncludesLowercased(symbols, ['quer']);
198
});
199
200
stest('rename a SCREAMING_SNAKE_CASE enum member', async (testingServiceCollection) => {
201
const fileContents = (outdent`
202
enum LoadStatus {
203
NOT_LOADED,
204
LOADING_FROM_CACHE,
205
<<LOADING_FROM_SRVER>>,
206
LOADED,
207
}
208
`);
209
210
const file: IRenameScenarioFile = {
211
kind: 'relativeFile',
212
fileName: 'queryDiagnosticsProvider.ts',
213
languageId: 'typescript',
214
fileContents,
215
isCurrent: true,
216
};
217
218
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
219
220
assert.ok(symbols, `Expected symbols to be non-null`);
221
assertLength(symbols);
222
assert.ok(symbols.every(symbol => guessNamingConvention(symbol.newSymbolName) === NamingConvention.ScreamingSnakeCase), 'Expected all symbols to be SCREAMING_SNAKE_CASE');
223
});
224
225
stest('respect context: infer name based on existing code - enum member', async (testingServiceCollection) => {
226
const fileContents = (outdent`
227
enum Direction {
228
UP,
229
DOWN,
230
RIGHT,
231
<<TODO>>,
232
}
233
`);
234
235
const file: IRenameScenarioFile = {
236
kind: 'relativeFile',
237
fileName: 'direction.ts',
238
languageId: 'typescript',
239
fileContents,
240
isCurrent: true,
241
};
242
243
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
244
245
assert.ok(symbols, `Expected symbols to be non-null`);
246
assertLength(symbols);
247
assert.ok(symbols.every(symbol => [NamingConvention.Uppercase, NamingConvention.ScreamingSnakeCase].includes(guessNamingConvention(symbol.newSymbolName))), 'Expected all symbols to be SCREAMING_SNAKE_CASE or UPPERCASE');
248
});
249
250
stest('rename a function call - definition in same file', async (testingServiceCollection) => {
251
const fileContents = (outdent`
252
export function f(n: number): number {
253
if (n <= 1) {
254
return 1;
255
}
256
return f(n - 1) + f(n - 2);
257
}
258
259
const result = <<f>>(10);
260
`);
261
const file: IRenameScenarioFile = {
262
kind: 'relativeFile',
263
fileName: 'script.ts',
264
languageId: 'typescript',
265
fileContents,
266
isCurrent: true,
267
};
268
269
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
270
271
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
272
assertIncludesLowercased(symbols, ['fib', 'sequence']);
273
});
274
275
stest('rename a function call - definition in different file', async (testingServiceCollection) => {
276
const currentFile: IRenameScenarioFile = {
277
kind: 'relativeFile',
278
fileName: 'script.ts',
279
languageId: 'typescript',
280
fileContents: outdent`
281
import { f } from './impl';
282
283
const result = <<f>>(10);
284
`,
285
isCurrent: true,
286
};
287
288
const fileWithFnDef: IRelativeFile = {
289
kind: 'relativeFile',
290
fileName: 'impl.ts',
291
languageId: 'typescript',
292
fileContents: outdent`
293
export function f(n: number): number {
294
if (n <= 1) {
295
return 1;
296
}
297
return f(n - 1) + f(n - 2);
298
}
299
`,
300
};
301
302
const symbols = await provideNewSymbolNames(testingServiceCollection, [currentFile, fileWithFnDef]);
303
304
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
305
assertIncludesLowercased(symbols, ['fib', 'sequence']);
306
});
307
308
stest('rename type definition', async (testingServiceCollection) => {
309
const fileContents = (outdent`
310
type <<t>> = {
311
firstName: string;
312
lastName: string;
313
}
314
`);
315
const file: IRenameScenarioFile = {
316
kind: 'relativeFile',
317
fileName: 'script.ts',
318
languageId: 'typescript',
319
fileContents,
320
isCurrent: true,
321
};
322
323
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
324
325
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
326
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');
327
});
328
329
stest('rename type definition when it is used in the same file', async (testingServiceCollection) => {
330
const fileContents = (outdent`
331
type <<t>> = {
332
firstName: string;
333
lastName: string;
334
}
335
336
function greet(p: t): string {
337
return 'Hello ' + p.firstName + ' ' + p.lastName;
338
}
339
`);
340
const file: IRenameScenarioFile = {
341
kind: 'relativeFile',
342
fileName: 'script.ts',
343
languageId: 'typescript',
344
fileContents,
345
isCurrent: true,
346
};
347
348
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
349
350
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
351
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');
352
});
353
354
stest('rename type reference - same file', async (testingServiceCollection) => {
355
const fileContents = (outdent`
356
type t = {
357
firstName: string;
358
lastName: string;
359
}
360
361
function greet(p: <<t>>): string {
362
return 'Hello ' + p.firstName + ' ' + p.lastName;
363
}
364
`);
365
const file: IRenameScenarioFile = {
366
kind: 'relativeFile',
367
fileName: 'script.ts',
368
languageId: 'typescript',
369
fileContents,
370
isCurrent: true,
371
};
372
373
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
374
375
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
376
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Includes person');
377
});
378
379
stest('rename type reference - same file with 2 possible defs', async (testingServiceCollection) => {
380
const fileContents = (outdent`
381
type t = {
382
firstName: string;
383
lastName: string;
384
}
385
386
const t = {
387
bar: 1
388
}
389
390
function greet(p: <<t>>): string {
391
return 'Hello ' + p.firstName + ' ' + p.lastName;
392
}
393
`);
394
const file: IRenameScenarioFile = {
395
kind: 'relativeFile',
396
fileName: 'script.ts',
397
languageId: 'typescript',
398
fileContents,
399
isCurrent: true,
400
};
401
402
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
403
404
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
405
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Includes person');
406
});
407
408
stest('rename class - same file', async (testingServiceCollection) => {
409
const fileContents = outdent`
410
class <<P>> {
411
firstName: string;
412
lastName: string;
413
}
414
`;
415
const file: IRenameScenarioFile = {
416
kind: 'relativeFile',
417
fileName: 'script.ts',
418
languageId: 'typescript',
419
fileContents,
420
isCurrent: true,
421
};
422
423
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
424
425
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
426
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');
427
});
428
429
stest('rename class reference - same file', async (testingServiceCollection) => {
430
const fileContents = outdent`
431
class P {
432
firstName: string;
433
lastName: string;
434
}
435
436
function greet(p: <<P>>): string {
437
return 'Hello ' + p.firstName + ' ' + p.lastName;
438
}
439
`;
440
const file: IRenameScenarioFile = {
441
kind: 'relativeFile',
442
fileName: 'script.ts',
443
languageId: 'typescript',
444
fileContents,
445
isCurrent: true,
446
};
447
448
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
449
450
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
451
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('person')), 'Inludes person');
452
});
453
454
stest('rename method with field-awareness', async (testingServiceCollection) => {
455
const fileContents = outdent`
456
class Processor {
457
private stdoutBuffer: string = '';
458
459
<<clearBuffer>>() {
460
// TODO: implement
461
}
462
}
463
`;
464
const file: IRenameScenarioFile = {
465
kind: 'relativeFile',
466
fileName: 'script.ts',
467
languageId: 'typescript',
468
fileContents,
469
isCurrent: true,
470
};
471
472
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
473
474
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
475
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('stdout')), 'Knows about `stdoutBuffer`');
476
});
477
478
stest('non-tree-sitter language', async (testingServiceCollection) => {
479
const fileContents = outdent`
480
let rec <<f>> n = if n <= 1 then 1 else f (n - 1) + f (n - 2)
481
`;
482
const file: IRenameScenarioFile = {
483
kind: 'relativeFile',
484
fileName: 'impl.ml',
485
languageId: 'ocaml',
486
fileContents,
487
isCurrent: true,
488
};
489
490
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
491
492
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
493
assert.ok(symbols.some(s => s.newSymbolName.toLowerCase().includes('fib')), 'Includes fib');
494
});
495
496
stest('rename class name - CSS', async (testingServiceCollection) => {
497
const fileContents = outdent`
498
.box {
499
background-color: #fff;
500
}
501
502
<<.button>> {
503
color: #fff;
504
background-color: #000;
505
}
506
`;
507
const file: IRenameScenarioFile = {
508
kind: 'relativeFile',
509
fileName: 'style.css',
510
languageId: 'css',
511
fileContents,
512
isCurrent: true,
513
};
514
515
const symbols = await provideNewSymbolNames(testingServiceCollection, [file]);
516
517
assert.ok(symbols && symbols.length > 1, 'Expected to provide > 1 symbols');
518
assert.ok(symbols.every(s => s.newSymbolName.match(/^\.([a-zA-Z]+)/)), 'All symbols are class names');
519
});
520
521
});
522
523