mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
move ChatEditingModifiedDocumentEntry into its own file (#241121)
This commit is contained in:
+4
-4
@@ -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,
|
||||
|
||||
+457
@@ -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;
|
||||
|
||||
+3
-2
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user