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