Path: blob/main/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTextEditContentPart.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../../base/browser/dom.js';6import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';7import { Emitter, Event } from '../../../../../base/common/event.js';8import { Disposable, IDisposable, IReference, RefCountedDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';9import { Schemas } from '../../../../../base/common/network.js';10import { isEqual } from '../../../../../base/common/resources.js';11import { assertType } from '../../../../../base/common/types.js';12import { URI } from '../../../../../base/common/uri.js';13import { generateUuid } from '../../../../../base/common/uuid.js';14import { ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js';15import { TextEdit } from '../../../../../editor/common/languages.js';16import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js';17import { IModelService } from '../../../../../editor/common/services/model.js';18import { DefaultModelSHA1Computer } from '../../../../../editor/common/services/modelService.js';19import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';20import { localize } from '../../../../../nls.js';21import { MenuId } from '../../../../../platform/actions/common/actions.js';22import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';23import { createDecorator, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';24import { IChatListItemRendererOptions } from '../chat.js';25import { IDisposableReference, ResourcePool } from './chatCollections.js';26import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';27import { IChatRendererDelegate } from '../chatListRenderer.js';28import { ChatEditorOptions } from '../chatOptions.js';29import { CodeCompareBlockPart, ICodeCompareBlockData, ICodeCompareBlockDiffData } from '../codeBlockPart.js';30import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from '../../common/chatModel.js';31import { IChatService } from '../../common/chatService.js';32import { IChatResponseViewModel, isResponseVM } from '../../common/chatViewModel.js';3334const $ = dom.$;3536const ICodeCompareModelService = createDecorator<ICodeCompareModelService>('ICodeCompareModelService');3738interface ICodeCompareModelService {39_serviceBrand: undefined;40createModel(response: IChatResponseViewModel, chatTextEdit: IChatTextEditGroup): Promise<IReference<{ originalSha1: string; original: IResolvedTextEditorModel; modified: IResolvedTextEditorModel }>>;41}4243export class ChatTextEditContentPart extends Disposable implements IChatContentPart {44public readonly domNode: HTMLElement;45private readonly comparePart: IDisposableReference<CodeCompareBlockPart> | undefined;4647private readonly _onDidChangeHeight = this._register(new Emitter<void>());48public readonly onDidChangeHeight = this._onDidChangeHeight.event;4950constructor(51chatTextEdit: IChatTextEditGroup,52context: IChatContentPartRenderContext,53rendererOptions: IChatListItemRendererOptions,54diffEditorPool: DiffEditorPool,55currentWidth: number,56@ICodeCompareModelService private readonly codeCompareModelService: ICodeCompareModelService57) {58super();59const element = context.element;6061assertType(isResponseVM(element));6263// TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen64if (rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) {65if (element.response.value.every(item => item.kind === 'textEditGroup')) {66this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete67? ''68: element.isCanceled69? localize('edits0', "Making changes was aborted.")70: localize('editsSummary', "Made changes."));71} else {72this.domNode = $('div');73}7475// TODO@roblourens this case is now handled outside this Part in ChatListRenderer, but can it be cleaned up?76// return;77} else {787980const cts = new CancellationTokenSource();8182let isDisposed = false;83this._register(toDisposable(() => {84isDisposed = true;85cts.dispose(true);86}));8788this.comparePart = this._register(diffEditorPool.get());8990// Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping)91// not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render)92this._register(this.comparePart.object.onDidChangeContentHeight(() => {93this._onDidChangeHeight.fire();94}));9596const data: ICodeCompareBlockData = {97element,98edit: chatTextEdit,99diffData: (async () => {100101const ref = await this.codeCompareModelService.createModel(element, chatTextEdit);102103if (isDisposed) {104ref.dispose();105return;106}107108this._register(ref);109110return {111modified: ref.object.modified.textEditorModel,112original: ref.object.original.textEditorModel,113originalSha1: ref.object.originalSha1114} satisfies ICodeCompareBlockDiffData;115})()116};117this.comparePart.object.render(data, currentWidth, cts.token);118119this.domNode = this.comparePart.object.element;120}121}122123layout(width: number): void {124this.comparePart?.object.layout(width);125}126127hasSameContent(other: IChatProgressRenderableResponseContent): boolean {128// No other change allowed for this content type129return other.kind === 'textEditGroup';130}131132addDisposable(disposable: IDisposable): void {133this._register(disposable);134}135}136137export class DiffEditorPool extends Disposable {138139private readonly _pool: ResourcePool<CodeCompareBlockPart>;140141public inUse(): Iterable<CodeCompareBlockPart> {142return this._pool.inUse;143}144145constructor(146options: ChatEditorOptions,147delegate: IChatRendererDelegate,148overflowWidgetsDomNode: HTMLElement | undefined,149private readonly isSimpleWidget: boolean = false,150@IInstantiationService instantiationService: IInstantiationService,151) {152super();153this._pool = this._register(new ResourcePool(() => {154return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.ChatCompareBlock, delegate, overflowWidgetsDomNode, this.isSimpleWidget);155}));156}157158get(): IDisposableReference<CodeCompareBlockPart> {159const codeBlock = this._pool.get();160let stale = false;161return {162object: codeBlock,163isStale: () => stale,164dispose: () => {165codeBlock.reset();166stale = true;167this._pool.release(codeBlock);168}169};170}171}172173class CodeCompareModelService implements ICodeCompareModelService {174175declare readonly _serviceBrand: undefined;176177constructor(178@ITextModelService private readonly textModelService: ITextModelService,179@IModelService private readonly modelService: IModelService,180@IChatService private readonly chatService: IChatService,181) { }182183async createModel(element: IChatResponseViewModel, chatTextEdit: IChatTextEditGroup): Promise<IReference<{ originalSha1: string; original: IResolvedTextEditorModel; modified: IResolvedTextEditorModel }>> {184185const original = await this.textModelService.createModelReference(chatTextEdit.uri);186187const modified = await this.textModelService.createModelReference((this.modelService.createModel(188createTextBufferFactoryFromSnapshot(original.object.textEditorModel.createSnapshot()),189{ languageId: original.object.textEditorModel.getLanguageId(), onDidChange: Event.None },190URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: chatTextEdit.uri.path, query: generateUuid() }),191false192)).uri);193194const d = new RefCountedDisposable(toDisposable(() => {195original.dispose();196modified.dispose();197}));198199// compute the sha1 of the original model200let originalSha1: string = '';201if (chatTextEdit.state) {202originalSha1 = chatTextEdit.state.sha1;203} else {204const sha1 = new DefaultModelSHA1Computer();205if (sha1.canComputeSHA1(original.object.textEditorModel)) {206originalSha1 = sha1.computeSHA1(original.object.textEditorModel);207chatTextEdit.state = { sha1: originalSha1, applied: 0 };208}209}210211// apply edits to the "modified" model212const chatModel = this.chatService.getSession(element.sessionId)!;213const editGroups: ISingleEditOperation[][] = [];214for (const request of chatModel.getRequests()) {215if (!request.response) {216continue;217}218for (const item of request.response.response.value) {219if (item.kind !== 'textEditGroup' || item.state?.applied || !isEqual(item.uri, chatTextEdit.uri)) {220continue;221}222for (const group of item.edits) {223const edits = group.map(TextEdit.asEditOperation);224editGroups.push(edits);225}226}227if (request.response === element.model) {228break;229}230}231for (const edits of editGroups) {232modified.object.textEditorModel.pushEditOperations(null, edits, () => null);233}234235// self-acquire a reference to diff models for a short while236// because streaming usually means we will be using the original-model237// repeatedly and thereby also should reuse the modified-model and just238// update it with more edits239d.acquire();240setTimeout(() => d.release(), 5000);241242return {243object: {244originalSha1,245original: original.object,246modified: modified.object247},248dispose() {249d.release();250},251};252}253}254255registerSingleton(ICodeCompareModelService, CodeCompareModelService, InstantiationType.Delayed);256257258