move ChatEditingModifiedDocumentEntry into its own file (#241121)

This commit is contained in:
Johannes Rieken
2025-02-18 21:17:00 +01:00
committed by GitHub
parent 6856a3dd7f
commit dfad570d15
8 changed files with 498 additions and 476 deletions
@@ -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,
@@ -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<any> | undefined;
private _diffOperationIds: number = 0;
private readonly _diffInfo = observableValue<IDocumentDiff>(this, nullDocumentDiff);
get diffInfo(): IObservable<IDocumentDiff> {
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<boolean>;
constructor(
resourceRef: IReference<IResolvedTextEditorModel>,
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<boolean> {
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<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
this._multiDiffEntryDelegate.collapse(transaction);
}
protected _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration {
const codeEditor = getCodeEditor(editor.getControl());
assertType(codeEditor);
return this._instantiationService.createInstance(ChatEditingCodeEditorIntegration, codeEditor, this);
}
}
@@ -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<any> | undefined;
private _diffOperationIds: number = 0;
private readonly _diffInfo = observableValue<IDocumentDiff>(this, nullDocumentDiff);
get diffInfo(): IObservable<IDocumentDiff> {
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<boolean>;
constructor(
resourceRef: IReference<IResolvedTextEditorModel>,
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<boolean> {
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<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
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;
@@ -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(
@@ -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();
}
@@ -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<string>();
private readonly _entriesObs = observableValue<readonly ChatEditingModifiedFileEntry[]>(this, []);
public get entries(): IObservable<readonly ChatEditingModifiedFileEntry[]> {
private readonly _entriesObs = observableValue<readonly ChatEditingModifiedDocumentEntry[]>(this, []);
public get entries(): IObservable<readonly ChatEditingModifiedDocumentEntry[]> {
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<ChatEditingModifiedFileEntry>());
const attachToEntry = (entry: ChatEditingModifiedFileEntry) => {
const im = this._register(new DisposableMap<ChatEditingModifiedDocumentEntry>());
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<ChatEditingModifiedFileEntry> {
private async _getOrCreateModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo): Promise<ChatEditingModifiedDocumentEntry> {
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<ChatEditingModifiedFileEntry> {
private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise<ChatEditingModifiedDocumentEntry> {
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) {
@@ -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>('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);
@@ -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<NotebookTextModel | undefined> {
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);