From dfad570d15959a6ce7210a313a1190e76e8fe2e2 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 18 Feb 2025 21:17:00 +0100 Subject: [PATCH] move `ChatEditingModifiedDocumentEntry` into its own file (#241121) --- .../chatEditingCodeEditorIntegration.ts | 8 +- .../chatEditingModifiedDocumentEntry.ts | 457 ++++++++++++++++++ .../chatEditingModifiedFileEntry.ts | 451 +---------------- .../chatEditingModifiedNotebookEntry.ts | 5 +- .../chatEditing/chatEditingServiceImpl.ts | 6 +- .../browser/chatEditing/chatEditingSession.ts | 27 +- .../notebookOriginalModelRefFactory.ts | 4 +- .../contrib/chatEdit/notebookSynchronizer.ts | 16 +- 8 files changed, 498 insertions(+), 476 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index a47ad21bedb..e280581fe27 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -39,7 +39,7 @@ import { overviewRulerModifiedForeground, minimapGutterModifiedBackground, overv import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatEditingSessionState, IChatEditingService, IModifiedFileEntry, IModifiedFileEntryChangeHunk, IModifiedFileEntryEditorIntegration, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { isDiffEditorForEntry } from './chatEditing.js'; -import { ChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEditorIntegration { @@ -60,7 +60,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito constructor( private readonly _editor: ICodeEditor, - private readonly _entry: ChatEditingModifiedFileEntry, + private readonly _entry: ChatEditingModifiedDocumentEntry, @IChatEditingService chatEditingService: IChatEditingService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @@ -264,7 +264,7 @@ export class ChatEditingCodeEditorIntegration implements IModifiedFileEntryEdito this._diffVisualDecorations.clear(); } - private _updateDiffRendering(entry: ChatEditingModifiedFileEntry, diff: IDocumentDiff, reviewMode: boolean): void { + private _updateDiffRendering(entry: ChatEditingModifiedDocumentEntry, diff: IDocumentDiff, reviewMode: boolean): void { const originalModel = entry.originalModel; @@ -669,7 +669,7 @@ class DiffHunkWidget implements IOverlayWidget, IModifiedFileEntryChangeHunk { constructor( - readonly entry: ChatEditingModifiedFileEntry, + readonly entry: ChatEditingModifiedDocumentEntry, private readonly _change: DetailedLineRangeMapping, private readonly _versionId: number, private readonly _editor: ICodeEditor, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts new file mode 100644 index 00000000000..6c542e34104 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -0,0 +1,457 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { observableValue, IObservable, ITransaction, autorun, transaction } from '../../../../../base/common/observable.js'; +import { themeColorFromId } from '../../../../../base/common/themables.js'; +import { assertType } from '../../../../../base/common/types.js'; +import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { ISingleEditOperation, EditOperation } from '../../../../../editor/common/core/editOperation.js'; +import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; +import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; +import { OverviewRulerLane, MinimapPosition, ITextModel, IModelDeltaDecoration } from '../../../../../editor/common/model.js'; +import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js'; +import { ModelDecorationOptions, createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js'; +import { OffsetEdits } from '../../../../../editor/common/model/textModelOffsetEdit.js'; +import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js'; +import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; +import { SaveReason, IEditorPane } from '../../../../common/editor.js'; +import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; +import { IResolvedTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; +import { IModifiedFileEntry, ChatEditKind, WorkingSetEntryState, IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js'; +import { IChatResponseModel } from '../../common/chatModel.js'; +import { IChatService } from '../../common/chatService.js'; +import { ChatEditingCodeEditorIntegration } from './chatEditingCodeEditorIntegration.js'; +import { AbstractChatEditingModifiedFileEntry, pendingRewriteMinimap, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingSnapshotTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; + + +export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifiedFileEntry implements IModifiedFileEntry { + + private static readonly _lastEditDecorationOptions = ModelDecorationOptions.register({ + isWholeLine: true, + description: 'chat-last-edit', + className: 'chat-editing-last-edit-line', + marginClassName: 'chat-editing-last-edit', + overviewRuler: { + position: OverviewRulerLane.Full, + color: themeColorFromId(editorSelectionBackground) + }, + }); + + private static readonly _pendingEditDecorationOptions = ModelDecorationOptions.register({ + isWholeLine: true, + description: 'chat-pending-edit', + className: 'chat-editing-pending-edit', + minimap: { + position: MinimapPosition.Inline, + color: themeColorFromId(pendingRewriteMinimap) + } + }); + + + private readonly docSnapshot: ITextModel; + readonly initialContent: string; + private readonly doc: ITextModel; + private readonly docFileEditorModel: IResolvedTextFileEditorModel; + private _allEditsAreFromUs: boolean = true; + + get originalModel(): ITextModel { + return this.docSnapshot; + } + + get modifiedModel(): ITextModel { + return this.doc; + } + + private _isFirstEditAfterStartOrSnapshot: boolean = true; + private _edit: OffsetEdit = OffsetEdit.empty; + private _isEditFromUs: boolean = false; + private _diffOperation: Promise | undefined; + private _diffOperationIds: number = 0; + + private readonly _diffInfo = observableValue(this, nullDocumentDiff); + get diffInfo(): IObservable { + return this._diffInfo; + } + + readonly changesCount = this._diffInfo.map(diff => diff.changes.length); + + private readonly _editDecorationClear = this._register(new RunOnceScheduler(() => { this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); }, 500)); + private _editDecorations: string[] = []; + + + private readonly _diffTrimWhitespace: IObservable; + + constructor( + resourceRef: IReference, + private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, + telemetryInfo: IModifiedEntryTelemetryInfo, + kind: ChatEditKind, + initialContent: string | undefined, + @IModelService modelService: IModelService, + @ITextModelService textModelService: ITextModelService, + @ILanguageService languageService: ILanguageService, + @IConfigurationService configService: IConfigurationService, + @IFilesConfigurationService fileConfigService: IFilesConfigurationService, + @IChatService chatService: IChatService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + @IFileService fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService + ) { + super( + resourceRef.object.textEditorModel.uri, + telemetryInfo, + kind, + configService, + fileConfigService, + chatService, + fileService, + instantiationService + ); + + this.docFileEditorModel = this._register(resourceRef).object as IResolvedTextFileEditorModel; + this.doc = resourceRef.object.textEditorModel; + + this.initialContent = initialContent ?? this.doc.getValue(); + const docSnapshot = this.docSnapshot = this._register( + modelService.createModel( + createTextBufferFactoryFromSnapshot(initialContent ? stringToSnapshot(initialContent) : this.doc.createSnapshot()), + languageService.createById(this.doc.getLanguageId()), + this.originalURI, + false + ) + ); + + // Create a reference to this model to avoid it being disposed from under our nose + (async () => { + const reference = await textModelService.createModelReference(docSnapshot.uri); + if (this._store.isDisposed) { + reference.dispose(); + return; + } + this._register(reference); + })(); + + + this._register(this.doc.onDidChangeContent(e => this._mirrorEdits(e))); + + + + this._register(toDisposable(() => { + this._clearCurrentEditLineDecoration(); + })); + + this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService); + this._register(autorun(r => { + this._diffTrimWhitespace.read(r); + this._updateDiffInfoSeq(); + })); + } + + private _clearCurrentEditLineDecoration() { + this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); + } + + equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean { + return !!snapshot && + this.modifiedURI.toString() === snapshot.resource.toString() && + this.modifiedModel.getLanguageId() === snapshot.languageId && + this.originalModel.getValue() === snapshot.original && + this.modifiedModel.getValue() === snapshot.current && + this._edit.equals(snapshot.originalToCurrentEdit) && + this.state.get() === snapshot.state; + } + + createSnapshot(requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry { + this._isFirstEditAfterStartOrSnapshot = true; + return { + resource: this.modifiedURI, + languageId: this.modifiedModel.getLanguageId(), + snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(this._telemetryInfo.sessionId, requestId, undoStop, this.modifiedURI.path), + original: this.originalModel.getValue(), + current: this.modifiedModel.getValue(), + originalToCurrentEdit: this._edit, + state: this.state.get(), + telemetryInfo: this._telemetryInfo + }; + } + + restoreFromSnapshot(snapshot: ISnapshotEntry) { + this._stateObs.set(snapshot.state, undefined); + this.docSnapshot.setValue(snapshot.original); + this._setDocValue(snapshot.current); + this._edit = snapshot.originalToCurrentEdit; + this._updateDiffInfoSeq(); + } + + resetToInitialValue() { + this._setDocValue(this.initialContent); + } + + override async acceptStreamingEditsEnd(tx: ITransaction) { + await this._diffOperation; + super.acceptStreamingEditsEnd(tx); + } + + protected override _resetEditsState(tx: ITransaction): void { + super._resetEditsState(tx); + this._clearCurrentEditLineDecoration(); + } + + private _mirrorEdits(event: IModelContentChangedEvent) { + const edit = OffsetEdits.fromContentChanges(event.changes); + + if (this._isEditFromUs) { + const e_sum = this._edit; + const e_ai = edit; + this._edit = e_sum.compose(e_ai); + + } else { + + // e_ai + // d0 ---------------> s0 + // | | + // | | + // | e_user_r | e_user + // | | + // | | + // v e_ai_r v + /// d1 ---------------> s1 + // + // d0 - document snapshot + // s0 - document + // e_ai - ai edits + // e_user - user edits + // + const e_ai = this._edit; + const e_user = edit; + + const e_user_r = e_user.tryRebase(e_ai.inverse(this.docSnapshot.getValue()), true); + + if (e_user_r === undefined) { + // user edits overlaps/conflicts with AI edits + this._edit = e_ai.compose(e_user); + } else { + const edits = OffsetEdits.asEditOperations(e_user_r, this.docSnapshot); + this.docSnapshot.applyEdits(edits); + this._edit = e_ai.tryRebase(e_user_r); + } + + this._allEditsAreFromUs = false; + this._updateDiffInfoSeq(); + + const didResetToOriginalContent = this.doc.getValue() === this.initialContent; + const currentState = this._stateObs.get(); + switch (currentState) { + case WorkingSetEntryState.Modified: + if (didResetToOriginalContent) { + this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + break; + } + } + } + } + + acceptAgentEdits(textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void { + + // push stack element for the first edit + if (this._isFirstEditAfterStartOrSnapshot) { + this._isFirstEditAfterStartOrSnapshot = false; + const request = this._chatService.getSession(this._telemetryInfo.sessionId)?.getRequests().at(-1); + const label = request?.message.text ? localize('chatEditing1', "Chat Edit: '{0}'", request.message.text) : localize('chatEditing2', "Chat Edit"); + this._undoRedoService.pushElement(new SingleModelEditStackElement(label, 'chat.edit', this.doc, null)); + } + + const ops = textEdits.map(TextEdit.asEditOperation); + const undoEdits = this._applyEdits(ops); + + const maxLineNumber = undoEdits.reduce((max, op) => Math.max(max, op.range.startLineNumber), 0); + + const newDecorations: IModelDeltaDecoration[] = [ + // decorate pending edit (region) + { + options: ChatEditingModifiedDocumentEntry._pendingEditDecorationOptions, + range: new Range(maxLineNumber + 1, 1, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) + } + ]; + + if (maxLineNumber > 0) { + // decorate last edit + newDecorations.push({ + options: ChatEditingModifiedDocumentEntry._lastEditDecorationOptions, + range: new Range(maxLineNumber, 1, maxLineNumber, Number.MAX_SAFE_INTEGER) + }); + } + + this._editDecorations = this.doc.deltaDecorations(this._editDecorations, newDecorations); + + + transaction((tx) => { + if (!isLastEdits) { + this._stateObs.set(WorkingSetEntryState.Modified, tx); + this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); + const lineCount = this.doc.getLineCount(); + this._rewriteRatioObs.set(Math.min(1, maxLineNumber / lineCount), tx); + + } else { + this._resetEditsState(tx); + this._updateDiffInfoSeq(); + this._rewriteRatioObs.set(1, tx); + this._editDecorationClear.schedule(); + } + }); + } + + async acceptHunk(change: DetailedLineRangeMapping): Promise { + if (!this._diffInfo.get().changes.includes(change)) { + // diffInfo should have model version ids and check them (instead of the caller doing that) + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.modifiedModel.getValueInRange(edit.modifiedRange); + edits.push(EditOperation.replace(edit.originalRange, newText)); + } + this.docSnapshot.pushEditOperations(null, edits, _ => null); + await this._updateDiffInfoSeq(); + if (this.diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Accepted, undefined); + } + return true; + } + + async rejectHunk(change: DetailedLineRangeMapping): Promise { + if (!this._diffInfo.get().changes.includes(change)) { + return false; + } + const edits: ISingleEditOperation[] = []; + for (const edit of change.innerChanges ?? []) { + const newText = this.docSnapshot.getValueInRange(edit.originalRange); + edits.push(EditOperation.replace(edit.modifiedRange, newText)); + } + this.doc.pushEditOperations(null, edits, _ => null); + await this._updateDiffInfoSeq(); + if (this.diffInfo.get().identical) { + this._stateObs.set(WorkingSetEntryState.Rejected, undefined); + } + return true; + } + + private _applyEdits(edits: ISingleEditOperation[]) { + // make the actual edit + this._isEditFromUs = true; + try { + let result: ISingleEditOperation[] = []; + this.doc.pushEditOperations(null, edits, (undoEdits) => { + result = undoEdits; + return null; + }); + return result; + } finally { + this._isEditFromUs = false; + } + } + + private async _updateDiffInfoSeq() { + const myDiffOperationId = ++this._diffOperationIds; + await Promise.resolve(this._diffOperation); + if (this._diffOperationIds === myDiffOperationId) { + const thisDiffOperation = this._updateDiffInfo(); + this._diffOperation = thisDiffOperation; + await thisDiffOperation; + } + } + + private async _updateDiffInfo(): Promise { + + if (this.docSnapshot.isDisposed() || this.doc.isDisposed()) { + return; + } + + const docVersionNow = this.doc.getVersionId(); + const snapshotVersionNow = this.docSnapshot.getVersionId(); + + const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); + + const diff = await this._editorWorkerService.computeDiff( + this.docSnapshot.uri, + this.doc.uri, + { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, + 'advanced' + ); + + if (this.docSnapshot.isDisposed() || this.doc.isDisposed()) { + return; + } + + // only update the diff if the documents didn't change in the meantime + if (this.doc.getVersionId() === docVersionNow && this.docSnapshot.getVersionId() === snapshotVersionNow) { + const diff2 = diff ?? nullDocumentDiff; + this._diffInfo.set(diff2, undefined); + this._edit = OffsetEdits.fromLineRangeMapping(this.docSnapshot, this.doc, diff2.changes); + } + } + + protected override async _doAccept(tx: ITransaction | undefined): Promise { + this.docSnapshot.setValue(this.doc.createSnapshot()); + this._diffInfo.set(nullDocumentDiff, tx); + this._edit = OffsetEdit.empty; + await this._collapse(tx); + } + + protected override async _doReject(tx: ITransaction | undefined): Promise { + if (this.createdInRequestId === this._telemetryInfo.requestId) { + await this.docFileEditorModel.revert({ soft: true }); + await this._fileService.del(this.modifiedURI); + this._onDidDelete.fire(); + } else { + this._setDocValue(this.docSnapshot.getValue()); + if (this._allEditsAreFromUs) { + // save the file after discarding so that the dirty indicator goes away + // and so that an intermediate saved state gets reverted + await this.docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); + } + await this._collapse(tx); + } + } + + private _setDocValue(value: string): void { + if (this.doc.getValue() !== value) { + + this.doc.pushStackElement(); + const edit = EditOperation.replace(this.doc.getFullModelRange(), value); + + this._applyEdits([edit]); + this._updateDiffInfoSeq(); + this.doc.pushStackElement(); + } + } + + private async _collapse(transaction: ITransaction | undefined): Promise { + this._multiDiffEntryDelegate.collapse(transaction); + } + + protected _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration { + const codeEditor = getCodeEditor(editor.getControl()); + assertType(codeEditor); + return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, codeEditor, this); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index d1393e67874..a106c74b0c3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -3,47 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, DisposableMap, IReference, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; import { clamp } from '../../../../../base/common/numbers.js'; -import { autorun, derived, IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; -import { themeColorFromId } from '../../../../../base/common/themables.js'; -import { assertType } from '../../../../../base/common/types.js'; +import { autorun, derived, IObservable, ITransaction, observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; -import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; -import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; -import { TextEdit } from '../../../../../editor/common/languages.js'; -import { ILanguageService } from '../../../../../editor/common/languages/language.js'; -import { IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane } from '../../../../../editor/common/model.js'; -import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js'; -import { ModelDecorationOptions, createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js'; -import { OffsetEdits } from '../../../../../editor/common/model/textModelOffsetEdit.js'; -import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; +import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; import { localize } from '../../../../../nls.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; -import { editorBackground, editorSelectionBackground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; -import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; -import { IEditorPane, SaveReason } from '../../../../common/editor.js'; +import { editorBackground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; +import { IEditorPane } from '../../../../common/editor.js'; import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { IResolvedTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; import { IChatAgentResult } from '../../common/chatAgents.js'; import { ChatEditKind, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { ChatEditingCodeEditorIntegration } from './chatEditingCodeEditorIntegration.js'; -import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; +import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; class AutoAcceptControl { constructor( @@ -53,7 +33,7 @@ class AutoAcceptControl { ) { } } -const pendingRewriteMinimap = registerColor('chatEdits.minimapColor', +export const pendingRewriteMinimap = registerColor('chatEdits.minimapColor', transparent(editorBackground, 0.6), localize('editorSelectionBackground', "Color of pending edit regions in the minimap")); @@ -286,423 +266,6 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im } } -export class ChatEditingModifiedFileEntry extends AbstractChatEditingModifiedFileEntry implements IModifiedFileEntry { - - private static readonly _lastEditDecorationOptions = ModelDecorationOptions.register({ - isWholeLine: true, - description: 'chat-last-edit', - className: 'chat-editing-last-edit-line', - marginClassName: 'chat-editing-last-edit', - overviewRuler: { - position: OverviewRulerLane.Full, - color: themeColorFromId(editorSelectionBackground) - }, - }); - - private static readonly _pendingEditDecorationOptions = ModelDecorationOptions.register({ - isWholeLine: true, - description: 'chat-pending-edit', - className: 'chat-editing-pending-edit', - minimap: { - position: MinimapPosition.Inline, - color: themeColorFromId(pendingRewriteMinimap) - } - }); - - - private readonly docSnapshot: ITextModel; - readonly initialContent: string; - private readonly doc: ITextModel; - private readonly docFileEditorModel: IResolvedTextFileEditorModel; - private _allEditsAreFromUs: boolean = true; - - get originalModel(): ITextModel { - return this.docSnapshot; - } - - get modifiedModel(): ITextModel { - return this.doc; - } - - private _isFirstEditAfterStartOrSnapshot: boolean = true; - private _edit: OffsetEdit = OffsetEdit.empty; - private _isEditFromUs: boolean = false; - private _diffOperation: Promise | undefined; - private _diffOperationIds: number = 0; - - private readonly _diffInfo = observableValue(this, nullDocumentDiff); - get diffInfo(): IObservable { - return this._diffInfo; - } - - readonly changesCount = this._diffInfo.map(diff => diff.changes.length); - - private readonly _editDecorationClear = this._register(new RunOnceScheduler(() => { this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); }, 500)); - private _editDecorations: string[] = []; - - - private readonly _diffTrimWhitespace: IObservable; - - constructor( - resourceRef: IReference, - private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, - telemetryInfo: IModifiedEntryTelemetryInfo, - kind: ChatEditKind, - initialContent: string | undefined, - @IModelService modelService: IModelService, - @ITextModelService textModelService: ITextModelService, - @ILanguageService languageService: ILanguageService, - @IConfigurationService configService: IConfigurationService, - @IFilesConfigurationService fileConfigService: IFilesConfigurationService, - @IChatService chatService: IChatService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, - @IFileService fileService: IFileService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super( - resourceRef.object.textEditorModel.uri, - telemetryInfo, - kind, - configService, - fileConfigService, - chatService, - fileService, - instantiationService - ); - - this.docFileEditorModel = this._register(resourceRef).object as IResolvedTextFileEditorModel; - this.doc = resourceRef.object.textEditorModel; - - this.initialContent = initialContent ?? this.doc.getValue(); - const docSnapshot = this.docSnapshot = this._register( - modelService.createModel( - createTextBufferFactoryFromSnapshot(initialContent ? stringToSnapshot(initialContent) : this.doc.createSnapshot()), - languageService.createById(this.doc.getLanguageId()), - this.originalURI, - false - ) - ); - - // Create a reference to this model to avoid it being disposed from under our nose - (async () => { - const reference = await textModelService.createModelReference(docSnapshot.uri); - if (this._store.isDisposed) { - reference.dispose(); - return; - } - this._register(reference); - })(); - - - this._register(this.doc.onDidChangeContent(e => this._mirrorEdits(e))); - - - - this._register(toDisposable(() => { - this._clearCurrentEditLineDecoration(); - })); - - this._diffTrimWhitespace = observableConfigValue('diffEditor.ignoreTrimWhitespace', true, configService); - this._register(autorun(r => { - this._diffTrimWhitespace.read(r); - this._updateDiffInfoSeq(); - })); - } - - private _clearCurrentEditLineDecoration() { - this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); - } - - equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean { - return !!snapshot && - this.modifiedURI.toString() === snapshot.resource.toString() && - this.modifiedModel.getLanguageId() === snapshot.languageId && - this.originalModel.getValue() === snapshot.original && - this.modifiedModel.getValue() === snapshot.current && - this._edit.equals(snapshot.originalToCurrentEdit) && - this.state.get() === snapshot.state; - } - - createSnapshot(requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry { - this._isFirstEditAfterStartOrSnapshot = true; - return { - resource: this.modifiedURI, - languageId: this.modifiedModel.getLanguageId(), - snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(this._telemetryInfo.sessionId, requestId, undoStop, this.modifiedURI.path), - original: this.originalModel.getValue(), - current: this.modifiedModel.getValue(), - originalToCurrentEdit: this._edit, - state: this.state.get(), - telemetryInfo: this._telemetryInfo - }; - } - - restoreFromSnapshot(snapshot: ISnapshotEntry) { - this._stateObs.set(snapshot.state, undefined); - this.docSnapshot.setValue(snapshot.original); - this._setDocValue(snapshot.current); - this._edit = snapshot.originalToCurrentEdit; - this._updateDiffInfoSeq(); - } - - resetToInitialValue() { - this._setDocValue(this.initialContent); - } - - override async acceptStreamingEditsEnd(tx: ITransaction) { - await this._diffOperation; - super.acceptStreamingEditsEnd(tx); - } - - protected override _resetEditsState(tx: ITransaction): void { - super._resetEditsState(tx); - this._clearCurrentEditLineDecoration(); - } - - private _mirrorEdits(event: IModelContentChangedEvent) { - const edit = OffsetEdits.fromContentChanges(event.changes); - - if (this._isEditFromUs) { - const e_sum = this._edit; - const e_ai = edit; - this._edit = e_sum.compose(e_ai); - - } else { - - // e_ai - // d0 ---------------> s0 - // | | - // | | - // | e_user_r | e_user - // | | - // | | - // v e_ai_r v - /// d1 ---------------> s1 - // - // d0 - document snapshot - // s0 - document - // e_ai - ai edits - // e_user - user edits - // - - const e_ai = this._edit; - const e_user = edit; - - const e_user_r = e_user.tryRebase(e_ai.inverse(this.docSnapshot.getValue()), true); - - if (e_user_r === undefined) { - // user edits overlaps/conflicts with AI edits - this._edit = e_ai.compose(e_user); - } else { - const edits = OffsetEdits.asEditOperations(e_user_r, this.docSnapshot); - this.docSnapshot.applyEdits(edits); - this._edit = e_ai.tryRebase(e_user_r); - } - - this._allEditsAreFromUs = false; - this._updateDiffInfoSeq(); - } - - if (!this.isCurrentlyBeingModifiedBy.get()) { - const didResetToOriginalContent = this.doc.getValue() === this.initialContent; - const currentState = this._stateObs.get(); - switch (currentState) { - case WorkingSetEntryState.Modified: - if (didResetToOriginalContent) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); - break; - } - } - } - } - - acceptAgentEdits(textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void { - - // push stack element for the first edit - if (this._isFirstEditAfterStartOrSnapshot) { - this._isFirstEditAfterStartOrSnapshot = false; - const request = this._chatService.getSession(this._telemetryInfo.sessionId)?.getRequests().at(-1); - const label = request?.message.text ? localize('chatEditing1', "Chat Edit: '{0}'", request.message.text) : localize('chatEditing2', "Chat Edit"); - this._undoRedoService.pushElement(new SingleModelEditStackElement(label, 'chat.edit', this.doc, null)); - } - - const ops = textEdits.map(TextEdit.asEditOperation); - const undoEdits = this._applyEdits(ops); - - const maxLineNumber = undoEdits.reduce((max, op) => Math.max(max, op.range.startLineNumber), 0); - - const newDecorations: IModelDeltaDecoration[] = [ - // decorate pending edit (region) - { - options: ChatEditingModifiedFileEntry._pendingEditDecorationOptions, - range: new Range(maxLineNumber + 1, 1, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) - } - ]; - - if (maxLineNumber > 0) { - // decorate last edit - newDecorations.push({ - options: ChatEditingModifiedFileEntry._lastEditDecorationOptions, - range: new Range(maxLineNumber, 1, maxLineNumber, Number.MAX_SAFE_INTEGER) - }); - } - - this._editDecorations = this.doc.deltaDecorations(this._editDecorations, newDecorations); - - - transaction((tx) => { - if (!isLastEdits) { - this._stateObs.set(WorkingSetEntryState.Modified, tx); - this._isCurrentlyBeingModifiedByObs.set(responseModel, tx); - const lineCount = this.doc.getLineCount(); - this._rewriteRatioObs.set(Math.min(1, maxLineNumber / lineCount), tx); - - } else { - this._resetEditsState(tx); - this._updateDiffInfoSeq(); - this._rewriteRatioObs.set(1, tx); - this._editDecorationClear.schedule(); - } - }); - } - - async acceptHunk(change: DetailedLineRangeMapping): Promise { - if (!this._diffInfo.get().changes.includes(change)) { - // diffInfo should have model version ids and check them (instead of the caller doing that) - return false; - } - const edits: ISingleEditOperation[] = []; - for (const edit of change.innerChanges ?? []) { - const newText = this.modifiedModel.getValueInRange(edit.modifiedRange); - edits.push(EditOperation.replace(edit.originalRange, newText)); - } - this.docSnapshot.pushEditOperations(null, edits, _ => null); - await this._updateDiffInfoSeq(); - if (this.diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Accepted, undefined); - } - return true; - } - - async rejectHunk(change: DetailedLineRangeMapping): Promise { - if (!this._diffInfo.get().changes.includes(change)) { - return false; - } - const edits: ISingleEditOperation[] = []; - for (const edit of change.innerChanges ?? []) { - const newText = this.docSnapshot.getValueInRange(edit.originalRange); - edits.push(EditOperation.replace(edit.modifiedRange, newText)); - } - this.doc.pushEditOperations(null, edits, _ => null); - await this._updateDiffInfoSeq(); - if (this.diffInfo.get().identical) { - this._stateObs.set(WorkingSetEntryState.Rejected, undefined); - } - return true; - } - - private _applyEdits(edits: ISingleEditOperation[]) { - // make the actual edit - this._isEditFromUs = true; - try { - let result: ISingleEditOperation[] = []; - this.doc.pushEditOperations(null, edits, (undoEdits) => { - result = undoEdits; - return null; - }); - return result; - } finally { - this._isEditFromUs = false; - } - } - - private async _updateDiffInfoSeq() { - const myDiffOperationId = ++this._diffOperationIds; - await Promise.resolve(this._diffOperation); - if (this._diffOperationIds === myDiffOperationId) { - const thisDiffOperation = this._updateDiffInfo(); - this._diffOperation = thisDiffOperation; - await thisDiffOperation; - } - } - - private async _updateDiffInfo(): Promise { - - if (this.docSnapshot.isDisposed() || this.doc.isDisposed()) { - return; - } - - const docVersionNow = this.doc.getVersionId(); - const snapshotVersionNow = this.docSnapshot.getVersionId(); - - const ignoreTrimWhitespace = this._diffTrimWhitespace.get(); - - const diff = await this._editorWorkerService.computeDiff( - this.docSnapshot.uri, - this.doc.uri, - { ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 }, - 'advanced' - ); - - if (this.docSnapshot.isDisposed() || this.doc.isDisposed()) { - return; - } - - // only update the diff if the documents didn't change in the meantime - if (this.doc.getVersionId() === docVersionNow && this.docSnapshot.getVersionId() === snapshotVersionNow) { - const diff2 = diff ?? nullDocumentDiff; - this._diffInfo.set(diff2, undefined); - this._edit = OffsetEdits.fromLineRangeMapping(this.docSnapshot, this.doc, diff2.changes); - } - } - - protected override async _doAccept(tx: ITransaction | undefined): Promise { - this.docSnapshot.setValue(this.doc.createSnapshot()); - this._diffInfo.set(nullDocumentDiff, tx); - this._edit = OffsetEdit.empty; - await this._collapse(tx); - } - - protected override async _doReject(tx: ITransaction | undefined): Promise { - if (this.createdInRequestId === this._telemetryInfo.requestId) { - await this.docFileEditorModel.revert({ soft: true }); - await this._fileService.del(this.modifiedURI); - this._onDidDelete.fire(); - } else { - this._setDocValue(this.docSnapshot.getValue()); - if (this._allEditsAreFromUs) { - // save the file after discarding so that the dirty indicator goes away - // and so that an intermediate saved state gets reverted - await this.docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); - } - await this._collapse(tx); - } - } - - private _setDocValue(value: string): void { - if (this.doc.getValue() !== value) { - - this.doc.pushStackElement(); - const edit = EditOperation.replace(this.doc.getFullModelRange(), value); - - this._applyEdits([edit]); - this._updateDiffInfoSeq(); - this.doc.pushStackElement(); - } - } - - private async _collapse(transaction: ITransaction | undefined): Promise { - this._multiDiffEntryDelegate.collapse(transaction); - } - - protected _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration { - const codeEditor = getCodeEditor(editor.getControl()); - assertType(codeEditor); - return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, codeEditor, this); - } -} - export interface IModifiedEntryTelemetryInfo { readonly agentId: string | undefined; readonly command: string | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index 93286eb2b25..2f8e8eff790 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -18,9 +18,10 @@ import { IFilesConfigurationService } from '../../../../services/filesConfigurat import { IResolvedTextFileEditorModel } from '../../../../services/textfile/common/textfiles.js'; import { ChatEditKind } from '../../common/chatEditingService.js'; import { IChatService } from '../../common/chatService.js'; -import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo } from './chatEditingModifiedFileEntry.js'; +import { IModifiedEntryTelemetryInfo } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; -export class ChatEditingModifiedNotebookEntry extends ChatEditingModifiedFileEntry { +export class ChatEditingModifiedNotebookEntry extends ChatEditingModifiedDocumentEntry { private readonly resolveTextFileEditorModel: IResolvedTextFileEditorModel; constructor( diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 9a0bcff2c27..4bda391f57a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -38,7 +38,7 @@ import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingResourceContextKey, ChatEditingSessionState, IChatEditingService, IChatEditingSession, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, IStreamingEdits, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { ChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -172,11 +172,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } - private _lookupEntry(uri: URI): ChatEditingModifiedFileEntry | undefined { + private _lookupEntry(uri: URI): ChatEditingModifiedDocumentEntry | undefined { for (const item of Iterable.concat(this.editingSessionsObs.get())) { const candidate = item.getEntry(uri); - if (candidate instanceof ChatEditingModifiedFileEntry) { + if (candidate instanceof ChatEditingModifiedDocumentEntry) { // make sure to ref-count this object return candidate.acquire(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 1d53f309888..4ead9f58888 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -47,7 +47,8 @@ import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; -import { AbstractChatEditingModifiedFileEntry, ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -105,8 +106,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio */ private readonly _initialFileContents = new ResourceMap(); - private readonly _entriesObs = observableValue(this, []); - public get entries(): IObservable { + private readonly _entriesObs = observableValue(this, []); + public get entries(): IObservable { this._assertNotDisposed(); return this._entriesObs; } @@ -169,7 +170,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio constructor( readonly chatSessionId: string, readonly isGlobalEditingSession: boolean, - private _lookupExternalEntry: (uri: URI) => ChatEditingModifiedFileEntry | undefined, + private _lookupExternalEntry: (uri: URI) => ChatEditingModifiedDocumentEntry | undefined, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, @ILanguageService private readonly _languageService: ILanguageService, @@ -214,7 +215,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio })); } - private _getEntry(uri: URI): ChatEditingModifiedFileEntry | undefined { + private _getEntry(uri: URI): ChatEditingModifiedDocumentEntry | undefined { return this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri)); } @@ -239,8 +240,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private _triggerSaveParticipantsOnAccept() { - const im = this._register(new DisposableMap()); - const attachToEntry = (entry: ChatEditingModifiedFileEntry) => { + const im = this._register(new DisposableMap()); + const attachToEntry = (entry: ChatEditingModifiedDocumentEntry) => { return autorunDelta(entry.state, ({ lastValue, newValue }) => { if (newValue === WorkingSetEntryState.Accepted && lastValue === WorkingSetEntryState.Modified) { // Don't save a file if there's still pending changes. If there's not (e.g. @@ -501,7 +502,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } } - const entriesArr: ChatEditingModifiedFileEntry[] = []; + const entriesArr: ChatEditingModifiedDocumentEntry[] = []; // Restore all entries from the snapshot for (const snapshotEntry of entries.values()) { const entry = await this._getOrCreateModifiedFileEntry(snapshotEntry.resource, snapshotEntry.telemetryInfo); @@ -859,7 +860,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio * * @param next If true, this will edit the snapshot _after_ the undo stop */ - private ensureEditInUndoStopMatches(requestId: string, undoStop: string | undefined, entry: ChatEditingModifiedFileEntry, next: boolean, tx: ITransaction) { + private ensureEditInUndoStopMatches(requestId: string, undoStop: string | undefined, entry: ChatEditingModifiedDocumentEntry, next: boolean, tx: ITransaction) { const history = this._linearHistory.get(); const snapIndex = history.findIndex(s => s.requestId === requestId); if (snapIndex === -1) { @@ -931,7 +932,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio * * @returns The modified file entry. */ - private async _getOrCreateModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo): Promise { + private async _getOrCreateModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo): Promise { const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)); if (existingEntry) { if (telemetryInfo.requestId !== existingEntry.telemetryInfo.requestId) { @@ -940,7 +941,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return existingEntry; } - let entry: ChatEditingModifiedFileEntry; + let entry: ChatEditingModifiedDocumentEntry; const existingExternalEntry = this._lookupExternalEntry(resource); if (existingExternalEntry) { entry = existingExternalEntry; @@ -979,10 +980,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return entry; } - private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise { + private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise { try { const ref = await this._textModelService.createModelReference(resource); - const ctor = this._notebookService.hasSupportedNotebooks(resource) ? ChatEditingModifiedNotebookEntry : ChatEditingModifiedFileEntry; + const ctor = this._notebookService.hasSupportedNotebooks(resource) ? ChatEditingModifiedNotebookEntry : ChatEditingModifiedDocumentEntry; return this._instantiationService.createInstance(ctor, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, telemetryInfo, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent); } catch (err) { if (mustExist) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts index 0f45374a556..2ab6ed9f0b3 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts @@ -9,7 +9,7 @@ import { INotebookService } from '../../../common/notebookService.js'; import { bufferToStream, VSBuffer } from '../../../../../../base/common/buffer.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { createDecorator, IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { ChatEditingModifiedFileEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; export const INotebookOriginalModelReferenceFactory = createDecorator('INotebookOriginalModelReferenceFactory'); @@ -34,7 +34,7 @@ export class OriginalNotebookModelReferenceCollection extends ReferenceCollectio return model; } // TODO@DonJayamanne FIX ME, don't use `originalModel` - const bytes = VSBuffer.fromString((fileEntry as ChatEditingModifiedFileEntry).originalModel.getValue()); + const bytes = VSBuffer.fromString((fileEntry as ChatEditingModifiedDocumentEntry).originalModel.getValue()); const stream = bufferToStream(bytes); return this.notebookService.createNotebookTextModel(viewType, uri, stream); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts index 93e41797e69..6a6e6da8c25 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts @@ -13,7 +13,7 @@ import { raceCancellation, ThrottledDelayer } from '../../../../../../base/commo import { CellDiffInfo, computeDiff, prettyChanges } from '../../diff/notebookDiffViewModel.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { INotebookEditorWorkerService } from '../../../common/services/notebookWorkerService.js'; -import { ChatEditingModifiedFileEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; import { CellEditType, ICellDto2, ICellEditOperation, ICellReplaceEdit, NotebookData, NotebookSetting } from '../../../common/notebookCommon.js'; import { URI } from '../../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -129,11 +129,11 @@ export class NotebookModelSynchronizer extends Disposable { } // TODO@DonJayamanne FIX ME, don't use `modifiedModel` - const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel; + const modifiedModel = (entry as ChatEditingModifiedDocumentEntry).modifiedModel; let cancellationToken = store.add(new CancellationTokenSource()); store.add(modifiedModel.onDidChangeContent(async () => { // TODO@DonJayamanne FIX ME, don't use `originalModel` - if (!this.isTextEditFromUs && !modifiedModel.isDisposed() && !(entry as ChatEditingModifiedFileEntry).originalModel.isDisposed() && modifiedModel.getValue() !== (entry as ChatEditingModifiedFileEntry).originalModel.getValue()) { + if (!this.isTextEditFromUs && !modifiedModel.isDisposed() && !(entry as ChatEditingModifiedDocumentEntry).originalModel.isDisposed() && modifiedModel.getValue() !== (entry as ChatEditingModifiedDocumentEntry).originalModel.getValue()) { cancellationToken = store.add(new CancellationTokenSource()); updateNotebookModel(entry, cancellationToken.token); } @@ -258,7 +258,7 @@ export class NotebookModelSynchronizer extends Disposable { } private async accept(entry: IModifiedFileEntry) { - const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel; + const modifiedModel = (entry as ChatEditingModifiedDocumentEntry).modifiedModel; const content = modifiedModel.getValue(); await this.updateNotebook(VSBuffer.fromString(content), false); this._diffInfo.set(undefined, undefined); @@ -270,7 +270,7 @@ export class NotebookModelSynchronizer extends Disposable { } private async updateTextDocumentModel(entry: IModifiedFileEntry) { - const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel; + const modifiedModel = (entry as ChatEditingModifiedDocumentEntry).modifiedModel; const stream = await this.notebookService.createNotebookTextDocumentSnapshot(this.model.uri, SnapshotContext.Save, CancellationToken.None); const buffer = await streamToBuffer(stream); const text = new TextDecoder().decode(buffer.buffer); @@ -300,7 +300,7 @@ export class NotebookModelSynchronizer extends Disposable { } private async updateNotebookModel(entry: IModifiedFileEntry, token: CancellationToken) { - const modifiedModelVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId(); + const modifiedModelVersion = (entry as ChatEditingModifiedDocumentEntry).modifiedModel.getVersionId(); const currentModel = this.model; const modelVersion = currentModel?.versionId ?? 0; const modelWithChatEdits = await this.getModifiedModelForDiff(entry, token); @@ -312,7 +312,7 @@ export class NotebookModelSynchronizer extends Disposable { const cellDiffInfo = (await this.computeDiff(originalModel, modelWithChatEdits, token))?.cellDiffInfo; // This is the diff from the current model to the model with chat edits. const cellDiffInfoToApplyEdits = (await this.computeDiff(currentModel, modelWithChatEdits, token))?.cellDiffInfo; - const currentVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId(); + const currentVersion = (entry as ChatEditingModifiedDocumentEntry).modifiedModel.getVersionId(); if (!cellDiffInfo || !cellDiffInfoToApplyEdits || token.isCancellationRequested || currentVersion !== modifiedModelVersion || modelVersion !== currentModel.versionId) { return; } @@ -443,7 +443,7 @@ export class NotebookModelSynchronizer extends Disposable { private previousUriOfModelForDiff?: URI; private async getModifiedModelForDiff(entry: IModifiedFileEntry, token: CancellationToken): Promise { - const text = (entry as ChatEditingModifiedFileEntry).modifiedModel.getValue(); + const text = (entry as ChatEditingModifiedDocumentEntry).modifiedModel.getValue(); const bytes = VSBuffer.fromString(text); const uri = entry.modifiedURI.with({ scheme: `NotebookChatEditorController.modifiedScheme${Date.now().toString()}` }); const stream = bufferToStream(bytes);