Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/node/editCodeStep.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Raw } from '@vscode/prompt-tsx';
7
import type { ChatPromptReference, ChatResult } from 'vscode';
8
import { getTextPart } from '../../../platform/chat/common/globalStringUtils';
9
import { NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot';
10
import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
11
import { IChatEndpoint } from '../../../platform/networking/common/networking';
12
import { getAltNotebookRange, IAlternativeNotebookContentService } from '../../../platform/notebook/common/alternativeContent';
13
import { INotebookService } from '../../../platform/notebook/common/notebookService';
14
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
15
import { findCell, findNotebook, getNotebookAndCellFromUri } from '../../../util/common/notebooks';
16
import { isLocation, isUri } from '../../../util/common/types';
17
import { ResourceSet } from '../../../util/vs/base/common/map';
18
import { Schemas } from '../../../util/vs/base/common/network';
19
import { isEqual } from '../../../util/vs/base/common/resources';
20
import { isNumber, isString } from '../../../util/vs/base/common/types';
21
import { isUriComponents, URI } from '../../../util/vs/base/common/uri';
22
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
23
import { Range, Uri } from '../../../vscodeTypes';
24
import { ChatVariablesCollection, isCustomizationsIndex, isInstructionFile } from '../../prompt/common/chatVariablesCollection';
25
import { Turn } from '../../prompt/common/conversation';
26
import { IBuildPromptContext, IWorkingSet, WorkingSetEntryState } from '../../prompt/common/intents';
27
28
29
export class EditCodeStepTelemetryInfo {
30
public codeblockUris = new ResourceSet();
31
32
public codeblockCount: number = 0;
33
public codeblockWithUriCount: number = 0;
34
public codeblockWithElidedCodeCount: number = 0;
35
36
public shellCodeblockCount: number = 0;
37
public shellCodeblockWithUriCount: number = 0;
38
public shellCodeblockWithElidedCodeCount: number = 0;
39
}
40
41
export interface IPreviousWorkingSetEntry {
42
readonly document: { readonly uri: Uri; readonly languageId: string; readonly version: number; readonly text: string };
43
state: WorkingSetEntryState;
44
}
45
46
export interface IPreviousPromptInstruction {
47
readonly document: { readonly uri: Uri; readonly version: number; readonly text: string };
48
}
49
50
export class PreviousEditCodeStep {
51
public static fromChatResultMetaData(chatResult: ChatResult): PreviousEditCodeStep | undefined {
52
const edits = chatResult.metadata?.edits;
53
if (isEditHistoryDTO(edits)) {
54
const entries = edits.workingSet.map(entry => {
55
return {
56
document: { uri: URI.revive(entry.uri), languageId: entry.languageId, version: entry.version, text: entry.text },
57
state: entry.state,
58
} satisfies IPreviousWorkingSetEntry;
59
});
60
const promptInstructions = edits.promptInstructions?.map(entry => {
61
return {
62
document: { uri: URI.revive(entry.uri), version: entry.version, text: entry.text }
63
} satisfies IPreviousPromptInstruction;
64
}) ?? [];
65
return new PreviousEditCodeStep(entries, edits.request, edits.response, promptInstructions);
66
}
67
return undefined;
68
}
69
70
public static fromTurn(turn: Turn): PreviousEditCodeStep | undefined {
71
let editCodeStep = turn.getMetadata(EditCodeStepTurnMetaData)?.value;
72
if (!editCodeStep && turn.responseChatResult) {
73
editCodeStep = PreviousEditCodeStep.fromChatResultMetaData(turn.responseChatResult);
74
if (editCodeStep) {
75
turn.setMetadata(new EditCodeStepTurnMetaData(editCodeStep));
76
}
77
}
78
return editCodeStep;
79
}
80
81
public static fromEditCodeStep(editCodeStep: EditCodeStep) {
82
const workingSet = editCodeStep.workingSet.map(entry => ({
83
document: { uri: entry.document.uri, languageId: entry.document.languageId, version: entry.document.version, text: entry.document.getText() },
84
state: entry.state,
85
}));
86
const promptInstructions = editCodeStep.promptInstructions.map(entry => ({
87
document: { uri: entry.uri, version: entry.version, text: entry.getText() }
88
}));
89
return new PreviousEditCodeStep(workingSet, editCodeStep.userMessage, editCodeStep.assistantReply, promptInstructions);
90
}
91
92
constructor(
93
public readonly workingSet: readonly IPreviousWorkingSetEntry[],
94
public readonly request: string,
95
public readonly response: string,
96
public readonly promptInstructions: readonly IPreviousPromptInstruction[]
97
) { }
98
99
public setWorkingSetEntryState(uri: URI, state: { accepted: boolean; hasRemainingEdits: boolean }): void {
100
for (const entry of this.workingSet) {
101
if (isEqual(entry.document.uri, uri)) {
102
entry.state = this._getUpdatedState(entry, state.accepted, state.hasRemainingEdits);
103
}
104
}
105
}
106
107
private _getUpdatedState(workingSetEntry: IPreviousWorkingSetEntry, accepted: boolean, hasRemainingEdits: boolean): WorkingSetEntryState {
108
const { state } = workingSetEntry;
109
110
if (state === WorkingSetEntryState.Accepted || state === WorkingSetEntryState.Rejected) {
111
return state;
112
}
113
114
if (accepted && !hasRemainingEdits) {
115
return WorkingSetEntryState.Accepted;
116
}
117
118
if (!accepted && !hasRemainingEdits) {
119
return WorkingSetEntryState.Rejected;
120
}
121
122
// TODO: reflect partial accepts/rejects within a file when we add support for that
123
return WorkingSetEntryState.Undecided;
124
}
125
126
public toChatResultMetaData(): any {
127
const edits = {
128
workingSet: this.workingSet.map(entry => {
129
return {
130
uri: entry.document.uri,
131
text: entry.document.text,
132
languageId: entry.document.languageId,
133
version: entry.document.version,
134
state: entry.state,
135
};
136
}),
137
promptInstructions: this.promptInstructions.map(entry => ({
138
uri: entry.document.uri,
139
text: entry.document.text,
140
version: entry.document.version
141
})),
142
request: this.request,
143
response: this.response
144
} satisfies EditHistoryDTO;
145
return { edits };
146
}
147
148
}
149
150
export class EditCodeStepTurnMetaData {
151
constructor(public readonly value: PreviousEditCodeStep) {
152
}
153
}
154
155
156
export class EditCodeStep {
157
158
public static async create(instantiationService: IInstantiationService, history: readonly Turn[], chatVariables: ChatVariablesCollection, endpoint: IChatEndpoint): Promise<EditCodeStepChatVariablesPair> {
159
const factory = instantiationService.createInstance(EditCodeStepFactory);
160
return factory.createNextStep(history, chatVariables, endpoint);
161
}
162
163
/**
164
* The user message that was sent with this step
165
*/
166
private _userMessage: string = '';
167
public get userMessage(): string {
168
return this._userMessage;
169
}
170
171
/**
172
* The assistant reply that came back with this step
173
*/
174
private _assistantReply: string = '';
175
public get assistantReply(): string {
176
return this._assistantReply;
177
}
178
179
/**
180
* The working set (it is initially the list of files sent by the user).
181
* If the assistant replies with a code suggestion for a file contained here, it's status will be changed to undecided.
182
* If the assistant replies with a code suggestion for a file not contained here, the working set will not reflect this in any way.
183
* If the user makes a decision in the ui, the working set entry will update to reflect this.
184
*/
185
private readonly _workingSet: readonly IMutableWorkingSetEntry[];
186
public get workingSet(): IWorkingSet {
187
return this._workingSet;
188
}
189
190
public get promptInstructions(): readonly TextDocumentSnapshot[] {
191
return this._promptInstructions;
192
}
193
194
public readonly telemetryInfo = new EditCodeStepTelemetryInfo();
195
196
constructor(
197
public readonly previousStep: PreviousEditCodeStep | null,
198
workingSet: readonly IMutableWorkingSetEntry[],
199
private readonly _promptInstructions: TextDocumentSnapshot[]
200
) {
201
this._workingSet = workingSet;
202
}
203
204
setUserMessage(userMessage: Raw.UserChatMessage): void {
205
this._userMessage = getTextPart(userMessage.content);
206
}
207
208
setAssistantReply(reply: string): void {
209
this._assistantReply = reply;
210
}
211
212
public setWorkingSetEntryState(uri: URI, state: WorkingSetEntryState): void {
213
for (const entry of this._workingSet) {
214
if (isEqual(entry.document.uri, uri)) {
215
entry.state = state;
216
}
217
}
218
}
219
220
public getPredominantScheme(): string | undefined {
221
const schemes = new Map<string, number>();
222
for (const entry of this._workingSet) {
223
const scheme = entry.document.uri.scheme;
224
schemes.set(scheme, (schemes.get(scheme) ?? 0) + 1);
225
}
226
let maxCount = 0;
227
let maxScheme = undefined;
228
for (const [scheme, count] of schemes) {
229
if (count > maxCount) {
230
maxCount = count;
231
maxScheme = scheme;
232
}
233
}
234
return maxScheme;
235
}
236
}
237
238
interface EditCodeStepChatVariablesPair {
239
readonly editCodeStep: EditCodeStep;
240
readonly chatVariables: ChatVariablesCollection;
241
}
242
243
class EditCodeStepFactory {
244
245
constructor(
246
@IWorkspaceService private readonly _workspaceService: IWorkspaceService,
247
@INotebookService private readonly _notebookService: INotebookService,
248
@IAlternativeNotebookContentService private readonly alternativeNotebookContentService: IAlternativeNotebookContentService
249
) { }
250
251
/**
252
* Update the working set taking into account the passed in chat variables.
253
* Returns the filtered chat variables that should be used for rendering
254
*/
255
public async createNextStep(history: readonly Turn[], chatVariables: ChatVariablesCollection, endpoint: IChatEndpoint): Promise<EditCodeStepChatVariablesPair> {
256
257
const findPreviousStepEntry = () => {
258
for (let i = history.length - 1; i >= 0; i--) {
259
const entry = PreviousEditCodeStep.fromTurn(history[i]);
260
if (entry) {
261
return entry;
262
}
263
}
264
return null;
265
};
266
267
const prevStep = findPreviousStepEntry();
268
269
const workingSet: IMutableWorkingSetEntry[] = [];
270
271
const getWorkingSetEntry = (uri: Uri) => {
272
return workingSet.find(entry => isEqual(entry.document.uri, uri));
273
};
274
275
const getCurrentOrPreviousWorkingSetEntryState = (uri: Uri) => {
276
const currentEntry = getWorkingSetEntry(uri);
277
if (currentEntry) {
278
return currentEntry.state;
279
}
280
if (prevStep) {
281
const previousStepEntry = prevStep.workingSet.find(entry => isEqual(entry.document.uri, uri));
282
if (previousStepEntry) {
283
return previousStepEntry.state;
284
}
285
}
286
return WorkingSetEntryState.Initial;
287
};
288
289
const addWorkingSetEntry = async (documentOrCellUri: URI, isMarkedReadonly: boolean | undefined, range?: Range) => {
290
try {
291
const uri = this._notebookService.hasSupportedNotebooks(documentOrCellUri) ? (findNotebook(documentOrCellUri, this._workspaceService.notebookDocuments)?.uri ?? documentOrCellUri) : documentOrCellUri;
292
if (!getWorkingSetEntry(uri)) {
293
const state = getCurrentOrPreviousWorkingSetEntryState(uri);
294
if (this._notebookService.hasSupportedNotebooks(uri)) {
295
const format = this.alternativeNotebookContentService.getFormat(endpoint);
296
const [document, notebook] = await Promise.all([
297
this._workspaceService.openNotebookDocumentAndSnapshot(uri, format),
298
this._workspaceService.openNotebookDocument(uri)
299
]);
300
const cell = findCell(documentOrCellUri, notebook);
301
if (cell) {
302
range = range ?? new Range(cell.document.lineAt(0).range.start, cell.document.lineAt(cell.document.lineCount - 1).range.end);
303
range = getAltNotebookRange(range, cell.document.uri, document.document, format);
304
} else {
305
range = undefined;
306
}
307
workingSet.push({
308
state: state,
309
document,
310
isMarkedReadonly,
311
range
312
});
313
} else {
314
workingSet.push({
315
state: state,
316
document: await this._workspaceService.openTextDocumentAndSnapshot(uri),
317
isMarkedReadonly,
318
range
319
});
320
}
321
}
322
323
324
} catch (err) {
325
return null;
326
}
327
};
328
329
330
// here we reverse to account for the UI passing the elements in reversed order
331
chatVariables = chatVariables.reverse();
332
333
const promptInstructions: TextDocumentSnapshot[] = [];
334
335
// We extract all files or selections from the chat variables
336
const otherChatVariables: ChatPromptReference[] = [];
337
for (const chatVariable of chatVariables) {
338
if (isInstructionFile(chatVariable) || isCustomizationsIndex(chatVariable)) {
339
otherChatVariables.push(chatVariable.reference);
340
// take a snapshot of the prompt instruction file so we know if it changed
341
if (isUri(chatVariable.value)) {
342
const textDocument = await this._workspaceService.openTextDocument(chatVariable.value);
343
promptInstructions.push(TextDocumentSnapshot.create(textDocument));
344
}
345
} else if (isNotebookVariable(chatVariable.value)) {
346
const [notebook,] = getNotebookAndCellFromUri(chatVariable.value, this._workspaceService.notebookDocuments);
347
if (!notebook) {
348
continue;
349
}
350
// No need to explicitly add the notebook to the working set, let the user do this.
351
if (chatVariable.value.scheme !== Schemas.vscodeNotebookCellOutput) {
352
await addWorkingSetEntry(notebook.uri, false);
353
}
354
if (chatVariable.value.scheme === Schemas.vscodeNotebookCellOutput) {
355
otherChatVariables.push(chatVariable.reference);
356
}
357
} else if (isUri(chatVariable.value)) {
358
await addWorkingSetEntry(chatVariable.value, chatVariable.isMarkedReadonly);
359
} else if (isLocation(chatVariable.value)) {
360
await addWorkingSetEntry(chatVariable.value.uri, chatVariable.isMarkedReadonly, chatVariable.value.range);
361
} else {
362
otherChatVariables.push(chatVariable.reference);
363
}
364
}
365
return {
366
editCodeStep: new EditCodeStep(prevStep, workingSet, promptInstructions),
367
chatVariables: new ChatVariablesCollection(otherChatVariables)
368
};
369
}
370
}
371
372
373
export function isNotebookVariable(chatVariableValue?: unknown): chatVariableValue is URI | Uri {
374
if (!chatVariableValue || !isUri(chatVariableValue)) {
375
return false;
376
}
377
return chatVariableValue.scheme === Schemas.vscodeNotebookCell || chatVariableValue.scheme === Schemas.vscodeNotebookCellOutput;
378
}
379
380
interface ITextDocumentMutableWorkingSetEntry {
381
readonly document: TextDocumentSnapshot;
382
readonly range?: Range | undefined;
383
readonly isMarkedReadonly: boolean | undefined;
384
state: WorkingSetEntryState;
385
}
386
387
interface INotebookMutableWorkingSetEntry {
388
readonly document: NotebookDocumentSnapshot;
389
readonly range?: Range | undefined;
390
readonly isMarkedReadonly: boolean | undefined;
391
state: WorkingSetEntryState;
392
}
393
394
type IMutableWorkingSetEntry = ITextDocumentMutableWorkingSetEntry | INotebookMutableWorkingSetEntry;
395
396
export interface IEditStepBuildPromptContext extends IBuildPromptContext {
397
readonly workingSet: IWorkingSet;
398
readonly promptInstructions: readonly TextDocumentSnapshot[];
399
}
400
401
interface WorkingSetEntryDTO {
402
uri: URI;
403
text: string;
404
version: number;
405
languageId: string;
406
state: number;
407
}
408
409
interface PromptInstructionsDTO {
410
uri: URI;
411
text: string;
412
version: number;
413
}
414
415
interface EditHistoryDTO {
416
workingSet: WorkingSetEntryDTO[];
417
promptInstructions?: PromptInstructionsDTO[];
418
request: string;
419
response: string;
420
}
421
422
function isWorkingSetEntryDTO(data: any): data is WorkingSetEntryDTO {
423
return data && isUriComponents(data.uri) && isString(data.text) && isNumber(data.version) && isString(data.languageId) && isNumber(data.state);
424
}
425
426
function isEditHistoryDTO(data: any): data is EditHistoryDTO {
427
return data && Array.isArray(data.workingSet) && data.workingSet.every(isWorkingSetEntryDTO) && isString(data.request) && isString(data.response);
428
}
429
430