Path: blob/main/extensions/copilot/src/extension/intents/node/searchIntent.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*--------------------------------------------------------------------------------------------*/45import * as l10n from '@vscode/l10n';6import { parse } from 'jsonc-parser';7import type * as vscode from 'vscode';8import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';9import { ChatLocation } from '../../../platform/chat/common/commonTypes';10import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';11import { isPreRelease } from '../../../platform/env/common/packagejson';12import { IResponseDelta } from '../../../platform/networking/common/fetch';13import { IChatEndpoint } from '../../../platform/networking/common/networking';14import { extractCodeBlocks } from '../../../util/common/markdown';15import { CancellationToken } from '../../../util/vs/base/common/cancellation';16import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';17import { Intent } from '../../common/constants';18import { IBuildPromptContext } from '../../prompt/common/intents';19import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo, IResponseProcessorContext } from '../../prompt/node/intents';20import { PseudoStopStartResponseProcessor } from '../../prompt/node/pseudoStartStopConversationCallback';21import { PromptRenderer, RendererIntentInvocation } from '../../prompts/node/base/promptRenderer';22import { SearchPrompt } from '../../prompts/node/panel/search';232425export interface FindInFilesArgs {26query: string;27replace: string;28filesToInclude: string;29filesToExclude: string;30isRegex: boolean;31isCaseSensitive: boolean;32}3334function createSearchFollowUps(args: any): vscode.Command[] {35if (!args) {36return [];37}38const searchResponses: vscode.Command[] = [];3940const searchArg: FindInFilesArgs = {41query: args.query ?? '',42replace: args.replace ?? '',43filesToInclude: args.filesToInclude ?? '',44filesToExclude: args.filesToExclude ?? '',45isRegex: args.isRegex ?? false,46isCaseSensitive: args.isRegex ?? false,47};48searchResponses.push({49command: 'github.copilot.executeSearch',50arguments: [searchArg],51title: l10n.t("Search"),52});53return searchResponses;54}5556export function parseSearchParams(modelResponseString: string): any {57const codeBlock = extractCodeBlocks(modelResponseString).at(0);58let args: any = undefined;59if (codeBlock) {60let parsed: any | undefined;61try {62parsed = parse(codeBlock.code);63} catch (e) {64// Ignore65}6667if (parsed) {68args = parsed;69}70}71return args;72}7374function jsonToTable(args: any): string[] {75if (!args) {76return [];77}78const table = ['| Parameter | Value |\n', '| ------ | ----- |\n'];79for (const [key, value] of Object.entries(args)) {80if (value === '') {81continue;82}83let nonEscapeValue = value;84if (typeof value === 'string' || value instanceof String) {85// CodeQL [SM02383] Since this is inside of a markdown table cell, only a `|` pipe character would interfere with formatting.86nonEscapeValue = value.replace(/\|/g, '\\|');87}88table.push(`| ${key} | \`${nonEscapeValue}\` |\n`);89}90table.push(`\n`);91return table;92}9394export const searchIntentPromptSnippet = `Search for 'foo' in all files under my 'src' directory`;9596class SearchIntentInvocation extends RendererIntentInvocation implements IIntentInvocation {9798constructor(99intent: IIntent,100location: ChatLocation,101endpoint: IChatEndpoint,102@IInstantiationService private readonly instantiationService: IInstantiationService,103) {104super(intent, location, endpoint);105}106107createRenderer(promptContext: IBuildPromptContext, endpoint: IChatEndpoint, progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, token: vscode.CancellationToken) {108return PromptRenderer.create(this.instantiationService, endpoint, SearchPrompt, {109promptContext110});111}112113processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {114const responseProcessor = this.instantiationService.createInstance(SearchResponseProcessor);115return responseProcessor.processResponse(context, inputStream, outputStream, token);116}117}118119class SearchResponseProcessor extends PseudoStopStartResponseProcessor {120121private _response = '';122123constructor() {124super(125[{ start: '[ARGS END]', stop: '[ARGS START]' }],126(delta) => jsonToTable(parseSearchParams(delta.join(''))),127);128}129130override async doProcessResponse(responseStream: AsyncIterable<IResponsePart>, progress: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {131await super.doProcessResponse(responseStream, progress, token);132const args = parseSearchParams(this._response ?? '');133for (const command of createSearchFollowUps(args)) {134progress.button(command);135}136}137138protected override applyDelta(delta: IResponseDelta, progress: vscode.ChatResponseStream): void {139this._response += delta.text;140super.applyDelta(delta, progress);141}142}143144export class SearchIntent implements IIntent {145146static readonly ID = Intent.Search;147readonly id: string = Intent.Search;148readonly locations = [ChatLocation.Panel];149readonly description: string = l10n.t('Generate query parameters for workspace search');150151readonly commandInfo: IIntentSlashCommandInfo = {152allowsEmptyArgs: false,153defaultEnablement: isPreRelease,154};155156constructor(157@IInstantiationService private readonly instantiationService: IInstantiationService,158@IEndpointProvider private readonly endpointProvider: IEndpointProvider,159) { }160161async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {162const location = invocationContext.location;163const endpoint = await this.endpointProvider.getChatEndpoint(invocationContext.request);164return this.instantiationService.createInstance(SearchIntentInvocation, this, location, endpoint);165}166}167168169