Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/terminalFixGenerator.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
7
import * as l10n from '@vscode/l10n';
8
import * as vscode from 'vscode';
9
import { Uri } from 'vscode';
10
import { ChatLocation } from '../../../platform/chat/common/commonTypes';
11
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
12
import { ILogService } from '../../../platform/log/common/logService';
13
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
14
import { extractCodeBlocks } from '../../../util/common/markdown';
15
import { IntervalTimer } from '../../../util/vs/base/common/async';
16
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
17
import { isAbsolute } from '../../../util/vs/base/common/path';
18
import { URI } from '../../../util/vs/base/common/uri';
19
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
20
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
21
import { TerminalQuickFixFileContextPrompt, TerminalQuickFixPrompt } from '../../prompts/node/panel/terminalQuickFix';
22
23
const enum CommandRelevance {
24
Low = 1,
25
Medium = 2,
26
High = 3,
27
}
28
29
function relevanceToString(relevance: CommandRelevance): string {
30
switch (relevance) {
31
case CommandRelevance.High: return l10n.t('high relevance');
32
case CommandRelevance.Medium: return l10n.t('medium relevance');
33
case CommandRelevance.Low: return l10n.t('low relevance');
34
}
35
}
36
37
function parseRelevance(relevance: 'low' | 'medium' | 'high'): CommandRelevance {
38
switch (relevance) {
39
case 'high': return CommandRelevance.High;
40
case 'medium': return CommandRelevance.Medium;
41
case 'low': return CommandRelevance.Low;
42
}
43
}
44
45
export interface ICommandSuggestion {
46
command: string;
47
description: string;
48
relevance: CommandRelevance;
49
}
50
51
export function setLastCommandMatchResult(value: vscode.TerminalCommandMatchResult) { lastCommandMatchResult = value; }
52
export let lastCommandMatchResult: vscode.TerminalCommandMatchResult | undefined;
53
54
export async function generateTerminalFixes(instantiationService: IInstantiationService) {
55
const commandMatchResult = lastCommandMatchResult;
56
if (!commandMatchResult) {
57
return;
58
}
59
type CommandPick = vscode.QuickPickItem & { suggestion: ICommandSuggestion };
60
const picksPromise: Promise<(CommandPick | vscode.QuickPickItem)[]> = new Promise(r => {
61
instantiationService.createInstance(TerminalQuickFixGenerator).generateTerminalQuickFix(commandMatchResult, CancellationToken.None).then(fixes => {
62
const picks: (CommandPick | vscode.QuickPickItem)[] = (fixes ?? []).sort((a, b) => b.relevance - a.relevance).map(e => ({
63
label: e.command,
64
description: e.description,
65
suggestion: e
66
}) satisfies CommandPick);
67
let currentRelevance: CommandRelevance | undefined;
68
for (let i = 0; i < picks.length; i++) {
69
const pick = picks[i];
70
const lastPick = picks.at(i - 1)!;
71
if (
72
'suggestion' in pick &&
73
(
74
!currentRelevance ||
75
(i > 0 && 'suggestion' in lastPick && pick.suggestion.relevance !== lastPick.suggestion.relevance)
76
)
77
) {
78
currentRelevance = pick.suggestion.relevance;
79
picks.splice(i++, 0, { label: relevanceToString(currentRelevance), kind: vscode.QuickPickItemKind.Separator });
80
}
81
}
82
r(picks);
83
});
84
});
85
picksPromise.then(picks => {
86
if (picks.length === 0) {
87
vscode.window.showInformationMessage('No fixes found');
88
}
89
});
90
const pick = vscode.window.createQuickPick<(vscode.QuickPickItem | CommandPick)>();
91
pick.canSelectMany = false;
92
93
// Setup loading state
94
const generatingString = l10n.t('Generating');
95
pick.placeholder = generatingString;
96
pick.busy = true;
97
let dots = 0;
98
const dotTimer = new IntervalTimer();
99
dotTimer.cancelAndSet(() => {
100
dots++;
101
if (dots > 3) {
102
dots = 0;
103
}
104
pick.placeholder = generatingString + '.'.repeat(dots);
105
}, 250);
106
107
pick.show();
108
pick.items = await picksPromise;
109
110
// Clear loading state
111
dotTimer.cancel();
112
pick.placeholder = '';
113
pick.busy = false;
114
115
await new Promise<void>(r => pick.onDidAccept(() => r()));
116
117
const item = pick.activeItems[0];
118
if (item && 'suggestion' in item) {
119
const shouldExecute = !item.suggestion.command.match(/{.+}/);
120
vscode.window.activeTerminal?.sendText(item.suggestion.command, shouldExecute);
121
}
122
123
pick.dispose();
124
}
125
126
class TerminalQuickFixGenerator {
127
128
constructor(
129
@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,
130
@IInstantiationService private readonly _instantiationService: IInstantiationService,
131
@ILogService private readonly _logService: ILogService,
132
@IWorkspaceService private readonly _workspaceService: IWorkspaceService,
133
) {
134
}
135
136
async generateTerminalQuickFix(commandMatchResult: vscode.TerminalCommandMatchResult, token: CancellationToken): Promise<ICommandSuggestion[] | undefined> {
137
const unverifiedContextUris = await this._generateTerminalQuickFixFileContext(commandMatchResult, token);
138
if (!unverifiedContextUris || token.isCancellationRequested) {
139
return;
140
}
141
142
const verifiedContextUris: Uri[] = [];
143
const verifiedContextDirectoryUris: Uri[] = [];
144
const nonExistentContextUris: Uri[] = [];
145
for (const uri of unverifiedContextUris) {
146
try {
147
const exists = await vscode.workspace.fs.stat(uri);
148
// This does not support binary files
149
if (exists.type === vscode.FileType.File || exists.type === vscode.FileType.SymbolicLink) {
150
verifiedContextUris.push(uri);
151
} else if (exists.type === vscode.FileType.Directory) {
152
verifiedContextDirectoryUris.push(uri);
153
} else {
154
nonExistentContextUris.push(uri);
155
}
156
} catch {
157
nonExistentContextUris.push(uri);
158
}
159
}
160
161
const endpoint = await this._endpointProvider.getChatEndpoint('copilot-fast');
162
163
const promptRenderer = PromptRenderer.create(this._instantiationService, endpoint, TerminalQuickFixPrompt, {
164
commandLine: commandMatchResult.commandLine,
165
output: [],
166
verifiedContextUris,
167
verifiedContextDirectoryUris,
168
nonExistentContextUris,
169
});
170
171
const prompt = await promptRenderer.render(undefined, undefined);
172
173
const fetchResult = await endpoint.makeChatRequest(
174
'terminalQuickFixGenerator',
175
prompt.messages,
176
undefined,
177
token,
178
ChatLocation.Other
179
);
180
this._logService.info('Terminal QuickFix FetchResult ' + fetchResult);
181
if (token.isCancellationRequested) {
182
return;
183
}
184
if (fetchResult.type !== 'success') {
185
throw new Error(vscode.l10n.t('Encountered an error while determining terminal quick fixes: {0}', fetchResult.type));
186
}
187
this._logService.debug('generalTerminalQuickFix fetchResult.value ' + fetchResult.value);
188
189
// Parse result json
190
const parsedResults: ICommandSuggestion[] = [];
191
try {
192
// The result may come in a md fenced code block
193
const codeblocks = extractCodeBlocks(fetchResult.value);
194
const json = JSON.parse(codeblocks.length > 0 ? codeblocks[0].code : fetchResult.value) as unknown;
195
if (json && Array.isArray(json)) {
196
for (const entry of (json as unknown[])) {
197
if (typeof entry === 'object' && entry) {
198
const command = 'command' in entry && typeof entry.command === 'string' ? entry.command : undefined;
199
const description = 'description' in entry && typeof entry.description === 'string' ? entry.description : undefined;
200
const relevance = 'relevance' in entry && typeof entry.relevance === 'string' && (entry.relevance === 'low' || entry.relevance === 'medium' || entry.relevance === 'high') ? entry.relevance : undefined;
201
if (command && description && relevance) {
202
parsedResults.push({
203
command,
204
description,
205
relevance: parseRelevance(relevance)
206
});
207
}
208
}
209
}
210
}
211
} catch (e) {
212
this._logService.error('Error parsing terminal quick fix results: ' + e);
213
}
214
215
return parsedResults;
216
}
217
218
private async _generateTerminalQuickFixFileContext(commandMatchResult: vscode.TerminalCommandMatchResult, token: CancellationToken) {
219
const endpoint = await this._endpointProvider.getChatEndpoint('copilot-fast');
220
221
const promptRenderer = PromptRenderer.create(this._instantiationService, endpoint, TerminalQuickFixFileContextPrompt, {
222
commandLine: commandMatchResult.commandLine,
223
output: [],
224
});
225
226
const prompt = await promptRenderer.render(undefined, undefined);
227
this._logService.debug('_generalTerminalQuickFixFileContext prompt.messages: ' + prompt.messages);
228
229
const fetchResult = await endpoint.makeChatRequest(
230
'terminalQuickFixGenerator',
231
prompt.messages,
232
async _ => void 0,
233
token,
234
ChatLocation.Other
235
);
236
this._logService.info('Terminal Quick Fix Fetch Result: ' + fetchResult);
237
if (token.isCancellationRequested) {
238
return;
239
}
240
if (fetchResult.type !== 'success') {
241
throw new Error(vscode.l10n.t('Encountered an error while fetching quick fix file context: {0}', fetchResult.type));
242
}
243
244
this._logService.debug('_generalTerminalQuickFixFileContext fetchResult.value' + fetchResult.value);
245
246
// Parse result json
247
const parsedResults: { fileName: string }[] = [];
248
try {
249
const json = JSON.parse(fetchResult.value) as unknown;
250
if (json && Array.isArray(json)) {
251
for (const entry of (json as unknown[])) {
252
if (typeof entry === 'object' && entry) {
253
const fileName = 'fileName' in entry && typeof entry.fileName === 'string' ? entry.fileName : undefined;
254
if (fileName) {
255
parsedResults.push({ fileName });
256
}
257
}
258
}
259
}
260
} catch {
261
// no-op
262
}
263
264
const uris: Uri[] = [];
265
const requestedFiles: Set<string> = new Set();
266
const folders = this._workspaceService.getWorkspaceFolders();
267
const tryAddFileVariables = async (file: string) => {
268
for (const rootFolder of folders) {
269
const uri = URI.joinPath(rootFolder, file);
270
if (requestedFiles.has(uri.toString())) {
271
return;
272
}
273
requestedFiles.add(uri.toString());
274
// Do not stat here as the follow up wants to know whether it exists
275
uris.push(uri);
276
}
277
};
278
279
for (const { fileName } of parsedResults) {
280
if (fileName.endsWith('.exe') || (fileName.includes('/bin/') && !fileName.endsWith('activate'))) {
281
continue;
282
}
283
if (isAbsolute(fileName)) {
284
uris.push(Uri.file(fileName));
285
} else {
286
await tryAddFileVariables(fileName);
287
}
288
}
289
290
return uris;
291
}
292
}
293
294