Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/terminalFixGenerator.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import * as l10n from '@vscode/l10n';7import * as vscode from 'vscode';8import { Uri } from 'vscode';9import { ChatLocation } from '../../../platform/chat/common/commonTypes';10import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';11import { ILogService } from '../../../platform/log/common/logService';12import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';13import { extractCodeBlocks } from '../../../util/common/markdown';14import { IntervalTimer } from '../../../util/vs/base/common/async';15import { CancellationToken } from '../../../util/vs/base/common/cancellation';16import { isAbsolute } from '../../../util/vs/base/common/path';17import { URI } from '../../../util/vs/base/common/uri';18import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';19import { PromptRenderer } from '../../prompts/node/base/promptRenderer';20import { TerminalQuickFixFileContextPrompt, TerminalQuickFixPrompt } from '../../prompts/node/panel/terminalQuickFix';2122const enum CommandRelevance {23Low = 1,24Medium = 2,25High = 3,26}2728function relevanceToString(relevance: CommandRelevance): string {29switch (relevance) {30case CommandRelevance.High: return l10n.t('high relevance');31case CommandRelevance.Medium: return l10n.t('medium relevance');32case CommandRelevance.Low: return l10n.t('low relevance');33}34}3536function parseRelevance(relevance: 'low' | 'medium' | 'high'): CommandRelevance {37switch (relevance) {38case 'high': return CommandRelevance.High;39case 'medium': return CommandRelevance.Medium;40case 'low': return CommandRelevance.Low;41}42}4344export interface ICommandSuggestion {45command: string;46description: string;47relevance: CommandRelevance;48}4950export function setLastCommandMatchResult(value: vscode.TerminalCommandMatchResult) { lastCommandMatchResult = value; }51export let lastCommandMatchResult: vscode.TerminalCommandMatchResult | undefined;5253export async function generateTerminalFixes(instantiationService: IInstantiationService) {54const commandMatchResult = lastCommandMatchResult;55if (!commandMatchResult) {56return;57}58type CommandPick = vscode.QuickPickItem & { suggestion: ICommandSuggestion };59const picksPromise: Promise<(CommandPick | vscode.QuickPickItem)[]> = new Promise(r => {60instantiationService.createInstance(TerminalQuickFixGenerator).generateTerminalQuickFix(commandMatchResult, CancellationToken.None).then(fixes => {61const picks: (CommandPick | vscode.QuickPickItem)[] = (fixes ?? []).sort((a, b) => b.relevance - a.relevance).map(e => ({62label: e.command,63description: e.description,64suggestion: e65}) satisfies CommandPick);66let currentRelevance: CommandRelevance | undefined;67for (let i = 0; i < picks.length; i++) {68const pick = picks[i];69const lastPick = picks.at(i - 1)!;70if (71'suggestion' in pick &&72(73!currentRelevance ||74(i > 0 && 'suggestion' in lastPick && pick.suggestion.relevance !== lastPick.suggestion.relevance)75)76) {77currentRelevance = pick.suggestion.relevance;78picks.splice(i++, 0, { label: relevanceToString(currentRelevance), kind: vscode.QuickPickItemKind.Separator });79}80}81r(picks);82});83});84picksPromise.then(picks => {85if (picks.length === 0) {86vscode.window.showInformationMessage('No fixes found');87}88});89const pick = vscode.window.createQuickPick<(vscode.QuickPickItem | CommandPick)>();90pick.canSelectMany = false;9192// Setup loading state93const generatingString = l10n.t('Generating');94pick.placeholder = generatingString;95pick.busy = true;96let dots = 0;97const dotTimer = new IntervalTimer();98dotTimer.cancelAndSet(() => {99dots++;100if (dots > 3) {101dots = 0;102}103pick.placeholder = generatingString + '.'.repeat(dots);104}, 250);105106pick.show();107pick.items = await picksPromise;108109// Clear loading state110dotTimer.cancel();111pick.placeholder = '';112pick.busy = false;113114await new Promise<void>(r => pick.onDidAccept(() => r()));115116const item = pick.activeItems[0];117if (item && 'suggestion' in item) {118const shouldExecute = !item.suggestion.command.match(/{.+}/);119vscode.window.activeTerminal?.sendText(item.suggestion.command, shouldExecute);120}121122pick.dispose();123}124125class TerminalQuickFixGenerator {126127constructor(128@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,129@IInstantiationService private readonly _instantiationService: IInstantiationService,130@ILogService private readonly _logService: ILogService,131@IWorkspaceService private readonly _workspaceService: IWorkspaceService,132) {133}134135async generateTerminalQuickFix(commandMatchResult: vscode.TerminalCommandMatchResult, token: CancellationToken): Promise<ICommandSuggestion[] | undefined> {136const unverifiedContextUris = await this._generateTerminalQuickFixFileContext(commandMatchResult, token);137if (!unverifiedContextUris || token.isCancellationRequested) {138return;139}140141const verifiedContextUris: Uri[] = [];142const verifiedContextDirectoryUris: Uri[] = [];143const nonExistentContextUris: Uri[] = [];144for (const uri of unverifiedContextUris) {145try {146const exists = await vscode.workspace.fs.stat(uri);147// This does not support binary files148if (exists.type === vscode.FileType.File || exists.type === vscode.FileType.SymbolicLink) {149verifiedContextUris.push(uri);150} else if (exists.type === vscode.FileType.Directory) {151verifiedContextDirectoryUris.push(uri);152} else {153nonExistentContextUris.push(uri);154}155} catch {156nonExistentContextUris.push(uri);157}158}159160const endpoint = await this._endpointProvider.getChatEndpoint('copilot-fast');161162const promptRenderer = PromptRenderer.create(this._instantiationService, endpoint, TerminalQuickFixPrompt, {163commandLine: commandMatchResult.commandLine,164output: [],165verifiedContextUris,166verifiedContextDirectoryUris,167nonExistentContextUris,168});169170const prompt = await promptRenderer.render(undefined, undefined);171172const fetchResult = await endpoint.makeChatRequest(173'terminalQuickFixGenerator',174prompt.messages,175undefined,176token,177ChatLocation.Other178);179this._logService.info('Terminal QuickFix FetchResult ' + fetchResult);180if (token.isCancellationRequested) {181return;182}183if (fetchResult.type !== 'success') {184throw new Error(vscode.l10n.t('Encountered an error while determining terminal quick fixes: {0}', fetchResult.type));185}186this._logService.debug('generalTerminalQuickFix fetchResult.value ' + fetchResult.value);187188// Parse result json189const parsedResults: ICommandSuggestion[] = [];190try {191// The result may come in a md fenced code block192const codeblocks = extractCodeBlocks(fetchResult.value);193const json = JSON.parse(codeblocks.length > 0 ? codeblocks[0].code : fetchResult.value) as unknown;194if (json && Array.isArray(json)) {195for (const entry of (json as unknown[])) {196if (typeof entry === 'object' && entry) {197const command = 'command' in entry && typeof entry.command === 'string' ? entry.command : undefined;198const description = 'description' in entry && typeof entry.description === 'string' ? entry.description : undefined;199const relevance = 'relevance' in entry && typeof entry.relevance === 'string' && (entry.relevance === 'low' || entry.relevance === 'medium' || entry.relevance === 'high') ? entry.relevance : undefined;200if (command && description && relevance) {201parsedResults.push({202command,203description,204relevance: parseRelevance(relevance)205});206}207}208}209}210} catch (e) {211this._logService.error('Error parsing terminal quick fix results: ' + e);212}213214return parsedResults;215}216217private async _generateTerminalQuickFixFileContext(commandMatchResult: vscode.TerminalCommandMatchResult, token: CancellationToken) {218const endpoint = await this._endpointProvider.getChatEndpoint('copilot-fast');219220const promptRenderer = PromptRenderer.create(this._instantiationService, endpoint, TerminalQuickFixFileContextPrompt, {221commandLine: commandMatchResult.commandLine,222output: [],223});224225const prompt = await promptRenderer.render(undefined, undefined);226this._logService.debug('_generalTerminalQuickFixFileContext prompt.messages: ' + prompt.messages);227228const fetchResult = await endpoint.makeChatRequest(229'terminalQuickFixGenerator',230prompt.messages,231async _ => void 0,232token,233ChatLocation.Other234);235this._logService.info('Terminal Quick Fix Fetch Result: ' + fetchResult);236if (token.isCancellationRequested) {237return;238}239if (fetchResult.type !== 'success') {240throw new Error(vscode.l10n.t('Encountered an error while fetching quick fix file context: {0}', fetchResult.type));241}242243this._logService.debug('_generalTerminalQuickFixFileContext fetchResult.value' + fetchResult.value);244245// Parse result json246const parsedResults: { fileName: string }[] = [];247try {248const json = JSON.parse(fetchResult.value) as unknown;249if (json && Array.isArray(json)) {250for (const entry of (json as unknown[])) {251if (typeof entry === 'object' && entry) {252const fileName = 'fileName' in entry && typeof entry.fileName === 'string' ? entry.fileName : undefined;253if (fileName) {254parsedResults.push({ fileName });255}256}257}258}259} catch {260// no-op261}262263const uris: Uri[] = [];264const requestedFiles: Set<string> = new Set();265const folders = this._workspaceService.getWorkspaceFolders();266const tryAddFileVariables = async (file: string) => {267for (const rootFolder of folders) {268const uri = URI.joinPath(rootFolder, file);269if (requestedFiles.has(uri.toString())) {270return;271}272requestedFiles.add(uri.toString());273// Do not stat here as the follow up wants to know whether it exists274uris.push(uri);275}276};277278for (const { fileName } of parsedResults) {279if (fileName.endsWith('.exe') || (fileName.includes('/bin/') && !fileName.endsWith('activate'))) {280continue;281}282if (isAbsolute(fileName)) {283uris.push(Uri.file(fileName));284} else {285await tryAddFileVariables(fileName);286}287}288289return uris;290}291}292293294