Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/node/testIntent/testIntent.tsx
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
6
import * as l10n from '@vscode/l10n';
7
import type { CancellationToken, ChatPromptReference, ChatRequest, ChatResponseStream, ChatResult } from 'vscode';
8
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
9
import { IChatHookService } from '../../../../platform/chat/common/chatHookService';
10
import { ChatLocation } from '../../../../platform/chat/common/commonTypes';
11
import { IConversationOptions } from '../../../../platform/chat/common/conversationOptions';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configurationService';
13
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
14
import { IEditSurvivalTrackerService } from '../../../../platform/editSurvivalTracking/common/editSurvivalTrackerService';
15
import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';
16
import { IOctoKitService } from '../../../../platform/github/common/githubService';
17
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
18
import { ILogService } from '../../../../platform/log/common/logService';
19
import { IRequestLogger } from '../../../../platform/requestLogger/common/requestLogger';
20
import { ISurveyService } from '../../../../platform/survey/common/surveyService';
21
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
22
import { ISetupTestsDetector, isStartSetupTestConfirmation, SetupTestActionType } from '../../../../platform/testing/node/setupTestDetector';
23
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
24
import { getLanguage } from '../../../../util/common/languages';
25
import { isUri } from '../../../../util/common/types';
26
import { URI } from '../../../../util/vs/base/common/uri';
27
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
28
import { Position, Range, Selection } from '../../../../vscodeTypes';
29
import { Intent } from '../../../common/constants';
30
import { Conversation } from '../../../prompt/common/conversation';
31
import { ChatTelemetryBuilder } from '../../../prompt/node/chatParticipantTelemetry';
32
import { DefaultIntentRequestHandler } from '../../../prompt/node/defaultIntentRequestHandler';
33
import { IDocumentContext } from '../../../prompt/node/documentContext';
34
import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo } from '../../../prompt/node/intents';
35
import { isTestFile } from '../../../prompt/node/testFiles';
36
import { ContributedToolName } from '../../../tools/common/toolNames';
37
import { TestFromSourceInvocation } from './testFromSrcInvocation';
38
import { TestFromTestInvocation } from './testFromTestInvocation';
39
import { UserQueryParser } from './userQueryParser';
40
41
42
export class TestsIntent implements IIntent {
43
44
static readonly ID = Intent.Tests;
45
46
readonly id = Intent.Tests;
47
48
readonly locations = [ChatLocation.Panel, ChatLocation.Editor];
49
50
readonly description = l10n.t('Generate unit tests for the selected code');
51
52
readonly commandInfo: IIntentSlashCommandInfo = { toolEquivalent: ContributedToolName.FindTestFiles };
53
54
constructor(
55
@IInstantiationService private readonly instantiationService: IInstantiationService,
56
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
57
@IIgnoreService private readonly ignoreService: IIgnoreService,
58
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
59
@ILogService private readonly logService: ILogService,
60
) { }
61
62
handleRequest(conversation: Conversation, request: ChatRequest, stream: ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext | undefined, agentName: string, location: ChatLocation, chatTelemetry: ChatTelemetryBuilder): Promise<ChatResult> {
63
return this.instantiationService.createInstance(RequestHandler, this, conversation, request, stream, token, documentContext, location, chatTelemetry).getResult();
64
}
65
66
async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {
67
68
let documentContext = invocationContext.documentContext;
69
let alreadyConsumedChatVariable: ChatPromptReference | undefined;
70
71
// try resolving the document context programmatically
72
if (!documentContext) {
73
const r = await this.resolveDocContextProgrammatically(invocationContext);
74
if (r) {
75
documentContext = r.documentContext;
76
alreadyConsumedChatVariable = r.alreadyConsumedChatVariable;
77
}
78
}
79
80
// try resolving the document context using LLM
81
if (!documentContext) {
82
const r = await this.resolveDocContextUsingLlm(invocationContext);
83
if (r) {
84
documentContext = r.documentContext;
85
alreadyConsumedChatVariable = r.alreadyConsumedChatVariable;
86
}
87
}
88
89
if (!documentContext) {
90
throw new Error('To generate tests, open a file and select code to test.');
91
}
92
93
if (await this.ignoreService.isCopilotIgnored(documentContext.document.uri)) {
94
throw new Error('Copilot is disabled for this file.');
95
}
96
97
const location = invocationContext.location;
98
99
const endpoint = await this.endpointProvider.getChatEndpoint(invocationContext.request);
100
101
return isTestFile(documentContext.document)
102
? this.instantiationService.createInstance(TestFromTestInvocation, this, endpoint, location, documentContext, alreadyConsumedChatVariable)
103
: this.instantiationService.createInstance(TestFromSourceInvocation, this, endpoint, location, documentContext, alreadyConsumedChatVariable);
104
}
105
106
private async resolveDocContextProgrammatically(invocationContext: IIntentInvocationContext) {
107
108
const refs = invocationContext.request.references;
109
110
// find a #file to use for testing
111
112
// count #file's because we use LLM if there're more than 1 in the prompt
113
let hashFileCount = 0;
114
115
const fileRefs: [ChatPromptReference, URI][] = [];
116
117
for (const ref of refs) {
118
if (ref.id === 'copilot.file' || ref.id === 'vscode.file') {
119
if (isUri(ref.value)) {
120
hashFileCount += 1;
121
fileRefs.push([ref, ref.value]);
122
}
123
} else {
124
if (!isUri(ref.id)) {
125
continue;
126
}
127
const uri = URI.parse(ref.id);
128
if (uri !== undefined) {
129
fileRefs.push([ref, uri]);
130
}
131
}
132
}
133
134
if (hashFileCount > 1 // use LLM if there's more than 1 file reference
135
|| fileRefs.length === 0
136
) {
137
return;
138
}
139
140
const [ref, fileUri] = fileRefs[0];
141
142
return {
143
documentContext: await this.createDocumentContext(fileUri),
144
alreadyConsumedChatVariable: ref,
145
};
146
}
147
148
private async resolveDocContextUsingLlm(invocationContext: IIntentInvocationContext) {
149
150
const queryParser = this.instantiationService.createInstance(UserQueryParser);
151
const parsedQuery = await queryParser.parse(invocationContext.request.prompt);
152
153
if (parsedQuery === null) {
154
return;
155
}
156
157
// FIXME@ulugbekna: UserQueryParser also returns symbols that need testing; we should use that info
158
const { fileToTest, } = parsedQuery;
159
160
// if parser couldn't identify the file, if there's only one file referenced, use that
161
if (fileToTest === undefined) {
162
return;
163
}
164
165
for (let i = 0; i < invocationContext.request.references.length; i++) {
166
167
const ref = invocationContext.request.references[i];
168
169
// FIXME@ulugbekna: I don't like how I fish for #file references
170
171
if (ref.id !== 'vscode.file' && ref.id !== 'copilot.file') {
172
continue;
173
}
174
175
const [kind, fileName] = ref.name.trim().split(':');
176
if (kind !== 'file' ||
177
fileName === undefined ||
178
!(URI.isUri(ref.value)) ||
179
fileName !== fileToTest
180
) {
181
continue;
182
}
183
184
return {
185
documentContext: await this.createDocumentContext(ref.value),
186
alreadyConsumedChatVariable: ref,
187
};
188
}
189
}
190
191
/**
192
*
193
* @param selection defaults to whole file
194
*/
195
private async createDocumentContext(file: URI, selection?: Range) {
196
let td: TextDocumentSnapshot | undefined;
197
try {
198
td = await this.workspaceService.openTextDocumentAndSnapshot(file);
199
} catch (e) {
200
this.log(`Tried opening file ${file.toString()} but got error: ${e}`);
201
return;
202
}
203
204
const wholeFile = selection ?? new Range(
205
new Position(0, 0),
206
new Position(td.lineCount - 1, td.lineAt(td.lineCount - 1).text.length)
207
);
208
209
return {
210
document: td,
211
fileIndentInfo: undefined,
212
language: getLanguage(td.languageId),
213
wholeRange: wholeFile,
214
selection: new Selection(wholeFile.start, wholeFile.end),
215
} satisfies IDocumentContext;
216
}
217
218
private log(...args: any[]): void {
219
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, '\t') : arg).join('\n');
220
this.logService.debug(`[TestsIntent] ${message}`);
221
}
222
}
223
224
class RequestHandler extends DefaultIntentRequestHandler {
225
constructor(
226
intent: IIntent,
227
conversation: Conversation,
228
request: ChatRequest,
229
stream: ChatResponseStream,
230
token: CancellationToken,
231
documentContext: IDocumentContext | undefined,
232
location: ChatLocation,
233
chatTelemetry: ChatTelemetryBuilder,
234
@IInstantiationService instantiationService: IInstantiationService,
235
@IConversationOptions conversationOptions: IConversationOptions,
236
@ITelemetryService telemetryService: ITelemetryService,
237
@ILogService logService: ILogService,
238
@ISurveyService surveyService: ISurveyService,
239
@ISetupTestsDetector private readonly setupTestsDetector: ISetupTestsDetector,
240
@IRequestLogger requestLogger: IRequestLogger,
241
@IEditSurvivalTrackerService editSurvivalTrackerService: IEditSurvivalTrackerService,
242
@IAuthenticationService authenticationService: IAuthenticationService,
243
@IChatHookService chatHookService: IChatHookService,
244
@IOctoKitService octoKitService: IOctoKitService,
245
@IConfigurationService configurationService: IConfigurationService,
246
) {
247
super(intent, conversation, request, stream, token, documentContext, location, chatTelemetry, undefined, undefined, instantiationService, conversationOptions, telemetryService, logService, surveyService, requestLogger, editSurvivalTrackerService, authenticationService, chatHookService, octoKitService, configurationService);
248
}
249
250
/**
251
* - Delegates out to setting up tests if the user confirmed they wanted to do that
252
* - Otherwise try to detect if setup should be shown
253
* - If not, just delegate to the base class
254
* - If so, either return just that or append a reminder.
255
*/
256
public override async getResult(): Promise<ChatResult> {
257
// if the user is starting test setup, we need to finish this request
258
// before they can prompt us with the new one
259
if (this.request.acceptedConfirmationData?.some(isStartSetupTestConfirmation)) {
260
setTimeout(() => this.getResultInner());
261
return {};
262
}
263
264
return this.getResultInner();
265
}
266
private async getResultInner(): Promise<ChatResult> {
267
const suggestion = this.documentContext && await this.setupTestsDetector.shouldSuggestSetup(this.documentContext, this.request, this.stream);
268
if (!suggestion) {
269
return super.getResult();
270
}
271
272
let result: ChatResult = {};
273
if (suggestion.type === SetupTestActionType.Remind) {
274
result = await super.getResult();
275
}
276
277
this.setupTestsDetector.showSuggestion(suggestion).forEach(p => this.stream.push(p));
278
279
return result;
280
}
281
}
282
283