Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingDeletedFileEntry.ts
5241 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 { VSBuffer } from '../../../../../base/common/buffer.js';
7
import { constObservable, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { LineRange } from '../../../../../editor/common/core/ranges/lineRange.js';
10
import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';
11
import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js';
12
import { TextEdit } from '../../../../../editor/common/languages.js';
13
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
14
import { ITextModel } from '../../../../../editor/common/model.js';
15
import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js';
16
import { IModelService } from '../../../../../editor/common/services/model.js';
17
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
18
import { IFileService } from '../../../../../platform/files/common/files.js';
19
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
20
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../../platform/undoRedo/common/undoRedo.js';
21
import { IEditorPane } from '../../../../common/editor.js';
22
import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';
23
import { stringToSnapshot } from '../../../../services/textfile/common/textfiles.js';
24
import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';
25
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
26
import { IChatService } from '../../common/chatService/chatService.js';
27
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
28
import { IChatResponseModel } from '../../common/model/chatModel.js';
29
import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';
30
import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
31
32
interface IMultiDiffEntryDelegate {
33
collapse: (transaction: ITransaction | undefined) => void;
34
}
35
36
/**
37
* Represents a file that has been deleted by the chat editing session.
38
* Unlike ChatEditingModifiedDocumentEntry, this doesn't maintain a live model
39
* since the file no longer exists on disk.
40
*/
41
export class ChatEditingDeletedFileEntry extends AbstractChatEditingModifiedFileEntry implements IModifiedFileEntry {
42
43
readonly initialContent: string;
44
45
/**
46
* The original content before deletion, stored for diff display and potential restoration.
47
*/
48
private readonly _originalContent: string;
49
50
/**
51
* Lazily created model for the original content (for diff display).
52
*/
53
private _originalModel: ITextModel | undefined;
54
55
/**
56
* Lazily created empty model representing the deleted state (for diff display).
57
*/
58
private _modifiedModel: ITextModel | undefined;
59
60
readonly originalURI: URI;
61
62
readonly diffInfo: IObservable<IDocumentDiff>;
63
readonly linesAdded: IObservable<number> = constObservable(0);
64
readonly linesRemoved: IObservable<number>;
65
66
private readonly _changesCount = observableValue<number>(this, 1);
67
override readonly changesCount = this._changesCount;
68
readonly isDeletion = true;
69
70
constructor(
71
resource: URI,
72
originalContent: string,
73
private readonly _multiDiffEntryDelegate: IMultiDiffEntryDelegate,
74
telemetryInfo: IModifiedEntryTelemetryInfo,
75
private readonly _languageId: string,
76
@IModelService private readonly _modelService: IModelService,
77
@ILanguageService private readonly _languageService: ILanguageService,
78
@IConfigurationService configService: IConfigurationService,
79
@IFilesConfigurationService fileConfigService: IFilesConfigurationService,
80
@IChatService chatService: IChatService,
81
@IFileService fileService: IFileService,
82
@IUndoRedoService undoRedoService: IUndoRedoService,
83
@IInstantiationService instantiationService: IInstantiationService,
84
@IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService,
85
) {
86
super(
87
resource,
88
telemetryInfo,
89
ChatEditKind.Deleted,
90
configService,
91
fileConfigService,
92
chatService,
93
fileService,
94
undoRedoService,
95
instantiationService,
96
aiEditTelemetryService,
97
);
98
99
this._originalContent = originalContent;
100
this.initialContent = originalContent;
101
this.originalURI = ChatEditingTextModelContentProvider.getFileURI(telemetryInfo.sessionResource, this.entryId, resource.path);
102
this.diffInfo = constObservable(this._diffInfo());
103
this.linesRemoved = constObservable(this._getOrCreateOriginalModel().getLineCount());
104
}
105
106
override dispose(): void {
107
this._originalModel?.dispose();
108
this._modifiedModel?.dispose();
109
super.dispose();
110
}
111
112
/**
113
* Gets or creates the original model for diff display.
114
*/
115
private _getOrCreateOriginalModel(): ITextModel {
116
if (!this._originalModel || this._originalModel.isDisposed()) {
117
this._originalModel = this._modelService.createModel(
118
createTextBufferFactoryFromSnapshot(stringToSnapshot(this._originalContent)),
119
this._languageService.createById(this._languageId),
120
this.originalURI,
121
false
122
);
123
}
124
return this._originalModel;
125
}
126
127
/**
128
* Gets or creates an empty model representing the deleted state.
129
*/
130
private _getOrCreateModifiedModel(): ITextModel {
131
if (!this._modifiedModel || this._modifiedModel.isDisposed()) {
132
// Create empty model - file is deleted so content is empty
133
this._modifiedModel = this._modelService.createModel(
134
'',
135
this._languageService.createById(this._languageId),
136
this.modifiedURI.with({ scheme: 'deleted-file' }),
137
false
138
);
139
}
140
return this._modifiedModel;
141
}
142
143
private _diffInfo() {
144
// For deleted files, return a simple diff showing all content removed
145
const originalModel = this._getOrCreateOriginalModel();
146
this._getOrCreateModifiedModel(); // Ensure the modified model exists for the diff view
147
const originalLineCount = originalModel.getLineCount();
148
149
return {
150
changes: [new DetailedLineRangeMapping(
151
new LineRange(1, originalLineCount + 1),
152
new LineRange(1, 1),
153
undefined
154
)],
155
quitEarly: false,
156
identical: false,
157
moves: []
158
};
159
}
160
161
getDiffInfo(): Promise<IDocumentDiff> {
162
return Promise.resolve(this._diffInfo());
163
}
164
165
equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean {
166
return !!snapshot &&
167
this.modifiedURI.toString() === snapshot.resource.toString() &&
168
this._languageId === snapshot.languageId &&
169
this._originalContent === snapshot.original &&
170
snapshot.current === '' &&
171
this.state.get() === snapshot.state;
172
}
173
174
createSnapshot(chatSessionResource: URI, requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry {
175
return {
176
resource: this.modifiedURI,
177
languageId: this._languageId,
178
snapshotUri: this.originalURI,
179
original: this._originalContent,
180
current: '', // File is deleted, so current content is empty
181
state: this.state.get(),
182
telemetryInfo: this._telemetryInfo,
183
isDeleted: true,
184
};
185
}
186
187
async restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk = true): Promise<void> {
188
this._stateObs.set(snapshot.state, undefined);
189
190
if (restoreToDisk && snapshot.current !== '') {
191
// Restore file to disk with the snapshot content
192
await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(snapshot.current));
193
}
194
}
195
196
async resetToInitialContent(): Promise<void> {
197
// Restore the file with original content
198
await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(this._originalContent));
199
}
200
201
protected override async _areOriginalAndModifiedIdentical(): Promise<boolean> {
202
// A deleted file is never identical to its original (unless original was empty)
203
return this._originalContent === '';
204
}
205
206
protected override _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement {
207
return {
208
type: UndoRedoElementType.Resource,
209
resource: this.modifiedURI,
210
label: 'Chat File Deletion',
211
code: 'chat.delete',
212
undo: async () => {
213
// Restore the file
214
await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(this._originalContent));
215
},
216
redo: async () => {
217
// Delete the file again
218
await this._fileService.del(this.modifiedURI, { useTrash: false });
219
}
220
};
221
}
222
223
async acceptAgentEdits(_uri: URI, _edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, _responseModel: IChatResponseModel | undefined): Promise<void> {
224
// For deleted files, there are no incremental edits - the file is just deleted
225
transaction((tx) => {
226
this._waitsForLastEdits.set(!isLastEdits, tx);
227
this._stateObs.set(ModifiedFileEntryState.Modified, tx);
228
229
if (isLastEdits) {
230
this._resetEditsState(tx);
231
this._rewriteRatioObs.set(1, tx);
232
}
233
});
234
}
235
236
protected override async _doAccept(): Promise<void> {
237
// File deletion is already done - just collapse the entry
238
this._multiDiffEntryDelegate.collapse(undefined);
239
}
240
241
protected override async _doReject(): Promise<void> {
242
// Restore the file from original content
243
await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(this._originalContent));
244
this._multiDiffEntryDelegate.collapse(undefined);
245
}
246
247
protected _createEditorIntegration(_editor: IEditorPane): IModifiedFileEntryEditorIntegration {
248
// Deleted files don't need complex editor integration since there's nothing to navigate
249
return {
250
currentIndex: observableValue(this, 0),
251
reveal: () => { },
252
next: () => false,
253
previous: () => false,
254
enableAccessibleDiffView: () => { },
255
acceptNearestChange: async () => { },
256
rejectNearestChange: async () => { },
257
toggleDiff: async () => { },
258
dispose: () => { }
259
};
260
}
261
262
async computeEditsFromSnapshots(_beforeSnapshot: string, _afterSnapshot: string): Promise<(TextEdit | ICellEditOperation)[]> {
263
// For deleted files, we don't compute incremental edits
264
return [];
265
}
266
267
async save(): Promise<void> {
268
// Nothing to save - file is deleted
269
}
270
271
async revertToDisk(): Promise<void> {
272
// Nothing to revert - file is deleted
273
}
274
}
275
276