Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts
5258 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 { decodeHex, encodeHex, VSBuffer } from '../../../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { CancellationError } from '../../../../../base/common/errors.js';
9
import { Event } from '../../../../../base/common/event.js';
10
import { IDisposable } from '../../../../../base/common/lifecycle.js';
11
import { autorunSelfDisposable, IObservable, IReader } from '../../../../../base/common/observable.js';
12
import { hasKey } from '../../../../../base/common/types.js';
13
import { URI } from '../../../../../base/common/uri.js';
14
import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';
15
import { TextEdit } from '../../../../../editor/common/languages.js';
16
import { ITextModel } from '../../../../../editor/common/model.js';
17
import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js';
18
import { localize } from '../../../../../nls.js';
19
import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
20
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
21
import { IEditorPane } from '../../../../common/editor.js';
22
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
23
import { IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatProgress, IChatWorkspaceEdit } from '../chatService/chatService.js';
24
import { ChatModel, IChatRequestDisablement, IChatResponseModel } from '../model/chatModel.js';
25
import { IChatAgentResult } from '../participants/chatAgents.js';
26
27
export const IChatEditingService = createDecorator<IChatEditingService>('chatEditingService');
28
29
export interface IChatEditingService {
30
31
_serviceBrand: undefined;
32
33
startOrContinueGlobalEditingSession(chatModel: ChatModel): IChatEditingSession;
34
35
getEditingSession(chatSessionResource: URI): IChatEditingSession | undefined;
36
37
/**
38
* All editing sessions, sorted by recency, e.g the last created session comes first.
39
*/
40
readonly editingSessionsObs: IObservable<readonly IChatEditingSession[]>;
41
42
/**
43
* Creates a new short lived editing session
44
*/
45
createEditingSession(chatModel: ChatModel): IChatEditingSession;
46
47
/**
48
* Creates an editing session with state transferred from the provided session.
49
*/
50
transferEditingSession(chatModel: ChatModel, session: IChatEditingSession): IChatEditingSession;
51
}
52
53
export interface WorkingSetDisplayMetadata {
54
state: ModifiedFileEntryState;
55
description?: string;
56
}
57
58
export interface IStreamingEdits {
59
pushText(edits: TextEdit[], isLastEdits: boolean): void;
60
pushNotebookCellText(cell: URI, edits: TextEdit[], isLastEdits: boolean): void;
61
pushNotebook(edits: ICellEditOperation[], isLastEdits: boolean): void;
62
/** Marks edits as done, idempotent */
63
complete(): void;
64
}
65
66
export interface IModifiedEntryTelemetryInfo {
67
readonly agentId: string | undefined;
68
readonly command: string | undefined;
69
readonly sessionResource: URI;
70
readonly requestId: string;
71
readonly result: IChatAgentResult | undefined;
72
readonly modelId: string | undefined;
73
readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined;
74
readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined;
75
readonly feature: 'sideBarChat' | 'inlineChat' | undefined;
76
}
77
78
export interface ISnapshotEntry {
79
readonly resource: URI;
80
readonly languageId: string;
81
readonly snapshotUri: URI;
82
readonly original: string;
83
readonly current: string;
84
readonly state: ModifiedFileEntryState;
85
telemetryInfo: IModifiedEntryTelemetryInfo;
86
/** True if this entry represents a deleted file */
87
readonly isDeleted?: boolean;
88
}
89
90
export interface IChatEditingSession extends IDisposable {
91
readonly isGlobalEditingSession: boolean;
92
readonly chatSessionResource: URI;
93
readonly onDidDispose: Event<void>;
94
readonly state: IObservable<ChatEditingSessionState>;
95
readonly entries: IObservable<readonly IModifiedFileEntry[]>;
96
/** Requests disabled by undo/redo in the session */
97
readonly requestDisablement: IObservable<IChatRequestDisablement[]>;
98
99
show(previousChanges?: boolean): Promise<void>;
100
accept(...uris: URI[]): Promise<void>;
101
reject(...uris: URI[]): Promise<void>;
102
getEntry(uri: URI): IModifiedFileEntry | undefined;
103
readEntry(uri: URI, reader: IReader): IModifiedFileEntry | undefined;
104
105
restoreSnapshot(requestId: string, stopId: string | undefined): Promise<void>;
106
107
/**
108
* Marks all edits to the given resources as agent edits until
109
* {@link stopExternalEdits} is called with the same ID. This is used for
110
* agents that make changes on-disk rather than streaming edits through the
111
* chat session.
112
*/
113
startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise<IChatProgress[]>;
114
stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise<IChatProgress[]>;
115
116
/**
117
* Gets the snapshot URI of a file at the request and _after_ changes made in the undo stop.
118
* @param uri File in the workspace
119
*/
120
getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined;
121
122
getSnapshotContents(requestId: string, uri: URI, stopId: string | undefined): Promise<VSBuffer | undefined>;
123
getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise<ITextModel | null>;
124
125
/**
126
* Will lead to this object getting disposed
127
*/
128
stop(clearState?: boolean): Promise<void>;
129
130
/**
131
* Starts making edits to the resource.
132
* @param resource URI that's being edited
133
* @param responseModel The response model making the edits
134
* @param inUndoStop The undo stop the edits will be grouped in
135
*/
136
startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits;
137
138
/**
139
* Applies a workspace edit (file deletions, creations, renames).
140
* @param edit The workspace edit containing file operations
141
* @param responseModel The response model making the edit
142
* @param undoStopId The undo stop ID for this edit
143
*/
144
applyWorkspaceEdit(edit: IChatWorkspaceEdit, responseModel: IChatResponseModel, undoStopId: string): void;
145
146
/**
147
* Gets the document diff of a change made to a URI between one undo stop and
148
* the next one.
149
* @returns The observable or undefined if there is no diff between the stops.
150
*/
151
getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable<IEditSessionEntryDiff | undefined> | undefined;
152
153
/**
154
* Gets the document diff of a change made to a URI between one request to another one.
155
* @returns The observable or undefined if there is no diff between the requests.
156
*/
157
getEntryDiffBetweenRequests(uri: URI, startRequestIs: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined>;
158
159
/**
160
* Gets the diff of each file modified in this session, comparing the initial
161
* baseline to the current state.
162
*/
163
getDiffsForFilesInSession(): IObservable<readonly IEditSessionEntryDiff[]>;
164
165
/**
166
* Gets the diff of each file modified in the request.
167
*/
168
getDiffsForFilesInRequest(requestId: string): IObservable<readonly IEditSessionEntryDiff[]>;
169
170
/**
171
* Whether there are any edits made in the given request.
172
*/
173
hasEditsInRequest(requestId: string, reader?: IReader): boolean;
174
175
/**
176
* Gets the aggregated diff stats for all files modified in this session.
177
*/
178
getDiffForSession(): IObservable<IEditSessionDiffStats>;
179
180
readonly canUndo: IObservable<boolean>;
181
readonly canRedo: IObservable<boolean>;
182
undoInteraction(): Promise<void>;
183
redoInteraction(): Promise<void>;
184
185
/**
186
* Triggers generation of explanations for all modified files in the session.
187
*/
188
triggerExplanationGeneration(): Promise<void>;
189
190
/**
191
* Clears any active explanation generation.
192
*/
193
clearExplanations(): void;
194
195
/**
196
* Whether explanations are currently being generated or displayed.
197
*/
198
hasExplanations(): boolean;
199
}
200
201
export function chatEditingSessionIsReady(session: IChatEditingSession): Promise<void> {
202
return new Promise<void>(resolve => {
203
autorunSelfDisposable(reader => {
204
const state = session.state.read(reader);
205
if (state !== ChatEditingSessionState.Initial) {
206
reader.dispose();
207
resolve();
208
}
209
});
210
});
211
}
212
213
export function editEntriesToMultiDiffData(entriesObs: IObservable<readonly IEditSessionEntryDiff[]>): IChatMultiDiffData {
214
const multiDiffData = entriesObs.map(entries => ({
215
title: localize('chatMultidiff.autoGenerated', 'Changes to {0} files', entries.length),
216
resources: entries.map(entry => ({
217
originalUri: entry.originalURI,
218
modifiedUri: entry.modifiedURI,
219
goToFileUri: entry.modifiedURI,
220
added: entry.added,
221
removed: entry.removed,
222
}))
223
}));
224
225
return {
226
kind: 'multiDiffData',
227
collapsed: true,
228
multiDiffData,
229
toJSON(): IChatMultiDiffDataSerialized {
230
return {
231
kind: 'multiDiffData',
232
collapsed: this.collapsed,
233
multiDiffData: multiDiffData.get(),
234
};
235
}
236
};
237
}
238
239
export function awaitCompleteChatEditingDiff(diff: IObservable<IEditSessionEntryDiff>, token?: CancellationToken): Promise<IEditSessionEntryDiff>;
240
export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[]>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[]>;
241
export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff> {
242
return new Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>((resolve, reject) => {
243
autorunSelfDisposable(reader => {
244
if (token) {
245
if (token.isCancellationRequested) {
246
reader.dispose();
247
return reject(new CancellationError());
248
}
249
reader.store.add(token.onCancellationRequested(() => {
250
reader.dispose();
251
reject(new CancellationError());
252
}));
253
}
254
255
const current = diff.read(reader);
256
if (current instanceof Array) {
257
if (!current.some(c => c.isBusy)) {
258
reader.dispose();
259
resolve(current);
260
}
261
} else if (!current.isBusy) {
262
reader.dispose();
263
resolve(current);
264
}
265
});
266
});
267
}
268
269
export interface IEditSessionDiffStats {
270
/** Added data (e.g. line numbers) to show in the UI */
271
added: number;
272
/** Removed data (e.g. line numbers) to show in the UI */
273
removed: number;
274
}
275
276
export interface IEditSessionEntryDiff extends IEditSessionDiffStats {
277
/** LHS and RHS of a diff editor, if opened: */
278
originalURI: URI;
279
modifiedURI: URI;
280
281
/** Diff state information: */
282
quitEarly: boolean;
283
identical: boolean;
284
285
/** True if nothing else will be added to this diff. */
286
isFinal: boolean;
287
288
/** True if the diff is currently being computed or updated. */
289
isBusy: boolean;
290
}
291
292
export function emptySessionEntryDiff(originalURI: URI, modifiedURI: URI): IEditSessionEntryDiff {
293
return {
294
originalURI,
295
modifiedURI,
296
added: 0,
297
removed: 0,
298
quitEarly: false,
299
identical: false,
300
isFinal: false,
301
isBusy: false,
302
};
303
}
304
305
export const enum ModifiedFileEntryState {
306
Modified,
307
Accepted,
308
Rejected,
309
}
310
311
/**
312
* Represents a part of a change
313
*/
314
export interface IModifiedFileEntryChangeHunk {
315
accept(): Promise<boolean>;
316
reject(): Promise<boolean>;
317
}
318
319
export interface IModifiedFileEntryEditorIntegration extends IDisposable {
320
321
/**
322
* The index of a change
323
*/
324
currentIndex: IObservable<number>;
325
326
/**
327
* Reveal the first (`true`) or last (`false`) change
328
*/
329
reveal(firstOrLast: boolean, preserveFocus?: boolean): void;
330
331
/**
332
* Go to next change and increate `currentIndex`
333
* @param wrap When at the last, start over again or not
334
* @returns If it went next
335
*/
336
next(wrap: boolean): boolean;
337
338
/**
339
* @see `next`
340
*/
341
previous(wrap: boolean): boolean;
342
343
/**
344
* Enable the accessible diff viewer for this editor
345
*/
346
enableAccessibleDiffView(): void;
347
348
/**
349
* Accept the change given or the nearest
350
* @param change An opaque change object
351
*/
352
acceptNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;
353
354
/**
355
* @see `acceptNearestChange`
356
*/
357
rejectNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;
358
359
/**
360
* Toggle between diff-editor and normal editor
361
* @param change An opaque change object
362
* @param show Optional boolean to control if the diff should show
363
*/
364
toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise<void>;
365
}
366
367
export interface IModifiedFileEntry {
368
readonly entryId: string;
369
readonly originalURI: URI;
370
readonly modifiedURI: URI;
371
readonly isDeletion?: boolean;
372
373
readonly lastModifyingRequestId: string;
374
375
readonly state: IObservable<ModifiedFileEntryState>;
376
readonly isCurrentlyBeingModifiedBy: IObservable<{ responseModel: IChatResponseModel; undoStopId: string | undefined } | undefined>;
377
readonly lastModifyingResponse: IObservable<IChatResponseModel | undefined>;
378
readonly rewriteRatio: IObservable<number>;
379
380
readonly waitsForLastEdits: IObservable<boolean>;
381
382
accept(): Promise<void>;
383
reject(): Promise<void>;
384
385
reviewMode: IObservable<boolean>;
386
autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>;
387
enableReviewModeUntilSettled(): void;
388
389
/**
390
* Number of changes for this file
391
*/
392
readonly changesCount: IObservable<number>;
393
394
/**
395
* Diff information for this entry
396
*/
397
readonly diffInfo?: IObservable<IDocumentDiff>;
398
399
/**
400
* Number of lines added in this entry.
401
*/
402
readonly linesAdded?: IObservable<number>;
403
404
/**
405
* Number of lines removed in this entry
406
*/
407
readonly linesRemoved?: IObservable<number>;
408
409
getEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration;
410
/**
411
* Gets the document diff info, waiting for any ongoing promises to flush.
412
*/
413
getDiffInfo?(): Promise<IDocumentDiff>;
414
}
415
416
export interface IChatEditingSessionStream {
417
textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void;
418
notebookEdits(resource: URI, edits: ICellEditOperation[], isLastEdits: boolean, responseModel: IChatResponseModel): void;
419
}
420
421
export const enum ChatEditingSessionState {
422
Initial = 0,
423
StreamingEdits = 1,
424
Idle = 2,
425
Disposed = 3
426
}
427
428
export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source';
429
430
export const chatEditingWidgetFileStateContextKey = new RawContextKey<ModifiedFileEntryState>('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget"));
431
export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey<boolean>('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)"));
432
export const decidedChatEditingResourceContextKey = new RawContextKey<string[]>('decidedChatEditingResource', []);
433
export const chatEditingResourceContextKey = new RawContextKey<string | undefined>('chatEditingResource', undefined);
434
export const inChatEditingSessionContextKey = new RawContextKey<boolean | undefined>('inChatEditingSession', undefined);
435
export const hasUndecidedChatEditingResourceContextKey = new RawContextKey<boolean | undefined>('hasUndecidedChatEditingResource', false);
436
export const hasAppliedChatEditsContextKey = new RawContextKey<boolean | undefined>('hasAppliedChatEdits', false);
437
export const applyingChatEditsFailedContextKey = new RawContextKey<boolean | undefined>('applyingChatEditsFailed', false);
438
439
export const chatEditingMaxFileAssignmentName = 'chatEditingSessionFileLimit';
440
export const defaultChatEditingMaxFileLimit = 10;
441
442
export const enum ChatEditKind {
443
Created,
444
Modified,
445
Deleted,
446
}
447
448
export interface IChatEditingActionContext {
449
// The chat session that this editing session is associated with
450
sessionResource: URI;
451
}
452
453
export function isChatEditingActionContext(thing: unknown): thing is IChatEditingActionContext {
454
return typeof thing === 'object' && !!thing && hasKey(thing, { sessionResource: true });
455
}
456
457
export function getMultiDiffSourceUri(session: IChatEditingSession, showPreviousChanges?: boolean): URI {
458
return URI.from({
459
scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME,
460
authority: encodeHex(VSBuffer.fromString(session.chatSessionResource.toString())),
461
query: showPreviousChanges ? 'previous' : undefined,
462
});
463
}
464
465
export function parseChatMultiDiffUri(uri: URI): { chatSessionResource: URI; showPreviousChanges: boolean } {
466
const chatSessionResource = URI.parse(decodeHex(uri.authority).toString());
467
const showPreviousChanges = uri.query === 'previous';
468
469
return { chatSessionResource, showPreviousChanges };
470
}
471
472