Path: blob/main/extensions/copilot/src/extension/intents/node/editCodeStep.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 { Raw } from '@vscode/prompt-tsx';6import type { ChatPromptReference, ChatResult } from 'vscode';7import { getTextPart } from '../../../platform/chat/common/globalStringUtils';8import { NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot';9import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';10import { IChatEndpoint } from '../../../platform/networking/common/networking';11import { getAltNotebookRange, IAlternativeNotebookContentService } from '../../../platform/notebook/common/alternativeContent';12import { INotebookService } from '../../../platform/notebook/common/notebookService';13import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';14import { findCell, findNotebook, getNotebookAndCellFromUri } from '../../../util/common/notebooks';15import { isLocation, isUri } from '../../../util/common/types';16import { ResourceSet } from '../../../util/vs/base/common/map';17import { Schemas } from '../../../util/vs/base/common/network';18import { isEqual } from '../../../util/vs/base/common/resources';19import { isNumber, isString } from '../../../util/vs/base/common/types';20import { isUriComponents, URI } from '../../../util/vs/base/common/uri';21import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';22import { Range, Uri } from '../../../vscodeTypes';23import { ChatVariablesCollection, isCustomizationsIndex, isInstructionFile } from '../../prompt/common/chatVariablesCollection';24import { Turn } from '../../prompt/common/conversation';25import { IBuildPromptContext, IWorkingSet, WorkingSetEntryState } from '../../prompt/common/intents';262728export class EditCodeStepTelemetryInfo {29public codeblockUris = new ResourceSet();3031public codeblockCount: number = 0;32public codeblockWithUriCount: number = 0;33public codeblockWithElidedCodeCount: number = 0;3435public shellCodeblockCount: number = 0;36public shellCodeblockWithUriCount: number = 0;37public shellCodeblockWithElidedCodeCount: number = 0;38}3940export interface IPreviousWorkingSetEntry {41readonly document: { readonly uri: Uri; readonly languageId: string; readonly version: number; readonly text: string };42state: WorkingSetEntryState;43}4445export interface IPreviousPromptInstruction {46readonly document: { readonly uri: Uri; readonly version: number; readonly text: string };47}4849export class PreviousEditCodeStep {50public static fromChatResultMetaData(chatResult: ChatResult): PreviousEditCodeStep | undefined {51const edits = chatResult.metadata?.edits;52if (isEditHistoryDTO(edits)) {53const entries = edits.workingSet.map(entry => {54return {55document: { uri: URI.revive(entry.uri), languageId: entry.languageId, version: entry.version, text: entry.text },56state: entry.state,57} satisfies IPreviousWorkingSetEntry;58});59const promptInstructions = edits.promptInstructions?.map(entry => {60return {61document: { uri: URI.revive(entry.uri), version: entry.version, text: entry.text }62} satisfies IPreviousPromptInstruction;63}) ?? [];64return new PreviousEditCodeStep(entries, edits.request, edits.response, promptInstructions);65}66return undefined;67}6869public static fromTurn(turn: Turn): PreviousEditCodeStep | undefined {70let editCodeStep = turn.getMetadata(EditCodeStepTurnMetaData)?.value;71if (!editCodeStep && turn.responseChatResult) {72editCodeStep = PreviousEditCodeStep.fromChatResultMetaData(turn.responseChatResult);73if (editCodeStep) {74turn.setMetadata(new EditCodeStepTurnMetaData(editCodeStep));75}76}77return editCodeStep;78}7980public static fromEditCodeStep(editCodeStep: EditCodeStep) {81const workingSet = editCodeStep.workingSet.map(entry => ({82document: { uri: entry.document.uri, languageId: entry.document.languageId, version: entry.document.version, text: entry.document.getText() },83state: entry.state,84}));85const promptInstructions = editCodeStep.promptInstructions.map(entry => ({86document: { uri: entry.uri, version: entry.version, text: entry.getText() }87}));88return new PreviousEditCodeStep(workingSet, editCodeStep.userMessage, editCodeStep.assistantReply, promptInstructions);89}9091constructor(92public readonly workingSet: readonly IPreviousWorkingSetEntry[],93public readonly request: string,94public readonly response: string,95public readonly promptInstructions: readonly IPreviousPromptInstruction[]96) { }9798public setWorkingSetEntryState(uri: URI, state: { accepted: boolean; hasRemainingEdits: boolean }): void {99for (const entry of this.workingSet) {100if (isEqual(entry.document.uri, uri)) {101entry.state = this._getUpdatedState(entry, state.accepted, state.hasRemainingEdits);102}103}104}105106private _getUpdatedState(workingSetEntry: IPreviousWorkingSetEntry, accepted: boolean, hasRemainingEdits: boolean): WorkingSetEntryState {107const { state } = workingSetEntry;108109if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) {110return state;111}112113if (accepted && !hasRemainingEdits) {114return WorkingSetEntryState.Accepted;115}116117if (!accepted && !hasRemainingEdits) {118return WorkingSetEntryState.Rejected;119}120121// TODO: reflect partial accepts/rejects within a file when we add support for that122return WorkingSetEntryState.Undecided;123}124125public toChatResultMetaData(): any {126const edits = {127workingSet: this.workingSet.map(entry => {128return {129uri: entry.document.uri,130text: entry.document.text,131languageId: entry.document.languageId,132version: entry.document.version,133state: entry.state,134};135}),136promptInstructions: this.promptInstructions.map(entry => ({137uri: entry.document.uri,138text: entry.document.text,139version: entry.document.version140})),141request: this.request,142response: this.response143} satisfies EditHistoryDTO;144return { edits };145}146147}148149export class EditCodeStepTurnMetaData {150constructor(public readonly value: PreviousEditCodeStep) {151}152}153154155export class EditCodeStep {156157public static async create(instantiationService: IInstantiationService, history: readonly Turn[], chatVariables: ChatVariablesCollection, endpoint: IChatEndpoint): Promise<EditCodeStepChatVariablesPair> {158const factory = instantiationService.createInstance(EditCodeStepFactory);159return factory.createNextStep(history, chatVariables, endpoint);160}161162/**163* The user message that was sent with this step164*/165private _userMessage: string = '';166public get userMessage(): string {167return this._userMessage;168}169170/**171* The assistant reply that came back with this step172*/173private _assistantReply: string = '';174public get assistantReply(): string {175return this._assistantReply;176}177178/**179* The working set (it is initially the list of files sent by the user).180* If the assistant replies with a code suggestion for a file contained here, it's status will be changed to undecided.181* If the assistant replies with a code suggestion for a file not contained here, the working set will not reflect this in any way.182* If the user makes a decision in the ui, the working set entry will update to reflect this.183*/184private readonly _workingSet: readonly IMutableWorkingSetEntry[];185public get workingSet(): IWorkingSet {186return this._workingSet;187}188189public get promptInstructions(): readonly TextDocumentSnapshot[] {190return this._promptInstructions;191}192193public readonly telemetryInfo = new EditCodeStepTelemetryInfo();194195constructor(196public readonly previousStep: PreviousEditCodeStep | null,197workingSet: readonly IMutableWorkingSetEntry[],198private readonly _promptInstructions: TextDocumentSnapshot[]199) {200this._workingSet = workingSet;201}202203setUserMessage(userMessage: Raw.UserChatMessage): void {204this._userMessage = getTextPart(userMessage.content);205}206207setAssistantReply(reply: string): void {208this._assistantReply = reply;209}210211public setWorkingSetEntryState(uri: URI, state: WorkingSetEntryState): void {212for (const entry of this._workingSet) {213if (isEqual(entry.document.uri, uri)) {214entry.state = state;215}216}217}218219public getPredominantScheme(): string | undefined {220const schemes = new Map<string, number>();221for (const entry of this._workingSet) {222const scheme = entry.document.uri.scheme;223schemes.set(scheme, (schemes.get(scheme) ?? 0) + 1);224}225let maxCount = 0;226let maxScheme = undefined;227for (const [scheme, count] of schemes) {228if (count > maxCount) {229maxCount = count;230maxScheme = scheme;231}232}233return maxScheme;234}235}236237interface EditCodeStepChatVariablesPair {238readonly editCodeStep: EditCodeStep;239readonly chatVariables: ChatVariablesCollection;240}241242class EditCodeStepFactory {243244constructor(245@IWorkspaceService private readonly _workspaceService: IWorkspaceService,246@INotebookService private readonly _notebookService: INotebookService,247@IAlternativeNotebookContentService private readonly alternativeNotebookContentService: IAlternativeNotebookContentService248) { }249250/**251* Update the working set taking into account the passed in chat variables.252* Returns the filtered chat variables that should be used for rendering253*/254public async createNextStep(history: readonly Turn[], chatVariables: ChatVariablesCollection, endpoint: IChatEndpoint): Promise<EditCodeStepChatVariablesPair> {255256const findPreviousStepEntry = () => {257for (let i = history.length - 1; i >= 0; i--) {258const entry = PreviousEditCodeStep.fromTurn(history[i]);259if (entry) {260return entry;261}262}263return null;264};265266const prevStep = findPreviousStepEntry();267268const workingSet: IMutableWorkingSetEntry[] = [];269270const getWorkingSetEntry = (uri: Uri) => {271return workingSet.find(entry => isEqual(entry.document.uri, uri));272};273274const getCurrentOrPreviousWorkingSetEntryState = (uri: Uri) => {275const currentEntry = getWorkingSetEntry(uri);276if (currentEntry) {277return currentEntry.state;278}279if (prevStep) {280const previousStepEntry = prevStep.workingSet.find(entry => isEqual(entry.document.uri, uri));281if (previousStepEntry) {282return previousStepEntry.state;283}284}285return WorkingSetEntryState.Initial;286};287288const addWorkingSetEntry = async (documentOrCellUri: URI, isMarkedReadonly: boolean | undefined, range?: Range) => {289try {290const uri = this._notebookService.hasSupportedNotebooks(documentOrCellUri) ? (findNotebook(documentOrCellUri, this._workspaceService.notebookDocuments)?.uri ?? documentOrCellUri) : documentOrCellUri;291if (!getWorkingSetEntry(uri)) {292const state = getCurrentOrPreviousWorkingSetEntryState(uri);293if (this._notebookService.hasSupportedNotebooks(uri)) {294const format = this.alternativeNotebookContentService.getFormat(endpoint);295const [document, notebook] = await Promise.all([296this._workspaceService.openNotebookDocumentAndSnapshot(uri, format),297this._workspaceService.openNotebookDocument(uri)298]);299const cell = findCell(documentOrCellUri, notebook);300if (cell) {301range = range ?? new Range(cell.document.lineAt(0).range.start, cell.document.lineAt(cell.document.lineCount - 1).range.end);302range = getAltNotebookRange(range, cell.document.uri, document.document, format);303} else {304range = undefined;305}306workingSet.push({307state: state,308document,309isMarkedReadonly,310range311});312} else {313workingSet.push({314state: state,315document: await this._workspaceService.openTextDocumentAndSnapshot(uri),316isMarkedReadonly,317range318});319}320}321322323} catch (err) {324return null;325}326};327328329// here we reverse to account for the UI passing the elements in reversed order330chatVariables = chatVariables.reverse();331332const promptInstructions: TextDocumentSnapshot[] = [];333334// We extract all files or selections from the chat variables335const otherChatVariables: ChatPromptReference[] = [];336for (const chatVariable of chatVariables) {337if (isInstructionFile(chatVariable) || isCustomizationsIndex(chatVariable)) {338otherChatVariables.push(chatVariable.reference);339// take a snapshot of the prompt instruction file so we know if it changed340if (isUri(chatVariable.value)) {341const textDocument = await this._workspaceService.openTextDocument(chatVariable.value);342promptInstructions.push(TextDocumentSnapshot.create(textDocument));343}344} else if (isNotebookVariable(chatVariable.value)) {345const [notebook,] = getNotebookAndCellFromUri(chatVariable.value, this._workspaceService.notebookDocuments);346if (!notebook) {347continue;348}349// No need to explicitly add the notebook to the working set, let the user do this.350if (chatVariable.value.scheme !== Schemas.vscodeNotebookCellOutput) {351await addWorkingSetEntry(notebook.uri, false);352}353if (chatVariable.value.scheme === Schemas.vscodeNotebookCellOutput) {354otherChatVariables.push(chatVariable.reference);355}356} else if (isUri(chatVariable.value)) {357await addWorkingSetEntry(chatVariable.value, chatVariable.isMarkedReadonly);358} else if (isLocation(chatVariable.value)) {359await addWorkingSetEntry(chatVariable.value.uri, chatVariable.isMarkedReadonly, chatVariable.value.range);360} else {361otherChatVariables.push(chatVariable.reference);362}363}364return {365editCodeStep: new EditCodeStep(prevStep, workingSet, promptInstructions),366chatVariables: new ChatVariablesCollection(otherChatVariables)367};368}369}370371372export function isNotebookVariable(chatVariableValue?: unknown): chatVariableValue is URI | Uri {373if (!chatVariableValue || !isUri(chatVariableValue)) {374return false;375}376return chatVariableValue.scheme === Schemas.vscodeNotebookCell || chatVariableValue.scheme === Schemas.vscodeNotebookCellOutput;377}378379interface ITextDocumentMutableWorkingSetEntry {380readonly document: TextDocumentSnapshot;381readonly range?: Range | undefined;382readonly isMarkedReadonly: boolean | undefined;383state: WorkingSetEntryState;384}385386interface INotebookMutableWorkingSetEntry {387readonly document: NotebookDocumentSnapshot;388readonly range?: Range | undefined;389readonly isMarkedReadonly: boolean | undefined;390state: WorkingSetEntryState;391}392393type IMutableWorkingSetEntry = ITextDocumentMutableWorkingSetEntry | INotebookMutableWorkingSetEntry;394395export interface IEditStepBuildPromptContext extends IBuildPromptContext {396readonly workingSet: IWorkingSet;397readonly promptInstructions: readonly TextDocumentSnapshot[];398}399400interface WorkingSetEntryDTO {401uri: URI;402text: string;403version: number;404languageId: string;405state: number;406}407408interface PromptInstructionsDTO {409uri: URI;410text: string;411version: number;412}413414interface EditHistoryDTO {415workingSet: WorkingSetEntryDTO[];416promptInstructions?: PromptInstructionsDTO[];417request: string;418response: string;419}420421function isWorkingSetEntryDTO(data: any): data is WorkingSetEntryDTO {422return data && isUriComponents(data.uri) && isString(data.text) && isNumber(data.version) && isString(data.languageId) && isNumber(data.state);423}424425function isEditHistoryDTO(data: any): data is EditHistoryDTO {426return data && Array.isArray(data.workingSet) && data.workingSet.every(isWorkingSetEntryDTO) && isString(data.request) && isString(data.response);427}428429430