Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.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 { allowedMarkdownHtmlAttributes, MarkdownRendererMarkedOptions, type MarkdownRenderOptions } from '../../../../../base/browser/markdownRenderer.js';
8
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
9
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
10
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
11
import { coalesce } from '../../../../../base/common/arrays.js';
12
import { findLast } from '../../../../../base/common/arraysFind.js';
13
import { Codicon } from '../../../../../base/common/codicons.js';
14
import { Emitter } from '../../../../../base/common/event.js';
15
import { Lazy } from '../../../../../base/common/lazy.js';
16
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
17
import { autorun, IObservable } from '../../../../../base/common/observable.js';
18
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';
19
import { equalsIgnoreCase } from '../../../../../base/common/strings.js';
20
import { ThemeIcon } from '../../../../../base/common/themables.js';
21
import { URI } from '../../../../../base/common/uri.js';
22
import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
23
import { Range } from '../../../../../editor/common/core/range.js';
24
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
25
import { ITextModel } from '../../../../../editor/common/model.js';
26
import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js';
27
import { IModelService } from '../../../../../editor/common/services/model.js';
28
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
29
import { localize } from '../../../../../nls.js';
30
import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
31
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
32
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
33
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
34
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
35
import { FileKind } from '../../../../../platform/files/common/files.js';
36
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
37
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
38
import { ILabelService } from '../../../../../platform/label/common/label.js';
39
import { IEditorService } from '../../../../services/editor/common/editorService.js';
40
import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';
41
import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js';
42
import { MarkedKatexSupport } from '../../../markdown/browser/markedKatexSupport.js';
43
import { IMarkdownVulnerability } from '../../common/annotations.js';
44
import { IEditSessionEntryDiff } from '../../common/chatEditingService.js';
45
import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js';
46
import { IChatMarkdownContent, IChatService, IChatUndoStop } from '../../common/chatService.js';
47
import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js';
48
import { CodeBlockEntry, CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js';
49
import { ChatConfiguration } from '../../common/constants.js';
50
import { IChatCodeBlockInfo } from '../chat.js';
51
import { IChatRendererDelegate } from '../chatListRenderer.js';
52
import { ChatMarkdownDecorationsRenderer } from '../chatMarkdownDecorationsRenderer.js';
53
import { allowedChatMarkdownHtmlTags } from '../chatMarkdownRenderer.js';
54
import { ChatEditorOptions } from '../chatOptions.js';
55
import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions, localFileLanguageId, parseLocalFileData } from '../codeBlockPart.js';
56
import '../media/chatCodeBlockPill.css';
57
import { IDisposableReference, ResourcePool } from './chatCollections.js';
58
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
59
import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js';
60
import './media/chatMarkdownPart.css';
61
62
const $ = dom.$;
63
64
export interface IChatMarkdownContentPartOptions {
65
readonly codeBlockRenderOptions?: ICodeBlockRenderOptions;
66
}
67
68
export class ChatMarkdownContentPart extends Disposable implements IChatContentPart {
69
private static idPool = 0;
70
71
public readonly codeblocksPartId = String(++ChatMarkdownContentPart.idPool);
72
public readonly domNode: HTMLElement;
73
private readonly allRefs: IDisposableReference<CodeBlockPart | CollapsedCodeBlock>[] = [];
74
75
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
76
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
77
78
public readonly codeblocks: IChatCodeBlockInfo[] = [];
79
80
private readonly mathLayoutParticipants = new Set<() => void>();
81
82
private _isDisposed = false;
83
84
constructor(
85
private readonly markdown: IChatMarkdownContent,
86
context: IChatContentPartRenderContext,
87
private readonly editorPool: EditorPool,
88
fillInIncompleteTokens = false,
89
codeBlockStartIndex = 0,
90
renderer: MarkdownRenderer,
91
markdownRenderOptions: MarkdownRenderOptions | undefined,
92
currentWidth: number,
93
private readonly codeBlockModelCollection: CodeBlockModelCollection,
94
private readonly rendererOptions: IChatMarkdownContentPartOptions,
95
@IContextKeyService contextKeyService: IContextKeyService,
96
@IConfigurationService configurationService: IConfigurationService,
97
@ITextModelService private readonly textModelService: ITextModelService,
98
@IInstantiationService private readonly instantiationService: IInstantiationService,
99
@IAiEditTelemetryService private readonly aiEditTelemetryService: IAiEditTelemetryService,
100
) {
101
super();
102
103
const element = context.element;
104
const inUndoStop = (findLast(context.content, e => e.kind === 'undoStop', context.contentIndex) as IChatUndoStop | undefined)?.id;
105
106
// We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering
107
const orderedDisposablesList: IDisposable[] = [];
108
109
// Need to track the index of the codeblock within the response so it can have a unique ID,
110
// and within this part to find it within the codeblocks array
111
let globalCodeBlockIndexStart = codeBlockStartIndex;
112
let thisPartCodeBlockIndexStart = 0;
113
114
this.domNode = $('div.chat-markdown-part');
115
116
const enableMath = configurationService.getValue<boolean>(ChatConfiguration.EnableMath);
117
118
const doRenderMarkdown = () => {
119
if (this._isDisposed) {
120
return;
121
}
122
123
// TODO: Move katex support into chatMarkdownRenderer
124
const markedExtensions = enableMath
125
? coalesce([MarkedKatexSupport.getExtension(dom.getWindow(context.container), {
126
throwOnError: false
127
})])
128
: [];
129
130
// Don't set to 'false' for responses, respect defaults
131
const markedOpts: MarkdownRendererMarkedOptions = isRequestVM(element) ? {
132
gfm: true,
133
breaks: true,
134
} : {};
135
136
const result = this._register(renderer.render(markdown.content, {
137
sanitizerConfig: MarkedKatexSupport.getSanitizerOptions({
138
allowedTags: allowedChatMarkdownHtmlTags,
139
allowedAttributes: allowedMarkdownHtmlAttributes,
140
}),
141
fillInIncompleteTokens,
142
codeBlockRendererSync: (languageId, text, raw) => {
143
const isCodeBlockComplete = !isResponseVM(context.element) || context.element.isComplete || !raw || codeblockHasClosingBackticks(raw);
144
if ((!text || (text.startsWith('<vscode_codeblock_uri') && !text.includes('\n'))) && !isCodeBlockComplete) {
145
const hideEmptyCodeblock = $('div');
146
hideEmptyCodeblock.style.display = 'none';
147
return hideEmptyCodeblock;
148
}
149
if (languageId === 'vscode-extensions') {
150
const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') }));
151
this._register(chatExtensions.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
152
return chatExtensions.domNode;
153
}
154
const globalIndex = globalCodeBlockIndexStart++;
155
const thisPartIndex = thisPartCodeBlockIndexStart++;
156
let textModel: Promise<ITextModel>;
157
let range: Range | undefined;
158
let vulns: readonly IMarkdownVulnerability[] | undefined;
159
let codeblockEntry: CodeBlockEntry | undefined;
160
if (equalsIgnoreCase(languageId, localFileLanguageId)) {
161
try {
162
const parsedBody = parseLocalFileData(text);
163
range = parsedBody.range && Range.lift(parsedBody.range);
164
textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object.textEditorModel);
165
} catch (e) {
166
return $('div');
167
}
168
} else {
169
const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : '';
170
const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, globalIndex);
171
const fastUpdateModelEntry = this.codeBlockModelCollection.updateSync(sessionId, element, globalIndex, { text, languageId, isComplete: isCodeBlockComplete });
172
vulns = modelEntry.vulns;
173
codeblockEntry = fastUpdateModelEntry;
174
textModel = modelEntry.model;
175
}
176
177
const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered;
178
const renderOptions = {
179
...this.rendererOptions.codeBlockRenderOptions,
180
};
181
if (hideToolbar !== undefined) {
182
renderOptions.hideToolbar = hideToolbar;
183
}
184
const codeBlockInfo: ICodeBlockData = { languageId, textModel, codeBlockIndex: globalIndex, codeBlockPartIndex: thisPartIndex, element, range, parentContextKeyService: contextKeyService, vulns, codemapperUri: codeblockEntry?.codemapperUri, renderOptions, chatSessionId: element.sessionId };
185
186
if (element.isCompleteAddedRequest || !codeblockEntry?.codemapperUri || !codeblockEntry.isEdit) {
187
const ref = this.renderCodeBlock(codeBlockInfo, text, isCodeBlockComplete, currentWidth);
188
this.allRefs.push(ref);
189
190
// Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping)
191
// not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render)
192
this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire()));
193
194
const ownerMarkdownPartId = this.codeblocksPartId;
195
const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo {
196
readonly ownerMarkdownPartId = ownerMarkdownPartId;
197
readonly codeBlockIndex = globalIndex;
198
readonly elementId = element.id;
199
readonly isStreaming = false;
200
readonly chatSessionId = element.sessionId;
201
readonly languageId = languageId;
202
readonly editDeltaInfo = EditDeltaInfo.fromText(text);
203
codemapperUri = undefined; // will be set async
204
public get uri() {
205
// here we must do a getter because the ref.object is rendered
206
// async and the uri might be undefined when it's read immediately
207
return ref.object.uri;
208
}
209
readonly uriPromise = textModel.then(model => model.uri);
210
public focus() {
211
ref.object.focus();
212
}
213
}();
214
this.codeblocks.push(info);
215
orderedDisposablesList.push(ref);
216
return ref.object.element;
217
} else {
218
const requestId = isRequestVM(element) ? element.id : element.requestId;
219
const ref = this.renderCodeBlockPill(element.sessionId, requestId, inUndoStop, codeBlockInfo.codemapperUri, !isCodeBlockComplete);
220
if (isResponseVM(codeBlockInfo.element)) {
221
// TODO@joyceerhl: remove this code when we change the codeblockUri API to make the URI available synchronously
222
this.codeBlockModelCollection.update(codeBlockInfo.element.sessionId, codeBlockInfo.element, codeBlockInfo.codeBlockIndex, { text, languageId: codeBlockInfo.languageId, isComplete: isCodeBlockComplete }).then((e) => {
223
// Update the existing object's codemapperUri
224
this.codeblocks[codeBlockInfo.codeBlockPartIndex].codemapperUri = e.codemapperUri;
225
this._onDidChangeHeight.fire();
226
});
227
}
228
this.allRefs.push(ref);
229
const ownerMarkdownPartId = this.codeblocksPartId;
230
const info: IChatCodeBlockInfo = new class implements IChatCodeBlockInfo {
231
readonly ownerMarkdownPartId = ownerMarkdownPartId;
232
readonly codeBlockIndex = globalIndex;
233
readonly elementId = element.id;
234
readonly isStreaming = !isCodeBlockComplete;
235
readonly codemapperUri = codeblockEntry?.codemapperUri;
236
readonly chatSessionId = element.sessionId;
237
public get uri() {
238
return undefined;
239
}
240
readonly uriPromise = Promise.resolve(undefined);
241
public focus() {
242
return ref.object.element.focus();
243
}
244
readonly languageId = languageId;
245
readonly editDeltaInfo = EditDeltaInfo.fromText(text);
246
}();
247
this.codeblocks.push(info);
248
orderedDisposablesList.push(ref);
249
return ref.object.element;
250
}
251
},
252
asyncRenderCallback: () => this._onDidChangeHeight.fire(),
253
markedOptions: markedOpts,
254
markedExtensions,
255
...markdownRenderOptions,
256
}, this.domNode));
257
258
// Ideally this would happen earlier, but we need to parse the markdown.
259
if (isResponseVM(element) && !element.model.codeBlockInfos && element.model.isComplete) {
260
element.model.initializeCodeBlockInfos(this.codeblocks.map(info => {
261
return {
262
suggestionId: this.aiEditTelemetryService.createSuggestionId({
263
presentation: 'codeBlock',
264
feature: 'sideBarChat',
265
editDeltaInfo: info.editDeltaInfo,
266
languageId: info.languageId,
267
modeId: element.model.request?.modeInfo?.modeId,
268
modelId: element.model.request?.modelId,
269
applyCodeBlockSuggestionId: undefined,
270
})
271
};
272
}));
273
}
274
275
const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer);
276
this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element));
277
278
const layoutParticipants = new Lazy(() => {
279
const observer = new ResizeObserver(() => this.mathLayoutParticipants.forEach(layout => layout()));
280
observer.observe(this.domNode);
281
this._register(toDisposable(() => observer.disconnect()));
282
return this.mathLayoutParticipants;
283
});
284
285
// Make katex blocks horizontally scrollable
286
for (const katexBlock of this.domNode.querySelectorAll('.katex-display')) {
287
if (!dom.isHTMLElement(katexBlock)) {
288
continue;
289
}
290
291
const scrollable = new DomScrollableElement(katexBlock.cloneNode(true) as HTMLElement, {
292
vertical: ScrollbarVisibility.Hidden,
293
horizontal: ScrollbarVisibility.Auto,
294
});
295
orderedDisposablesList.push(scrollable);
296
katexBlock.replaceWith(scrollable.getDomNode());
297
298
layoutParticipants.value.add(() => { scrollable.scanDomNode(); });
299
scrollable.scanDomNode();
300
}
301
302
orderedDisposablesList.reverse().forEach(d => this._register(d));
303
};
304
305
if (enableMath && !MarkedKatexSupport.getExtension(dom.getWindow(context.container))) {
306
// Need to load async
307
MarkedKatexSupport.loadExtension(dom.getWindow(context.container))
308
.catch(e => {
309
console.error('Failed to load MarkedKatexSupport extension:', e);
310
}).finally(() => {
311
doRenderMarkdown();
312
if (!this._isDisposed) {
313
this._onDidChangeHeight.fire();
314
}
315
});
316
} else {
317
doRenderMarkdown();
318
}
319
}
320
321
override dispose(): void {
322
this._isDisposed = true;
323
super.dispose();
324
}
325
326
private renderCodeBlockPill(sessionId: string, requestId: string, inUndoStop: string | undefined, codemapperUri: URI | undefined, isStreaming: boolean): IDisposableReference<CollapsedCodeBlock> {
327
const codeBlock = this.instantiationService.createInstance(CollapsedCodeBlock, sessionId, requestId, inUndoStop);
328
if (codemapperUri) {
329
codeBlock.render(codemapperUri, isStreaming);
330
}
331
return {
332
object: codeBlock,
333
isStale: () => false,
334
dispose: () => codeBlock.dispose()
335
};
336
}
337
338
private renderCodeBlock(data: ICodeBlockData, text: string, isComplete: boolean, currentWidth: number): IDisposableReference<CodeBlockPart> {
339
const ref = this.editorPool.get();
340
const editorInfo = ref.object;
341
if (isResponseVM(data.element)) {
342
this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId, isComplete }).then((e) => {
343
// Update the existing object's codemapperUri
344
this.codeblocks[data.codeBlockPartIndex].codemapperUri = e.codemapperUri;
345
this._onDidChangeHeight.fire();
346
});
347
}
348
349
editorInfo.render(data, currentWidth);
350
351
return ref;
352
}
353
354
hasSameContent(other: IChatProgressRenderableResponseContent): boolean {
355
return other.kind === 'markdownContent' && !!(other.content.value === this.markdown.content.value
356
|| this.codeblocks.at(-1)?.isStreaming && this.codeblocks.at(-1)?.codemapperUri !== undefined && other.content.value.lastIndexOf('```') === this.markdown.content.value.lastIndexOf('```'));
357
}
358
359
layout(width: number): void {
360
this.allRefs.forEach((ref, index) => {
361
if (ref.object instanceof CodeBlockPart) {
362
ref.object.layout(width);
363
} else if (ref.object instanceof CollapsedCodeBlock) {
364
const codeblockModel = this.codeblocks[index];
365
if (codeblockModel.codemapperUri && ref.object.uri?.toString() !== codeblockModel.codemapperUri.toString()) {
366
ref.object.render(codeblockModel.codemapperUri, codeblockModel.isStreaming);
367
}
368
}
369
});
370
371
this.mathLayoutParticipants.forEach(layout => layout());
372
}
373
374
addDisposable(disposable: IDisposable): void {
375
this._register(disposable);
376
}
377
}
378
379
export class EditorPool extends Disposable {
380
381
private readonly _pool: ResourcePool<CodeBlockPart>;
382
383
public inUse(): Iterable<CodeBlockPart> {
384
return this._pool.inUse;
385
}
386
387
constructor(
388
options: ChatEditorOptions,
389
delegate: IChatRendererDelegate,
390
overflowWidgetsDomNode: HTMLElement | undefined,
391
private readonly isSimpleWidget: boolean = false,
392
@IInstantiationService instantiationService: IInstantiationService,
393
) {
394
super();
395
this._pool = this._register(new ResourcePool(() => {
396
return instantiationService.createInstance(CodeBlockPart, options, MenuId.ChatCodeBlock, delegate, overflowWidgetsDomNode, this.isSimpleWidget);
397
}));
398
}
399
400
get(): IDisposableReference<CodeBlockPart> {
401
const codeBlock = this._pool.get();
402
let stale = false;
403
return {
404
object: codeBlock,
405
isStale: () => stale,
406
dispose: () => {
407
codeBlock.reset();
408
stale = true;
409
this._pool.release(codeBlock);
410
}
411
};
412
}
413
}
414
415
export function codeblockHasClosingBackticks(str: string): boolean {
416
str = str.trim();
417
return !!str.match(/\n```+$/);
418
}
419
420
export class CollapsedCodeBlock extends Disposable {
421
422
public readonly element: HTMLElement;
423
424
private readonly hover = this._register(new MutableDisposable());
425
private tooltip: string | undefined;
426
427
private _uri: URI | undefined;
428
public get uri(): URI | undefined {
429
return this._uri;
430
}
431
432
private _currentDiff: IEditSessionEntryDiff | undefined;
433
434
private readonly _progressStore = this._store.add(new DisposableStore());
435
436
constructor(
437
private readonly sessionId: string,
438
private readonly requestId: string,
439
private readonly inUndoStop: string | undefined,
440
@ILabelService private readonly labelService: ILabelService,
441
@IEditorService private readonly editorService: IEditorService,
442
@IModelService private readonly modelService: IModelService,
443
@ILanguageService private readonly languageService: ILanguageService,
444
@IContextMenuService private readonly contextMenuService: IContextMenuService,
445
@IContextKeyService private readonly contextKeyService: IContextKeyService,
446
@IMenuService private readonly menuService: IMenuService,
447
@IHoverService private readonly hoverService: IHoverService,
448
@IChatService private readonly chatService: IChatService,
449
) {
450
super();
451
this.element = $('.chat-codeblock-pill-widget');
452
this.element.tabIndex = 0;
453
this.element.classList.add('show-file-icons');
454
this.element.role = 'button';
455
this._register(dom.addDisposableListener(this.element, 'click', () => this._showDiff()));
456
this._register(dom.addDisposableListener(this.element, 'keydown', (e) => {
457
if (e.key === 'Enter' || e.key === ' ') {
458
this._showDiff();
459
}
460
}));
461
this._register(dom.addDisposableListener(this.element, dom.EventType.CONTEXT_MENU, domEvent => {
462
const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);
463
dom.EventHelper.stop(domEvent, true);
464
465
this.contextMenuService.showContextMenu({
466
contextKeyService: this.contextKeyService,
467
getAnchor: () => event,
468
getActions: () => {
469
const menu = this.menuService.getMenuActions(MenuId.ChatEditingCodeBlockContext, this.contextKeyService, { arg: { sessionId, requestId, uri: this.uri, stopId: inUndoStop } });
470
return getFlatContextMenuActions(menu);
471
},
472
});
473
}));
474
}
475
476
private _showDiff(): void {
477
if (this._currentDiff) {
478
this.editorService.openEditor({
479
original: { resource: this._currentDiff.originalURI },
480
modified: { resource: this._currentDiff.modifiedURI },
481
options: { transient: true },
482
});
483
} else if (this.uri) {
484
this.editorService.openEditor({ resource: this.uri });
485
}
486
}
487
488
render(uri: URI, isStreaming?: boolean): void {
489
this._progressStore.clear();
490
491
this._uri = uri;
492
493
const session = this.chatService.getSession(this.sessionId);
494
const iconText = this.labelService.getUriBasenameLabel(uri);
495
496
let editSession = session?.editingSessionObs?.promiseResult.get()?.data;
497
let modifiedEntry = editSession?.getEntry(uri);
498
let modifiedByResponse = modifiedEntry?.isCurrentlyBeingModifiedBy.get();
499
const isComplete = !modifiedByResponse || modifiedByResponse.requestId !== this.requestId;
500
501
let iconClasses: string[] = [];
502
if (isStreaming || !isComplete) {
503
const codicon = ThemeIcon.modify(Codicon.loading, 'spin');
504
iconClasses = ThemeIcon.asClassNameArray(codicon);
505
} else {
506
const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;
507
iconClasses = getIconClasses(this.modelService, this.languageService, uri, fileKind);
508
}
509
510
const iconEl = dom.$('span.icon');
511
iconEl.classList.add(...iconClasses);
512
513
const children = [dom.$('span.icon-label', {}, iconText)];
514
const labelDetail = dom.$('span.label-detail', {}, '');
515
children.push(labelDetail);
516
if (isStreaming) {
517
labelDetail.textContent = localize('chat.codeblock.generating', "Generating edits...");
518
}
519
520
this.element.replaceChildren(iconEl, ...children);
521
this.updateTooltip(this.labelService.getUriLabel(uri, { relative: false }));
522
523
const renderDiff = (changes: IEditSessionEntryDiff | undefined) => {
524
const labelAdded = this.element.querySelector('.label-added') ?? this.element.appendChild(dom.$('span.label-added'));
525
const labelRemoved = this.element.querySelector('.label-removed') ?? this.element.appendChild(dom.$('span.label-removed'));
526
if (changes && !changes?.identical && !changes?.quitEarly) {
527
this._currentDiff = changes;
528
labelAdded.textContent = `+${changes.added}`;
529
labelRemoved.textContent = `-${changes.removed}`;
530
const insertionsFragment = changes.added === 1 ? localize('chat.codeblock.insertions.one', "1 insertion") : localize('chat.codeblock.insertions', "{0} insertions", changes.added);
531
const deletionsFragment = changes.removed === 1 ? localize('chat.codeblock.deletions.one', "1 deletion") : localize('chat.codeblock.deletions', "{0} deletions", changes.removed);
532
const summary = localize('summary', 'Edited {0}, {1}, {2}', iconText, insertionsFragment, deletionsFragment);
533
this.element.ariaLabel = summary;
534
this.updateTooltip(summary);
535
}
536
};
537
538
let diffBetweenStops: IObservable<IEditSessionEntryDiff | undefined> | undefined;
539
540
// Show a percentage progress that is driven by the rewrite
541
542
this._progressStore.add(autorun(r => {
543
if (!editSession) {
544
editSession = session?.editingSessionObs?.promiseResult.read(r)?.data;
545
modifiedEntry = editSession?.getEntry(uri);
546
}
547
548
modifiedByResponse = modifiedEntry?.isCurrentlyBeingModifiedBy.read(r);
549
let diffValue = diffBetweenStops?.read(r);
550
const isComplete = !!diffValue || !modifiedByResponse || modifiedByResponse.requestId !== this.requestId;
551
const rewriteRatio = modifiedEntry?.rewriteRatio.read(r);
552
553
if (!isStreaming && !isComplete) {
554
const value = rewriteRatio;
555
labelDetail.textContent = value === 0 || !value ? localize('chat.codeblock.generating', "Generating edits...") : localize('chat.codeblock.applyingPercentage', "Applying edits ({0}%)...", Math.round(value * 100));
556
} else if (!isStreaming && isComplete) {
557
iconEl.classList.remove(...iconClasses);
558
const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;
559
iconEl.classList.add(...getIconClasses(this.modelService, this.languageService, uri, fileKind));
560
labelDetail.textContent = '';
561
}
562
563
if (!diffBetweenStops) {
564
diffBetweenStops = modifiedEntry && editSession
565
? editSession.getEntryDiffBetweenStops(modifiedEntry.modifiedURI, this.requestId, this.inUndoStop)
566
: undefined;
567
diffValue = diffBetweenStops?.read(r);
568
}
569
570
if (!isStreaming && isComplete) {
571
renderDiff(diffValue);
572
}
573
}));
574
}
575
576
private updateTooltip(tooltip: string): void {
577
this.tooltip = tooltip;
578
if (!this.hover.value) {
579
this.hover.value = this.hoverService.setupDelayedHover(this.element, () => (
580
{
581
content: this.tooltip!,
582
appearance: { compact: true, showPointer: true },
583
position: { hoverPosition: HoverPosition.BELOW },
584
persistence: { hideOnKeyDown: true },
585
}
586
));
587
}
588
}
589
}
590
591