From fce85e9d2722559857a28a7332bea88d24a3f0cb Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 15 Jan 2025 09:38:56 +0100 Subject: [PATCH] uses /edit editor UX for "Apply In Editor" (#237944) * WIP * uses /edit editor UX for "Apply In Editor" fixes https://github.com/microsoft/vscode-copilot/issues/8577 --- src/vs/base/common/iterator.ts | 12 ++- .../browser/actions/chatCodeblockActions.ts | 2 +- .../browser/actions/codeBlockOperations.ts | 2 +- .../chatEditingModifiedFileEntry.ts | 13 ++++ .../browser/chatEditing/chatEditingService.ts | 75 +++++++++++++++---- .../browser/chatEditing/chatEditingSession.ts | 33 ++++++-- .../contrib/chat/browser/chatEditorActions.ts | 15 ++-- .../chat/browser/chatEditorController.ts | 55 +++++++++----- .../contrib/chat/common/chatEditingService.ts | 17 +++-- .../browser/inlineChatController.ts | 59 +++++++++------ .../contrib/scm/browser/quickDiffModel.ts | 21 +++++- 11 files changed, 218 insertions(+), 86 deletions(-) diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index ca55068751d05..fedcfe7edefa8 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isIterable } from './types.js'; + export namespace Iterable { export function is(thing: any): thing is Iterable { @@ -90,9 +92,13 @@ export namespace Iterable { } } - export function* concat(...iterables: Iterable[]): Iterable { - for (const iterable of iterables) { - yield* iterable; + export function* concat(...iterables: (Iterable | T)[]): Iterable { + for (const item of iterables) { + if (isIterable(item)) { + yield* item; + } else { + yield item; + } } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 248bbd7da5314..33b9e4c323cc8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -558,7 +558,7 @@ export function registerChatCodeCompareBlockActions() { const inlineChatController = InlineChatController.get(editorToApply); if (inlineChatController) { editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - inlineChatController.reviewEdits(firstEdit.range, textEdits, CancellationToken.None); + inlineChatController.reviewEdits(textEdits, CancellationToken.None); response.setEditApplied(item, 1); return true; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 5fafadbd6c02d..f206b6e1065c8 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -266,7 +266,7 @@ export class ApplyCodeBlockOperation { const inlineChatController = InlineChatController.get(codeEditor); if (inlineChatController) { let isOpen = true; - const promise = inlineChatController.reviewEdits(codeEditor.getSelection(), edits, tokenSource.token); + const promise = inlineChatController.reviewEdits(edits, tokenSource.token); promise.finally(() => { isOpen = false; tokenSource.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index e022f402c79c2..604bf523b6000 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -134,6 +134,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie private readonly _diffTrimWhitespace: IObservable; + private _refCounter: number = 1; + constructor( resourceRef: IReference, private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, @@ -199,6 +201,17 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie })); } + override dispose(): void { + if (--this._refCounter === 0) { + super.dispose(); + } + } + + acquire() { + this._refCounter++; + return this; + } + private _clearCurrentEditLineDecoration() { this._editDecorations = this.doc.deltaDecorations(this._editDecorations, []); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index 9a2b6ae27eac6..cadf30ab16020 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -9,9 +9,11 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../base/ import { Codicon } from '../../../../../base/common/codicons.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { LinkedList } from '../../../../../base/common/linkedList.js'; import { ResourceMap } from '../../../../../base/common/map.js'; -import { derived, IObservable, observableValue, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; +import { derived, IObservable, observableValue, observableValueOpts, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isString } from '../../../../../base/common/types.js'; @@ -37,6 +39,7 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { applyingChatEditsContextKey, applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingMaxFileAssignmentName, chatEditingResourceContextKey, ChatEditingSessionState, decidedChatEditingResourceContextKey, defaultChatEditingMaxFileLimit, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel, IChatTextEditGroup } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; +import { ChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -50,6 +53,17 @@ export class ChatEditingService extends Disposable implements IChatEditingServic private readonly _currentSessionObs = observableValue(this, null); private readonly _currentSessionDisposables = this._register(new DisposableStore()); + private readonly _adhocSessionsObs = observableValueOpts>({ equalsFn: (a, b) => false }, new LinkedList()); + + readonly editingSessionsObs: IObservable = derived(r => { + const result = Array.from(this._adhocSessionsObs.read(r)); + const globalSession = this._currentSessionObs.read(r); + if (globalSession) { + result.push(globalSession); + } + return result; + }); + private readonly _currentAutoApplyOperationObs = observableValue(this, null); get currentAutoApplyOperation(): CancellationTokenSource | null { return this._currentAutoApplyOperationObs.get(); @@ -63,9 +77,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return this._currentSessionObs; } - private readonly _onDidChangeEditingSession = this._register(new Emitter()); - public readonly onDidChangeEditingSession = this._onDidChangeEditingSession.event; - private _editingSessionFileLimitPromise: Promise; private _editingSessionFileLimit: number | undefined; get editingSessionFileLimit() { @@ -111,13 +122,14 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return decidedEntries.map(entry => entry.entryId); })); this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => { - const currentSession = this._currentSessionObs.read(reader); - if (!currentSession) { - return; + + for (const session of this.editingSessionsObs.read(reader)) { + const entries = session.entries.read(reader); + const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); + return decidedEntries.length > 0; } - const entries = currentSession.entries.read(reader); - const decidedEntries = entries.filter(entry => entry.state.read(reader) === WorkingSetEntryState.Modified); - return decidedEntries.length > 0; + + return false; })); this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => { const currentSession = this._currentSessionObs.read(reader); @@ -211,6 +223,18 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } + private _lookupEntry(uri: URI): ChatEditingModifiedFileEntry | undefined { + + for (const item of Iterable.concat(this.editingSessionsObs.get())) { + const candidate = item.getEntry(uri); + if (candidate instanceof ChatEditingModifiedFileEntry) { + // make sure to ref-count this object + return candidate.acquire(); + } + } + return undefined; + } + private async _createEditingSession(chatSessionId: string): Promise { if (this._currentSessionObs.get()) { throw new BugIndicatingError('Cannot have more than one active editing session'); @@ -218,7 +242,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.clear(); - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise); + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); await session.init(); // listen for completed responses, run the code mapper and apply the edits to this edit session @@ -227,14 +251,33 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.add(session.onDidDispose(() => { this._currentSessionDisposables.clear(); this._currentSessionObs.set(null, undefined); - this._onDidChangeEditingSession.fire(); - })); - this._currentSessionDisposables.add(session.onDidChange(() => { - this._onDidChangeEditingSession.fire(); })); this._currentSessionObs.set(session, undefined); - this._onDidChangeEditingSession.fire(); + return session; + } + + async createAdhocEditingSession(chatSessionId: string): Promise { + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise, this._lookupEntry.bind(this)); + await session.init(); + + const list = this._adhocSessionsObs.get(); + const removeSession = list.unshift(session); + + const store = new DisposableStore(); + this._store.add(store); + + store.add(this.installAutoApplyObserver(session)); + + store.add(session.onDidDispose(e => { + removeSession(); + this._adhocSessionsObs.set(list, undefined); + this._store.deleteAndLeak(store); + store.dispose(); + })); + + this._adhocSessionsObs.set(list, undefined); + return session; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 4a1a7d6bf893b..32afe69b4f0dd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -163,6 +163,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio constructor( public readonly chatSessionId: string, private editingSessionFileLimitPromise: Promise, + private _lookupExternalEntry: (uri: URI) => ChatEditingModifiedFileEntry | undefined, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, @ILanguageService private readonly _languageService: ILanguageService, @@ -670,23 +671,39 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } return existingEntry; } - const initialContent = this._initialFileContents.get(resource); - // This gets manually disposed in .dispose() or in .restoreSnapshot() - const entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); - if (!initialContent) { - this._initialFileContents.set(resource, entry.initialContent); + + let entry: ChatEditingModifiedFileEntry; + const existingExternalEntry = this._lookupExternalEntry(resource); + if (existingExternalEntry) { + entry = existingExternalEntry; + } else { + const initialContent = this._initialFileContents.get(resource); + // This gets manually disposed in .dispose() or in .restoreSnapshot() + entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); + if (!initialContent) { + this._initialFileContents.set(resource, entry.initialContent); + } } + // If an entry is deleted e.g. reverting a created file, // remove it from the entries and don't show it in the working set anymore // so that it can be recreated e.g. through retry - this._register(entry.onDidDelete(() => { + const listener = entry.onDidDelete(() => { const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, entry.modifiedURI)); this._entriesObs.set(newEntries, undefined); this._workingSet.delete(entry.modifiedURI); this._editorService.closeEditors(this._editorService.findEditors(entry.modifiedURI)); - entry.dispose(); + + if (!existingExternalEntry) { + // don't dispose entries that are not yours! + entry.dispose(); + } + + this._store.delete(listener); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - })); + }); + this._store.add(listener); + const entriesArr = [...this._entriesObs.get(), entry]; this._entriesObs.set(entriesArr, undefined); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index 75822df0d7214..ebd21d41a0bbf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -61,14 +61,15 @@ abstract class NavigateAction extends Action2 { if (!isCodeEditor(editor) || !editor.hasModel()) { return; } - - const session = chatEditingService.currentEditingSession; - if (!session) { + const ctrl = ChatEditorController.get(editor); + if (!ctrl) { return; } - const ctrl = ChatEditorController.get(editor); - if (!ctrl) { + const session = chatEditingService.editingSessionsObs.get() + .find(candidate => candidate.getEntry(editor.getModel().uri)); + + if (!session) { return; } @@ -167,7 +168,9 @@ abstract class AcceptDiscardAction extends Action2 { return; } - const session = chatEditingService.currentEditingSession; + const session = chatEditingService.editingSessionsObs.get() + .find(candidate => candidate.getEntry(uri)); + if (!session) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index 03ea41e0ba408..f6f4e917c80fb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -73,8 +73,8 @@ export class ChatEditorController extends Disposable implements IEditorContribut constructor( private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEditorService private readonly _editorService: IEditorService, @IContextKeyService contextKeyService: IContextKeyService, @@ -92,27 +92,31 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._store.add(autorun(r => { - const session = this._chatEditingService.currentEditingSessionObs.read(r); - this._ctxRequestInProgress.set(session?.state.read(r) === ChatEditingSessionState.StreamingEdits); + let isStreamingEdits = false; + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + isStreamingEdits ||= session.state.read(r) === ChatEditingSessionState.StreamingEdits; + } + this._ctxRequestInProgress.set(isStreamingEdits); })); - const entryForEditor = derived(r => { const model = modelObs.read(r); - const session = this._chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return undefined; + if (!model) { + return; } - const entries = session.entries.read(r); - const idx = model?.uri - ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) - : -1; + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + const entries = session.entries.read(r); + const idx = model?.uri + ? entries.findIndex(e => isEqual(e.modifiedURI, model.uri)) + : -1; - if (idx < 0) { - return undefined; + if (idx >= 0) { + return { session, entry: entries[idx], entries, idx }; + } } - return { session, entry: entries[idx], entries, idx }; + + return undefined; }); @@ -185,13 +189,17 @@ export class ChatEditorController extends Disposable implements IEditorContribut // ---- readonly while streaming const shouldBeReadOnly = derived(this, r => { - const value = this._chatEditingService.currentEditingSessionObs.read(r); - if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) { - return false; - } const model = modelObs.read(r); - return model ? value.readEntry(model.uri, r) : undefined; + if (!model) { + return undefined; + } + for (const session of _chatEditingService.editingSessionsObs.read(r)) { + if (session.readEntry(model.uri, r) && session.state.read(r) === ChatEditingSessionState.StreamingEdits) { + return true; + } + } + return false; }); @@ -570,7 +578,14 @@ export class ChatEditorController extends Disposable implements IEditorContribut return; } - const entry = this._chatEditingService.currentEditingSessionObs.get()?.getEntry(this._editor.getModel().uri); + let entry: IModifiedFileEntry | undefined; + for (const session of this._chatEditingService.editingSessionsObs.get()) { + entry = session.getEntry(this._editor.getModel().uri); + if (entry) { + break; + } + } + if (!entry) { return; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index ec99832ce0c10..d4db6b9695e8b 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -24,11 +24,6 @@ export interface IChatEditingService { _serviceBrand: undefined; - /** - * emitted when a session is created, changed or disposed - */ - readonly onDidChangeEditingSession: Event; - readonly currentEditingSessionObs: IObservable; readonly currentEditingSession: IChatEditingSession | null; @@ -38,9 +33,21 @@ export interface IChatEditingService { startOrContinueEditingSession(chatSessionId: string): Promise; getOrRestoreEditingSession(): Promise; + + hasRelatedFilesProviders(): boolean; registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + + /** + * All editing sessions, sorted by recency, e.g the last created session comes first. + */ + readonly editingSessionsObs: IObservable; + + /** + * Creates a new short lived editing session + */ + createAdhocEditingSession(chatSessionId: string): Promise; } export interface IChatRequestDraft { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index d32358f537dbe..a68ca5c47cd62 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -12,6 +12,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MovingAverage } from '../../../../base/common/numbers.js'; +import { autorun } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; @@ -40,6 +41,7 @@ import { showChatView } from '../../chat/browser/chat.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; @@ -144,6 +146,7 @@ export class InlineChatController implements IEditorContribution { @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IEditorService private readonly _editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { @@ -1134,44 +1137,56 @@ export class InlineChatController implements IEditorContribution { return this._currentRun; } - async reviewEdits(anchor: IRange, stream: AsyncIterable, token: CancellationToken) { + async reviewEdits(stream: AsyncIterable, token: CancellationToken) { if (!this._editor.hasModel()) { return false; } - const session = await this._inlineChatSessionService.createSession(this._editor, { wholeRange: anchor, headless: true }, token); - if (!session) { + const uri = this._editor.getModel().uri; + const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); + + if (!chatModel) { return false; } - const request = session.chatModel.addRequest({ text: 'DUMMY', parts: [] }, { variables: [] }, 0); - const run = this.run({ - existingSession: session, - headless: true - }); + const editSession = await this._chatEditingService.createAdhocEditingSession(chatModel.sessionId); - await Event.toPromise(Event.filter(this._onDidEnterState.event, candidate => candidate === State.SHOW_REQUEST)); + // + const store = new DisposableStore(); + store.add(chatModel); + store.add(editSession); + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); for await (const chunk of stream) { - session.chatModel.acceptResponseProgress(request, { kind: 'textEdit', uri: this._editor.getModel()!.uri, edits: chunk }); + + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; + } + + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); } + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - if (token.isCancellationRequested) { - session.chatModel.cancelRequest(request); - } else { - session.chatModel.completeResponse(request); + if (!token.isCancellationRequested) { + chatRequest.response.complete(); } - await Event.toPromise(Event.filter(this._onDidEnterState.event, candidate => candidate === State.WAIT_FOR_INPUT)); + const whenDecided = new Promise(resolve => { + store.add(autorun(r => { + if (!editSession.entries.read(r).some(e => e.state.read(r) === WorkingSetEntryState.Modified)) { + resolve(undefined); + } + })); + }); - if (session.hunkData.pending === 0) { - // no real changes, just cancel - this.cancelSession(); - } + await raceCancellation(whenDecided, token); + + store.dispose(); - const dispo = token.onCancellationRequested(() => this.cancelSession()); - await raceCancellation(run, token); - dispo.dispose(); return true; } } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index c8a3d8b07dd9e..ce0f83a212f50 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -28,6 +28,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IChatEditingService, WorkingSetEntryState } from '../../chat/common/chatEditingService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; export const IQuickDiffModelService = createDecorator('IQuickDiffModelService'); @@ -155,7 +156,18 @@ export class QuickDiffModel extends Disposable { })); this._register(this.quickDiffService.onDidChangeQuickDiffProviders(() => this.triggerDiff())); - this._register(this._chatEditingService.onDidChangeEditingSession(() => this.triggerDiff())); + + this._register(autorunWithStore((r, store) => { + for (const session of this._chatEditingService.editingSessionsObs.read(r)) { + store.add(autorun(r => { + for (const entry of session.entries.read(r)) { + entry.state.read(r); // signal + } + this.triggerDiff(); + })); + } + })); + this.triggerDiff(); } @@ -344,9 +356,10 @@ export class QuickDiffModel extends Disposable { } const uri = this._model.resource; - const session = this._chatEditingService.currentEditingSession; - if (session && session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified) { - // disable dirty diff when doing chat edits + // disable dirty diff when doing chat edits + const isBeingModifiedByChatEdits = this._chatEditingService.editingSessionsObs.get() + .some(session => session.getEntry(uri)?.state.get() === WorkingSetEntryState.Modified); + if (isBeingModifiedByChatEdits) { return Promise.resolve([]); }