Path: blob/main/extensions/copilot/src/extension/intents/node/reviewIntent.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 { RenderPromptResult } from '@vscode/prompt-tsx';7import type * as vscode from 'vscode';8import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';9import { ChatLocation } from '../../../platform/chat/common/commonTypes';10import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';11import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';12import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';13import { ILogService } from '../../../platform/log/common/logService';14import { IChatEndpoint } from '../../../platform/networking/common/networking';15import { IReviewService, ReviewComment, ReviewRequest } from '../../../platform/review/common/reviewService';16import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService';17import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';18import { CancellationToken } from '../../../util/vs/base/common/cancellation';19import * as path from '../../../util/vs/base/common/path';20import { generateUuid } from '../../../util/vs/base/common/uuid';21import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';22import { MarkdownString } from '../../../vscodeTypes';23import { Intent } from '../../common/constants';24import { LinkifiedPart, LinkifiedText, LinkifyLocationAnchor } from '../../linkify/common/linkifiedText';25import { IContributedLinkifier, LinkifierContext } from '../../linkify/common/linkifyService';26import { IBuildPromptContext } from '../../prompt/common/intents';27import { IDocumentContext } from '../../prompt/node/documentContext';28import { parseFeedbackResponse, parseReviewComments } from '../../prompt/node/feedbackGenerator';29import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo, IResponseProcessorContext, ReplyInterpreter } from '../../prompt/node/intents';30import { PromptRenderer, RendererIntentInvocation } from '../../prompts/node/base/promptRenderer';31import { CurrentChange, CurrentChangeInput } from '../../prompts/node/feedback/currentChange';32import { ProvideFeedbackPrompt } from '../../prompts/node/feedback/provideFeedback';3334export const reviewIntentPromptSnippet = 'Review the currently selected code.';3536export const reviewLocalChangesMessage = l10n.t('local changes');373839class ReviewIntentInvocation extends RendererIntentInvocation implements IIntentInvocation {4041readonly linkification = {42additionaLinkifiers: [{ create: () => new LineLinkifier(this.documentContext.document.uri) }]43};4445constructor(46intent: IIntent,47location: ChatLocation,48endpoint: IChatEndpoint,49protected readonly documentContext: IDocumentContext,50@IInstantiationService protected readonly instantiationService: IInstantiationService,51@IWorkspaceService private readonly workspaceService: IWorkspaceService,52@ITabsAndEditorsService private readonly tabsAndEditorsService: ITabsAndEditorsService,53@ILogService private readonly logService: ILogService,54@IGitExtensionService private readonly gitExtensionService: IGitExtensionService,55) {56super(intent, location, endpoint);57}5859async createRenderer({ history, query, chatVariables }: IBuildPromptContext, endpoint: IChatEndpoint, progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, token: vscode.CancellationToken) {6061const input: CurrentChangeInput[] = [];62if (query === reviewLocalChangesMessage) {63const changes = await CurrentChange.getCurrentChanges(this.gitExtensionService, 'workingTree');64const documentsAndChanges = await Promise.all<CurrentChangeInput>(changes.map(async change => {65const document = await this.workspaceService.openTextDocumentAndSnapshot(change.uri);66return {67document,68relativeDocumentPath: path.relative(change.repository.rootUri.fsPath, change.uri.fsPath),69change,70};71}));72documentsAndChanges.map(i => input.push(i));73} else {74const editor = this.tabsAndEditorsService.activeTextEditor;75if (editor) {76input.push({77document: TextDocumentSnapshot.create(editor.document),78relativeDocumentPath: path.basename(editor.document.uri.fsPath),79selection: editor.selection,80});81}82}8384return PromptRenderer.create(this.instantiationService, endpoint, ProvideFeedbackPrompt, {85query,86history,87chatVariables,88input,89logService: this.logService,90});91}9293override async buildPrompt(context: IBuildPromptContext, progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, token: vscode.CancellationToken): Promise<RenderPromptResult> {94if (context.query === '') {95context = { ...context, query: reviewIntentPromptSnippet };96}97return super.buildPrompt(context, progress, token);98}99}100101class InlineReviewIntentInvocation extends ReviewIntentInvocation implements IIntentInvocation {102103processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {104const replyInterpreter = this.instantiationService.createInstance(ReviewReplyInterpreter, this.documentContext);105return replyInterpreter.processResponse(context, inputStream, outputStream, token);106}107}108109export class ReviewIntent implements IIntent {110111static readonly ID = Intent.Review;112readonly id = Intent.Review;113readonly locations = [ChatLocation.Panel, ChatLocation.Editor];114readonly description = l10n.t('Review the selected code in your active editor');115116readonly commandInfo: IIntentSlashCommandInfo | undefined;117118constructor(119@IInstantiationService private readonly instantiationService: IInstantiationService,120@IEndpointProvider private readonly endpointProvider: IEndpointProvider,121) { }122123async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {124125const documentContext = invocationContext.documentContext;126const location = invocationContext.location;127const endpoint = await this.endpointProvider.getChatEndpoint(invocationContext.request);128if (location === ChatLocation.Editor) {129return this.instantiationService.createInstance(InlineReviewIntentInvocation, this, location, endpoint, documentContext!);130}131return this.instantiationService.createInstance(ReviewIntentInvocation, this, location, endpoint, documentContext!);132}133}134135class LineLinkifier implements IContributedLinkifier {136137constructor(private readonly file: vscode.Uri) { }138139async linkify(newText: string, context: LinkifierContext, token?: vscode.CancellationToken): Promise<LinkifiedText | undefined> {140const parsedResponse = parseFeedbackResponse(newText);141if (!parsedResponse.length) {142return;143}144145let remaining = 0;146const parts: LinkifiedPart[] = [];147for (const match of parsedResponse) {148parts.push(newText.substring(remaining, match.linkOffset));149parts.push(new LinkifyLocationAnchor(this.file.with({ fragment: String(match.from + 1) }), newText.substring(match.linkOffset, match.linkOffset + match.linkLength)));150remaining = match.linkOffset + match.linkLength;151}152parts.push(newText.substring(remaining));153return { parts };154}155}156157class ReviewReplyInterpreter implements ReplyInterpreter {158159private updating = false;160private text = '';161private comments: ReviewComment[] = [];162163constructor(164private readonly documentContext: IDocumentContext,165@IReviewService private readonly reviewService: IReviewService166) {167}168169async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {170const request: ReviewRequest = {171source: 'vscodeCopilotChat',172promptCount: 1,173messageId: generateUuid(), // TODO: Use from request?174inputType: 'selection',175inputRanges: [176{177uri: this.documentContext.document.uri,178ranges: [this.documentContext.selection]179}180],181};182183for await (const part of inputStream) {184this.text += part.delta.text;185if (!this.updating) {186this.updating = true;187const content = new MarkdownString(l10n.t({188message: 'Reviewing your code...\n',189comment: `{Locked='](command:workbench.panel.markers.view.focus)'}`,190}));191content.isTrusted = {192enabledCommands: ['workbench.panel.markers.view.focus']193};194outputStream.markdown(content);195}196const comments = parseReviewComments(request, [197{198document: this.documentContext.document,199relativeDocumentPath: path.basename(this.documentContext.document.uri.fsPath),200selection: this.documentContext.selection201}202], this.text, true);203if (comments.length > this.comments.length) {204this.reviewService.addReviewComments(comments.slice(this.comments.length));205this.comments = comments;206}207}208209const comments = parseReviewComments(request, [210{211document: this.documentContext.document,212relativeDocumentPath: path.basename(this.documentContext.document.uri.fsPath),213selection: this.documentContext.selection214}215], this.text, false); // parse all216if (comments.length > this.comments.length) {217this.reviewService.addReviewComments(comments.slice(this.comments.length));218this.comments = comments;219}220outputStream.markdown(l10n.t('Reviewed your code and generated {0} suggestions.', comments.length));221}222}223224225