Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts
3296 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 { DeferredPromise, ITask, Sequencer, SequencerByKey, timeout } from '../../../../../base/common/async.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { BugIndicatingError } from '../../../../../base/common/errors.js';
9
import { Emitter } from '../../../../../base/common/event.js';
10
import { Iterable } from '../../../../../base/common/iterator.js';
11
import { Disposable, dispose } from '../../../../../base/common/lifecycle.js';
12
import { ResourceMap } from '../../../../../base/common/map.js';
13
import { autorun, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js';
14
import { isEqual } from '../../../../../base/common/resources.js';
15
import { URI } from '../../../../../base/common/uri.js';
16
import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js';
17
import { TextEdit } from '../../../../../editor/common/languages.js';
18
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
19
import { ITextModel } from '../../../../../editor/common/model.js';
20
import { IModelService } from '../../../../../editor/common/services/model.js';
21
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
22
import { localize } from '../../../../../nls.js';
23
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
24
import { EditorActivation } from '../../../../../platform/editor/common/editor.js';
25
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
26
import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js';
27
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
28
import { IEditorService } from '../../../../services/editor/common/editorService.js';
29
import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js';
30
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
31
import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
32
import { INotebookService } from '../../../notebook/common/notebookService.js';
33
import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js';
34
import { IChatResponseModel } from '../../common/chatModel.js';
35
import { IChatService } from '../../common/chatService.js';
36
import { ChatAgentLocation } from '../../common/constants.js';
37
import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js';
38
import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';
39
import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js';
40
import { ChatEditingSessionStorage, IChatEditingSessionSnapshot, IChatEditingSessionStop, StoredSessionState } from './chatEditingSessionStorage.js';
41
import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
42
import { ChatEditingTimeline } from './chatEditingTimeline.js';
43
44
const enum NotExistBehavior {
45
Create,
46
Abort,
47
}
48
49
class ThrottledSequencer extends Sequencer {
50
51
private _size = 0;
52
53
constructor(
54
private readonly _minDuration: number,
55
private readonly _maxOverallDelay: number
56
) {
57
super();
58
}
59
60
override queue<T>(promiseTask: ITask<Promise<T>>): Promise<T> {
61
62
this._size += 1;
63
64
const noDelay = this._size * this._minDuration > this._maxOverallDelay;
65
66
return super.queue(async () => {
67
try {
68
const p1 = promiseTask();
69
const p2 = noDelay
70
? Promise.resolve(undefined)
71
: timeout(this._minDuration, CancellationToken.None);
72
73
const [result] = await Promise.all([p1, p2]);
74
return result;
75
76
} finally {
77
this._size -= 1;
78
}
79
});
80
}
81
}
82
83
function getCurrentAndNextStop(requestId: string, stopId: string | undefined, history: readonly IChatEditingSessionSnapshot[]) {
84
const snapshotIndex = history.findIndex(s => s.requestId === requestId);
85
if (snapshotIndex === -1) { return undefined; }
86
const snapshot = history[snapshotIndex];
87
const stopIndex = snapshot.stops.findIndex(s => s.stopId === stopId);
88
if (stopIndex === -1) { return undefined; }
89
90
const current = snapshot.stops[stopIndex].entries;
91
const next = stopIndex < snapshot.stops.length - 1
92
? snapshot.stops[stopIndex + 1].entries
93
: history[snapshotIndex + 1]?.stops[0].entries;
94
95
96
if (!next) {
97
return undefined;
98
}
99
100
return { current, next };
101
}
102
103
export class ChatEditingSession extends Disposable implements IChatEditingSession {
104
private readonly _state = observableValue<ChatEditingSessionState>(this, ChatEditingSessionState.Initial);
105
private readonly _timeline: ChatEditingTimeline;
106
107
/**
108
* Contains the contents of a file when the AI first began doing edits to it.
109
*/
110
private readonly _initialFileContents = new ResourceMap<string>();
111
112
private readonly _entriesObs = observableValue<readonly AbstractChatEditingModifiedFileEntry[]>(this, []);
113
public get entries(): IObservable<readonly IModifiedFileEntry[]> {
114
this._assertNotDisposed();
115
return this._entriesObs;
116
}
117
118
private _editorPane: MultiDiffEditor | undefined;
119
120
get state(): IObservable<ChatEditingSessionState> {
121
return this._state;
122
}
123
124
public readonly canUndo: IObservable<boolean>;
125
public readonly canRedo: IObservable<boolean>;
126
127
private readonly _onDidDispose = new Emitter<void>();
128
get onDidDispose() {
129
this._assertNotDisposed();
130
return this._onDidDispose.event;
131
}
132
133
constructor(
134
readonly chatSessionId: string,
135
readonly isGlobalEditingSession: boolean,
136
private _lookupExternalEntry: (uri: URI) => AbstractChatEditingModifiedFileEntry | undefined,
137
@IInstantiationService private readonly _instantiationService: IInstantiationService,
138
@IModelService private readonly _modelService: IModelService,
139
@ILanguageService private readonly _languageService: ILanguageService,
140
@ITextModelService private readonly _textModelService: ITextModelService,
141
@IBulkEditService public readonly _bulkEditService: IBulkEditService,
142
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
143
@IEditorService private readonly _editorService: IEditorService,
144
@IChatService private readonly _chatService: IChatService,
145
@INotebookService private readonly _notebookService: INotebookService,
146
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,
147
) {
148
super();
149
this._timeline = _instantiationService.createInstance(ChatEditingTimeline);
150
this.canRedo = this._timeline.canRedo.map((hasHistory, reader) =>
151
hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle);
152
this.canUndo = this._timeline.canUndo.map((hasHistory, reader) =>
153
hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle);
154
155
this._register(autorun(reader => {
156
const disabled = this._timeline.requestDisablement.read(reader);
157
this._chatService.getSession(this.chatSessionId)?.setDisabledRequests(disabled);
158
}));
159
}
160
161
public async init(): Promise<void> {
162
const restoredSessionState = await this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId).restoreState();
163
if (restoredSessionState) {
164
for (const [uri, content] of restoredSessionState.initialFileContents) {
165
this._initialFileContents.set(uri, content);
166
}
167
await this._restoreSnapshot(restoredSessionState.recentSnapshot, false);
168
transaction(tx => {
169
this._pendingSnapshot.set(restoredSessionState.pendingSnapshot, tx);
170
this._timeline.restoreFromState({ history: restoredSessionState.linearHistory, index: restoredSessionState.linearHistoryIndex }, tx);
171
this._state.set(ChatEditingSessionState.Idle, tx);
172
});
173
} else {
174
this._state.set(ChatEditingSessionState.Idle, undefined);
175
}
176
177
this._register(autorun(reader => {
178
const entries = this.entries.read(reader);
179
entries.forEach(entry => {
180
entry.state.read(reader);
181
});
182
}));
183
}
184
185
private _getEntry(uri: URI): AbstractChatEditingModifiedFileEntry | undefined {
186
uri = CellUri.parse(uri)?.notebook ?? uri;
187
return this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri));
188
}
189
190
public getEntry(uri: URI): IModifiedFileEntry | undefined {
191
return this._getEntry(uri);
192
}
193
194
public readEntry(uri: URI, reader: IReader | undefined): IModifiedFileEntry | undefined {
195
uri = CellUri.parse(uri)?.notebook ?? uri;
196
return this._entriesObs.read(reader).find(e => isEqual(e.modifiedURI, uri));
197
}
198
199
public storeState(): Promise<void> {
200
const storage = this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId);
201
const timelineState = this._timeline.getStateForPersistence();
202
const state: StoredSessionState = {
203
initialFileContents: this._initialFileContents,
204
pendingSnapshot: this._pendingSnapshot.get(),
205
recentSnapshot: this._createSnapshot(undefined, undefined),
206
linearHistoryIndex: timelineState.index,
207
linearHistory: timelineState.history,
208
};
209
return storage.storeState(state);
210
}
211
212
private _ensurePendingSnapshot() {
213
const prev = this._pendingSnapshot.get();
214
if (!prev) {
215
this._pendingSnapshot.set(this._createSnapshot(undefined, undefined), undefined);
216
}
217
}
218
219
public getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined) {
220
return this._timeline.getEntryDiffBetweenStops(uri, requestId, stopId);
221
}
222
223
public getEntryDiffBetweenRequests(uri: URI, startRequestId: string, stopRequestId: string) {
224
return this._timeline.getEntryDiffBetweenRequests(uri, startRequestId, stopRequestId);
225
}
226
227
public createSnapshot(requestId: string, undoStop: string | undefined, makeEmpty = undoStop !== undefined): void {
228
this._timeline.pushSnapshot(
229
requestId,
230
undoStop,
231
makeEmpty ? ChatEditingTimeline.createEmptySnapshot(undoStop) : this._createSnapshot(requestId, undoStop),
232
);
233
}
234
235
private _createSnapshot(requestId: string | undefined, stopId: string | undefined): IChatEditingSessionStop {
236
const entries = new ResourceMap<ISnapshotEntry>();
237
for (const entry of this._entriesObs.get()) {
238
entries.set(entry.modifiedURI, entry.createSnapshot(requestId, stopId));
239
}
240
return { stopId, entries };
241
}
242
243
public getSnapshot(requestId: string, undoStop: string | undefined, snapshotUri: URI): ISnapshotEntry | undefined {
244
const stopRef = this._timeline.getSnapshotForRestore(requestId, undoStop);
245
const entries = stopRef?.stop.entries;
246
return entries && [...entries.values()].find((e) => isEqual(e.snapshotUri, snapshotUri));
247
}
248
249
public async getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise<ITextModel | null> {
250
const snapshotEntry = this.getSnapshot(requestId, undoStop, snapshotUri);
251
if (!snapshotEntry) {
252
return null;
253
}
254
255
return this._modelService.createModel(snapshotEntry.current, this._languageService.createById(snapshotEntry.languageId), snapshotUri, false);
256
}
257
258
public getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined {
259
// This should be encapsulated in the timeline, but for now, fallback to legacy logic if needed.
260
// TODO: Move this logic into a timeline method if required by the design.
261
const timelineState = this._timeline.getStateForPersistence();
262
const stops = getCurrentAndNextStop(requestId, stopId, timelineState.history);
263
return stops?.next.get(uri)?.snapshotUri;
264
}
265
266
/**
267
* A snapshot representing the state of the working set before a new request has been sent
268
*/
269
private _pendingSnapshot = observableValue<IChatEditingSessionStop | undefined>(this, undefined);
270
271
public async restoreSnapshot(requestId: string | undefined, stopId: string | undefined): Promise<void> {
272
if (requestId !== undefined) {
273
const stopRef = this._timeline.getSnapshotForRestore(requestId, stopId);
274
if (stopRef) {
275
this._ensurePendingSnapshot();
276
await this._restoreSnapshot(stopRef.stop);
277
stopRef.apply();
278
}
279
} else {
280
const pendingSnapshot = this._pendingSnapshot.get();
281
if (!pendingSnapshot) {
282
return; // We don't have a pending snapshot that we can restore
283
}
284
this._pendingSnapshot.set(undefined, undefined);
285
await this._restoreSnapshot(pendingSnapshot, undefined);
286
}
287
}
288
289
private async _restoreSnapshot({ entries }: IChatEditingSessionStop, restoreResolvedToDisk = true): Promise<void> {
290
291
// Reset all the files which are modified in this session state
292
// but which are not found in the snapshot
293
for (const entry of this._entriesObs.get()) {
294
const snapshotEntry = entries.get(entry.modifiedURI);
295
if (!snapshotEntry) {
296
await entry.resetToInitialContent();
297
entry.dispose();
298
}
299
}
300
301
const entriesArr: AbstractChatEditingModifiedFileEntry[] = [];
302
// Restore all entries from the snapshot
303
for (const snapshotEntry of entries.values()) {
304
const entry = await this._getOrCreateModifiedFileEntry(snapshotEntry.resource, restoreResolvedToDisk ? NotExistBehavior.Create : NotExistBehavior.Abort, snapshotEntry.telemetryInfo);
305
if (entry) {
306
const restoreToDisk = snapshotEntry.state === ModifiedFileEntryState.Modified || restoreResolvedToDisk;
307
await entry.restoreFromSnapshot(snapshotEntry, restoreToDisk);
308
entriesArr.push(entry);
309
}
310
}
311
312
this._entriesObs.set(entriesArr, undefined);
313
}
314
315
private _assertNotDisposed(): void {
316
if (this._state.get() === ChatEditingSessionState.Disposed) {
317
throw new BugIndicatingError(`Cannot access a disposed editing session`);
318
}
319
}
320
321
async accept(...uris: URI[]): Promise<void> {
322
this._assertNotDisposed();
323
324
if (uris.length === 0) {
325
await Promise.all(this._entriesObs.get().map(entry => entry.accept()));
326
}
327
328
for (const uri of uris) {
329
const entry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri));
330
if (entry) {
331
await entry.accept();
332
}
333
}
334
this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true });
335
}
336
337
async reject(...uris: URI[]): Promise<void> {
338
this._assertNotDisposed();
339
340
if (uris.length === 0) {
341
await Promise.all(this._entriesObs.get().map(entry => entry.reject()));
342
}
343
344
for (const uri of uris) {
345
const entry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri));
346
if (entry) {
347
await entry.reject();
348
}
349
}
350
this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true });
351
}
352
353
async show(previousChanges?: boolean): Promise<void> {
354
this._assertNotDisposed();
355
if (this._editorPane) {
356
if (this._editorPane.isVisible()) {
357
return;
358
} else if (this._editorPane.input) {
359
await this._editorGroupsService.activeGroup.openEditor(this._editorPane.input, { pinned: true, activation: EditorActivation.ACTIVATE });
360
return;
361
}
362
}
363
const input = MultiDiffEditorInput.fromResourceMultiDiffEditorInput({
364
multiDiffSource: getMultiDiffSourceUri(this, previousChanges),
365
label: localize('multiDiffEditorInput.name', "Suggested Edits")
366
}, this._instantiationService);
367
368
this._editorPane = await this._editorGroupsService.activeGroup.openEditor(input, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined;
369
}
370
371
private _stopPromise: Promise<void> | undefined;
372
373
async stop(clearState = false): Promise<void> {
374
this._stopPromise ??= Promise.allSettled([this._performStop(), this.storeState()]).then(() => { });
375
await this._stopPromise;
376
if (clearState) {
377
await this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId).clearState();
378
}
379
}
380
381
private async _performStop(): Promise<void> {
382
// Close out all open files
383
const schemes = [AbstractChatEditingModifiedFileEntry.scheme, ChatEditingTextModelContentProvider.scheme];
384
await Promise.allSettled(this._editorGroupsService.groups.flatMap(async (g) => {
385
return g.editors.map(async (e) => {
386
if ((e instanceof MultiDiffEditorInput && e.initialResources?.some(r => r.originalUri && schemes.indexOf(r.originalUri.scheme) !== -1))
387
|| (e instanceof DiffEditorInput && e.original.resource && schemes.indexOf(e.original.resource.scheme) !== -1)) {
388
await g.closeEditor(e);
389
}
390
});
391
}));
392
}
393
394
override dispose() {
395
this._assertNotDisposed();
396
397
this._chatService.cancelCurrentRequestForSession(this.chatSessionId);
398
399
dispose(this._entriesObs.get());
400
super.dispose();
401
this._state.set(ChatEditingSessionState.Disposed, undefined);
402
this._onDidDispose.fire();
403
this._onDidDispose.dispose();
404
}
405
406
private _streamingEditLocks = new SequencerByKey</* URI */ string>();
407
408
private get isDisposed() {
409
return this._state.get() === ChatEditingSessionState.Disposed;
410
}
411
412
startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits {
413
const completePromise = new DeferredPromise<void>();
414
const startPromise = new DeferredPromise<void>();
415
416
// Sequence all edits made this this resource in this streaming edits instance,
417
// and also sequence the resource overall in the rare (currently invalid?) case
418
// that edits are made in parallel to the same resource,
419
const sequencer = new ThrottledSequencer(15, 1000);
420
sequencer.queue(() => startPromise.p);
421
422
this._streamingEditLocks.queue(resource.toString(), async () => {
423
if (!this.isDisposed) {
424
await this._acceptStreamingEditsStart(responseModel, inUndoStop, resource);
425
}
426
427
startPromise.complete();
428
return completePromise.p;
429
});
430
431
432
let didComplete = false;
433
434
return {
435
pushText: (edits, isLastEdits) => {
436
sequencer.queue(async () => {
437
if (!this.isDisposed) {
438
await this._acceptEdits(resource, edits, isLastEdits, responseModel);
439
}
440
});
441
},
442
pushNotebookCellText: (cell, edits, isLastEdits) => {
443
sequencer.queue(async () => {
444
if (!this.isDisposed) {
445
await this._acceptEdits(cell, edits, isLastEdits, responseModel);
446
}
447
});
448
},
449
pushNotebook: (edits, isLastEdits) => {
450
sequencer.queue(async () => {
451
if (!this.isDisposed) {
452
await this._acceptEdits(resource, edits, isLastEdits, responseModel);
453
}
454
});
455
},
456
complete: () => {
457
if (didComplete) {
458
return;
459
}
460
461
didComplete = true;
462
sequencer.queue(async () => {
463
if (!this.isDisposed) {
464
await this._acceptEdits(resource, [], true, responseModel);
465
await this._resolve(responseModel.requestId, inUndoStop, resource);
466
completePromise.complete();
467
}
468
});
469
},
470
};
471
}
472
473
async undoInteraction(): Promise<void> {
474
const undo = this._timeline.getUndoSnapshot();
475
if (!undo) {
476
return;
477
}
478
this._ensurePendingSnapshot();
479
await this._restoreSnapshot(undo.stop);
480
undo.apply();
481
}
482
483
async redoInteraction(): Promise<void> {
484
const redo = this._timeline.getRedoSnapshot();
485
const nextSnapshot = redo?.stop || this._pendingSnapshot.get();
486
if (!nextSnapshot) {
487
return;
488
}
489
await this._restoreSnapshot(nextSnapshot);
490
if (redo) {
491
redo.apply();
492
} else {
493
this._pendingSnapshot.set(undefined, undefined);
494
}
495
}
496
497
private async _acceptStreamingEditsStart(responseModel: IChatResponseModel, undoStop: string | undefined, resource: URI) {
498
const entry = await this._getOrCreateModifiedFileEntry(resource, NotExistBehavior.Create, this._getTelemetryInfoForModel(responseModel));
499
transaction((tx) => {
500
this._state.set(ChatEditingSessionState.StreamingEdits, tx);
501
entry.acceptStreamingEditsStart(responseModel, tx);
502
this._timeline.ensureEditInUndoStopMatches(responseModel.requestId, undoStop, entry, false, tx);
503
});
504
}
505
506
private async _acceptEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<void> {
507
const entry = await this._getOrCreateModifiedFileEntry(resource, NotExistBehavior.Create, this._getTelemetryInfoForModel(responseModel));
508
await entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel);
509
}
510
511
private _getTelemetryInfoForModel(responseModel: IChatResponseModel): IModifiedEntryTelemetryInfo {
512
// Make these getters because the response result is not available when the file first starts to be edited
513
return new class implements IModifiedEntryTelemetryInfo {
514
get agentId() { return responseModel.agent?.id; }
515
get modelId() { return responseModel.request?.modelId; }
516
get modeId() { return responseModel.request?.modeInfo?.modeId; }
517
get command() { return responseModel.slashCommand?.name; }
518
get sessionId() { return responseModel.session.sessionId; }
519
get requestId() { return responseModel.requestId; }
520
get result() { return responseModel.result; }
521
get applyCodeBlockSuggestionId() { return responseModel.request?.modeInfo?.applyCodeBlockSuggestionId; }
522
523
get feature(): string {
524
if (responseModel.session.initialLocation === ChatAgentLocation.Panel) {
525
return 'sideBarChat';
526
} else if (responseModel.session.initialLocation === ChatAgentLocation.Editor) {
527
return 'inlineChat';
528
}
529
return responseModel.session.initialLocation;
530
}
531
};
532
}
533
534
private async _resolve(requestId: string, undoStop: string | undefined, resource: URI): Promise<void> {
535
const hasOtherTasks = Iterable.some(this._streamingEditLocks.keys(), k => k !== resource.toString());
536
if (!hasOtherTasks) {
537
this._state.set(ChatEditingSessionState.Idle, undefined);
538
}
539
540
const entry = this._getEntry(resource);
541
if (!entry) {
542
return;
543
}
544
545
this._timeline.ensureEditInUndoStopMatches(requestId, undoStop, entry, /* next= */ true, undefined);
546
return entry.acceptStreamingEditsEnd();
547
548
}
549
550
/**
551
* Retrieves or creates a modified file entry.
552
*
553
* @returns The modified file entry.
554
*/
555
private async _getOrCreateModifiedFileEntry(resource: URI, ifNotExists: NotExistBehavior.Create, telemetryInfo: IModifiedEntryTelemetryInfo): Promise<AbstractChatEditingModifiedFileEntry>;
556
private async _getOrCreateModifiedFileEntry(resource: URI, ifNotExists: NotExistBehavior, telemetryInfo: IModifiedEntryTelemetryInfo): Promise<AbstractChatEditingModifiedFileEntry | undefined>;
557
private async _getOrCreateModifiedFileEntry(resource: URI, ifNotExists: NotExistBehavior, telemetryInfo: IModifiedEntryTelemetryInfo): Promise<AbstractChatEditingModifiedFileEntry | undefined> {
558
559
resource = CellUri.parse(resource)?.notebook ?? resource;
560
561
const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource));
562
if (existingEntry) {
563
if (telemetryInfo.requestId !== existingEntry.telemetryInfo.requestId) {
564
existingEntry.updateTelemetryInfo(telemetryInfo);
565
}
566
return existingEntry;
567
}
568
569
let entry: AbstractChatEditingModifiedFileEntry;
570
const existingExternalEntry = this._lookupExternalEntry(resource);
571
if (existingExternalEntry) {
572
entry = existingExternalEntry;
573
574
if (telemetryInfo.requestId !== entry.telemetryInfo.requestId) {
575
entry.updateTelemetryInfo(telemetryInfo);
576
}
577
} else {
578
const initialContent = this._initialFileContents.get(resource);
579
// This gets manually disposed in .dispose() or in .restoreSnapshot()
580
const maybeEntry = await this._createModifiedFileEntry(resource, telemetryInfo, ifNotExists, initialContent);
581
if (!maybeEntry) {
582
return undefined;
583
}
584
entry = maybeEntry;
585
if (!initialContent) {
586
this._initialFileContents.set(resource, entry.initialContent);
587
}
588
}
589
590
// If an entry is deleted e.g. reverting a created file,
591
// remove it from the entries and don't show it in the working set anymore
592
// so that it can be recreated e.g. through retry
593
const listener = entry.onDidDelete(() => {
594
const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, entry.modifiedURI));
595
this._entriesObs.set(newEntries, undefined);
596
this._editorService.closeEditors(this._editorService.findEditors(entry.modifiedURI));
597
598
if (!existingExternalEntry) {
599
// don't dispose entries that are not yours!
600
entry.dispose();
601
}
602
603
this._store.delete(listener);
604
});
605
this._store.add(listener);
606
607
const entriesArr = [...this._entriesObs.get(), entry];
608
this._entriesObs.set(entriesArr, undefined);
609
610
return entry;
611
}
612
613
private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, ifNotExists: NotExistBehavior.Create, initialContent: string | undefined): Promise<AbstractChatEditingModifiedFileEntry>;
614
private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, ifNotExists: NotExistBehavior, initialContent: string | undefined): Promise<AbstractChatEditingModifiedFileEntry | undefined>;
615
616
private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, ifNotExists: NotExistBehavior, initialContent: string | undefined): Promise<AbstractChatEditingModifiedFileEntry | undefined> {
617
const multiDiffEntryDelegate = { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) };
618
const notebookUri = CellUri.parse(resource)?.notebook || resource;
619
const doCreate = async (chatKind: ChatEditKind) => {
620
if (this._notebookService.hasSupportedNotebooks(notebookUri)) {
621
return await ChatEditingModifiedNotebookEntry.create(notebookUri, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent, this._instantiationService);
622
} else {
623
const ref = await this._textModelService.createModelReference(resource);
624
return this._instantiationService.createInstance(ChatEditingModifiedDocumentEntry, ref, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent);
625
}
626
};
627
628
try {
629
return await doCreate(ChatEditKind.Modified);
630
} catch (err) {
631
if (ifNotExists === NotExistBehavior.Abort) {
632
return undefined;
633
}
634
635
// this file does not exist yet, create it and try again
636
await this._bulkEditService.apply({ edits: [{ newResource: resource }] });
637
this._editorService.openEditor({ resource, options: { inactive: true, preserveFocus: true, pinned: true } });
638
639
if (this._notebookService.hasSupportedNotebooks(notebookUri)) {
640
return await ChatEditingModifiedNotebookEntry.create(resource, multiDiffEntryDelegate, telemetryInfo, ChatEditKind.Created, initialContent, this._instantiationService);
641
} else {
642
return await doCreate(ChatEditKind.Created);
643
}
644
}
645
}
646
647
private _collapse(resource: URI, transaction: ITransaction | undefined) {
648
const multiDiffItem = this._editorPane?.findDocumentDiffItem(resource);
649
if (multiDiffItem) {
650
this._editorPane?.viewModel?.items.get().find((documentDiffItem) =>
651
isEqual(documentDiffItem.originalUri, multiDiffItem.originalUri) &&
652
isEqual(documentDiffItem.modifiedUri, multiDiffItem.modifiedUri))
653
?.collapsed.set(true, transaction);
654
}
655
}
656
}
657
658