diff --git a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts index c7d37ef0c4e..467518518d1 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffProviderFactoryService.ts @@ -67,6 +67,16 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I return this.diffAlgorithm.computeDiff(original, modified, options, cancellationToken); } + if (original.isDisposed() || modified.isDisposed()) { + // TODO@hediet + return { + changes: [], + identical: true, + quitEarly: false, + moves: [], + }; + } + // This significantly speeds up the case when the original file is empty if (original.getLineCount() === 1 && original.getLineMaxColumn(1) === 1) { if (modified.getLineCount() === 1 && modified.getLineMaxColumn(1) === 1) { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 6e2a4fa79f0..7ad898e7bd0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -616,6 +616,9 @@ export function registerChatCodeCompareBlockActions() { } async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { + + const editorService = accessor.get(IEditorService); + const model = context.diffEditor.getModel(); if (!model) { return; @@ -635,6 +638,12 @@ export function registerChatCodeCompareBlockActions() { model.original.pushStackElement(); model.original.pushEditOperations(null, edits, () => null); model.original.pushStackElement(); + + + await editorService.openEditor({ + resource: model.original.uri, + options: { revealIfVisible: true }, + }); } }); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 5518413bb9e..fb03e5a7f08 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -380,11 +380,7 @@ export class ReplyResponse { languageId: langSelection.languageId }); this.untitledTextModel = untitledTextModel; - - untitledTextModel.resolve().then(async () => { - const model = untitledTextModel.textEditorModel!; - model.applyEdits(flatEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); - }); + untitledTextModel.resolve(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 386b17449ad..6650804f2f7 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -506,7 +506,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } else if (item.kind === 'textEdit') { for (const edit of item.edits) { raw.edits.edits.push({ - resource: session.textModelN.uri, + resource: item.uri, textEdit: edit, versionId: undefined }); @@ -570,19 +570,9 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const id = generateUuid(); const targetUri = textModel.uri; - let textModelN: ITextModel; - if (options.editMode === EditMode.Preview) { - // AI edits happen in a copy - textModelN = store.add(this._modelService.createModel( - createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), - { languageId: textModel.getLanguageId(), onDidChange: Event.None }, - targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModelN': '' }).toString() }) - )); - } else { - // AI edits happen in the actual model, keep a reference but make no copy - store.add((await this._textModelService.createModelReference(textModel.uri))); - textModelN = textModel; - } + // AI edits happen in the actual model, keep a reference but make no copy + store.add((await this._textModelService.createModelReference(textModel.uri))); + const textModelN = textModel; // create: keep a snapshot of the "actual" model const textModel0 = store.add(this._modelService.createModel( diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 370ce665d30..92d28171e07 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -7,13 +7,12 @@ import { WindowIntervalTimer } from 'vs/base/browser/dom'; import { coalesceInPlace } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { themeColorFromId } from 'vs/base/common/themables'; import { ICodeEditor, IViewZone, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines'; -import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -24,11 +23,9 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Progress } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { InlineChatFileCreatePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget'; import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; @@ -38,6 +35,8 @@ import { IModelService } from 'vs/editor/common/services/model'; import { performAsyncTextEdit, asProgressiveEdit } from './utils'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TextEdit } from 'vs/editor/common/languages'; +import { isEqual } from 'vs/base/common/resources'; export interface IEditObserver { start(): void; @@ -74,7 +73,38 @@ export abstract class EditModeStrategy { this._store.dispose(); } - abstract apply(): Promise; + async apply(): Promise { + + if (this._session.lastExchange?.response instanceof ReplyResponse) { + const { untitledTextModel } = this._session.lastExchange.response; + if (untitledTextModel && !untitledTextModel.isDisposed()) { + + await untitledTextModel.resolve(); + if (!untitledTextModel.textEditorModel) { + return; + } + + // TODO@jrieken + // apply changes only when not dirty. This is very proper and needs to + // fixed in the future + if (!untitledTextModel.isDirty()) { + const allEdits: TextEdit[] = []; + for (const request of this._session.chatModel.getRequests()) { + for (const item of request.response?.response.value ?? []) { + if (item.kind === 'textEdit' && isEqual(item.uri, untitledTextModel.resource)) { + allEdits.push(...item.edits); + } + } + } + untitledTextModel.textEditorModel.pushStackElement(); + untitledTextModel.textEditorModel.pushEditOperations(null, allEdits.map(TextEdit.asEditOperation), () => null); + untitledTextModel.textEditorModel.pushStackElement(); + } + + await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); + } + } + } cancel() { return this._session.hunkData.discardAll(); @@ -140,7 +170,6 @@ export abstract class EditModeStrategy { export class PreviewStrategy extends EditModeStrategy { private readonly _ctxDocumentChanged: IContextKey; - private readonly _previewZone: Lazy; constructor( session: Session, @@ -148,7 +177,6 @@ export class PreviewStrategy extends EditModeStrategy { zone: InlineChatZoneWidget, @IModelService modelService: IModelService, @IContextKeyService contextKeyService: IContextKeyService, - @IInstantiationService instaService: IInstantiationService, ) { super(session, editor, zone); @@ -160,50 +188,40 @@ export class PreviewStrategy extends EditModeStrategy { this._ctxDocumentChanged.set(session.hasChangedText); } }, undefined, this._store); - - this._previewZone = new Lazy(() => instaService.createInstance(InlineChatFileCreatePreviewWidget, editor)); } override dispose(): void { this._ctxDocumentChanged.reset(); - this._previewZone.rawValue?.dispose(); super.dispose(); } - async apply() { + override async apply() { - // (1) ensure the editor still shows the original text - // (2) accept all pending hunks (moves changes from N to 0) - // (3) replace editor model with textModel0 + // apply all edits from all responses const textModel = this._editor.getModel(); if (textModel?.equalsTextBuffer(this._session.textModel0.getTextBuffer())) { - this._session.hunkData.getInfo().forEach(item => item.acceptChanges()); - - const newText = this._session.textModel0.getValue(); - const range = textModel.getFullModelRange(); - - textModel.pushStackElement(); - textModel.pushEditOperations(null, [EditOperation.replace(range, newText)], () => null); - textModel.pushStackElement(); - } - - if (this._session.lastExchange?.response instanceof ReplyResponse) { - const { untitledTextModel } = this._session.lastExchange.response; - if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { - await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); + const allEdits: TextEdit[] = []; + for (const request of this._session.chatModel.getRequests()) { + for (const item of request.response?.response.value ?? []) { + if (item.kind === 'textEdit' && isEqual(item.uri, textModel.uri)) { + allEdits.push(...item.edits); + } + } } + + textModel.pushStackElement(); + textModel.pushEditOperations(null, allEdits.map(TextEdit.asEditOperation), () => null); + textModel.pushStackElement(); } + + await super.apply(); } override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise { - return this._makeChanges(edits, obs, undefined, undefined); } override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise { - await this._makeChanges(edits, obs, opts, new Progress(() => { - this._zone.widget.showEditsPreview(this._session.hunkData, this._session.textModel0, this._session.textModelN); - })); } override async undoChanges(altVersionId: number): Promise { @@ -212,17 +230,7 @@ export class PreviewStrategy extends EditModeStrategy { } override async renderChanges(response: ReplyResponse): Promise { - if (response.allLocalEdits.length > 0) { - this._zone.widget.showEditsPreview(this._session.hunkData, this._session.textModel0, this._session.textModelN); - } else { - this._zone.widget.hideEditsPreview(); - } - if (response.untitledTextModel && !response.untitledTextModel.isDisposed()) { - this._previewZone.value.showCreation(this._session.wholeRange.value.getStartPosition().delta(-1), response.untitledTextModel); - } else { - this._previewZone.rawValue?.hide(); - } } hasFocus(): boolean { @@ -279,8 +287,6 @@ export class LiveStrategy extends EditModeStrategy { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }); - private readonly _previewZone: Lazy; - private readonly _ctxCurrentChangeHasDiff: IContextKey; private readonly _ctxCurrentChangeShowsDiff: IContextKey; @@ -297,20 +303,17 @@ export class LiveStrategy extends EditModeStrategy { @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configService: IConfigurationService, - @IInstantiationService protected readonly _instaService: IInstantiationService, ) { super(session, editor, zone); this._ctxCurrentChangeHasDiff = CTX_INLINE_CHAT_CHANGE_HAS_DIFF.bindTo(contextKeyService); this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService); this._progressiveEditingDecorations = this._editor.createDecorationsCollection(); - this._previewZone = new Lazy(() => _instaService.createInstance(InlineChatFileCreatePreviewWidget, editor)); } override dispose(): void { this._resetDiff(); - this._previewZone.rawValue?.dispose(); super.dispose(); } @@ -326,18 +329,12 @@ export class LiveStrategy extends EditModeStrategy { } } - async apply() { + override async apply() { this._resetDiff(); if (this._editCount > 0) { this._editor.pushUndoStop(); } - if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { - return; - } - const { untitledTextModel } = this._session.lastExchange.response; - if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { - await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); - } + await super.apply(); } override cancel() { @@ -381,12 +378,6 @@ export class LiveStrategy extends EditModeStrategy { override async renderChanges(response: ReplyResponse) { - if (response.untitledTextModel && !response.untitledTextModel.isDisposed()) { - this._previewZone.value.showCreation(this._session.wholeRange.value.getStartPosition().delta(-1), response.untitledTextModel); - } else { - this._previewZone.rawValue?.hide(); - } - this._progressiveEditingDecorations.clear(); const renderHunks = () => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 5135643cb57..597c88e8aa6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -14,12 +14,13 @@ import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { EditorBasedInlineChatWidget } from './inlineChatWidget'; import { MenuId } from 'vs/platform/actions/common/actions'; import { isEqual } from 'vs/base/common/resources'; import { StableEditorBottomScrollState } from 'vs/editor/browser/stableEditorScroll'; import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class InlineChatZoneWidget extends ZoneWidget { @@ -33,7 +34,8 @@ export class InlineChatZoneWidget extends ZoneWidget { constructor( editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, ) { super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); @@ -63,9 +65,10 @@ export class InlineChatZoneWidget extends ZoneWidget { }, rendererOptions: { renderTextEditsAsSummary: (uri) => { + // render edits as summary only when using Live mode and when + // dealing with the current file in the editor return isEqual(uri, editor.getModel()?.uri) - // && !"true" - ; + && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; }, } }); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 9c65a30b47e..d911ba6bd3b 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -8,7 +8,6 @@ import { equals } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; import { mock } from 'vs/base/test/common/mock'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -561,7 +560,6 @@ suite('InteractiveChatController', function () { assert.strictEqual(requests.length, 2); assert.strictEqual(requests[0].previewDocument.toString(), model.uri.toString()); // live - assert.strictEqual(requests[1].previewDocument.scheme, Schemas.vscode); // preview - assert.strictEqual(requests[1].previewDocument.authority, 'inline-chat'); + assert.strictEqual(requests[1].previewDocument.toString(), model.uri.toString()); // preview (both use the same but edits aren't applied like that) }); });