Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { RunOnceScheduler } from '../../../../../base/common/async.js';6import { Emitter } from '../../../../../base/common/event.js';7import { Disposable, DisposableMap, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';8import { Schemas } from '../../../../../base/common/network.js';9import { clamp } from '../../../../../base/common/numbers.js';10import { autorun, derived, IObservable, ITransaction, observableValue, observableValueOpts, transaction } from '../../../../../base/common/observable.js';11import { URI } from '../../../../../base/common/uri.js';12import { TextEdit } from '../../../../../editor/common/languages.js';13import { localize } from '../../../../../nls.js';14import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';15import { IFileService } from '../../../../../platform/files/common/files.js';16import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';17import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';18import { editorBackground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';19import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';20import { IEditorPane } from '../../../../common/editor.js';21import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';22import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';23import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js';24import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';25import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js';26import { IChatResponseModel } from '../../common/chatModel.js';27import { ChatUserAction, IChatService } from '../../common/chatService.js';2829class AutoAcceptControl {30constructor(31readonly total: number,32readonly remaining: number,33readonly cancel: () => void34) { }35}3637export const pendingRewriteMinimap = registerColor('minimap.chatEditHighlight',38transparent(editorBackground, 0.6),39localize('editorSelectionBackground', "Color of pending edit regions in the minimap"));404142export abstract class AbstractChatEditingModifiedFileEntry extends Disposable implements IModifiedFileEntry {4344static readonly scheme = 'modified-file-entry';4546private static lastEntryId = 0;4748readonly entryId = `${AbstractChatEditingModifiedFileEntry.scheme}::${++AbstractChatEditingModifiedFileEntry.lastEntryId}`;4950protected readonly _onDidDelete = this._register(new Emitter<void>());51readonly onDidDelete = this._onDidDelete.event;5253protected readonly _stateObs = observableValue<ModifiedFileEntryState>(this, ModifiedFileEntryState.Modified);54readonly state: IObservable<ModifiedFileEntryState> = this._stateObs;5556protected readonly _waitsForLastEdits = observableValue<boolean>(this, false);57readonly waitsForLastEdits: IObservable<boolean> = this._waitsForLastEdits;5859protected readonly _isCurrentlyBeingModifiedByObs = observableValue<IChatResponseModel | undefined>(this, undefined);60readonly isCurrentlyBeingModifiedBy: IObservable<IChatResponseModel | undefined> = this._isCurrentlyBeingModifiedByObs;6162protected readonly _lastModifyingResponseObs = observableValueOpts<IChatResponseModel | undefined>({ equalsFn: (a, b) => a?.requestId === b?.requestId }, undefined);63readonly lastModifyingResponse: IObservable<IChatResponseModel | undefined> = this._lastModifyingResponseObs;6465protected readonly _lastModifyingResponseInProgressObs = this._lastModifyingResponseObs.map((value, r) => {66return value?.isInProgress.read(r) ?? false;67});6869protected readonly _rewriteRatioObs = observableValue<number>(this, 0);70readonly rewriteRatio: IObservable<number> = this._rewriteRatioObs;7172private readonly _reviewModeTempObs = observableValue<true | undefined>(this, undefined);73readonly reviewMode: IObservable<boolean>;7475private readonly _autoAcceptCtrl = observableValue<AutoAcceptControl | undefined>(this, undefined);76readonly autoAcceptController: IObservable<AutoAcceptControl | undefined> = this._autoAcceptCtrl;7778protected readonly _autoAcceptTimeout: IObservable<number>;7980get telemetryInfo(): IModifiedEntryTelemetryInfo {81return this._telemetryInfo;82}8384readonly createdInRequestId: string | undefined;8586get lastModifyingRequestId() {87return this._telemetryInfo.requestId;88}8990private _refCounter: number = 1;9192readonly abstract originalURI: URI;9394protected readonly _userEditScheduler = this._register(new RunOnceScheduler(() => this._notifySessionAction('userModified'), 1000));9596constructor(97readonly modifiedURI: URI,98protected _telemetryInfo: IModifiedEntryTelemetryInfo,99kind: ChatEditKind,100@IConfigurationService configService: IConfigurationService,101@IFilesConfigurationService protected _fileConfigService: IFilesConfigurationService,102@IChatService protected readonly _chatService: IChatService,103@IFileService protected readonly _fileService: IFileService,104@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,105@IInstantiationService protected readonly _instantiationService: IInstantiationService,106@IAiEditTelemetryService private readonly _aiEditTelemetryService: IAiEditTelemetryService,107) {108super();109110if (kind === ChatEditKind.Created) {111this.createdInRequestId = this._telemetryInfo.requestId;112}113114if (this.modifiedURI.scheme !== Schemas.untitled && this.modifiedURI.scheme !== Schemas.vscodeNotebookCell) {115this._register(this._fileService.watch(this.modifiedURI));116this._register(this._fileService.onDidFilesChange(e => {117if (e.affects(this.modifiedURI) && kind === ChatEditKind.Created && e.gotDeleted()) {118this._onDidDelete.fire();119}120}));121}122123// review mode depends on setting and temporary override124const autoAcceptRaw = observableConfigValue('chat.editing.autoAcceptDelay', 0, configService);125this._autoAcceptTimeout = derived(r => {126const value = autoAcceptRaw.read(r);127return clamp(value, 0, 100);128});129this.reviewMode = derived(r => {130const configuredValue = this._autoAcceptTimeout.read(r);131const tempValue = this._reviewModeTempObs.read(r);132return tempValue ?? configuredValue === 0;133});134135this._store.add(toDisposable(() => this._lastModifyingResponseObs.set(undefined, undefined)));136137const autoSaveOff = this._store.add(new MutableDisposable());138this._store.add(autorun(r => {139if (this._waitsForLastEdits.read(r)) {140autoSaveOff.value = _fileConfigService.disableAutoSave(this.modifiedURI);141} else {142autoSaveOff.clear();143}144}));145146this._store.add(autorun(r => {147const inProgress = this._lastModifyingResponseInProgressObs.read(r);148if (inProgress === false && !this.reviewMode.read(r)) {149// AUTO accept mode (when request is done)150151const acceptTimeout = this._autoAcceptTimeout.get() * 1000;152const future = Date.now() + acceptTimeout;153const update = () => {154155const reviewMode = this.reviewMode.get();156if (reviewMode) {157// switched back to review mode158this._autoAcceptCtrl.set(undefined, undefined);159return;160}161162const remain = Math.round(future - Date.now());163if (remain <= 0) {164this.accept();165} else {166const handle = setTimeout(update, 100);167this._autoAcceptCtrl.set(new AutoAcceptControl(acceptTimeout, remain, () => {168clearTimeout(handle);169this._autoAcceptCtrl.set(undefined, undefined);170}), undefined);171}172};173update();174}175}));176}177178override dispose(): void {179if (--this._refCounter === 0) {180super.dispose();181}182}183184acquire() {185this._refCounter++;186return this;187}188189enableReviewModeUntilSettled(): void {190191this._reviewModeTempObs.set(true, undefined);192193const cleanup = autorun(r => {194// reset config when settled195const resetConfig = this.state.read(r) !== ModifiedFileEntryState.Modified;196if (resetConfig) {197this._store.delete(cleanup);198this._reviewModeTempObs.set(undefined, undefined);199}200});201202this._store.add(cleanup);203}204205updateTelemetryInfo(telemetryInfo: IModifiedEntryTelemetryInfo) {206this._telemetryInfo = telemetryInfo;207}208209async accept(): Promise<void> {210if (this._stateObs.get() !== ModifiedFileEntryState.Modified) {211// already accepted or rejected212return;213}214215await this._doAccept();216transaction(tx => {217this._stateObs.set(ModifiedFileEntryState.Accepted, tx);218this._autoAcceptCtrl.set(undefined, tx);219});220221this._notifySessionAction('accepted');222}223224protected abstract _doAccept(): Promise<void>;225226async reject(): Promise<void> {227if (this._stateObs.get() !== ModifiedFileEntryState.Modified) {228// already accepted or rejected229return;230}231232this._notifySessionAction('rejected');233await this._doReject();234transaction(tx => {235this._stateObs.set(ModifiedFileEntryState.Rejected, tx);236this._autoAcceptCtrl.set(undefined, tx);237});238}239240protected abstract _doReject(): Promise<void>;241242protected _notifySessionAction(outcome: 'accepted' | 'rejected' | 'userModified') {243this._notifyAction({ kind: 'chatEditingSessionAction', uri: this.modifiedURI, hasRemainingEdits: false, outcome });244}245246protected _notifyAction(action: ChatUserAction) {247if (action.kind === 'chatEditingHunkAction') {248this._aiEditTelemetryService.handleCodeAccepted({249suggestionId: undefined, // TODO@hediet try to figure this out250acceptanceMethod: 'accept',251presentation: 'highlightedEdit',252modelId: this._telemetryInfo.modelId,253modeId: this._telemetryInfo.modeId,254applyCodeBlockSuggestionId: this._telemetryInfo.applyCodeBlockSuggestionId,255editDeltaInfo: new EditDeltaInfo(256action.linesAdded,257action.linesRemoved,258-1,259-1,260),261feature: this._telemetryInfo.feature,262languageId: action.languageId,263});264}265266this._chatService.notifyUserAction({267action,268agentId: this._telemetryInfo.agentId,269modelId: this._telemetryInfo.modelId,270modeId: this._telemetryInfo.modeId,271command: this._telemetryInfo.command,272sessionId: this._telemetryInfo.sessionId,273requestId: this._telemetryInfo.requestId,274result: this._telemetryInfo.result275});276}277278private readonly _editorIntegrations = this._register(new DisposableMap<IEditorPane, IModifiedFileEntryEditorIntegration>());279280getEditorIntegration(pane: IEditorPane): IModifiedFileEntryEditorIntegration {281let value = this._editorIntegrations.get(pane);282if (!value) {283value = this._createEditorIntegration(pane);284this._editorIntegrations.set(pane, value);285}286return value;287}288289/**290* Create the editor integration for this entry and the given editor pane. This will only be called291* once (and cached) per pane. The integration is meant to be scoped to this entry only and when the292* passed pane/editor changes input, then the editor integration must handle that, e.g use default/null293* values294*/295protected abstract _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration;296297abstract readonly changesCount: IObservable<number>;298299acceptStreamingEditsStart(responseModel: IChatResponseModel, tx: ITransaction) {300this._resetEditsState(tx);301this._isCurrentlyBeingModifiedByObs.set(responseModel, tx);302this._lastModifyingResponseObs.set(responseModel, tx);303this._autoAcceptCtrl.get()?.cancel();304305const undoRedoElement = this._createUndoRedoElement(responseModel);306if (undoRedoElement) {307this._undoRedoService.pushElement(undoRedoElement);308}309}310311protected abstract _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement | undefined;312313abstract acceptAgentEdits(uri: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<void>;314315async acceptStreamingEditsEnd() {316this._resetEditsState(undefined);317318if (await this._areOriginalAndModifiedIdentical()) {319// ACCEPT if identical320await this.accept();321}322}323324protected abstract _areOriginalAndModifiedIdentical(): Promise<boolean>;325326protected _resetEditsState(tx: ITransaction | undefined): void {327this._isCurrentlyBeingModifiedByObs.set(undefined, tx);328this._rewriteRatioObs.set(0, tx);329this._waitsForLastEdits.set(false, tx);330}331332// --- snapshot333334abstract createSnapshot(requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry;335336abstract equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean;337338abstract restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk?: boolean): Promise<void>;339340// --- inital content341342abstract resetToInitialContent(): Promise<void>;343344abstract initialContent: string;345}346347348