Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingDeletedFileEntry.ts
5241 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 { VSBuffer } from '../../../../../base/common/buffer.js';6import { constObservable, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js';7import { URI } from '../../../../../base/common/uri.js';8import { LineRange } from '../../../../../editor/common/core/ranges/lineRange.js';9import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';10import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js';11import { TextEdit } from '../../../../../editor/common/languages.js';12import { ILanguageService } from '../../../../../editor/common/languages/language.js';13import { ITextModel } from '../../../../../editor/common/model.js';14import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js';15import { IModelService } from '../../../../../editor/common/services/model.js';16import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';17import { IFileService } from '../../../../../platform/files/common/files.js';18import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';19import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../../platform/undoRedo/common/undoRedo.js';20import { IEditorPane } from '../../../../common/editor.js';21import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';22import { stringToSnapshot } from '../../../../services/textfile/common/textfiles.js';23import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';24import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';25import { IChatService } from '../../common/chatService/chatService.js';26import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';27import { IChatResponseModel } from '../../common/model/chatModel.js';28import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';29import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';3031interface IMultiDiffEntryDelegate {32collapse: (transaction: ITransaction | undefined) => void;33}3435/**36* Represents a file that has been deleted by the chat editing session.37* Unlike ChatEditingModifiedDocumentEntry, this doesn't maintain a live model38* since the file no longer exists on disk.39*/40export class ChatEditingDeletedFileEntry extends AbstractChatEditingModifiedFileEntry implements IModifiedFileEntry {4142readonly initialContent: string;4344/**45* The original content before deletion, stored for diff display and potential restoration.46*/47private readonly _originalContent: string;4849/**50* Lazily created model for the original content (for diff display).51*/52private _originalModel: ITextModel | undefined;5354/**55* Lazily created empty model representing the deleted state (for diff display).56*/57private _modifiedModel: ITextModel | undefined;5859readonly originalURI: URI;6061readonly diffInfo: IObservable<IDocumentDiff>;62readonly linesAdded: IObservable<number> = constObservable(0);63readonly linesRemoved: IObservable<number>;6465private readonly _changesCount = observableValue<number>(this, 1);66override readonly changesCount = this._changesCount;67readonly isDeletion = true;6869constructor(70resource: URI,71originalContent: string,72private readonly _multiDiffEntryDelegate: IMultiDiffEntryDelegate,73telemetryInfo: IModifiedEntryTelemetryInfo,74private readonly _languageId: string,75@IModelService private readonly _modelService: IModelService,76@ILanguageService private readonly _languageService: ILanguageService,77@IConfigurationService configService: IConfigurationService,78@IFilesConfigurationService fileConfigService: IFilesConfigurationService,79@IChatService chatService: IChatService,80@IFileService fileService: IFileService,81@IUndoRedoService undoRedoService: IUndoRedoService,82@IInstantiationService instantiationService: IInstantiationService,83@IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService,84) {85super(86resource,87telemetryInfo,88ChatEditKind.Deleted,89configService,90fileConfigService,91chatService,92fileService,93undoRedoService,94instantiationService,95aiEditTelemetryService,96);9798this._originalContent = originalContent;99this.initialContent = originalContent;100this.originalURI = ChatEditingTextModelContentProvider.getFileURI(telemetryInfo.sessionResource, this.entryId, resource.path);101this.diffInfo = constObservable(this._diffInfo());102this.linesRemoved = constObservable(this._getOrCreateOriginalModel().getLineCount());103}104105override dispose(): void {106this._originalModel?.dispose();107this._modifiedModel?.dispose();108super.dispose();109}110111/**112* Gets or creates the original model for diff display.113*/114private _getOrCreateOriginalModel(): ITextModel {115if (!this._originalModel || this._originalModel.isDisposed()) {116this._originalModel = this._modelService.createModel(117createTextBufferFactoryFromSnapshot(stringToSnapshot(this._originalContent)),118this._languageService.createById(this._languageId),119this.originalURI,120false121);122}123return this._originalModel;124}125126/**127* Gets or creates an empty model representing the deleted state.128*/129private _getOrCreateModifiedModel(): ITextModel {130if (!this._modifiedModel || this._modifiedModel.isDisposed()) {131// Create empty model - file is deleted so content is empty132this._modifiedModel = this._modelService.createModel(133'',134this._languageService.createById(this._languageId),135this.modifiedURI.with({ scheme: 'deleted-file' }),136false137);138}139return this._modifiedModel;140}141142private _diffInfo() {143// For deleted files, return a simple diff showing all content removed144const originalModel = this._getOrCreateOriginalModel();145this._getOrCreateModifiedModel(); // Ensure the modified model exists for the diff view146const originalLineCount = originalModel.getLineCount();147148return {149changes: [new DetailedLineRangeMapping(150new LineRange(1, originalLineCount + 1),151new LineRange(1, 1),152undefined153)],154quitEarly: false,155identical: false,156moves: []157};158}159160getDiffInfo(): Promise<IDocumentDiff> {161return Promise.resolve(this._diffInfo());162}163164equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean {165return !!snapshot &&166this.modifiedURI.toString() === snapshot.resource.toString() &&167this._languageId === snapshot.languageId &&168this._originalContent === snapshot.original &&169snapshot.current === '' &&170this.state.get() === snapshot.state;171}172173createSnapshot(chatSessionResource: URI, requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry {174return {175resource: this.modifiedURI,176languageId: this._languageId,177snapshotUri: this.originalURI,178original: this._originalContent,179current: '', // File is deleted, so current content is empty180state: this.state.get(),181telemetryInfo: this._telemetryInfo,182isDeleted: true,183};184}185186async restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk = true): Promise<void> {187this._stateObs.set(snapshot.state, undefined);188189if (restoreToDisk && snapshot.current !== '') {190// Restore file to disk with the snapshot content191await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(snapshot.current));192}193}194195async resetToInitialContent(): Promise<void> {196// Restore the file with original content197await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(this._originalContent));198}199200protected override async _areOriginalAndModifiedIdentical(): Promise<boolean> {201// A deleted file is never identical to its original (unless original was empty)202return this._originalContent === '';203}204205protected override _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement {206return {207type: UndoRedoElementType.Resource,208resource: this.modifiedURI,209label: 'Chat File Deletion',210code: 'chat.delete',211undo: async () => {212// Restore the file213await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(this._originalContent));214},215redo: async () => {216// Delete the file again217await this._fileService.del(this.modifiedURI, { useTrash: false });218}219};220}221222async acceptAgentEdits(_uri: URI, _edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, _responseModel: IChatResponseModel | undefined): Promise<void> {223// For deleted files, there are no incremental edits - the file is just deleted224transaction((tx) => {225this._waitsForLastEdits.set(!isLastEdits, tx);226this._stateObs.set(ModifiedFileEntryState.Modified, tx);227228if (isLastEdits) {229this._resetEditsState(tx);230this._rewriteRatioObs.set(1, tx);231}232});233}234235protected override async _doAccept(): Promise<void> {236// File deletion is already done - just collapse the entry237this._multiDiffEntryDelegate.collapse(undefined);238}239240protected override async _doReject(): Promise<void> {241// Restore the file from original content242await this._fileService.writeFile(this.modifiedURI, VSBuffer.fromString(this._originalContent));243this._multiDiffEntryDelegate.collapse(undefined);244}245246protected _createEditorIntegration(_editor: IEditorPane): IModifiedFileEntryEditorIntegration {247// Deleted files don't need complex editor integration since there's nothing to navigate248return {249currentIndex: observableValue(this, 0),250reveal: () => { },251next: () => false,252previous: () => false,253enableAccessibleDiffView: () => { },254acceptNearestChange: async () => { },255rejectNearestChange: async () => { },256toggleDiff: async () => { },257dispose: () => { }258};259}260261async computeEditsFromSnapshots(_beforeSnapshot: string, _afterSnapshot: string): Promise<(TextEdit | ICellEditOperation)[]> {262// For deleted files, we don't compute incremental edits263return [];264}265266async save(): Promise<void> {267// Nothing to save - file is deleted268}269270async revertToDisk(): Promise<void> {271// Nothing to revert - file is deleted272}273}274275276