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