Path: blob/main/extensions/copilot/src/extension/prompts/node/inline/promptingSummarizedDocument.ts
13405 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 type * as vscode from 'vscode';6import { IResponsePart } from '../../../../platform/chat/common/chatMLFetcher';7import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';8import { IParserService } from '../../../../platform/parser/node/parserService';9import { findLastIdx } from '../../../../util/vs/base/common/arraysFind';10import { CancellationToken } from '../../../../util/vs/base/common/cancellation';11import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';12import { Range, TextEdit } from '../../../../vscodeTypes';13import { ISessionTurnStorage, OutcomeAnnotationLabel } from '../../../inlineChat/node/promptCraftingTypes';14import { isImportStatement } from '../../../prompt/common/importStatement';15import { EditStrategy, trimLeadingWhitespace } from '../../../prompt/node/editGeneration';16import { EarlyStopping, IResponseProcessorContext, LeadingMarkdownStreaming, ReplyInterpreter, StreamingEditsController } from '../../../prompt/node/intents';17import { ILineFilter, IStreamingEditsStrategyFactory, IStreamingTextPieceClassifier, InsertOrReplaceStreamingEdits, InsertionStreamingEdits, LineRange, ReplaceSelectionStreamingEdits, SentInCodeBlock, SentLine, StreamingWorkingCopyDocument } from '../../../prompt/node/streamingEdits';18import { ProjectedDocument } from './summarizedDocument/summarizeDocument';19import { adjustSelectionAndSummarizeDocument } from './summarizedDocument/summarizeDocumentHelpers';20import { DocumentSnapshot, WorkingCopyDerivedDocument } from './workingCopies';2122export async function createPromptingSummarizedDocument(23parserService: IParserService,24document: TextDocumentSnapshot,25formattingOptions: vscode.FormattingOptions | undefined,26userSelection: Range,27tokensBudget: number,28): Promise<PromptingSummarizedDocument> {29const result = await adjustSelectionAndSummarizeDocument(parserService, document, formattingOptions, userSelection, tokensBudget);30return new PromptingSummarizedDocument(31result.selection,32result.adjustedSelection,33result.document,34document,35formattingOptions,36);37}3839export class PromptingSummarizedDocument {4041public get uri(): vscode.Uri {42return this._document.uri;43}4445public get languageId(): string {46return this._document.languageId;47}4849constructor(50private readonly _selection: OffsetRange,51private readonly _adjustedSelection: OffsetRange,52private readonly _projectedDocument: ProjectedDocument,53private readonly _document: TextDocumentSnapshot,54private readonly _formattingOptions: vscode.FormattingOptions | undefined,55) { }5657public splitAroundAdjustedSelection(): SummarizedDocumentSplit {58return new SummarizedDocumentSplit(59this._projectedDocument,60this.uri,61this._formattingOptions,62this._adjustedSelection63);64}6566public splitAroundOriginalSelectionEnd(): SummarizedDocumentSplit {67return new SummarizedDocumentSplit(68this._projectedDocument,69this.uri,70this._formattingOptions,71new OffsetRange(72this._selection.endExclusive,73this._selection.endExclusive74)75);76}77}7879export class SummarizedDocumentSplit {8081public readonly codeAbove: string;82public readonly codeSelected: string;83public readonly codeBelow: string;84private readonly _selection: vscode.Range;8586public get hasCodeWithoutSelection(): boolean {87return (88this.codeAbove.trim().length > 089|| this.codeBelow.trim().length > 090);91}9293public get hasContent(): boolean {94return (95this.codeAbove.trim().length > 096|| this.codeSelected.trim().length > 097|| this.codeBelow.trim().length > 098);99}100101constructor(102private readonly _projectedDocument: ProjectedDocument,103private readonly _uri: vscode.Uri,104private readonly _formattingOptions: vscode.FormattingOptions | undefined,105offsetSelection: OffsetRange106) {107this._selection = this._projectedDocument.positionOffsetTransformer.toRange(offsetSelection);108this.codeAbove = this._projectedDocument.text.substring(0, offsetSelection.start);109this.codeSelected = this._projectedDocument.text.substring(offsetSelection.start, offsetSelection.endExclusive);110this.codeBelow = this._projectedDocument.text.substring(offsetSelection.endExclusive);111}112113public get replaceSelectionStreaming(): IStreamingEditsStrategyFactory {114return (lineFilter, streamingWorkingCopyDocument) => new ReplaceSelectionStreamingEdits(115streamingWorkingCopyDocument,116this._selection,117lineFilter118);119}120121public get insertStreaming(): IStreamingEditsStrategyFactory {122return (lineFilter, streamingWorkingCopyDocument) => new InsertionStreamingEdits(123streamingWorkingCopyDocument,124this._selection.end,125lineFilter126);127}128129public get insertOrReplaceStreaming(): IStreamingEditsStrategyFactory {130return (lineFilter, streamingWorkingCopyDocument) => new InsertOrReplaceStreamingEdits(131streamingWorkingCopyDocument,132this._selection,133this._selection,134EditStrategy.FallbackToInsertBelowRange,135true,136lineFilter137);138}139140public createReplyInterpreter(141leadingMarkdownStreaming: LeadingMarkdownStreaming,142earlyStopping: EarlyStopping,143streamingStrategyFactory: IStreamingEditsStrategyFactory,144textPieceClassifier: IStreamingTextPieceClassifier,145lineFilter: ILineFilter146): ReplyInterpreter {147return new InlineReplyInterpreter(148this._uri,149this._projectedDocument,150this._formattingOptions,151leadingMarkdownStreaming,152earlyStopping,153streamingStrategyFactory,154textPieceClassifier,155lineFilter156);157}158}159160export class InlineReplyInterpreter implements ReplyInterpreter {161162private readonly _initialDocumentSnapshot: DocumentSnapshot;163private readonly _workingCopySummarizedDoc: WorkingCopyDerivedDocument;164private _lastText: string = '';165166constructor(167private readonly _uri: vscode.Uri,168summarizedDoc: ProjectedDocument,169private readonly _fileIndentInfo: vscode.FormattingOptions | undefined,170private readonly _leadingMarkdownStreaming: LeadingMarkdownStreaming,171private readonly _earlyStopping: EarlyStopping,172private readonly _streamingStrategyFactory: IStreamingEditsStrategyFactory,173private readonly _textPieceClassifier: IStreamingTextPieceClassifier,174private readonly _lineFilter: ILineFilter175) {176this._initialDocumentSnapshot = new DocumentSnapshot(summarizedDoc.originalText);177this._workingCopySummarizedDoc = new WorkingCopyDerivedDocument(summarizedDoc);178}179180async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, _outputStream: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {181const outputStream = this._workingCopySummarizedDoc.createDerivedDocumentChatResponseStream(_outputStream);182const streamingWorkingCopyDocument = new StreamingWorkingCopyDocument(183outputStream,184this._uri,185this._workingCopySummarizedDoc.text,186this._workingCopySummarizedDoc.text.split('\n').map((_, index) => new SentLine(index, SentInCodeBlock.Other)), // not used187new LineRange(0, 0), // not used188this._workingCopySummarizedDoc.languageId,189this._fileIndentInfo190);191192const streaming = new StreamingEditsController(193outputStream,194this._leadingMarkdownStreaming,195this._earlyStopping,196this._textPieceClassifier,197this._streamingStrategyFactory(this._lineFilter, streamingWorkingCopyDocument),198);199200for await (const part of inputStream) {201this._lastText += part.delta.text;202const { shouldFinish } = streaming.update(this._lastText);203if (shouldFinish) {204break;205}206}207208const { didEdits, didNoopEdits, additionalImports } = await streaming.finish();209if (didEdits) {210const additionalImportsEdits = this._generateAdditionalImportsEdits(additionalImports);211212const reversedEdits = this._workingCopySummarizedDoc.allReportedEdits.inverse(this._initialDocumentSnapshot.text);213const entireModifiedRangeOffsets = reversedEdits.replacements.reduce((prev, curr) => prev.join(curr.replaceRange), reversedEdits.replacements[0].replaceRange);214const entireModifiedRange = this._workingCopySummarizedDoc.originalDocumentTransformer.toRange(entireModifiedRangeOffsets);215const store = {216lastDocumentContent: this._workingCopySummarizedDoc.originalText,217lastWholeRange: entireModifiedRange,218} satisfies ISessionTurnStorage;219220_outputStream.textEdit(this._uri, additionalImportsEdits);221context.storeInInlineSession(store);222return;223}224225if (additionalImports.length > 0) {226// No edits, but imports encountered227_outputStream.textEdit(this._uri, this._generateAdditionalImportsEdits(additionalImports));228return;229}230231if (didNoopEdits) {232// we attempted to do edits, but they were not meaningful, i.e. they didn't change anything233context.addAnnotations([{ label: OutcomeAnnotationLabel.NOOP_EDITS, message: 'Edits were not applied because they were having no actual effects.', severity: 'info' }]);234return;235}236237if (!this._lastText) {238return;239}240241outputStream.markdown(this._lastText);242}243244private _generateAdditionalImportsEdits(additionalImports: string[]): vscode.TextEdit[] {245if (additionalImports.length === 0) {246return [];247}248249const documentLines = this._workingCopySummarizedDoc.originalText.split(/\r\n|\r|\n/g);250const lastImportStatementLineIdx = findLastIdx(documentLines, l => isImportStatement(l, this._workingCopySummarizedDoc.languageId));251if (lastImportStatementLineIdx === -1) {252// no existing import statements, we insert it on line 0253return [new TextEdit(new Range(0, 0, 0, 0), additionalImports.join('\n') + '\n\n')];254}255256// traverse lines upward starting at `lastImportStatementLineIdx` to capture all existing imports257const existingImports = new Set<string>();258for (let i = lastImportStatementLineIdx; i >= 0; i--) {259const line = documentLines[i];260if (line.trim() === '') { // skip empty lines261continue;262}263if (isImportStatement(line, this._workingCopySummarizedDoc.languageId)) {264existingImports.add(trimLeadingWhitespace(line));265} else {266break;267}268}269270additionalImports = additionalImports.filter(i => !existingImports.has(i));271if (additionalImports.length === 0) {272return [];273}274275const lastImportStatementLineLength = documentLines[lastImportStatementLineIdx].length;276return [new TextEdit(new Range(lastImportStatementLineIdx, lastImportStatementLineLength, lastImportStatementLineIdx, lastImportStatementLineLength), '\n' + additionalImports.join('\n'))];277}278}279280281