Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts
5251 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 { IReference, MutableDisposable } from '../../../../../base/common/lifecycle.js';
7
import { Schemas } from '../../../../../base/common/network.js';
8
import { ITransaction, autorun, transaction } from '../../../../../base/common/observable.js';
9
import { assertType } from '../../../../../base/common/types.js';
10
import { URI } from '../../../../../base/common/uri.js';
11
import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
12
import { TextEdit as EditorTextEdit } from '../../../../../editor/common/core/edits/textEdit.js';
13
import { StringText } from '../../../../../editor/common/core/text/abstractText.js';
14
import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';
15
import { TextEdit } from '../../../../../editor/common/languages.js';
16
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
17
import { ITextModel } from '../../../../../editor/common/model.js';
18
import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js';
19
import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js';
20
import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';
21
import { IModelService } from '../../../../../editor/common/services/model.js';
22
import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';
23
import { localize } from '../../../../../nls.js';
24
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
25
import { IFileService } from '../../../../../platform/files/common/files.js';
26
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
27
import { IMarkerService } from '../../../../../platform/markers/common/markers.js';
28
import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
29
import { IEditorPane, SaveReason } from '../../../../common/editor.js';
30
import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';
31
import { ITextFileService, isTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js';
32
import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';
33
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
34
import { IChatService } from '../../common/chatService/chatService.js';
35
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
36
import { IChatResponseModel } from '../../common/model/chatModel.js';
37
import { ChatEditingCodeEditorIntegration } from './chatEditingCodeEditorIntegration.js';
38
import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';
39
import { ChatEditingTextModelChangeService } from './chatEditingTextModelChangeService.js';
40
import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
41
42
interface IMultiDiffEntryDelegate {
43
collapse: (transaction: ITransaction | undefined) => void;
44
}
45
46
47
export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifiedFileEntry implements IModifiedFileEntry {
48
49
readonly initialContent: string;
50
51
readonly originalModel: ITextModel;
52
readonly modifiedModel: ITextModel;
53
54
private readonly _docFileEditorModel: IResolvedTextEditorModel;
55
56
override get changesCount() {
57
return this._textModelChangeService.diffInfo.map(diff => diff.changes.length);
58
}
59
60
get diffInfo() {
61
return this._textModelChangeService.diffInfo;
62
}
63
64
get linesAdded() {
65
return this._textModelChangeService.diffInfo.map(diff => {
66
let added = 0;
67
for (const c of diff.changes) {
68
added += Math.max(0, c.modified.endLineNumberExclusive - c.modified.startLineNumber);
69
}
70
return added;
71
});
72
}
73
get linesRemoved() {
74
return this._textModelChangeService.diffInfo.map(diff => {
75
let removed = 0;
76
for (const c of diff.changes) {
77
removed += Math.max(0, c.original.endLineNumberExclusive - c.original.startLineNumber);
78
}
79
return removed;
80
});
81
}
82
83
readonly originalURI: URI;
84
private readonly _textModelChangeService: ChatEditingTextModelChangeService;
85
86
constructor(
87
resourceRef: IReference<IResolvedTextEditorModel>,
88
private readonly _multiDiffEntryDelegate: IMultiDiffEntryDelegate,
89
telemetryInfo: IModifiedEntryTelemetryInfo,
90
kind: ChatEditKind,
91
initialContent: string | undefined,
92
@IMarkerService markerService: IMarkerService,
93
@IModelService modelService: IModelService,
94
@ITextModelService textModelService: ITextModelService,
95
@ILanguageService languageService: ILanguageService,
96
@IConfigurationService configService: IConfigurationService,
97
@IFilesConfigurationService fileConfigService: IFilesConfigurationService,
98
@IChatService chatService: IChatService,
99
@ITextFileService private readonly _textFileService: ITextFileService,
100
@IFileService fileService: IFileService,
101
@IUndoRedoService undoRedoService: IUndoRedoService,
102
@IInstantiationService instantiationService: IInstantiationService,
103
@IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService,
104
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
105
) {
106
super(
107
resourceRef.object.textEditorModel.uri,
108
telemetryInfo,
109
kind,
110
configService,
111
fileConfigService,
112
chatService,
113
fileService,
114
undoRedoService,
115
instantiationService,
116
aiEditTelemetryService,
117
);
118
119
this._docFileEditorModel = this._register(resourceRef).object;
120
this.modifiedModel = resourceRef.object.textEditorModel;
121
this.originalURI = ChatEditingTextModelContentProvider.getFileURI(telemetryInfo.sessionResource, this.entryId, this.modifiedURI.path);
122
123
this.initialContent = initialContent ?? this.modifiedModel.getValue();
124
const docSnapshot = this.originalModel = this._register(
125
modelService.createModel(
126
createTextBufferFactoryFromSnapshot(initialContent !== undefined ? stringToSnapshot(initialContent) : this.modifiedModel.createSnapshot()),
127
languageService.createById(this.modifiedModel.getLanguageId()),
128
this.originalURI,
129
false
130
)
131
);
132
133
this._textModelChangeService = this._register(instantiationService.createInstance(ChatEditingTextModelChangeService,
134
this.originalModel, this.modifiedModel, this._stateObs, () => this._isExternalEditInProgress));
135
136
this._register(this._textModelChangeService.onDidAcceptOrRejectAllHunks(action => {
137
this._stateObs.set(action, undefined);
138
this._notifySessionAction(action === ModifiedFileEntryState.Accepted ? 'accepted' : 'rejected');
139
}));
140
141
this._register(this._textModelChangeService.onDidAcceptOrRejectLines(action => {
142
this._notifyAction({
143
kind: 'chatEditingHunkAction',
144
uri: this.modifiedURI,
145
outcome: action.state,
146
languageId: this.modifiedModel.getLanguageId(),
147
...action
148
});
149
}));
150
151
// Create a reference to this model to avoid it being disposed from under our nose
152
(async () => {
153
const reference = await textModelService.createModelReference(docSnapshot.uri);
154
if (this._store.isDisposed) {
155
reference.dispose();
156
return;
157
}
158
this._register(reference);
159
})();
160
161
162
this._register(this._textModelChangeService.onDidUserEditModel(() => {
163
this._userEditScheduler.schedule();
164
const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent;
165
if (this._stateObs.get() === ModifiedFileEntryState.Modified && didResetToOriginalContent) {
166
this._stateObs.set(ModifiedFileEntryState.Rejected, undefined);
167
}
168
}));
169
170
const resourceFilter = this._register(new MutableDisposable());
171
this._register(autorun(r => {
172
const inProgress = this._waitsForLastEdits.read(r);
173
if (inProgress) {
174
const res = this._lastModifyingResponseObs.read(r);
175
const req = res && res.session.getRequests().find(value => value.id === res.requestId);
176
resourceFilter.value = markerService.installResourceFilter(this.modifiedURI, req?.message.text || localize('default', "Chat Edits"));
177
} else {
178
resourceFilter.clear();
179
}
180
}));
181
}
182
183
getDiffInfo(): Promise<IDocumentDiff> {
184
return this._textModelChangeService.getDiffInfo();
185
}
186
187
equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean {
188
return !!snapshot &&
189
this.modifiedURI.toString() === snapshot.resource.toString() &&
190
this.modifiedModel.getLanguageId() === snapshot.languageId &&
191
this.originalModel.getValue() === snapshot.original &&
192
this.modifiedModel.getValue() === snapshot.current &&
193
this.state.get() === snapshot.state;
194
}
195
196
createSnapshot(chatSessionResource: URI, requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry {
197
return {
198
resource: this.modifiedURI,
199
languageId: this.modifiedModel.getLanguageId(),
200
snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(chatSessionResource, requestId, undoStop, this.modifiedURI.path),
201
original: this.originalModel.getValue(),
202
current: this.modifiedModel.getValue(),
203
state: this.state.get(),
204
telemetryInfo: this._telemetryInfo
205
};
206
}
207
208
public getCurrentContents() {
209
return this.modifiedModel.getValue();
210
}
211
212
async restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk = true) {
213
this._stateObs.set(snapshot.state, undefined);
214
await this._textModelChangeService.resetDocumentValues(snapshot.original, restoreToDisk ? snapshot.current : undefined);
215
}
216
217
async resetToInitialContent() {
218
await this._textModelChangeService.resetDocumentValues(undefined, this.initialContent);
219
}
220
221
protected override async _areOriginalAndModifiedIdentical(): Promise<boolean> {
222
return this._textModelChangeService.areOriginalAndModifiedIdentical();
223
}
224
225
protected override _resetEditsState(tx: ITransaction): void {
226
super._resetEditsState(tx);
227
this._textModelChangeService.clearCurrentEditLineDecoration();
228
}
229
230
protected override _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement {
231
const request = response.session.getRequests().find(req => req.id === response.requestId);
232
const label = request?.message.text ? localize('chatEditing1', "Chat Edit: '{0}'", request.message.text) : localize('chatEditing2', "Chat Edit");
233
return new SingleModelEditStackElement(label, 'chat.edit', this.modifiedModel, null);
234
}
235
236
async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel | undefined): Promise<void> {
237
238
const result = await this._textModelChangeService.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel);
239
240
transaction((tx) => {
241
this._waitsForLastEdits.set(!isLastEdits, tx);
242
this._stateObs.set(ModifiedFileEntryState.Modified, tx);
243
244
if (!isLastEdits) {
245
this._rewriteRatioObs.set(result.rewriteRatio, tx);
246
} else {
247
this._resetEditsState(tx);
248
this._rewriteRatioObs.set(1, tx);
249
}
250
});
251
if (isLastEdits && this._shouldAutoSave()) {
252
await this._textFileService.save(this.modifiedModel.uri, {
253
reason: SaveReason.AUTO,
254
skipSaveParticipants: true,
255
});
256
}
257
}
258
259
260
protected override async _doAccept(): Promise<void> {
261
this._textModelChangeService.keep();
262
this._multiDiffEntryDelegate.collapse(undefined);
263
264
const config = this._fileConfigService.getAutoSaveConfiguration(this.modifiedURI);
265
if (!config.autoSave || !this._textFileService.isDirty(this.modifiedURI)) {
266
// SAVE after accept for manual-savers, for auto-savers
267
// trigger explict save to get save participants going
268
try {
269
await this._textFileService.save(this.modifiedURI, {
270
reason: SaveReason.EXPLICIT,
271
force: true,
272
ignoreErrorHandler: true
273
});
274
} catch {
275
// ignored
276
}
277
}
278
}
279
280
protected override async _doReject(): Promise<void> {
281
if (this.createdInRequestId === this._telemetryInfo.requestId) {
282
if (isTextFileEditorModel(this._docFileEditorModel)) {
283
await this._docFileEditorModel.revert({ soft: true });
284
await this._fileService.del(this.modifiedURI).catch(err => {
285
// don't block if file is already deleted
286
});
287
}
288
this._onDidDelete.fire();
289
} else {
290
this._textModelChangeService.undo();
291
if (this._textModelChangeService.allEditsAreFromUs && isTextFileEditorModel(this._docFileEditorModel) && this._shouldAutoSave()) {
292
// save the file after discarding so that the dirty indicator goes away
293
// and so that an intermediate saved state gets reverted
294
await this._docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true });
295
}
296
this._multiDiffEntryDelegate.collapse(undefined);
297
}
298
}
299
300
protected _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration {
301
const codeEditor = getCodeEditor(editor.getControl());
302
assertType(codeEditor);
303
304
const diffInfo = this._textModelChangeService.diffInfo;
305
306
return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, this, codeEditor, diffInfo, false);
307
}
308
309
private _shouldAutoSave() {
310
return this.modifiedURI.scheme !== Schemas.untitled;
311
}
312
313
async computeEditsFromSnapshots(beforeSnapshot: string, afterSnapshot: string): Promise<(TextEdit | ICellEditOperation)[]> {
314
const stringEdit = await this._editorWorkerService.computeStringEditFromDiff(
315
beforeSnapshot,
316
afterSnapshot,
317
{ maxComputationTimeMs: 5000 },
318
'advanced'
319
);
320
321
const editorTextEdit = EditorTextEdit.fromStringEdit(stringEdit, new StringText(beforeSnapshot));
322
return editorTextEdit.replacements.slice();
323
}
324
325
async save(): Promise<void> {
326
if (this.modifiedModel.uri.scheme === Schemas.untitled) {
327
return;
328
}
329
330
// Save the current model state to disk if dirty
331
if (this._textFileService.isDirty(this.modifiedModel.uri)) {
332
await this._textFileService.save(this.modifiedModel.uri, {
333
reason: SaveReason.EXPLICIT,
334
skipSaveParticipants: true
335
});
336
}
337
}
338
339
async revertToDisk(): Promise<void> {
340
if (this.modifiedModel.uri.scheme === Schemas.untitled) {
341
return;
342
}
343
344
// Revert to reload from disk, ensuring in-memory model matches disk
345
const fileModel = this._textFileService.files.get(this.modifiedModel.uri);
346
if (fileModel && !fileModel.isDisposed()) {
347
await fileModel.revert({ soft: false });
348
}
349
}
350
}
351
352