Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.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 * as dom from '../../../../../base/browser/dom.js';
7
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { Disposable, IDisposable, IReference, RefCountedDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
10
import { Schemas } from '../../../../../base/common/network.js';
11
import { isEqual } from '../../../../../base/common/resources.js';
12
import { assertType } from '../../../../../base/common/types.js';
13
import { URI } from '../../../../../base/common/uri.js';
14
import { generateUuid } from '../../../../../base/common/uuid.js';
15
import { ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js';
16
import { TextEdit } from '../../../../../editor/common/languages.js';
17
import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js';
18
import { IModelService } from '../../../../../editor/common/services/model.js';
19
import { DefaultModelSHA1Computer } from '../../../../../editor/common/services/modelService.js';
20
import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';
21
import { localize } from '../../../../../nls.js';
22
import { MenuId } from '../../../../../platform/actions/common/actions.js';
23
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
24
import { createDecorator, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
25
import { IChatListItemRendererOptions } from '../chat.js';
26
import { IDisposableReference, ResourcePool } from './chatCollections.js';
27
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
28
import { IChatRendererDelegate } from '../chatListRenderer.js';
29
import { ChatEditorOptions } from '../chatOptions.js';
30
import { CodeCompareBlockPart, ICodeCompareBlockData, ICodeCompareBlockDiffData } from '../codeBlockPart.js';
31
import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from '../../common/chatModel.js';
32
import { IChatService } from '../../common/chatService.js';
33
import { IChatResponseViewModel, isResponseVM } from '../../common/chatViewModel.js';
34
35
const $ = dom.$;
36
37
const ICodeCompareModelService = createDecorator<ICodeCompareModelService>('ICodeCompareModelService');
38
39
interface ICodeCompareModelService {
40
_serviceBrand: undefined;
41
createModel(response: IChatResponseViewModel, chatTextEdit: IChatTextEditGroup): Promise<IReference<{ originalSha1: string; original: IResolvedTextEditorModel; modified: IResolvedTextEditorModel }>>;
42
}
43
44
export class ChatTextEditContentPart extends Disposable implements IChatContentPart {
45
public readonly domNode: HTMLElement;
46
private readonly comparePart: IDisposableReference<CodeCompareBlockPart> | undefined;
47
48
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
49
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
50
51
constructor(
52
chatTextEdit: IChatTextEditGroup,
53
context: IChatContentPartRenderContext,
54
rendererOptions: IChatListItemRendererOptions,
55
diffEditorPool: DiffEditorPool,
56
currentWidth: number,
57
@ICodeCompareModelService private readonly codeCompareModelService: ICodeCompareModelService
58
) {
59
super();
60
const element = context.element;
61
62
assertType(isResponseVM(element));
63
64
// TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen
65
if (rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) {
66
if (element.response.value.every(item => item.kind === 'textEditGroup')) {
67
this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete
68
? ''
69
: element.isCanceled
70
? localize('edits0', "Making changes was aborted.")
71
: localize('editsSummary', "Made changes."));
72
} else {
73
this.domNode = $('div');
74
}
75
76
// TODO@roblourens this case is now handled outside this Part in ChatListRenderer, but can it be cleaned up?
77
// return;
78
} else {
79
80
81
const cts = new CancellationTokenSource();
82
83
let isDisposed = false;
84
this._register(toDisposable(() => {
85
isDisposed = true;
86
cts.dispose(true);
87
}));
88
89
this.comparePart = this._register(diffEditorPool.get());
90
91
// Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping)
92
// not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render)
93
this._register(this.comparePart.object.onDidChangeContentHeight(() => {
94
this._onDidChangeHeight.fire();
95
}));
96
97
const data: ICodeCompareBlockData = {
98
element,
99
edit: chatTextEdit,
100
diffData: (async () => {
101
102
const ref = await this.codeCompareModelService.createModel(element, chatTextEdit);
103
104
if (isDisposed) {
105
ref.dispose();
106
return;
107
}
108
109
this._register(ref);
110
111
return {
112
modified: ref.object.modified.textEditorModel,
113
original: ref.object.original.textEditorModel,
114
originalSha1: ref.object.originalSha1
115
} satisfies ICodeCompareBlockDiffData;
116
})()
117
};
118
this.comparePart.object.render(data, currentWidth, cts.token);
119
120
this.domNode = this.comparePart.object.element;
121
}
122
}
123
124
layout(width: number): void {
125
this.comparePart?.object.layout(width);
126
}
127
128
hasSameContent(other: IChatProgressRenderableResponseContent): boolean {
129
// No other change allowed for this content type
130
return other.kind === 'textEditGroup';
131
}
132
133
addDisposable(disposable: IDisposable): void {
134
this._register(disposable);
135
}
136
}
137
138
export class DiffEditorPool extends Disposable {
139
140
private readonly _pool: ResourcePool<CodeCompareBlockPart>;
141
142
public inUse(): Iterable<CodeCompareBlockPart> {
143
return this._pool.inUse;
144
}
145
146
constructor(
147
options: ChatEditorOptions,
148
delegate: IChatRendererDelegate,
149
overflowWidgetsDomNode: HTMLElement | undefined,
150
private readonly isSimpleWidget: boolean = false,
151
@IInstantiationService instantiationService: IInstantiationService,
152
) {
153
super();
154
this._pool = this._register(new ResourcePool(() => {
155
return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode, this.isSimpleWidget);
156
}));
157
}
158
159
get(): IDisposableReference<CodeCompareBlockPart> {
160
const codeBlock = this._pool.get();
161
let stale = false;
162
return {
163
object: codeBlock,
164
isStale: () => stale,
165
dispose: () => {
166
codeBlock.reset();
167
stale = true;
168
this._pool.release(codeBlock);
169
}
170
};
171
}
172
}
173
174
class CodeCompareModelService implements ICodeCompareModelService {
175
176
declare readonly _serviceBrand: undefined;
177
178
constructor(
179
@ITextModelService private readonly textModelService: ITextModelService,
180
@IModelService private readonly modelService: IModelService,
181
@IChatService private readonly chatService: IChatService,
182
) { }
183
184
async createModel(element: IChatResponseViewModel, chatTextEdit: IChatTextEditGroup): Promise<IReference<{ originalSha1: string; original: IResolvedTextEditorModel; modified: IResolvedTextEditorModel }>> {
185
186
const original = await this.textModelService.createModelReference(chatTextEdit.uri);
187
188
const modified = await this.textModelService.createModelReference((this.modelService.createModel(
189
createTextBufferFactoryFromSnapshot(original.object.textEditorModel.createSnapshot()),
190
{ languageId: original.object.textEditorModel.getLanguageId(), onDidChange: Event.None },
191
URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: chatTextEdit.uri.path, query: generateUuid() }),
192
false
193
)).uri);
194
195
const d = new RefCountedDisposable(toDisposable(() => {
196
original.dispose();
197
modified.dispose();
198
}));
199
200
// compute the sha1 of the original model
201
let originalSha1: string = '';
202
if (chatTextEdit.state) {
203
originalSha1 = chatTextEdit.state.sha1;
204
} else {
205
const sha1 = new DefaultModelSHA1Computer();
206
if (sha1.canComputeSHA1(original.object.textEditorModel)) {
207
originalSha1 = sha1.computeSHA1(original.object.textEditorModel);
208
chatTextEdit.state = { sha1: originalSha1, applied: 0 };
209
}
210
}
211
212
// apply edits to the "modified" model
213
const chatModel = this.chatService.getSession(element.sessionId)!;
214
const editGroups: ISingleEditOperation[][] = [];
215
for (const request of chatModel.getRequests()) {
216
if (!request.response) {
217
continue;
218
}
219
for (const item of request.response.response.value) {
220
if (item.kind !== 'textEditGroup' || item.state?.applied || !isEqual(item.uri, chatTextEdit.uri)) {
221
continue;
222
}
223
for (const group of item.edits) {
224
const edits = group.map(TextEdit.asEditOperation);
225
editGroups.push(edits);
226
}
227
}
228
if (request.response === element.model) {
229
break;
230
}
231
}
232
for (const edits of editGroups) {
233
modified.object.textEditorModel.pushEditOperations(null, edits, () => null);
234
}
235
236
// self-acquire a reference to diff models for a short while
237
// because streaming usually means we will be using the original-model
238
// repeatedly and thereby also should reuse the modified-model and just
239
// update it with more edits
240
d.acquire();
241
setTimeout(() => d.release(), 5000);
242
243
return {
244
object: {
245
originalSha1,
246
original: original.object,
247
modified: modified.object
248
},
249
dispose() {
250
d.release();
251
},
252
};
253
}
254
}
255
256
registerSingleton(ICodeCompareModelService, CodeCompareModelService, InstantiationType.Delayed);
257
258