Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts
5251 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 { IReference, MutableDisposable } from '../../../../../base/common/lifecycle.js';6import { Schemas } from '../../../../../base/common/network.js';7import { ITransaction, autorun, transaction } from '../../../../../base/common/observable.js';8import { assertType } from '../../../../../base/common/types.js';9import { URI } from '../../../../../base/common/uri.js';10import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';11import { TextEdit as EditorTextEdit } from '../../../../../editor/common/core/edits/textEdit.js';12import { StringText } from '../../../../../editor/common/core/text/abstractText.js';13import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';14import { TextEdit } from '../../../../../editor/common/languages.js';15import { ILanguageService } from '../../../../../editor/common/languages/language.js';16import { ITextModel } from '../../../../../editor/common/model.js';17import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js';18import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js';19import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';20import { IModelService } from '../../../../../editor/common/services/model.js';21import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';22import { localize } from '../../../../../nls.js';23import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';24import { IFileService } from '../../../../../platform/files/common/files.js';25import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';26import { IMarkerService } from '../../../../../platform/markers/common/markers.js';27import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';28import { IEditorPane, SaveReason } from '../../../../common/editor.js';29import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';30import { ITextFileService, isTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js';31import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';32import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';33import { IChatService } from '../../common/chatService/chatService.js';34import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';35import { IChatResponseModel } from '../../common/model/chatModel.js';36import { ChatEditingCodeEditorIntegration } from './chatEditingCodeEditorIntegration.js';37import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';38import { ChatEditingTextModelChangeService } from './chatEditingTextModelChangeService.js';39import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';4041interface IMultiDiffEntryDelegate {42collapse: (transaction: ITransaction | undefined) => void;43}444546export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifiedFileEntry implements IModifiedFileEntry {4748readonly initialContent: string;4950readonly originalModel: ITextModel;51readonly modifiedModel: ITextModel;5253private readonly _docFileEditorModel: IResolvedTextEditorModel;5455override get changesCount() {56return this._textModelChangeService.diffInfo.map(diff => diff.changes.length);57}5859get diffInfo() {60return this._textModelChangeService.diffInfo;61}6263get linesAdded() {64return this._textModelChangeService.diffInfo.map(diff => {65let added = 0;66for (const c of diff.changes) {67added += Math.max(0, c.modified.endLineNumberExclusive - c.modified.startLineNumber);68}69return added;70});71}72get linesRemoved() {73return this._textModelChangeService.diffInfo.map(diff => {74let removed = 0;75for (const c of diff.changes) {76removed += Math.max(0, c.original.endLineNumberExclusive - c.original.startLineNumber);77}78return removed;79});80}8182readonly originalURI: URI;83private readonly _textModelChangeService: ChatEditingTextModelChangeService;8485constructor(86resourceRef: IReference<IResolvedTextEditorModel>,87private readonly _multiDiffEntryDelegate: IMultiDiffEntryDelegate,88telemetryInfo: IModifiedEntryTelemetryInfo,89kind: ChatEditKind,90initialContent: string | undefined,91@IMarkerService markerService: IMarkerService,92@IModelService modelService: IModelService,93@ITextModelService textModelService: ITextModelService,94@ILanguageService languageService: ILanguageService,95@IConfigurationService configService: IConfigurationService,96@IFilesConfigurationService fileConfigService: IFilesConfigurationService,97@IChatService chatService: IChatService,98@ITextFileService private readonly _textFileService: ITextFileService,99@IFileService fileService: IFileService,100@IUndoRedoService undoRedoService: IUndoRedoService,101@IInstantiationService instantiationService: IInstantiationService,102@IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService,103@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,104) {105super(106resourceRef.object.textEditorModel.uri,107telemetryInfo,108kind,109configService,110fileConfigService,111chatService,112fileService,113undoRedoService,114instantiationService,115aiEditTelemetryService,116);117118this._docFileEditorModel = this._register(resourceRef).object;119this.modifiedModel = resourceRef.object.textEditorModel;120this.originalURI = ChatEditingTextModelContentProvider.getFileURI(telemetryInfo.sessionResource, this.entryId, this.modifiedURI.path);121122this.initialContent = initialContent ?? this.modifiedModel.getValue();123const docSnapshot = this.originalModel = this._register(124modelService.createModel(125createTextBufferFactoryFromSnapshot(initialContent !== undefined ? stringToSnapshot(initialContent) : this.modifiedModel.createSnapshot()),126languageService.createById(this.modifiedModel.getLanguageId()),127this.originalURI,128false129)130);131132this._textModelChangeService = this._register(instantiationService.createInstance(ChatEditingTextModelChangeService,133this.originalModel, this.modifiedModel, this._stateObs, () => this._isExternalEditInProgress));134135this._register(this._textModelChangeService.onDidAcceptOrRejectAllHunks(action => {136this._stateObs.set(action, undefined);137this._notifySessionAction(action === ModifiedFileEntryState.Accepted ? 'accepted' : 'rejected');138}));139140this._register(this._textModelChangeService.onDidAcceptOrRejectLines(action => {141this._notifyAction({142kind: 'chatEditingHunkAction',143uri: this.modifiedURI,144outcome: action.state,145languageId: this.modifiedModel.getLanguageId(),146...action147});148}));149150// Create a reference to this model to avoid it being disposed from under our nose151(async () => {152const reference = await textModelService.createModelReference(docSnapshot.uri);153if (this._store.isDisposed) {154reference.dispose();155return;156}157this._register(reference);158})();159160161this._register(this._textModelChangeService.onDidUserEditModel(() => {162this._userEditScheduler.schedule();163const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent;164if (this._stateObs.get() === ModifiedFileEntryState.Modified && didResetToOriginalContent) {165this._stateObs.set(ModifiedFileEntryState.Rejected, undefined);166}167}));168169const resourceFilter = this._register(new MutableDisposable());170this._register(autorun(r => {171const inProgress = this._waitsForLastEdits.read(r);172if (inProgress) {173const res = this._lastModifyingResponseObs.read(r);174const req = res && res.session.getRequests().find(value => value.id === res.requestId);175resourceFilter.value = markerService.installResourceFilter(this.modifiedURI, req?.message.text || localize('default', "Chat Edits"));176} else {177resourceFilter.clear();178}179}));180}181182getDiffInfo(): Promise<IDocumentDiff> {183return this._textModelChangeService.getDiffInfo();184}185186equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean {187return !!snapshot &&188this.modifiedURI.toString() === snapshot.resource.toString() &&189this.modifiedModel.getLanguageId() === snapshot.languageId &&190this.originalModel.getValue() === snapshot.original &&191this.modifiedModel.getValue() === snapshot.current &&192this.state.get() === snapshot.state;193}194195createSnapshot(chatSessionResource: URI, requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry {196return {197resource: this.modifiedURI,198languageId: this.modifiedModel.getLanguageId(),199snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(chatSessionResource, requestId, undoStop, this.modifiedURI.path),200original: this.originalModel.getValue(),201current: this.modifiedModel.getValue(),202state: this.state.get(),203telemetryInfo: this._telemetryInfo204};205}206207public getCurrentContents() {208return this.modifiedModel.getValue();209}210211async restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk = true) {212this._stateObs.set(snapshot.state, undefined);213await this._textModelChangeService.resetDocumentValues(snapshot.original, restoreToDisk ? snapshot.current : undefined);214}215216async resetToInitialContent() {217await this._textModelChangeService.resetDocumentValues(undefined, this.initialContent);218}219220protected override async _areOriginalAndModifiedIdentical(): Promise<boolean> {221return this._textModelChangeService.areOriginalAndModifiedIdentical();222}223224protected override _resetEditsState(tx: ITransaction): void {225super._resetEditsState(tx);226this._textModelChangeService.clearCurrentEditLineDecoration();227}228229protected override _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement {230const request = response.session.getRequests().find(req => req.id === response.requestId);231const label = request?.message.text ? localize('chatEditing1', "Chat Edit: '{0}'", request.message.text) : localize('chatEditing2', "Chat Edit");232return new SingleModelEditStackElement(label, 'chat.edit', this.modifiedModel, null);233}234235async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel | undefined): Promise<void> {236237const result = await this._textModelChangeService.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel);238239transaction((tx) => {240this._waitsForLastEdits.set(!isLastEdits, tx);241this._stateObs.set(ModifiedFileEntryState.Modified, tx);242243if (!isLastEdits) {244this._rewriteRatioObs.set(result.rewriteRatio, tx);245} else {246this._resetEditsState(tx);247this._rewriteRatioObs.set(1, tx);248}249});250if (isLastEdits && this._shouldAutoSave()) {251await this._textFileService.save(this.modifiedModel.uri, {252reason: SaveReason.AUTO,253skipSaveParticipants: true,254});255}256}257258259protected override async _doAccept(): Promise<void> {260this._textModelChangeService.keep();261this._multiDiffEntryDelegate.collapse(undefined);262263const config = this._fileConfigService.getAutoSaveConfiguration(this.modifiedURI);264if (!config.autoSave || !this._textFileService.isDirty(this.modifiedURI)) {265// SAVE after accept for manual-savers, for auto-savers266// trigger explict save to get save participants going267try {268await this._textFileService.save(this.modifiedURI, {269reason: SaveReason.EXPLICIT,270force: true,271ignoreErrorHandler: true272});273} catch {274// ignored275}276}277}278279protected override async _doReject(): Promise<void> {280if (this.createdInRequestId === this._telemetryInfo.requestId) {281if (isTextFileEditorModel(this._docFileEditorModel)) {282await this._docFileEditorModel.revert({ soft: true });283await this._fileService.del(this.modifiedURI).catch(err => {284// don't block if file is already deleted285});286}287this._onDidDelete.fire();288} else {289this._textModelChangeService.undo();290if (this._textModelChangeService.allEditsAreFromUs && isTextFileEditorModel(this._docFileEditorModel) && this._shouldAutoSave()) {291// save the file after discarding so that the dirty indicator goes away292// and so that an intermediate saved state gets reverted293await this._docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true });294}295this._multiDiffEntryDelegate.collapse(undefined);296}297}298299protected _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration {300const codeEditor = getCodeEditor(editor.getControl());301assertType(codeEditor);302303const diffInfo = this._textModelChangeService.diffInfo;304305return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, this, codeEditor, diffInfo, false);306}307308private _shouldAutoSave() {309return this.modifiedURI.scheme !== Schemas.untitled;310}311312async computeEditsFromSnapshots(beforeSnapshot: string, afterSnapshot: string): Promise<(TextEdit | ICellEditOperation)[]> {313const stringEdit = await this._editorWorkerService.computeStringEditFromDiff(314beforeSnapshot,315afterSnapshot,316{ maxComputationTimeMs: 5000 },317'advanced'318);319320const editorTextEdit = EditorTextEdit.fromStringEdit(stringEdit, new StringText(beforeSnapshot));321return editorTextEdit.replacements.slice();322}323324async save(): Promise<void> {325if (this.modifiedModel.uri.scheme === Schemas.untitled) {326return;327}328329// Save the current model state to disk if dirty330if (this._textFileService.isDirty(this.modifiedModel.uri)) {331await this._textFileService.save(this.modifiedModel.uri, {332reason: SaveReason.EXPLICIT,333skipSaveParticipants: true334});335}336}337338async revertToDisk(): Promise<void> {339if (this.modifiedModel.uri.scheme === Schemas.untitled) {340return;341}342343// Revert to reload from disk, ensuring in-memory model matches disk344const fileModel = this._textFileService.files.get(this.modifiedModel.uri);345if (fileModel && !fileModel.isDisposed()) {346await fileModel.revert({ soft: false });347}348}349}350351352