Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/renameSuggestions/node/renameSuggestionsProvider.ts
13399 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 type * as vscode from 'vscode';
7
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
8
import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';
9
import { IInteractionService } from '../../../platform/chat/common/interactionService';
10
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
11
import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
12
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
13
import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';
14
import { IChatEndpoint } from '../../../platform/networking/common/networking';
15
import { INotificationService } from '../../../platform/notification/common/notificationService';
16
import { ISimulationTestContext } from '../../../platform/simulationTestContext/common/simulationTestContext';
17
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
18
import { StopWatch } from '../../../util/vs/base/common/stopwatch';
19
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
20
import { NewSymbolName, NewSymbolNameTag, NewSymbolNameTriggerKind } from '../../../vscodeTypes';
21
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
22
import { enforceNamingConvention, guessNamingConvention, NamingConvention } from '../common/namingConvention';
23
import { RenameSuggestionsPrompt } from './renameSuggestionsPrompt';
24
25
/**
26
* The format of the reply from the model.
27
*/
28
type ReplyFormat =
29
/** When the reply was a JSON array of strings as instructed in the prompt */
30
| 'jsonStringArray'
31
32
/** When there were multiple JSON array's matched by the regex */
33
| 'multiJsonStringArray'
34
35
/** When the reply was an ordered or unordered list */
36
| 'list'
37
38
/** When we couldn't parse the response */
39
| 'unknown'
40
;
41
42
enum ProvideCallCancellationReason {
43
None = '',
44
AfterEnablementCheck = 'afterEnablementCheck',
45
AfterRunParametersFetch = 'afterRunParametersFetch',
46
AfterPromptCompute = 'afterPromptCompute',
47
AfterDelay = 'afterDelay',
48
AfterFetchStarted = 'afterFetchStarted',
49
}
50
51
export class RenameSuggestionsProvider implements vscode.NewSymbolNamesProvider {
52
53
public readonly supportsAutomaticTriggerKind: Promise<boolean>;
54
55
constructor(
56
@IInstantiationService private readonly _instaService: IInstantiationService,
57
@IIgnoreService private readonly _ignoreService: IIgnoreService,
58
@ITelemetryService private readonly _telemetryService: ITelemetryService,
59
@IConfigurationService private readonly _configurationService: IConfigurationService,
60
@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,
61
@ISimulationTestContext private readonly _simulationTestContext: ISimulationTestContext,
62
@IAuthenticationService private readonly _authService: IAuthenticationService,
63
@INotificationService private readonly _notificationService: INotificationService,
64
@IInteractionService private readonly _interactionService: IInteractionService
65
) {
66
67
this.supportsAutomaticTriggerKind = Promise.resolve(this.isEnabled(NewSymbolNameTriggerKind.Automatic));
68
}
69
70
protected isEnabled(triggerKind: NewSymbolNameTriggerKind) {
71
if (triggerKind === NewSymbolNameTriggerKind.Invoke) {
72
return true;
73
} else if (this._authService.copilotToken?.isFreeUser || this._authService.copilotToken?.isNoAuthUser) {
74
return false;
75
} else {
76
return this._configurationService.getConfig(ConfigKey.AutomaticRenameSuggestions);
77
}
78
}
79
80
/**
81
* @throws {Error} with `message = 'CopilotFeatureUnavailableOrDisabled' if the feature is not available
82
* @throws {Error} with `message = 'CopilotIgnoredDocument' if the document is Copilot-ignored
83
*/
84
async provideNewSymbolNames(_document: vscode.TextDocument, range: vscode.Range, triggerKind: NewSymbolNameTriggerKind, token: vscode.CancellationToken): Promise<NewSymbolName[] | null> {
85
const document = TextDocumentSnapshot.create(_document);
86
87
let cancellationReason: ProvideCallCancellationReason = ProvideCallCancellationReason.None;
88
89
const beforeDelaySW = new StopWatch();
90
91
// @ulugbekna: capture the symbol name that is being renamed before an await to avoid document being changed under us
92
const currentSymbolName = document.getText(range);
93
94
if (!this.isEnabled(triggerKind)) {
95
throw new Error('CopilotFeatureUnavailableOrDisabled');
96
}
97
98
if (await this._ignoreService.isCopilotIgnored(document.uri)) {
99
throw new Error('CopilotIgnoredDocument');
100
}
101
102
const languageId = document.languageId;
103
104
let expectedDelayBeforeFetch: number | undefined;
105
let timeElapsedBeforeDelay: number | undefined;
106
107
if (token.isCancellationRequested) {
108
cancellationReason = ProvideCallCancellationReason.AfterEnablementCheck;
109
} else {
110
const endpoint = await this._endpointProvider.getChatEndpoint('copilot-fast');
111
expectedDelayBeforeFetch = this.delayBeforeFetchMs;
112
113
if (token.isCancellationRequested) {
114
cancellationReason = ProvideCallCancellationReason.AfterRunParametersFetch;
115
} else {
116
117
const sw = new StopWatch(false);
118
119
sw.reset();
120
const promptRenderResult = await this._computePrompt(document, range, endpoint, token);
121
const promptConstructionTime = sw.elapsed();
122
123
if (token.isCancellationRequested) {
124
cancellationReason = ProvideCallCancellationReason.AfterPromptCompute;
125
} else {
126
127
timeElapsedBeforeDelay = beforeDelaySW.elapsed();
128
129
let actualDelayBeforeFetch: number | undefined;
130
if (triggerKind === NewSymbolNameTriggerKind.Automatic) {
131
actualDelayBeforeFetch = expectedDelayBeforeFetch ? Math.max(0, expectedDelayBeforeFetch - timeElapsedBeforeDelay) : undefined;
132
if (actualDelayBeforeFetch !== undefined && actualDelayBeforeFetch > 0) {
133
await new Promise(resolve => setTimeout(resolve, actualDelayBeforeFetch));
134
}
135
}
136
137
if (token.isCancellationRequested) {
138
cancellationReason = ProvideCallCancellationReason.AfterDelay;
139
} else {
140
141
sw.reset();
142
this._interactionService.startInteraction();
143
const fetchResult = await endpoint.makeChatRequest(
144
'renameSuggestionsProvider',
145
promptRenderResult.messages,
146
undefined, // TODO@ulugbekna: should we terminate on `]` (closing for JSON array that we expect to receive from the model)
147
token,
148
ChatLocation.Other,
149
undefined,
150
{
151
top_p: undefined,
152
temperature: undefined
153
},
154
true
155
);
156
const fetchTime = sw.elapsed();
157
158
if (fetchResult.type === ChatFetchResponseType.QuotaExceeded || (fetchResult.type === ChatFetchResponseType.RateLimited && this._authService.copilotToken?.isNoAuthUser)) {
159
await this._notificationService.showQuotaExceededDialog({ isNoAuthUser: this._authService.copilotToken?.isNoAuthUser ?? false });
160
}
161
162
if (token.isCancellationRequested) {
163
cancellationReason = ProvideCallCancellationReason.AfterFetchStarted;
164
}
165
166
switch (fetchResult.type) {
167
case ChatFetchResponseType.Success: {
168
const reply = fetchResult.value;
169
const { replyFormat, symbolNames, redundantCharCount: responseUnusedCharCount } = RenameSuggestionsProvider.parseResponse(reply);
170
if (replyFormat === 'unknown') {
171
this._sendInternalTelemetry({ languageId, reply });
172
}
173
this._sendPublicTelemetry({
174
triggerKind,
175
languageId,
176
cancellationReason,
177
fetchResultType: fetchResult.type,
178
promptConstructionTime,
179
promptTokenCount: promptRenderResult.tokenCount,
180
expectedDelayBeforeFetch,
181
actualDelayBeforeFetch,
182
timeElapsedBeforeDelay,
183
successResponseCharCount: reply.length,
184
responseUnusedCharCount,
185
fetchTime,
186
replyFormat,
187
symbolNamesCount: symbolNames.length,
188
});
189
190
const processedSymbolNames = RenameSuggestionsProvider.preprocessSymbolNames({ currentSymbolName, newSymbolNames: symbolNames, languageId });
191
return processedSymbolNames.map(symbolName => new NewSymbolName(symbolName, [NewSymbolNameTag.AIGenerated]));
192
}
193
default: {
194
this._sendPublicTelemetry({
195
triggerKind,
196
languageId,
197
cancellationReason,
198
fetchResultType: fetchResult.type,
199
promptConstructionTime,
200
promptTokenCount: promptRenderResult.tokenCount,
201
expectedDelayBeforeFetch,
202
actualDelayBeforeFetch,
203
timeElapsedBeforeDelay,
204
fetchTime,
205
});
206
return null;
207
}
208
}
209
}
210
}
211
}
212
}
213
214
this._sendPublicTelemetry({
215
triggerKind,
216
languageId,
217
cancellationReason,
218
expectedDelayBeforeFetch,
219
timeElapsedBeforeDelay,
220
});
221
return null;
222
}
223
224
/**
225
* The delay before fetching from the model.
226
*/
227
private get delayBeforeFetchMs() {
228
if (this._simulationTestContext.isInSimulationTests) {
229
return 0;
230
} else {
231
const DELAY_BEFORE_FETCH = 250 /* milliseconds */;
232
return DELAY_BEFORE_FETCH;
233
}
234
}
235
236
// @ulugbekna: notes:
237
// - FIXME: currently, we fail with very large definitions such as big classes or functions -- we need summarization by category, e.g., remove method implementations if we're renaming a class
238
// - idea: include hover info (i.e., usually type info & corresponding document) of the symbol being renamed in the prompt
239
// - idea: include usages of the symbol being renamed in the prompt
240
// - idea: include peer symbols (e.g., other methods in the same class) in the prompt for copilot to see conventions in the code
241
private _computePrompt(document: TextDocumentSnapshot, range: vscode.Range, chatEndpoint: IChatEndpoint, token: vscode.CancellationToken) {
242
const promptRenderer = PromptRenderer.create(
243
this._instaService,
244
chatEndpoint,
245
RenameSuggestionsPrompt,
246
{
247
document,
248
range
249
}
250
);
251
return promptRenderer.render(undefined, token);
252
}
253
254
public static preprocessSymbolNames({ currentSymbolName, newSymbolNames, languageId }: { currentSymbolName: string; newSymbolNames: string[]; languageId: string }): string[] {
255
256
const currentNameConvention = guessNamingConvention(currentSymbolName);
257
258
let targetNamingConvention: NamingConvention;
259
switch (currentNameConvention) {
260
case NamingConvention.LowerCase:
261
if (languageId === 'python') {
262
targetNamingConvention = NamingConvention.SnakeCase;
263
} else {
264
targetNamingConvention = NamingConvention.CamelCase;
265
}
266
break;
267
case NamingConvention.Uppercase:
268
case NamingConvention.CamelCase:
269
case NamingConvention.PascalCase:
270
case NamingConvention.SnakeCase:
271
case NamingConvention.ScreamingSnakeCase:
272
case NamingConvention.CapitalSnakeCase:
273
case NamingConvention.KebabCase:
274
case NamingConvention.Capitalized:
275
case NamingConvention.Unknown:
276
targetNamingConvention = currentNameConvention;
277
break;
278
default: {
279
const _exhaustiveCheck: never = currentNameConvention;
280
return _exhaustiveCheck;
281
}
282
}
283
284
if (targetNamingConvention === NamingConvention.Unknown) {
285
return newSymbolNames;
286
}
287
288
return newSymbolNames.map(newSymbolName => enforceNamingConvention(newSymbolName, targetNamingConvention));
289
}
290
291
public static parseResponse(reply: string): { replyFormat: ReplyFormat; redundantCharCount: number; symbolNames: string[] } {
292
293
const parsedAsJSONStringArray = RenameSuggestionsProvider._parseReplyAsJSONStringArray(reply);
294
if (parsedAsJSONStringArray !== undefined) {
295
return parsedAsJSONStringArray;
296
}
297
298
const parsedAsList = RenameSuggestionsProvider._parseReplyAsList(reply);
299
if (parsedAsList !== undefined) {
300
return parsedAsList;
301
}
302
303
return { replyFormat: 'unknown', symbolNames: [], redundantCharCount: reply.length };
304
}
305
306
/** try extracting from JSON string array */
307
private static _parseReplyAsJSONStringArray(reply: string) {
308
309
const jsonArrayRe = /\[.*?\]/gs; // `s` regex flag allows matching newlines using `.`
310
311
const matches = [...reply.matchAll(jsonArrayRe)];
312
313
for (let i = 0; i < matches.length; i++) {
314
const match = matches[i];
315
try {
316
const parsedJSONArray: unknown = JSON.parse(match[0]);
317
318
if (Array.isArray(parsedJSONArray)) {
319
320
const symbolNames = parsedJSONArray.filter(v => typeof v === 'string');
321
322
if (symbolNames.length > 0) {
323
const replyFormat: ReplyFormat = i === 0 ? 'jsonStringArray' : 'multiJsonStringArray';
324
const redundantCharCount = reply.length - match[0].length;
325
return { replyFormat, redundantCharCount, symbolNames: symbolNames.map(s => s.trim()) } as const;
326
}
327
}
328
} catch (error) {
329
}
330
}
331
}
332
333
private static _parseReplyAsList(reply: string) {
334
// try extracting from an ordered or unordered list
335
const listLineRe = /(?:\d+[\.|\)]|[\*\-])\s*(.*)/g;
336
const matches = reply.matchAll(listLineRe);
337
338
const symbolNames: string[] = [];
339
for (const match of matches) {
340
let symbolName = match[1].trim();
341
const punctuation = ['\'', '"', '`'];
342
if (punctuation.includes(symbolName[0])) {
343
symbolName = symbolName.slice(1);
344
}
345
if (punctuation.includes(symbolName[symbolName.length - 1])) {
346
symbolName = symbolName.slice(0, -1);
347
}
348
if (symbolName) {
349
symbolNames.push(symbolName);
350
}
351
}
352
353
if (symbolNames.length === 0) {
354
return;
355
}
356
357
const redundantCharCount = reply.length - symbolNames.reduce((acc, name) => acc + name.length, 0);
358
359
return { replyFormat: 'list' satisfies ReplyFormat, redundantCharCount, symbolNames } as const;
360
}
361
362
private _sendPublicTelemetry({
363
triggerKind,
364
languageId,
365
cancellationReason,
366
fetchResultType,
367
timeElapsedBeforeDelay,
368
promptConstructionTime,
369
promptTokenCount,
370
expectedDelayBeforeFetch,
371
actualDelayBeforeFetch,
372
successResponseCharCount,
373
responseUnusedCharCount,
374
fetchTime,
375
replyFormat,
376
symbolNamesCount
377
}: {
378
triggerKind: NewSymbolNameTriggerKind;
379
languageId: string;
380
cancellationReason: ProvideCallCancellationReason;
381
fetchResultType?: ChatFetchResponseType;
382
timeElapsedBeforeDelay?: number;
383
promptConstructionTime?: number;
384
promptTokenCount?: number;
385
fetchTime?: number;
386
expectedDelayBeforeFetch?: number;
387
actualDelayBeforeFetch?: number;
388
successResponseCharCount?: number;
389
responseUnusedCharCount?: number;
390
replyFormat?: ReplyFormat;
391
symbolNamesCount?: number;
392
}) {
393
/* __GDPR__
394
"provideRenameSuggestions" : {
395
"owner": "ulugbekna",
396
"comment": "Telemetry for rename suggestions provided",
397
"languageId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Language ID of the document." },
398
"cancellationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Specify when exactly during the provider call the cancellation happened. Empty string if the cancellation didn't happen." },
399
"fetchResultType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Result of a fetch to endpoint" },
400
"replyFormat": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Copilot reply format: 'jsonStringArray' | 'multiJsonStringArray' | 'list' | 'unknown'" },
401
"triggerKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Rename suggestion trigger kind - 'automatic' | 'manual'" },
402
"promptConstructionTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time it took to construct the prompt", "isMeasurement": true },
403
"timeElapsedBeforeDelay": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time elapsed before delay starts", "isMeasurement": true },
404
"promptTokenCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Token count of the prompt", "isMeasurement": true },
405
"expectedDelayBeforeFetch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Expected delay before fetch dictated by the experiment 'renameSuggestionsDelayBeforeFetch'", "isMeasurement": true },
406
"actualDelayBeforeFetch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Actual delay before fetch computed as 'expectedDelay - promptComputationTime'", "isMeasurement": true },
407
"successResponseCharCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Character count in model response (for response.type == 'success')", "isMeasurement": true },
408
"responseUnusedCharCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Character count in model response that was unused, e.g., rename explanations, response format overhead", "isMeasurement": true },
409
"fetchTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Time it took to fetch from endpoint", "isMeasurement": true },
410
"symbolNamesCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of suggested names", "isMeasurement": true }
411
}
412
*/
413
this._telemetryService.sendMSFTTelemetryEvent(
414
'provideRenameSuggestions',
415
{
416
languageId,
417
cancellationReason,
418
fetchResultType,
419
replyFormat,
420
triggerKind: triggerKind === NewSymbolNameTriggerKind.Automatic ? 'automatic' : 'manual',
421
},
422
{
423
promptConstructionTime,
424
promptTokenCount,
425
expectedDelayBeforeFetch,
426
actualDelayBeforeFetch,
427
timeElapsedBeforeFetch: timeElapsedBeforeDelay,
428
fetchTime,
429
successResponseCharCount,
430
responseUnusedCharCount,
431
symbolNamesCount,
432
}
433
);
434
}
435
436
private _sendInternalTelemetry({ languageId, reply }: { languageId: string; reply: string }) {
437
this._telemetryService.sendMSFTTelemetryEvent(
438
'provideRenameSuggestionsIncorrectFormatResponse',
439
{
440
languageId,
441
reply
442
}
443
);
444
}
445
446
public static _determinePrefix(name: string): string | undefined {
447
const prefix = name.match(/^([\\.\\$\\_]+)/)?.[0];
448
return prefix;
449
}
450
}
451
452