Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts
5220 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 { streamToBuffer } from '../../../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { StringSHA1 } from '../../../../../base/common/hash.js';
9
import { DisposableStore, IReference, thenRegisterOrDispose } from '../../../../../base/common/lifecycle.js';
10
import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';
11
import { Schemas } from '../../../../../base/common/network.js';
12
import { autorun, IObservable, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js';
13
import { isEqual } from '../../../../../base/common/resources.js';
14
import { assertType } from '../../../../../base/common/types.js';
15
import { URI } from '../../../../../base/common/uri.js';
16
import { generateUuid } from '../../../../../base/common/uuid.js';
17
import { Range } from '../../../../../editor/common/core/range.js';
18
import { LineRange } from '../../../../../editor/common/core/ranges/lineRange.js';
19
import { nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';
20
import { DetailedLineRangeMapping, RangeMapping } from '../../../../../editor/common/diff/rangeMapping.js';
21
import { TextEdit } from '../../../../../editor/common/languages.js';
22
import { ITextModel } from '../../../../../editor/common/model.js';
23
import { IModelService } from '../../../../../editor/common/services/model.js';
24
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
25
import { localize } from '../../../../../nls.js';
26
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
27
import { IFileService } from '../../../../../platform/files/common/files.js';
28
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
29
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../../platform/undoRedo/common/undoRedo.js';
30
import { IEditorPane, SaveReason } from '../../../../common/editor.js';
31
import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';
32
import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js';
33
import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';
34
import { NotebookTextDiffEditor } from '../../../notebook/browser/diff/notebookDiffEditor.js';
35
import { INotebookTextDiffEditor } from '../../../notebook/browser/diff/notebookDiffEditorBrowser.js';
36
import { CellDiffInfo } from '../../../notebook/browser/diff/notebookDiffViewModel.js';
37
import { getNotebookEditorFromEditorPane } from '../../../notebook/browser/notebookBrowser.js';
38
import { NotebookCellTextModel } from '../../../notebook/common/model/notebookCellTextModel.js';
39
import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js';
40
import { CellEditType, ICellDto2, ICellEditOperation, ICellReplaceEdit, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookSetting, NotebookTextModelChangedEvent, TransientOptions } from '../../../notebook/common/notebookCommon.js';
41
import { computeDiff } from '../../../notebook/common/notebookDiff.js';
42
import { INotebookEditorModelResolverService } from '../../../notebook/common/notebookEditorModelResolverService.js';
43
import { INotebookLoggingService } from '../../../notebook/common/notebookLoggingService.js';
44
import { INotebookService } from '../../../notebook/common/notebookService.js';
45
import { INotebookEditorWorkerService } from '../../../notebook/common/services/notebookWorkerService.js';
46
import { IChatService } from '../../common/chatService/chatService.js';
47
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
48
import { IChatResponseModel } from '../../common/model/chatModel.js';
49
import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';
50
import { createSnapshot, deserializeSnapshot, getNotebookSnapshotFileURI, restoreSnapshot, SnapshotComparer } from './notebook/chatEditingModifiedNotebookSnapshot.js';
51
import { ChatEditingNewNotebookContentEdits } from './notebook/chatEditingNewNotebookContentEdits.js';
52
import { ChatEditingNotebookCellEntry } from './notebook/chatEditingNotebookCellEntry.js';
53
import { ChatEditingNotebookDiffEditorIntegration, ChatEditingNotebookEditorIntegration } from './notebook/chatEditingNotebookEditorIntegration.js';
54
import { ChatEditingNotebookFileSystemProvider } from './notebook/chatEditingNotebookFileSystemProvider.js';
55
import { adjustCellDiffAndOriginalModelBasedOnCellAddDelete, adjustCellDiffAndOriginalModelBasedOnCellMovements, adjustCellDiffForKeepingAnInsertedCell, adjustCellDiffForRevertingADeletedCell, adjustCellDiffForRevertingAnInsertedCell, calculateNotebookRewriteRatio, getCorrespondingOriginalCellIndex, isTransientIPyNbExtensionEvent } from './notebook/helpers.js';
56
import { countChanges, ICellDiffInfo, sortCellChanges } from './notebook/notebookCellChanges.js';
57
58
59
const SnapshotLanguageId = 'VSCodeChatNotebookSnapshotLanguage';
60
61
export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifiedFileEntry {
62
static NewModelCounter: number = 0;
63
private readonly modifiedModel: NotebookTextModel;
64
private readonly originalModel: NotebookTextModel;
65
override originalURI: URI;
66
/**
67
* JSON stringified version of the original notebook.
68
*/
69
override initialContent: string;
70
/**
71
* Whether we're still generating diffs from a response.
72
*/
73
private _isProcessingResponse = observableValue<boolean>('isProcessingResponse', false);
74
get isProcessingResponse(): IObservable<boolean> {
75
return this._isProcessingResponse;
76
}
77
private _isEditFromUs: boolean = false;
78
/**
79
* Whether all edits are from us, e.g. is possible a user has made edits, then this will be false.
80
*/
81
private _allEditsAreFromUs: boolean = true;
82
private readonly _changesCount = observableValue<number>(this, 0);
83
override changesCount: IObservable<number> = this._changesCount;
84
85
private readonly cellEntryMap = new ResourceMap<ChatEditingNotebookCellEntry>();
86
private modifiedToOriginalCell = new ResourceMap<URI>();
87
private readonly _cellsDiffInfo = observableValue<ICellDiffInfo[]>('diffInfo', []);
88
89
get cellsDiffInfo(): IObservable<ICellDiffInfo[]> {
90
return this._cellsDiffInfo;
91
}
92
93
get viewType() {
94
return this.modifiedModel.viewType;
95
}
96
97
/**
98
* List of Cell URIs that are edited,
99
* Will be cleared once all edits have been accepted.
100
* I.e. this will only contain URIS while acceptAgentEdits is being called & before `isLastEdit` is sent.
101
* I.e. this is populated only when edits are being streamed.
102
*/
103
private readonly editedCells = new ResourceSet();
104
105
public static async create(uri: URI, _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, telemetryInfo: IModifiedEntryTelemetryInfo, chatKind: ChatEditKind, initialContent: string | undefined, instantiationService: IInstantiationService): Promise<AbstractChatEditingModifiedFileEntry> {
106
return instantiationService.invokeFunction(async accessor => {
107
const notebookService = accessor.get(INotebookService);
108
const resolver = accessor.get(INotebookEditorModelResolverService);
109
const configurationServie = accessor.get(IConfigurationService);
110
const resourceRef: IReference<IResolvedNotebookEditorModel> = await resolver.resolve(uri);
111
const notebook = resourceRef.object.notebook;
112
const originalUri = getNotebookSnapshotFileURI(telemetryInfo.sessionResource, telemetryInfo.requestId, generateUuid(), notebook.uri.scheme === Schemas.untitled ? `/${notebook.uri.path}` : notebook.uri.path, notebook.viewType);
113
const [options, buffer] = await Promise.all([
114
notebookService.withNotebookDataProvider(resourceRef.object.notebook.notebookType),
115
notebookService.createNotebookTextDocumentSnapshot(notebook.uri, SnapshotContext.Backup, CancellationToken.None).then(s => streamToBuffer(s))
116
]);
117
const disposables = new DisposableStore();
118
// Register so that we can load this from file system.
119
disposables.add(ChatEditingNotebookFileSystemProvider.registerFile(originalUri, buffer));
120
const originalRef = await resolver.resolve(originalUri, notebook.viewType);
121
if (initialContent !== undefined) {
122
try {
123
restoreSnapshot(originalRef.object.notebook, initialContent);
124
} catch (ex) {
125
console.error(`Error restoring snapshot: ${initialContent}`, ex);
126
initialContent = createSnapshot(notebook, options.serializer.options, configurationServie);
127
}
128
} else {
129
initialContent = createSnapshot(notebook, options.serializer.options, configurationServie);
130
// Both models are the same, ensure the cell ids are the same, this way we get a perfect diffing.
131
// No need to generate edits for this.
132
// We want to ensure they are identitcal, possible original notebook was open and got modified.
133
// Or something gets changed between serialization & deserialization of the snapshot into the original.
134
// E.g. in jupyter notebooks the metadata contains transient data that gets updated after deserialization.
135
restoreSnapshot(originalRef.object.notebook, initialContent);
136
const edits: ICellEditOperation[] = [];
137
notebook.cells.forEach((cell, index) => {
138
const internalId = generateCellHash(cell.uri);
139
edits.push({ editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { internalId } });
140
});
141
resourceRef.object.notebook.applyEdits(edits, true, undefined, () => undefined, undefined, false);
142
originalRef.object.notebook.applyEdits(edits, true, undefined, () => undefined, undefined, false);
143
}
144
const instance = instantiationService.createInstance(ChatEditingModifiedNotebookEntry, resourceRef, originalRef, _multiDiffEntryDelegate, options.serializer.options, telemetryInfo, chatKind, initialContent);
145
instance._register(disposables);
146
return instance;
147
});
148
}
149
150
public static canHandleSnapshotContent(initialContent: string | undefined): boolean {
151
if (!initialContent) {
152
return false;
153
}
154
155
try {
156
deserializeSnapshot(initialContent);
157
return true;
158
} catch (ex) {
159
// not a valid snapshot
160
return false;
161
}
162
}
163
164
public static canHandleSnapshot(snapshot: ISnapshotEntry): boolean {
165
if (snapshot.languageId === SnapshotLanguageId && ChatEditingModifiedNotebookEntry.canHandleSnapshotContent(snapshot.current)) {
166
return true;
167
}
168
return false;
169
}
170
171
private readonly initialContentComparer: SnapshotComparer;
172
173
constructor(
174
private readonly modifiedResourceRef: IReference<IResolvedNotebookEditorModel>,
175
originalResourceRef: IReference<IResolvedNotebookEditorModel>,
176
private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void },
177
private readonly transientOptions: TransientOptions | undefined,
178
telemetryInfo: IModifiedEntryTelemetryInfo,
179
kind: ChatEditKind,
180
initialContent: string,
181
@IConfigurationService private readonly configurationService: IConfigurationService,
182
@IFilesConfigurationService fileConfigService: IFilesConfigurationService,
183
@IChatService chatService: IChatService,
184
@IFileService fileService: IFileService,
185
@IInstantiationService instantiationService: IInstantiationService,
186
@ITextModelService private readonly textModelService: ITextModelService,
187
@IModelService private readonly modelService: IModelService,
188
@IUndoRedoService undoRedoService: IUndoRedoService,
189
@INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService,
190
@INotebookLoggingService private readonly loggingService: INotebookLoggingService,
191
@INotebookEditorModelResolverService private readonly notebookResolver: INotebookEditorModelResolverService,
192
@IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService,
193
) {
194
super(modifiedResourceRef.object.notebook.uri, telemetryInfo, kind, configurationService, fileConfigService, chatService, fileService, undoRedoService, instantiationService, aiEditTelemetryService);
195
this.initialContentComparer = new SnapshotComparer(initialContent);
196
this.modifiedModel = this._register(modifiedResourceRef).object.notebook;
197
this.originalModel = this._register(originalResourceRef).object.notebook;
198
this.originalURI = this.originalModel.uri;
199
this.initialContent = initialContent;
200
this.initializeModelsFromDiff();
201
this._register(this.modifiedModel.onDidChangeContent(this.mirrorNotebookEdits, this));
202
}
203
204
initializeModelsFromDiffImpl(cellsDiffInfo: CellDiffInfo[]) {
205
this.cellEntryMap.forEach(entry => entry.dispose());
206
this.cellEntryMap.clear();
207
const diffs = cellsDiffInfo.map((cellDiff, i) => {
208
switch (cellDiff.type) {
209
case 'delete':
210
return this.createDeleteCellDiffInfo(cellDiff.originalCellIndex);
211
case 'insert':
212
return this.createInsertedCellDiffInfo(cellDiff.modifiedCellIndex);
213
default:
214
return this.createModifiedCellDiffInfo(cellDiff.modifiedCellIndex, cellDiff.originalCellIndex);
215
}
216
});
217
this._cellsDiffInfo.set(diffs, undefined);
218
this._changesCount.set(countChanges(diffs), undefined);
219
}
220
221
getIndexOfCellHandle(handle: number) {
222
return this.modifiedModel.cells.findIndex(c => c.handle === handle);
223
}
224
225
private computeRequestId: number = 0;
226
async initializeModelsFromDiff() {
227
const id = ++this.computeRequestId;
228
if (this._areOriginalAndModifiedIdenticalImpl()) {
229
const cellsDiffInfo: CellDiffInfo[] = this.modifiedModel.cells.map((_, index) => {
230
return { type: 'unchanged', originalCellIndex: index, modifiedCellIndex: index } satisfies CellDiffInfo;
231
});
232
this.initializeModelsFromDiffImpl(cellsDiffInfo);
233
return;
234
}
235
const cellsDiffInfo: CellDiffInfo[] = [];
236
try {
237
this._isProcessingResponse.set(true, undefined);
238
const notebookDiff = await this.notebookEditorWorkerService.computeDiff(this.originalURI, this.modifiedURI);
239
if (id !== this.computeRequestId || this._store.isDisposed) {
240
return;
241
}
242
const result = computeDiff(this.originalModel, this.modifiedModel, notebookDiff);
243
if (result.cellDiffInfo.length) {
244
cellsDiffInfo.push(...result.cellDiffInfo);
245
}
246
} catch (ex) {
247
this.loggingService.error('Notebook Chat', 'Error computing diff:\n' + ex);
248
} finally {
249
this._isProcessingResponse.set(false, undefined);
250
}
251
this.initializeModelsFromDiffImpl(cellsDiffInfo);
252
}
253
updateCellDiffInfo(cellsDiffInfo: ICellDiffInfo[], transcation: ITransaction | undefined) {
254
this._cellsDiffInfo.set(sortCellChanges(cellsDiffInfo), transcation);
255
this._changesCount.set(countChanges(cellsDiffInfo), transcation);
256
}
257
258
mirrorNotebookEdits(e: NotebookTextModelChangedEvent) {
259
if (this._isEditFromUs || this._isExternalEditInProgress || Array.from(this.cellEntryMap.values()).some(entry => entry.isEditFromUs)) {
260
return;
261
}
262
263
// Possible user reverted the changes from SCM or the like.
264
// Or user just reverted the changes made via edits (e.g. edit made a change in a cell and user undid that change either by typing over or other).
265
// Computing snapshot is too slow, as this event gets triggered for every key stroke in a cell,
266
// const didResetToOriginalContent = createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService) === this.initialContent;
267
let didResetToOriginalContent = this.initialContentComparer.isEqual(this.modifiedModel);
268
const currentState = this._stateObs.get();
269
if (currentState === ModifiedFileEntryState.Modified && didResetToOriginalContent) {
270
this._stateObs.set(ModifiedFileEntryState.Rejected, undefined);
271
this.updateCellDiffInfo([], undefined);
272
this.initializeModelsFromDiff();
273
this._notifySessionAction('rejected');
274
return;
275
}
276
277
if (!e.rawEvents.length) {
278
return;
279
}
280
281
if (currentState === ModifiedFileEntryState.Rejected) {
282
return;
283
}
284
285
if (isTransientIPyNbExtensionEvent(this.modifiedModel.notebookType, e)) {
286
return;
287
}
288
289
this._allEditsAreFromUs = false;
290
this._userEditScheduler.schedule();
291
292
// Changes to cell text is sync'ed and handled separately.
293
// See ChatEditingNotebookCellEntry._mirrorEdits
294
for (const event of e.rawEvents.filter(event => event.kind !== NotebookCellsChangeType.ChangeCellContent)) {
295
switch (event.kind) {
296
case NotebookCellsChangeType.ChangeDocumentMetadata: {
297
const edit: ICellEditOperation = {
298
editType: CellEditType.DocumentMetadata,
299
metadata: this.modifiedModel.metadata
300
};
301
this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, false);
302
break;
303
}
304
case NotebookCellsChangeType.ModelChange: {
305
let cellDiffs = sortCellChanges(this._cellsDiffInfo.get());
306
// Ensure the new notebook cells have internalIds
307
this._applyEditsSync(() => {
308
event.changes.forEach(change => {
309
change[2].forEach((cell, i) => {
310
if (cell.internalMetadata.internalId) {
311
return;
312
}
313
const index = change[0] + i;
314
const internalId = generateCellHash(cell.uri);
315
const edits: ICellEditOperation[] = [{ editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { internalId } }];
316
this.modifiedModel.applyEdits(edits, true, undefined, () => undefined, undefined, false);
317
cell.internalMetadata ??= {};
318
cell.internalMetadata.internalId = internalId;
319
});
320
});
321
});
322
event.changes.forEach(change => {
323
cellDiffs = adjustCellDiffAndOriginalModelBasedOnCellAddDelete(change,
324
cellDiffs,
325
this.modifiedModel.cells.length,
326
this.originalModel.cells.length,
327
this.originalModel.applyEdits.bind(this.originalModel),
328
this.createModifiedCellDiffInfo.bind(this));
329
});
330
this.updateCellDiffInfo(cellDiffs, undefined);
331
this.disposeDeletedCellEntries();
332
break;
333
}
334
case NotebookCellsChangeType.ChangeCellLanguage: {
335
const index = getCorrespondingOriginalCellIndex(event.index, this._cellsDiffInfo.get());
336
if (typeof index === 'number') {
337
const edit: ICellEditOperation = {
338
editType: CellEditType.CellLanguage,
339
index,
340
language: event.language
341
};
342
this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, false);
343
}
344
break;
345
}
346
case NotebookCellsChangeType.ChangeCellMetadata: {
347
// ipynb and other extensions can alter metadata, ensure we update the original model in the corresponding cell.
348
const index = getCorrespondingOriginalCellIndex(event.index, this._cellsDiffInfo.get());
349
if (typeof index === 'number') {
350
const edit: ICellEditOperation = {
351
editType: CellEditType.Metadata,
352
index,
353
metadata: event.metadata
354
};
355
this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, false);
356
}
357
break;
358
}
359
case NotebookCellsChangeType.ChangeCellMime:
360
break;
361
case NotebookCellsChangeType.ChangeCellInternalMetadata: {
362
const index = getCorrespondingOriginalCellIndex(event.index, this._cellsDiffInfo.get());
363
if (typeof index === 'number') {
364
const edit: ICellEditOperation = {
365
editType: CellEditType.PartialInternalMetadata,
366
index,
367
internalMetadata: event.internalMetadata
368
};
369
this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, false);
370
}
371
break;
372
}
373
case NotebookCellsChangeType.Output: {
374
// User can run cells.
375
const index = getCorrespondingOriginalCellIndex(event.index, this._cellsDiffInfo.get());
376
if (typeof index === 'number') {
377
const edit: ICellEditOperation = {
378
editType: CellEditType.Output,
379
index,
380
append: event.append,
381
outputs: event.outputs
382
};
383
this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, false);
384
}
385
break;
386
}
387
case NotebookCellsChangeType.OutputItem: {
388
// outputs are shared between original and modified model, so the original model is already updated.
389
break;
390
}
391
case NotebookCellsChangeType.Move: {
392
const result = adjustCellDiffAndOriginalModelBasedOnCellMovements(event, this._cellsDiffInfo.get().slice());
393
if (result) {
394
this.originalModel.applyEdits(result[1], true, undefined, () => undefined, undefined, false);
395
this._cellsDiffInfo.set(result[0], undefined);
396
}
397
break;
398
}
399
default: {
400
break;
401
}
402
}
403
}
404
405
didResetToOriginalContent = this.initialContentComparer.isEqual(this.modifiedModel);
406
if (currentState === ModifiedFileEntryState.Modified && didResetToOriginalContent) {
407
this._stateObs.set(ModifiedFileEntryState.Rejected, undefined);
408
this.updateCellDiffInfo([], undefined);
409
this.initializeModelsFromDiff();
410
return;
411
}
412
}
413
414
protected override async _doAccept(): Promise<void> {
415
this.updateCellDiffInfo([], undefined);
416
const snapshot = createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService);
417
restoreSnapshot(this.originalModel, snapshot);
418
this.initializeModelsFromDiff();
419
await this._collapse(undefined);
420
421
const config = this._fileConfigService.getAutoSaveConfiguration(this.modifiedURI);
422
if (this.modifiedModel.uri.scheme !== Schemas.untitled && (!config.autoSave || !this.notebookResolver.isDirty(this.modifiedURI))) {
423
// SAVE after accept for manual-savers, for auto-savers
424
// trigger explict save to get save participants going
425
await this._applyEdits(async () => {
426
try {
427
await this.modifiedResourceRef.object.save({
428
reason: SaveReason.EXPLICIT,
429
force: true,
430
});
431
} catch {
432
// ignored
433
}
434
});
435
}
436
}
437
438
protected override async _doReject(): Promise<void> {
439
this.updateCellDiffInfo([], undefined);
440
if (this.createdInRequestId === this._telemetryInfo.requestId) {
441
await this._applyEdits(async () => {
442
await this.modifiedResourceRef.object.revert({ soft: true });
443
await this._fileService.del(this.modifiedURI);
444
});
445
this._onDidDelete.fire();
446
} else {
447
await this._applyEdits(async () => {
448
const snapshot = createSnapshot(this.originalModel, this.transientOptions, this.configurationService);
449
this.restoreSnapshotInModifiedModel(snapshot);
450
if (this._allEditsAreFromUs && Array.from(this.cellEntryMap.values()).every(entry => entry.allEditsAreFromUs)) {
451
// save the file after discarding so that the dirty indicator goes away
452
// and so that an intermediate saved state gets reverted
453
await this.modifiedResourceRef.object.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true });
454
}
455
});
456
this.initializeModelsFromDiff();
457
await this._collapse(undefined);
458
}
459
}
460
461
private async _collapse(transaction: ITransaction | undefined): Promise<void> {
462
this._multiDiffEntryDelegate.collapse(transaction);
463
}
464
465
protected override _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration {
466
const notebookEditor = getNotebookEditorFromEditorPane(editor);
467
if (!notebookEditor && editor.getId() === NotebookTextDiffEditor.ID) {
468
const diffEditor = (editor.getControl() as INotebookTextDiffEditor);
469
return this._instantiationService.createInstance(ChatEditingNotebookDiffEditorIntegration, diffEditor, this._cellsDiffInfo);
470
}
471
assertType(notebookEditor);
472
return this._instantiationService.createInstance(ChatEditingNotebookEditorIntegration, this, editor, this.modifiedModel, this.originalModel, this._cellsDiffInfo);
473
}
474
475
protected override _resetEditsState(tx: ITransaction): void {
476
super._resetEditsState(tx);
477
this.cellEntryMap.forEach(entry => !entry.isDisposed && entry.clearCurrentEditLineDecoration());
478
}
479
480
protected override _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement | undefined {
481
const request = response.session.getRequests().find(req => req.id === response.requestId);
482
const label = request?.message.text ? localize('chatNotebookEdit1', "Chat Edit: '{0}'", request.message.text) : localize('chatNotebookEdit2', "Chat Edit");
483
const transientOptions = this.transientOptions;
484
const outputSizeLimit = this.configurationService.getValue<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
485
486
// create a snapshot of the current state of the model, before the next set of edits
487
let initial = createSnapshot(this.modifiedModel, transientOptions, outputSizeLimit);
488
let last = '';
489
let redoState = ModifiedFileEntryState.Rejected;
490
491
return {
492
type: UndoRedoElementType.Resource,
493
resource: this.modifiedURI,
494
label,
495
code: 'chat.edit',
496
confirmBeforeUndo: false,
497
undo: async () => {
498
last = createSnapshot(this.modifiedModel, transientOptions, outputSizeLimit);
499
this._isEditFromUs = true;
500
try {
501
restoreSnapshot(this.modifiedModel, initial);
502
restoreSnapshot(this.originalModel, initial);
503
} finally {
504
this._isEditFromUs = false;
505
}
506
redoState = this._stateObs.get() === ModifiedFileEntryState.Accepted ? ModifiedFileEntryState.Accepted : ModifiedFileEntryState.Rejected;
507
this._stateObs.set(ModifiedFileEntryState.Rejected, undefined);
508
this.updateCellDiffInfo([], undefined);
509
this.initializeModelsFromDiff();
510
this._notifySessionAction('userModified');
511
},
512
redo: async () => {
513
initial = createSnapshot(this.modifiedModel, transientOptions, outputSizeLimit);
514
this._isEditFromUs = true;
515
try {
516
restoreSnapshot(this.modifiedModel, last);
517
restoreSnapshot(this.originalModel, last);
518
} finally {
519
this._isEditFromUs = false;
520
}
521
this._stateObs.set(redoState, undefined);
522
this.updateCellDiffInfo([], undefined);
523
this.initializeModelsFromDiff();
524
this._notifySessionAction('userModified');
525
}
526
};
527
}
528
529
protected override async _areOriginalAndModifiedIdentical(): Promise<boolean> {
530
return this._areOriginalAndModifiedIdenticalImpl();
531
}
532
533
private _areOriginalAndModifiedIdenticalImpl(): boolean {
534
const snapshot = createSnapshot(this.originalModel, this.transientOptions, this.configurationService);
535
return new SnapshotComparer(snapshot).isEqual(this.modifiedModel);
536
}
537
538
private newNotebookEditGenerator?: ChatEditingNewNotebookContentEdits;
539
override async acceptAgentEdits(resource: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel | undefined): Promise<void> {
540
const isCellUri = resource.scheme === Schemas.vscodeNotebookCell;
541
const cell = isCellUri && this.modifiedModel.cells.find(cell => isEqual(cell.uri, resource));
542
let cellEntry: ChatEditingNotebookCellEntry | undefined;
543
if (cell) {
544
const index = this.modifiedModel.cells.indexOf(cell);
545
const entry = this._cellsDiffInfo.get().slice().find(entry => entry.modifiedCellIndex === index);
546
if (!entry) {
547
// Not possible.
548
console.error('Original cell model not found');
549
return;
550
}
551
552
cellEntry = this.getOrCreateModifiedTextFileEntryForCell(cell, await entry.modifiedModel.promise, await entry.originalModel.promise);
553
}
554
555
// For all cells that were edited, send the `isLastEdits` flag.
556
const finishPreviousCells = async () => {
557
await Promise.all(Array.from(this.editedCells).map(async (uri) => {
558
const cell = this.modifiedModel.cells.find(cell => isEqual(cell.uri, uri));
559
const cellEntry = cell && this.cellEntryMap.get(cell.uri);
560
await cellEntry?.acceptAgentEdits([], true, responseModel);
561
}));
562
this.editedCells.clear();
563
};
564
565
await this._applyEdits(async () => {
566
await Promise.all(edits.map(async (edit, idx) => {
567
const last = isLastEdits && idx === edits.length - 1;
568
if (TextEdit.isTextEdit(edit)) {
569
// Possible we're getting the raw content for the notebook.
570
if (isEqual(resource, this.modifiedModel.uri)) {
571
this.newNotebookEditGenerator ??= this._instantiationService.createInstance(ChatEditingNewNotebookContentEdits, this.modifiedModel);
572
this.newNotebookEditGenerator.acceptTextEdits([edit]);
573
} else {
574
// If we get cell edits, its impossible to get text edits for the notebook uri.
575
this.newNotebookEditGenerator = undefined;
576
if (!this.editedCells.has(resource)) {
577
await finishPreviousCells();
578
this.editedCells.add(resource);
579
}
580
await cellEntry?.acceptAgentEdits([edit], last, responseModel);
581
}
582
} else {
583
// If we notebook edits, its impossible to get text edits for the notebook uri.
584
this.newNotebookEditGenerator = undefined;
585
this.acceptNotebookEdit(edit);
586
}
587
}));
588
});
589
590
// If the last edit for a cell was sent, then handle it
591
if (isLastEdits) {
592
await finishPreviousCells();
593
}
594
595
// isLastEdits can be true for cell Uris, but when its true for Cells edits.
596
// It cannot be true for the notebook itself.
597
isLastEdits = !isCellUri && isLastEdits;
598
599
// If this is the last edit and & we got regular text edits for generating new notebook content
600
// Then generate notebook edits from those text edits & apply those notebook edits.
601
if (isLastEdits && this.newNotebookEditGenerator) {
602
const notebookEdits = await this.newNotebookEditGenerator.generateEdits();
603
this.newNotebookEditGenerator = undefined;
604
notebookEdits.forEach(edit => this.acceptNotebookEdit(edit));
605
}
606
607
transaction((tx) => {
608
this._stateObs.set(ModifiedFileEntryState.Modified, tx);
609
if (!isLastEdits) {
610
const newRewriteRation = Math.max(this._rewriteRatioObs.get(), calculateNotebookRewriteRatio(this._cellsDiffInfo.get(), this.originalModel, this.modifiedModel));
611
this._rewriteRatioObs.set(Math.min(1, newRewriteRation), tx);
612
} else {
613
this.editedCells.clear();
614
this._resetEditsState(tx);
615
this._rewriteRatioObs.set(1, tx);
616
}
617
});
618
}
619
620
private disposeDeletedCellEntries() {
621
const cellsUris = new ResourceSet(this.modifiedModel.cells.map(cell => cell.uri));
622
Array.from(this.cellEntryMap.keys()).forEach(uri => {
623
if (cellsUris.has(uri)) {
624
return;
625
}
626
this.cellEntryMap.get(uri)?.dispose();
627
this.cellEntryMap.delete(uri);
628
});
629
}
630
631
acceptNotebookEdit(edit: ICellEditOperation): void {
632
// make the actual edit
633
this.modifiedModel.applyEdits([edit], true, undefined, () => undefined, undefined, false);
634
this.disposeDeletedCellEntries();
635
636
637
if (edit.editType !== CellEditType.Replace) {
638
return;
639
}
640
// Ensure cells have internal Ids.
641
edit.cells.forEach((_, i) => {
642
const index = edit.index + i;
643
const cell = this.modifiedModel.cells[index];
644
if (cell.internalMetadata.internalId) {
645
return;
646
}
647
const internalId = generateCellHash(cell.uri);
648
const edits: ICellEditOperation[] = [{ editType: CellEditType.PartialInternalMetadata, index, internalMetadata: { internalId } }];
649
this.modifiedModel.applyEdits(edits, true, undefined, () => undefined, undefined, false);
650
});
651
652
let diff: ICellDiffInfo[] = [];
653
if (edit.count === 0) {
654
// All existing indexes are shifted by number of cells added.
655
diff = sortCellChanges(this._cellsDiffInfo.get());
656
diff.forEach(d => {
657
if (d.type !== 'delete' && d.modifiedCellIndex >= edit.index) {
658
d.modifiedCellIndex += edit.cells.length;
659
}
660
});
661
const diffInsert = edit.cells.map((_, i) => this.createInsertedCellDiffInfo(edit.index + i));
662
diff.splice(edit.index, 0, ...diffInsert);
663
} else {
664
// All existing indexes are shifted by number of cells removed.
665
// And unchanged cells should be converted to deleted cells.
666
diff = sortCellChanges(this._cellsDiffInfo.get()).map((d) => {
667
if (d.type === 'unchanged' && d.modifiedCellIndex >= edit.index && d.modifiedCellIndex <= (edit.index + edit.count - 1)) {
668
return this.createDeleteCellDiffInfo(d.originalCellIndex);
669
}
670
if (d.type !== 'delete' && d.modifiedCellIndex >= (edit.index + edit.count)) {
671
d.modifiedCellIndex -= edit.count;
672
return d;
673
}
674
return d;
675
});
676
}
677
this.updateCellDiffInfo(diff, undefined);
678
}
679
680
private computeStateAfterAcceptingRejectingChanges(accepted: boolean) {
681
const currentSnapshot = createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService);
682
if (new SnapshotComparer(currentSnapshot).isEqual(this.originalModel)) {
683
const state = accepted ? ModifiedFileEntryState.Accepted : ModifiedFileEntryState.Rejected;
684
this._stateObs.set(state, undefined);
685
this._notifySessionAction(accepted ? 'accepted' : 'rejected');
686
}
687
}
688
689
createModifiedCellDiffInfo(modifiedCellIndex: number, originalCellIndex: number): ICellDiffInfo {
690
const modifiedCell = this.modifiedModel.cells[modifiedCellIndex];
691
const originalCell = this.originalModel.cells[originalCellIndex];
692
this.modifiedToOriginalCell.set(modifiedCell.uri, originalCell.uri);
693
const modifiedCellModelPromise = this.resolveCellModel(modifiedCell.uri);
694
const originalCellModelPromise = this.resolveCellModel(originalCell.uri);
695
696
Promise.all([modifiedCellModelPromise, originalCellModelPromise]).then(([modifiedCellModel, originalCellModel]) => {
697
this.getOrCreateModifiedTextFileEntryForCell(modifiedCell, modifiedCellModel, originalCellModel);
698
});
699
700
const diff = observableValue('diff', nullDocumentDiff);
701
const unchangedCell: ICellDiffInfo = {
702
type: 'unchanged',
703
modifiedCellIndex,
704
originalCellIndex,
705
keep: async (changes: DetailedLineRangeMapping) => {
706
const [modifiedCellModel, originalCellModel] = await Promise.all([modifiedCellModelPromise, originalCellModelPromise]);
707
const entry = this.getOrCreateModifiedTextFileEntryForCell(modifiedCell, modifiedCellModel, originalCellModel);
708
return entry ? entry.keep(changes) : false;
709
},
710
undo: async (changes: DetailedLineRangeMapping) => {
711
const [modifiedCellModel, originalCellModel] = await Promise.all([modifiedCellModelPromise, originalCellModelPromise]);
712
const entry = this.getOrCreateModifiedTextFileEntryForCell(modifiedCell, modifiedCellModel, originalCellModel);
713
return entry ? entry.undo(changes) : false;
714
},
715
modifiedModel: new ObservablePromise(modifiedCellModelPromise),
716
originalModel: new ObservablePromise(originalCellModelPromise),
717
diff
718
};
719
720
return unchangedCell;
721
722
}
723
createInsertedCellDiffInfo(modifiedCellIndex: number): ICellDiffInfo {
724
const cell = this.modifiedModel.cells[modifiedCellIndex];
725
const lines = cell.getValue().split(/\r?\n/);
726
const originalRange = new Range(1, 0, 1, 0);
727
const modifiedRange = new Range(1, 0, lines.length, lines[lines.length - 1].length);
728
const innerChanges = new RangeMapping(originalRange, modifiedRange);
729
const changes = [new DetailedLineRangeMapping(new LineRange(1, 1), new LineRange(1, lines.length), [innerChanges])];
730
// When a new cell is inserted, we use the ChatEditingCodeEditorIntegration to handle the edits.
731
// & to also display undo/redo and decorations.
732
// However that needs a modified and original model.
733
// For inserted cells there's no original model, so we create a new empty text model and pass that as the original.
734
const originalModelUri = this.modifiedModel.uri.with({ query: (ChatEditingModifiedNotebookEntry.NewModelCounter++).toString(), scheme: 'emptyCell' });
735
const originalModel = this.modelService.getModel(originalModelUri) || this._register(this.modelService.createModel('', null, originalModelUri));
736
this.modifiedToOriginalCell.set(cell.uri, originalModelUri);
737
const keep = async () => {
738
this._applyEditsSync(() => this.keepPreviouslyInsertedCell(cell));
739
this.computeStateAfterAcceptingRejectingChanges(true);
740
return true;
741
};
742
const undo = async () => {
743
this._applyEditsSync(() => this.undoPreviouslyInsertedCell(cell));
744
this.computeStateAfterAcceptingRejectingChanges(false);
745
return true;
746
};
747
this.resolveCellModel(cell.uri).then(modifiedModel => {
748
if (this._store.isDisposed) {
749
return;
750
}
751
// We want decorators for the cell just as we display decorators for modified cells.
752
// This way we have the ability to accept/reject the entire cell.
753
this.getOrCreateModifiedTextFileEntryForCell(cell, modifiedModel, originalModel);
754
});
755
return {
756
type: 'insert' as const,
757
originalCellIndex: undefined,
758
modifiedCellIndex: modifiedCellIndex,
759
keep,
760
undo,
761
modifiedModel: new ObservablePromise(this.resolveCellModel(cell.uri)),
762
originalModel: new ObservablePromise(Promise.resolve(originalModel)),
763
diff: observableValue('deletedCellDiff', {
764
changes,
765
identical: false,
766
moves: [],
767
quitEarly: false,
768
})
769
} satisfies ICellDiffInfo;
770
}
771
createDeleteCellDiffInfo(originalCellIndex: number): ICellDiffInfo {
772
const originalCell = this.originalModel.cells[originalCellIndex];
773
const lines = new Array(originalCell.textBuffer.getLineCount()).fill(0).map((_, i) => originalCell.textBuffer.getLineContent(i + 1));
774
const originalRange = new Range(1, 0, lines.length, lines[lines.length - 1].length);
775
const modifiedRange = new Range(1, 0, 1, 0);
776
const innerChanges = new RangeMapping(modifiedRange, originalRange);
777
const changes = [new DetailedLineRangeMapping(new LineRange(1, lines.length), new LineRange(1, 1), [innerChanges])];
778
const modifiedModelUri = this.modifiedModel.uri.with({ query: (ChatEditingModifiedNotebookEntry.NewModelCounter++).toString(), scheme: 'emptyCell' });
779
const modifiedModel = this.modelService.getModel(modifiedModelUri) || this._register(this.modelService.createModel('', null, modifiedModelUri));
780
const keep = async () => {
781
this._applyEditsSync(() => this.keepPreviouslyDeletedCell(this.originalModel.cells.indexOf(originalCell)));
782
this.computeStateAfterAcceptingRejectingChanges(true);
783
return true;
784
};
785
const undo = async () => {
786
this._applyEditsSync(() => this.undoPreviouslyDeletedCell(this.originalModel.cells.indexOf(originalCell), originalCell));
787
this.computeStateAfterAcceptingRejectingChanges(false);
788
return true;
789
};
790
791
// This will be deleted.
792
return {
793
type: 'delete' as const,
794
modifiedCellIndex: undefined,
795
originalCellIndex,
796
originalModel: new ObservablePromise(this.resolveCellModel(originalCell.uri)),
797
modifiedModel: new ObservablePromise(Promise.resolve(modifiedModel)),
798
keep,
799
undo,
800
diff: observableValue('cellDiff', {
801
changes,
802
identical: false,
803
moves: [],
804
quitEarly: false,
805
})
806
} satisfies ICellDiffInfo;
807
}
808
809
private undoPreviouslyInsertedCell(cell: NotebookCellTextModel) {
810
let diffs: ICellDiffInfo[] = [];
811
this._applyEditsSync(() => {
812
const index = this.modifiedModel.cells.indexOf(cell);
813
diffs = adjustCellDiffForRevertingAnInsertedCell(index,
814
this._cellsDiffInfo.get(),
815
this.modifiedModel.applyEdits.bind(this.modifiedModel));
816
});
817
this.disposeDeletedCellEntries();
818
this.updateCellDiffInfo(diffs, undefined);
819
}
820
821
private keepPreviouslyInsertedCell(cell: NotebookCellTextModel) {
822
const modifiedCellIndex = this.modifiedModel.cells.indexOf(cell);
823
if (modifiedCellIndex === -1) {
824
// Not possible.
825
return;
826
}
827
const cellToInsert: ICellDto2 = {
828
cellKind: cell.cellKind,
829
language: cell.language,
830
metadata: cell.metadata,
831
outputs: cell.outputs,
832
source: cell.getValue(),
833
mime: cell.mime,
834
internalMetadata: {
835
internalId: cell.internalMetadata.internalId
836
}
837
};
838
this.cellEntryMap.get(cell.uri)?.dispose();
839
this.cellEntryMap.delete(cell.uri);
840
const cellDiffs = adjustCellDiffForKeepingAnInsertedCell(
841
modifiedCellIndex,
842
this._cellsDiffInfo.get().slice(),
843
cellToInsert,
844
this.originalModel.applyEdits.bind(this.originalModel),
845
this.createModifiedCellDiffInfo.bind(this)
846
);
847
this.updateCellDiffInfo(cellDiffs, undefined);
848
}
849
850
private undoPreviouslyDeletedCell(deletedOriginalIndex: number, originalCell: NotebookCellTextModel) {
851
const cellToInsert: ICellDto2 = {
852
cellKind: originalCell.cellKind,
853
language: originalCell.language,
854
metadata: originalCell.metadata,
855
outputs: originalCell.outputs,
856
source: originalCell.getValue(),
857
mime: originalCell.mime,
858
internalMetadata: {
859
internalId: originalCell.internalMetadata.internalId
860
}
861
};
862
let cellDiffs: ICellDiffInfo[] = [];
863
this._applyEditsSync(() => {
864
cellDiffs = adjustCellDiffForRevertingADeletedCell(
865
deletedOriginalIndex,
866
this._cellsDiffInfo.get(),
867
cellToInsert,
868
this.modifiedModel.applyEdits.bind(this.modifiedModel),
869
this.createModifiedCellDiffInfo.bind(this)
870
);
871
});
872
this.updateCellDiffInfo(cellDiffs, undefined);
873
}
874
875
876
private keepPreviouslyDeletedCell(deletedOriginalIndex: number) {
877
// Delete this cell from original as well.
878
const edit: ICellReplaceEdit = { cells: [], count: 1, editType: CellEditType.Replace, index: deletedOriginalIndex, };
879
this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, false);
880
const diffs = sortCellChanges(this._cellsDiffInfo.get())
881
.filter(d => !(d.type === 'delete' && d.originalCellIndex === deletedOriginalIndex))
882
.map(diff => {
883
if (diff.type !== 'insert' && diff.originalCellIndex > deletedOriginalIndex) {
884
return {
885
...diff,
886
originalCellIndex: diff.originalCellIndex - 1,
887
};
888
}
889
return diff;
890
});
891
this.updateCellDiffInfo(diffs, undefined);
892
}
893
894
private async _applyEdits(operation: () => Promise<void>) {
895
// make the actual edit
896
this._isEditFromUs = true;
897
try {
898
await operation();
899
} finally {
900
this._isEditFromUs = false;
901
}
902
}
903
904
private _applyEditsSync(operation: () => void) {
905
// make the actual edit
906
this._isEditFromUs = true;
907
try {
908
operation();
909
} finally {
910
this._isEditFromUs = false;
911
}
912
}
913
914
public getCurrentSnapshot() {
915
return createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService);
916
}
917
918
override createSnapshot(chatSessionResource: URI, requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry {
919
return {
920
resource: this.modifiedURI,
921
languageId: SnapshotLanguageId,
922
snapshotUri: getNotebookSnapshotFileURI(chatSessionResource, requestId, undoStop, this.modifiedURI.path, this.modifiedModel.viewType),
923
original: createSnapshot(this.originalModel, this.transientOptions, this.configurationService),
924
current: createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService),
925
state: this.state.get(),
926
telemetryInfo: this.telemetryInfo,
927
};
928
}
929
930
override equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean {
931
return !!snapshot &&
932
isEqual(this.modifiedURI, snapshot.resource) &&
933
this.state.get() === snapshot.state &&
934
new SnapshotComparer(snapshot.original).isEqual(this.originalModel) &&
935
new SnapshotComparer(snapshot.current).isEqual(this.modifiedModel);
936
937
}
938
939
override async restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk = true): Promise<void> {
940
this.updateCellDiffInfo([], undefined);
941
this._stateObs.set(snapshot.state, undefined);
942
restoreSnapshot(this.originalModel, snapshot.original);
943
if (restoreToDisk) {
944
this.restoreSnapshotInModifiedModel(snapshot.current);
945
}
946
this.initializeModelsFromDiff();
947
}
948
949
override async resetToInitialContent(): Promise<void> {
950
this.updateCellDiffInfo([], undefined);
951
this.restoreSnapshotInModifiedModel(this.initialContent);
952
this.initializeModelsFromDiff();
953
}
954
955
public restoreModifiedModelFromSnapshot(snapshot: string) {
956
this.restoreSnapshotInModifiedModel(snapshot);
957
return this.initializeModelsFromDiff();
958
}
959
960
private restoreSnapshotInModifiedModel(snapshot: string) {
961
if (snapshot === createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService)) {
962
return;
963
}
964
965
this._applyEditsSync(() => {
966
// See private _setDocValue in chatEditingModifiedDocumentEntry.ts
967
this.modifiedModel.pushStackElement();
968
restoreSnapshot(this.modifiedModel, snapshot);
969
this.modifiedModel.pushStackElement();
970
});
971
}
972
973
private readonly cellTextModelMap = new ResourceMap<ITextModel>();
974
975
private async resolveCellModel(cellURI: URI): Promise<ITextModel> {
976
const cell = this.originalModel.cells.concat(this.modifiedModel.cells).find(cell => isEqual(cell.uri, cellURI));
977
if (!cell) {
978
throw new Error('Cell not found');
979
}
980
const model = this.cellTextModelMap.get(cell.uri);
981
if (model) {
982
this.cellTextModelMap.set(cell.uri, model);
983
return model;
984
} else {
985
const textEditorModel = await thenRegisterOrDispose(this.textModelService.createModelReference(cell.uri), this._store);
986
const model = textEditorModel.object.textEditorModel;
987
this.cellTextModelMap.set(cell.uri, model);
988
return model;
989
}
990
}
991
992
getOrCreateModifiedTextFileEntryForCell(cell: NotebookCellTextModel, modifiedCellModel: ITextModel, originalCellModel: ITextModel): ChatEditingNotebookCellEntry | undefined {
993
let cellEntry = this.cellEntryMap.get(cell.uri);
994
if (cellEntry) {
995
return cellEntry;
996
}
997
if (this._store.isDisposed) {
998
return;
999
}
1000
const disposables = new DisposableStore();
1001
cellEntry = this._register(this._instantiationService.createInstance(ChatEditingNotebookCellEntry, this.modifiedResourceRef.object.resource, cell, modifiedCellModel, originalCellModel, () => this._isExternalEditInProgress, disposables));
1002
this.cellEntryMap.set(cell.uri, cellEntry);
1003
disposables.add(autorun(r => {
1004
if (this.modifiedModel.cells.indexOf(cell) === -1) {
1005
return;
1006
}
1007
const diffs = this.cellsDiffInfo.read(undefined).slice();
1008
const index = this.modifiedModel.cells.indexOf(cell);
1009
let entry = diffs.find(entry => entry.modifiedCellIndex === index);
1010
if (!entry) {
1011
// Not possible.
1012
return;
1013
}
1014
const entryIndex = diffs.indexOf(entry);
1015
entry.diff.set(cellEntry.diffInfo.read(r), undefined);
1016
if (cellEntry.diffInfo.read(undefined).identical && entry.type === 'modified') {
1017
entry = {
1018
...entry,
1019
type: 'unchanged',
1020
};
1021
}
1022
if (!cellEntry.diffInfo.read(undefined).identical && entry.type === 'unchanged') {
1023
entry = {
1024
...entry,
1025
type: 'modified',
1026
};
1027
}
1028
diffs.splice(entryIndex, 1, { ...entry });
1029
1030
transaction(tx => {
1031
this.updateCellDiffInfo(diffs, tx);
1032
});
1033
}));
1034
1035
disposables.add(autorun(r => {
1036
if (this.modifiedModel.cells.indexOf(cell) === -1) {
1037
return;
1038
}
1039
1040
const cellState = cellEntry.state.read(r);
1041
if (cellState === ModifiedFileEntryState.Accepted) {
1042
this.computeStateAfterAcceptingRejectingChanges(true);
1043
} else if (cellState === ModifiedFileEntryState.Rejected) {
1044
this.computeStateAfterAcceptingRejectingChanges(false);
1045
}
1046
}));
1047
1048
return cellEntry;
1049
}
1050
1051
async computeEditsFromSnapshots(beforeSnapshot: string, afterSnapshot: string): Promise<(TextEdit | ICellEditOperation)[]> {
1052
// For notebooks, we restore the snapshot and compute the cell-level edits
1053
// This is a simplified approach that replaces cells as needed
1054
1055
const beforeData = deserializeSnapshot(beforeSnapshot);
1056
const afterData = deserializeSnapshot(afterSnapshot);
1057
1058
const edits: ICellEditOperation[] = [];
1059
1060
// Simple approach: replace all cells
1061
// A more sophisticated approach would diff individual cells
1062
if (beforeData.data.cells.length > 0) {
1063
edits.push({
1064
editType: CellEditType.Replace,
1065
index: 0,
1066
count: beforeData.data.cells.length,
1067
cells: afterData.data.cells
1068
});
1069
} else if (afterData.data.cells.length > 0) {
1070
edits.push({
1071
editType: CellEditType.Replace,
1072
index: 0,
1073
count: 0,
1074
cells: afterData.data.cells
1075
});
1076
}
1077
1078
return edits;
1079
}
1080
1081
async save(): Promise<void> {
1082
if (this.modifiedModel.uri.scheme === Schemas.untitled) {
1083
return;
1084
}
1085
1086
// Save the notebook if dirty
1087
if (this.notebookResolver.isDirty(this.modifiedModel.uri)) {
1088
await this.modifiedResourceRef.object.save({
1089
reason: SaveReason.EXPLICIT,
1090
skipSaveParticipants: true
1091
});
1092
}
1093
}
1094
1095
async revertToDisk(): Promise<void> {
1096
if (this.modifiedModel.uri.scheme === Schemas.untitled) {
1097
return;
1098
}
1099
1100
// Revert to reload from disk
1101
await this.modifiedResourceRef.object.revert({ soft: false });
1102
}
1103
}
1104
1105
1106
function generateCellHash(cellUri: URI) {
1107
const hash = new StringSHA1();
1108
hash.update(cellUri.toString());
1109
return hash.digest().substring(0, 8);
1110
}
1111
1112