Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.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 { IReference, MutableDisposable } from '../../../../../base/common/lifecycle.js';6import { ITransaction, autorun, transaction } from '../../../../../base/common/observable.js';7import { assertType } from '../../../../../base/common/types.js';8import { URI } from '../../../../../base/common/uri.js';9import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';10import { TextEdit } from '../../../../../editor/common/languages.js';11import { ILanguageService } from '../../../../../editor/common/languages/language.js';12import { ITextModel } from '../../../../../editor/common/model.js';13import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js';14import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js';15import { IModelService } from '../../../../../editor/common/services/model.js';16import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';17import { localize } from '../../../../../nls.js';18import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';19import { IFileService } from '../../../../../platform/files/common/files.js';20import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';21import { IMarkerService } from '../../../../../platform/markers/common/markers.js';22import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';23import { IEditorPane, SaveReason } from '../../../../common/editor.js';24import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';25import { ITextFileService, isTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js';26import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';27import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';28import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js';29import { IChatResponseModel } from '../../common/chatModel.js';30import { IChatService } from '../../common/chatService.js';31import { ChatEditingCodeEditorIntegration } from './chatEditingCodeEditorIntegration.js';32import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';33import { ChatEditingTextModelChangeService } from './chatEditingTextModelChangeService.js';34import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';353637export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifiedFileEntry implements IModifiedFileEntry {3839readonly initialContent: string;4041private readonly originalModel: ITextModel;42private readonly modifiedModel: ITextModel;4344private readonly _docFileEditorModel: IResolvedTextEditorModel;4546override get changesCount() {47return this._textModelChangeService.diffInfo.map(diff => diff.changes.length);48}4950get linesAdded() {51return this._textModelChangeService.diffInfo.map(diff => {52let added = 0;53for (const c of diff.changes) {54added += Math.max(0, c.modified.endLineNumberExclusive - c.modified.startLineNumber);55}56return added;57});58}59get linesRemoved() {60return this._textModelChangeService.diffInfo.map(diff => {61let removed = 0;62for (const c of diff.changes) {63removed += Math.max(0, c.original.endLineNumberExclusive - c.original.startLineNumber);64}65return removed;66});67}6869readonly originalURI: URI;70private readonly _textModelChangeService: ChatEditingTextModelChangeService;7172constructor(73resourceRef: IReference<IResolvedTextEditorModel>,74private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void },75telemetryInfo: IModifiedEntryTelemetryInfo,76kind: ChatEditKind,77initialContent: string | undefined,78@IMarkerService markerService: IMarkerService,79@IModelService modelService: IModelService,80@ITextModelService textModelService: ITextModelService,81@ILanguageService languageService: ILanguageService,82@IConfigurationService configService: IConfigurationService,83@IFilesConfigurationService fileConfigService: IFilesConfigurationService,84@IChatService chatService: IChatService,85@ITextFileService private readonly _textFileService: ITextFileService,86@IFileService fileService: IFileService,87@IUndoRedoService undoRedoService: IUndoRedoService,88@IInstantiationService instantiationService: IInstantiationService,89@IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService,90) {91super(92resourceRef.object.textEditorModel.uri,93telemetryInfo,94kind,95configService,96fileConfigService,97chatService,98fileService,99undoRedoService,100instantiationService,101aiEditTelemetryService,102);103104this._docFileEditorModel = this._register(resourceRef).object;105this.modifiedModel = resourceRef.object.textEditorModel;106this.originalURI = ChatEditingTextModelContentProvider.getFileURI(telemetryInfo.sessionId, this.entryId, this.modifiedURI.path);107108this.initialContent = initialContent ?? this.modifiedModel.getValue();109const docSnapshot = this.originalModel = this._register(110modelService.createModel(111createTextBufferFactoryFromSnapshot(initialContent ? stringToSnapshot(initialContent) : this.modifiedModel.createSnapshot()),112languageService.createById(this.modifiedModel.getLanguageId()),113this.originalURI,114false115)116);117118this._textModelChangeService = this._register(instantiationService.createInstance(ChatEditingTextModelChangeService,119this.originalModel, this.modifiedModel, this._stateObs));120121this._register(this._textModelChangeService.onDidAcceptOrRejectAllHunks(action => {122this._stateObs.set(action, undefined);123this._notifySessionAction(action === ModifiedFileEntryState.Accepted ? 'accepted' : 'rejected');124}));125126this._register(this._textModelChangeService.onDidAcceptOrRejectLines(action => {127this._notifyAction({128kind: 'chatEditingHunkAction',129uri: this.modifiedURI,130outcome: action.state,131languageId: this.modifiedModel.getLanguageId(),132...action133});134}));135136// Create a reference to this model to avoid it being disposed from under our nose137(async () => {138const reference = await textModelService.createModelReference(docSnapshot.uri);139if (this._store.isDisposed) {140reference.dispose();141return;142}143this._register(reference);144})();145146147this._register(this._textModelChangeService.onDidUserEditModel(() => {148this._userEditScheduler.schedule();149const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent;150if (this._stateObs.get() === ModifiedFileEntryState.Modified && didResetToOriginalContent) {151this._stateObs.set(ModifiedFileEntryState.Rejected, undefined);152}153}));154155const resourceFilter = this._register(new MutableDisposable());156this._register(autorun(r => {157const inProgress = this._waitsForLastEdits.read(r);158if (inProgress) {159const res = this._lastModifyingResponseObs.read(r);160const req = res && res.session.getRequests().find(value => value.id === res.requestId);161resourceFilter.value = markerService.installResourceFilter(this.modifiedURI, req?.message.text || localize('default', "Chat Edits"));162} else {163resourceFilter.clear();164}165}));166}167168equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean {169return !!snapshot &&170this.modifiedURI.toString() === snapshot.resource.toString() &&171this.modifiedModel.getLanguageId() === snapshot.languageId &&172this.originalModel.getValue() === snapshot.original &&173this.modifiedModel.getValue() === snapshot.current &&174this.state.get() === snapshot.state;175}176177createSnapshot(requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry {178return {179resource: this.modifiedURI,180languageId: this.modifiedModel.getLanguageId(),181snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(this._telemetryInfo.sessionId, requestId, undoStop, this.modifiedURI.path),182original: this.originalModel.getValue(),183current: this.modifiedModel.getValue(),184state: this.state.get(),185telemetryInfo: this._telemetryInfo186};187}188189async restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk = true) {190this._stateObs.set(snapshot.state, undefined);191await this._textModelChangeService.resetDocumentValues(snapshot.original, restoreToDisk ? snapshot.current : undefined);192}193194async resetToInitialContent() {195await this._textModelChangeService.resetDocumentValues(undefined, this.initialContent);196}197198protected override async _areOriginalAndModifiedIdentical(): Promise<boolean> {199return this._textModelChangeService.areOriginalAndModifiedIdentical();200}201202protected override _resetEditsState(tx: ITransaction): void {203super._resetEditsState(tx);204this._textModelChangeService.clearCurrentEditLineDecoration();205}206207protected override _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement {208const request = response.session.getRequests().find(req => req.id === response.requestId);209const label = request?.message.text ? localize('chatEditing1', "Chat Edit: '{0}'", request.message.text) : localize('chatEditing2', "Chat Edit");210return new SingleModelEditStackElement(label, 'chat.edit', this.modifiedModel, null);211}212213async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<void> {214215const result = await this._textModelChangeService.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel);216217transaction((tx) => {218this._waitsForLastEdits.set(!isLastEdits, tx);219this._stateObs.set(ModifiedFileEntryState.Modified, tx);220221if (!isLastEdits) {222this._isCurrentlyBeingModifiedByObs.set(responseModel, tx);223this._rewriteRatioObs.set(result.rewriteRatio, tx);224} else {225this._resetEditsState(tx);226this._rewriteRatioObs.set(1, tx);227}228});229if (isLastEdits) {230await this._textFileService.save(this.modifiedModel.uri, {231reason: SaveReason.AUTO,232skipSaveParticipants: true,233});234}235}236237238protected override async _doAccept(): Promise<void> {239this._textModelChangeService.keep();240this._multiDiffEntryDelegate.collapse(undefined);241242const config = this._fileConfigService.getAutoSaveConfiguration(this.modifiedURI);243if (!config.autoSave || !this._textFileService.isDirty(this.modifiedURI)) {244// SAVE after accept for manual-savers, for auto-savers245// trigger explict save to get save participants going246try {247await this._textFileService.save(this.modifiedURI, {248reason: SaveReason.EXPLICIT,249force: true,250ignoreErrorHandler: true251});252} catch {253// ignored254}255}256}257258protected override async _doReject(): Promise<void> {259if (this.createdInRequestId === this._telemetryInfo.requestId) {260if (isTextFileEditorModel(this._docFileEditorModel)) {261await this._docFileEditorModel.revert({ soft: true });262await this._fileService.del(this.modifiedURI);263}264this._onDidDelete.fire();265} else {266this._textModelChangeService.undo();267if (this._textModelChangeService.allEditsAreFromUs && isTextFileEditorModel(this._docFileEditorModel)) {268// save the file after discarding so that the dirty indicator goes away269// and so that an intermediate saved state gets reverted270await this._docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true });271}272this._multiDiffEntryDelegate.collapse(undefined);273}274}275276protected _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration {277const codeEditor = getCodeEditor(editor.getControl());278assertType(codeEditor);279280const diffInfo = this._textModelChangeService.diffInfo;281282return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, this, codeEditor, diffInfo, false);283}284}285286287