Path: blob/main/extensions/copilot/src/extension/prompts/node/inline/inlineChatFix3Prompt.tsx
13404 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*--------------------------------------------------------------------------------------------*/4import * as l10n from '@vscode/l10n';5import { PromptElement, PromptReference, PromptSizing, SystemMessage, TextChunk, UserMessage } from '@vscode/prompt-tsx';6import type { CancellationToken, ChatResponseStream, ChatVulnerability, MarkdownString } from 'vscode';7import { IResponsePart } from '../../../../platform/chat/common/chatMLFetcher';8import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';9import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';10import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';11import { ILanguageDiagnosticsService } from '../../../../platform/languages/common/languageDiagnosticsService';12import { KnownSources } from '../../../../platform/languageServer/common/languageContextService';13import { ILogService } from '../../../../platform/log/common/logService';14import { IParserService } from '../../../../platform/parser/node/parserService';15import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService';16import { isNotebookCellOrNotebookChatInput } from '../../../../util/common/notebooks';17import { illegalArgument } from '../../../../util/vs/base/common/errors';18import { isEqual } from '../../../../util/vs/base/common/resources';19import { URI } from '../../../../util/vs/base/common/uri';20import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringEdit';21import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';22import { Range, TextEdit, Uri } from '../../../../vscodeTypes';23import { CodeBlockInfo, CodeBlockProcessor, isCodeBlockWithResource } from '../../../codeBlocks/node/codeBlockProcessor';24import { findDiagnosticForSelectionAndPrompt } from '../../../context/node/resolvers/fixSelection';25import { InlineFixProps } from '../../../context/node/resolvers/inlineFixIntentInvocation';26import { getStructure } from '../../../context/node/resolvers/selectionContextHelpers';27import { OutcomeAnnotation, OutcomeAnnotationLabel } from '../../../inlineChat/node/promptCraftingTypes';28import { IResponseProcessorContext, ReplyInterpreter, ReplyInterpreterMetaData } from '../../../prompt/node/intents';29import { CompositeElement } from '../base/common';30import { InstructionMessage } from '../base/instructionMessage';31import { LegacySafetyRules } from '../base/safetyRules';32import { Tag } from '../base/tag';33import { ICodeMapperService, IMapCodeRequest, IMapCodeResult } from '../codeMapper/codeMapperService';34import { getCustomMarker, getPatchEditReplyProcessor, PatchEditExamplePatch, PatchEditInputCodeBlock, PatchEditInputCodeBlockProps, PatchEditReplyProcessor, PatchEditRules } from '../codeMapper/patchEditGeneration';35import { ChatToolReferences, renderChatVariables, UserQuery } from '../panel/chatVariables';36import { CodeBlockFormattingRules } from '../panel/codeBlockFormattingRules';37import { HistoryWithInstructions } from '../panel/conversationHistory';38import { CustomInstructions } from '../panel/customInstructions';39import { CodeBlock } from '../panel/safeElements';40import { Diagnostics } from './diagnosticsContext';41import { InlineChatWorkspaceSearch } from './inlineChatWorkspaceSearch';42import { LanguageServerContextPrompt } from './languageServerContextPrompt';43import { ProjectedDocument } from './summarizedDocument/summarizeDocument';44import { summarizeDocumentSync } from './summarizedDocument/summarizeDocumentHelpers';4546export class InlineFix3Prompt extends PromptElement<InlineFixProps> {4748constructor(props: InlineFixProps,49@IIgnoreService private readonly ignoreService: IIgnoreService,50@IFileSystemService private readonly fileSystemService: IFileSystemService,51@IParserService private readonly parserService: IParserService,52@ILanguageDiagnosticsService private readonly languageDiagnosticsService: ILanguageDiagnosticsService,53@IConfigurationService private readonly configurationService: IConfigurationService,54@IInstantiationService private readonly instantiationService: IInstantiationService,55) {56super(props);57}5859async render(state: void, sizing: PromptSizing) {60const { document, wholeRange, fileIndentInfo, selection, language } = this.props.documentContext;61const isIgnored = await this.ignoreService.isCopilotIgnored(document.uri);62if (isIgnored) {63return <ignoredFiles value={[document.uri]} />;64}65if (isNotebookCellOrNotebookChatInput(document.uri)) {66throw illegalArgument('InlineFix3PlusPrompt should not be used with a notebook!');67}6869const inputDocCharLimit = (sizing.endpoint.modelMaxPromptTokens / 3) * 4; // consume one 3rd of the model window, estimating roughly 4 chars per token;70let projectedDocument: ProjectedDocument;71let isSummarized = false;72if (document.getText().length > inputDocCharLimit) {73// only compute the summarized document if needed74const structure = await getStructure(this.parserService, document, fileIndentInfo);75projectedDocument = summarizeDocumentSync(inputDocCharLimit, document, wholeRange, structure, { tryPreserveTypeChecking: true });76isSummarized = true;77} else {78projectedDocument = new ProjectedDocument(document.getText(), StringEdit.empty, document.languageId);79}8081const { query, history, chatVariables, } = this.props.promptContext;82const { useWorkspaceChunksFromDiagnostics, useWorkspaceChunksFromSelection } = this.props.features;8384const adjustedSelection = projectedDocument.projectRange(selection);85const selectedLinesContent = document.getText(new Range(selection.start.line, 0, selection.end.line + 1, 0)).trimEnd();8687const diagnostics = findDiagnosticForSelectionAndPrompt(this.languageDiagnosticsService, document.uri, selection, query);8889const enableCodeMapper = this.configurationService.getConfig(ConfigKey.TeamInternal.InlineChatUseCodeMapper);9091const replyInterpreter = enableCodeMapper ?92this.instantiationService.createInstance(CodeMapperFixReplyInterpreter, document.uri) :93this.instantiationService.createInstance(PatchEditFixReplyInterpreter, projectedDocument, document.uri, adjustedSelection);9495const GenerationRulesAndExample = enableCodeMapper ? CodeMapperRulesAndExample : PatchEditFixRulesAndExample;96const InputCodeBlock = enableCodeMapper ? CodeMapperInputCodeBlock : PatchEditInputCodeBlock;9798const renderedChatVariables = await renderChatVariables(chatVariables, this.fileSystemService);99100return (101<>102<references value={[new PromptReference(document.uri)]} />103<meta value={new ReplyInterpreterMetaData(replyInterpreter)} />104<SystemMessage priority={1000}>105You are an AI programming assistant.<br />106When asked for your name, you must respond with "GitHub Copilot".<br />107The user has a {language.languageId} file opened in a code editor.<br />108The user expects you to propose a fix for one or more problems in that file.<br />109<LegacySafetyRules />110</SystemMessage>111<HistoryWithInstructions inline={true} historyPriority={700} passPriority history={history}>112<InstructionMessage priority={1000}>113For the response always follow these instructions:<br />114Describe in a single sentence how you would solve the problem. After that sentence, add an empty line. Then provide code changes or a terminal command to run.<br />115<GenerationRulesAndExample />116</InstructionMessage>117</HistoryWithInstructions>118119<UserMessage priority={700}>120<CustomInstructions /*priority={700}*/ languageId={language.languageId} chatVariables={chatVariables} />121<LanguageServerContextPrompt priority={700} document={document} position={selection.start} requestId={this.props.promptContext.requestId} source={KnownSources.fix} />122<CompositeElement priority={750} >{...renderedChatVariables}</CompositeElement>123<CompositeElement priority={600} >124{125projectedDocument.text.length > 0 ?126<>127I have the following code open in the editor, starting from line 1 to line {projectedDocument.lineCount}.<br />128</> :129<>130I am in an empty file:<br />131</>132}133<InputCodeBlock uri={document.uri} languageId={language.languageId} code={projectedDocument.text} shouldTrim={false} isSummarized={isSummarized} /><br />134</CompositeElement >135<CompositeElement /*priority={500}*/>136{137selection.isEmpty ?138<>139I have the selection at line {adjustedSelection.start.line + 1}, column {adjustedSelection.start.character + 1}<br />140</> :141<>142I have currently selected from line {adjustedSelection.start.line + 1}, column {adjustedSelection.start.character + 1} to line {adjustedSelection.end.line + 1} column {adjustedSelection.end.character + 1}.<br />143</>144}145</CompositeElement >146<CompositeElement /*priority={500}*/>147{148selectedLinesContent.length && !diagnostics.some(d => d.range.contains(selection)) &&149<>150The content of the lines at the selection is151<CodeBlock uri={document.uri} languageId={language.languageId} code={selectedLinesContent} shouldTrim={false} /><br />152</>153}154</CompositeElement >155<Diagnostics /*priority={500}*/ documentContext={this.props.documentContext} diagnostics={diagnostics} />156<InlineChatWorkspaceSearch /*priority={200}*/ diagnostics={diagnostics} documentContext={this.props.documentContext} useWorkspaceChunksFromDiagnostics={useWorkspaceChunksFromDiagnostics} useWorkspaceChunksFromSelection={useWorkspaceChunksFromSelection} />157<ChatToolReferences promptContext={this.props.promptContext} />158159<Tag name='userPrompt'>160<TextChunk /*priority={700}*/>161Please find a fix for my code so that the result is without any errors.162</TextChunk>163<UserQuery chatVariables={chatVariables} query={query} /><br />164</Tag>165</UserMessage>166</>167);168}169}170171const exampleUri = Uri.file('/someFolder/myFile.cs');172173class PatchEditFixRulesAndExample extends PromptElement {174175render() {176return (177<>178When proposing to fix the problem by running a terminal command, write `{getCustomMarker('TERMINAL')}` and provide a code block that starts with ```bash and contains the terminal script inside.<br />179<PatchEditRules />180<Tag name='example' priority={100}>181<Tag name='user'>182I have the following code open in the editor.<br />183<PatchEditInputCodeBlock184uri={exampleUri}185languageId='csharp'186code={['// This is my class', 'class C { }', '', 'new C().Field = 9;']}187/>188</Tag>189<Tag name='assistant'>190The problem is that the class 'C' does not have a field or property named 'Field'. To fix this, you need to add a 'Field' property to the 'C' class.<br />191<br />192<PatchEditExamplePatch193changes={194[195{196uri: exampleUri,197find: ['// This is my class', 'class C { }'],198replace: ['// This is my class', 'class C {', 'public int Field { get; set; }', '}']199},200{201uri: exampleUri,202find: ['new C().Field = 9;'],203replace: ['// set the field to 9', 'new C().Field = 9;']204}205]206}207/>208</Tag>209</Tag>210</>211);212}213}214215export class PatchEditFixReplyInterpreter implements ReplyInterpreter {216private _lastText: string = '';217private readonly _replyProcessor: PatchEditReplyProcessor;218219constructor(220private readonly projectedDocument: ProjectedDocument,221private readonly documentUri: URI,222private readonly adjustedSelection: Range,223@ILogService private readonly logService: ILogService,224@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService225226) {227this._replyProcessor = getPatchEditReplyProcessor(promptPathRepresentationService);228}229230async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: ChatResponseStream, token: CancellationToken): Promise<void> {231let inFirstParagraph = true; // print only the frist paragraph232let charactersSent = 0;233let newText = '';234for await (const part of inputStream) {235if (token.isCancellationRequested) {236return;237}238newText += part.delta.text;239if (newText.length > this._lastText.length) {240this._lastText = newText; // the new complete text241if (inFirstParagraph) {242// test if the new text added made the first paragraph complete243const paragraph = this._replyProcessor.getFirstParagraph(newText);244if (paragraph.length > charactersSent) {245// still in the first paragraph246outputStream.markdown(paragraph.substring(charactersSent));247charactersSent = paragraph.length;248} else {249// the first paragraph is complete250inFirstParagraph = false;251outputStream.markdown('\n\n');252outputStream.progress(l10n.t('Generating edits...'));253}254}255}256}257if (this._lastText.length === 0) {258outputStream.warning(l10n.t('Copilot did not provide a response. Please try again.'));259return;260}261262const res = this._replyProcessor.process(this._lastText, this.projectedDocument.text, this.documentUri, this.adjustedSelection.start.line);263if (res.otherSections.length) {264for (const section of res.otherSections) {265outputStream.markdown(section.content.join('\n\n'));266}267}268if (res.otherPatches.length) {269for (const patch of res.otherPatches) {270if (patch.replace.length) {271const uri = this.promptPathRepresentationService.resolveFilePath(patch.filePath, this.documentUri.scheme);272if (uri) {273outputStream.markdown(patch.replace[0]);274outputStream.codeblockUri(uri);275outputStream.markdown(patch.replace.slice(1).join('\n'));276} else {277outputStream.markdown(patch.replace.join('\n'));278}279}280}281}282let edits = res.edits;283if (edits.length) {284edits = this.projectedDocument.projectBackTextEdit(edits);285if (res.edits.length !== edits.length) {286res.annotations.push({ message: 'Some edits were not applied because they were out of bounds.', label: OutcomeAnnotationLabel.SUMMARIZE_CONFLICT, severity: 'error' });287} else {288const annot = this._validateTextEditProject(res.edits, edits, this.projectedDocument);289if (annot) {290res.annotations.push(annot);291}292}293}294context.addAnnotations(res.annotations);295if (edits.length) {296outputStream.textEdit(this.documentUri, edits);297} else if (!res.otherPatches.length && !res.otherSections.length) {298outputStream.warning(l10n.t('The edit generation was not successful. Please try again.'));299}300if (res.annotations.length) {301this.logService.info(`[inline fix] Problems generating edits: ${res.annotations.map(a => `${a.message} [${a.label}]`).join(', ')}, invalid patches: ${res.invalidPatches.length}`);302}303}304305private _validateTextEditProject(edits: TextEdit[], projectedBackEdits: TextEdit[], projectedDocument: ProjectedDocument): OutcomeAnnotation | undefined {306for (let i = 0; i < edits.length; i++) {307const projEditString = projectedDocument.positionOffsetTransformer.toOffsetRange(edits[i].range).substring(projectedDocument.text);308const origEditString = projectedDocument.originalPositionOffsetTransformer.toOffsetRange(projectedBackEdits[i].range).substring(projectedDocument.originalText);309if (projEditString !== origEditString) {310return { message: `Problem projecting edits: '${projEditString}' does not match '${origEditString}' (projectecBack)`, label: OutcomeAnnotationLabel.INVALID_PROJECTION, severity: 'error' };311}312}313return undefined;314}315}316317class CodeMapperInputCodeBlock extends PromptElement<PatchEditInputCodeBlockProps> {318render() {319return (320<CodeBlock321uri={this.props.uri}322languageId={this.props.languageId}323code={Array.isArray(this.props.code) ? this.props.code.join('\n') : this.props.code}324shouldTrim={this.props.shouldTrim}325includeFilepath={true}326/>327);328}329}330331class CodeMapperRulesAndExample extends PromptElement {332render() {333return (334<>335When proposing to fix the problem by running a terminal command, provide a code block that starts with ```bash and contains the terminal script inside.<br />336<CodeBlockFormattingRules />337<Tag name='example' priority={100}>338<Tag name='user'>339I have the following code open in the editor.<br />340<CodeMapperInputCodeBlock341uri={exampleUri}342languageId='csharp'343code={['// This is my class', 'class C { }', '', 'new C().Field = 9;'].join('\n')}344shouldTrim={false}345/>346</Tag>347<Tag name='assistant'>348The problem is that the class 'C' does not have a field or property named 'Field'. To fix this, you need to add a 'Field' property to the 'C' class.<br />349<br />350<CodeMapperInputCodeBlock351uri={exampleUri}352languageId='csharp'353code={['// This is my class', 'class C {', ' public int Field { get; set; }', '}', ''].join('\n')}354shouldTrim={false}355/>356</Tag>357</Tag>358</>359);360}361}362363class CodeMapperFixReplyInterpreter implements ReplyInterpreter {364365constructor(366private readonly documentUri: URI,367@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,368@ICodeMapperService private readonly codeMapperService: ICodeMapperService,369) {370}371372async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: ChatResponseStream, token: CancellationToken): Promise<void> {373let currentCodeBlock: CodeBlockInfo | undefined = undefined;374let applyCodeBlock = false;375let inFirstSentence = true;376const codeMapperWork: Promise<IMapCodeResult | undefined>[] = [];377const codeblockProcessor = new CodeBlockProcessor(378path => {379return this.promptPathRepresentationService.resolveFilePath(path);380},381(markdown: MarkdownString, codeBlockInfo: CodeBlockInfo | undefined, vulnerabilities: ChatVulnerability[] | undefined) => {382if (codeBlockInfo) {383inFirstSentence = false;384if (codeBlockInfo !== currentCodeBlock) {385// first time we see this code block386currentCodeBlock = codeBlockInfo;387applyCodeBlock = isEqual(codeBlockInfo.resource, this.documentUri);388if (!applyCodeBlock && codeBlockInfo.resource) {389outputStream.codeblockUri(codeBlockInfo.resource);390}391}392if (applyCodeBlock) {393return;394}395} else {396if (!inFirstSentence) {397return;398}399}400if (vulnerabilities) {401outputStream.markdownWithVulnerabilities(markdown, vulnerabilities);402} else {403outputStream.markdown(markdown);404}405},406codeBlock => {407if (isCodeBlockWithResource(codeBlock) && isEqual(codeBlock.resource, this.documentUri)) {408const request: IMapCodeRequest = { codeBlock };409outputStream.markdown('\n\n');410outputStream.progress(l10n.t('Generating edits...'));411const task = this.codeMapperService.mapCode(request, outputStream, { chatRequestId: context.turn.id, chatRequestSource: 'inline1Fix3', isAgent: false }, token).finally(() => {412if (!token.isCancellationRequested) {413// signal being done with this uri414outputStream.textEdit(codeBlock.resource, true);415}416});417codeMapperWork.push(task);418}419}420421);422423for await (const { delta } of inputStream) {424if (token.isCancellationRequested) {425return;426}427codeblockProcessor.processMarkdown(delta.text, delta.codeVulnAnnotations?.map(a => ({ title: a.details.type, description: a.details.description })));428}429codeblockProcessor.flush();430431const results = await Promise.all(codeMapperWork);432for (const result of results) {433if (!result) {434context.addAnnotations([{ severity: 'error', label: 'cancelled', message: 'CodeMapper cancelled' }]);435} else if (result.annotations) {436context.addAnnotations(result.annotations);437}438}439for (const result of results) {440if (result && result.errorDetails) {441outputStream.warning(`CodeMapper error: ${result.errorDetails}`);442}443}444}445}446447448