diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 03802665d4b..2f2cb558da9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -331,7 +331,7 @@ export class DiscardAction extends AbstractInlineChatAction { } async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - ctrl.cancelSession(); + await ctrl.cancelSession(); } } @@ -357,7 +357,7 @@ export class DiscardToClipboardAction extends AbstractInlineChatAction { override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): Promise { const clipboardService = accessor.get(IClipboardService); - const changedText = ctrl.cancelSession(); + const changedText = await ctrl.cancelSession(); if (changedText !== undefined) { clipboardService.writeText(changedText); } @@ -381,7 +381,7 @@ export class DiscardUndoToNewFileAction extends AbstractInlineChatAction { override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: any[]): Promise { const editorService = accessor.get(IEditorService); - const changedText = ctrl.cancelSession(); + const changedText = await ctrl.cancelSession(); if (changedText !== undefined) { const input: IUntitledTextResourceEditorInput = { forceUntitled: true, resource: undefined, contents: changedText, languageId: editor.getModel()?.getLanguageId() }; editorService.openEditor(input, SIDE_GROUP); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index c5d59349076..5ca18ef7a67 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -15,9 +15,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; -import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; -import { IModelService } from 'vs/editor/common/services/model'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -129,7 +127,6 @@ export class InlineChatController implements IEditorContribution { @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IModelService private readonly _modelService: IModelService, @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @@ -572,7 +569,7 @@ export class InlineChatController implements IEditorContribution { throw new Error('Progress in NOT supported in non-live mode'); } progressEdits.push(data.edits); - await this._makeChanges(progressEdits, false); + await this._makeChanges(data.edits, false); await this._strategy?.renderProgressChanges(); } }); @@ -594,8 +591,9 @@ export class InlineChatController implements IEditorContribution { response = new MarkdownResponse(this._activeSession.textModelN.uri, reply); } else if (reply) { const editResponse = new EditResponse(this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), reply, progressEdits); - if (editResponse.allLocalEdits.length > progressEdits.length) { - await this._makeChanges(editResponse.allLocalEdits, true); + const offset = editResponse.allLocalEdits.length - progressEdits.length; + for (let i = offset; i < editResponse.allLocalEdits.length; i++) { + await this._makeChanges(editResponse.allLocalEdits[i], true); } response = editResponse; } else { @@ -648,26 +646,11 @@ export class InlineChatController implements IEditorContribution { return State.SHOW_RESPONSE; } - private async _makeChanges(allEdits: TextEdit[][], computeMoreMinimalEdits: boolean) { + private async _makeChanges(lastEdits: TextEdit[], computeMoreMinimalEdits: boolean) { assertType(this._activeSession); assertType(this._strategy); - if (allEdits.length === 0) { - return; - } - - // diff-changes from model0 -> modelN+1 - { - const lastEdits = allEdits[allEdits.length - 1]; - const textModelNplus1 = this._modelService.createModel(createTextBufferFactoryFromSnapshot(this._activeSession.textModelN.createSnapshot()), null, undefined, true); - textModelNplus1.applyEdits(lastEdits.map(TextEdit.asEditOperation)); - const diff = await this._editorWorkerService.computeDiff(this._activeSession.textModel0.uri, textModelNplus1.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); - this._activeSession.lastTextModelChanges = diff?.changes ?? []; - textModelNplus1.dispose(); - } - // make changes from modelN -> modelN+1 - const lastEdits = allEdits[allEdits.length - 1]; const moreMinimalEdits = computeMoreMinimalEdits ? await this._editorWorkerService.computeHumanReadableDiff(this._activeSession.textModelN.uri, lastEdits) : undefined; const editOperations = (moreMinimalEdits ?? lastEdits).map(TextEdit.asEditOperation); this._log('edits from PROVIDER and after making them MORE MINIMAL', this._activeSession.provider.debugName, lastEdits, moreMinimalEdits); @@ -909,11 +892,19 @@ export class InlineChatController implements IEditorContribution { this._messages.fire(Message.ACCEPT_SESSION); } - cancelSession() { - const result = this._activeSession?.asChangedText(); - if (this._activeSession?.lastExchange && InlineChatController.isEditOrMarkdownResponse(this._activeSession.lastExchange.response)) { - this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, InlineChatResponseFeedbackKind.Undone); + async cancelSession() { + + let result: string | undefined; + if (this._activeSession) { + + const diff = await this._editorWorkerService.computeDiff(this._activeSession.textModel0.uri, this._activeSession.textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); + result = this._activeSession.asChangedText(diff?.changes ?? []); + + if (this._activeSession.lastExchange && InlineChatController.isEditOrMarkdownResponse(this._activeSession.lastExchange.response)) { + this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, InlineChatResponseFeedbackKind.Undone); + } } + this._messages.fire(Message.CANCEL_SESSION); return result; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 8de79727cea..b5daf7c9c27 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -23,9 +23,9 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Iterable } from 'vs/base/common/iterator'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isCancellationError } from 'vs/base/common/errors'; -import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { raceCancellation } from 'vs/base/common/async'; +import { LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; export type Recording = { when: Date; @@ -112,7 +112,6 @@ export class Session { private _lastInput: SessionPrompt | undefined; private _lastExpansionState: ExpansionState | undefined; - private _lastTextModelChanges: readonly DetailedLineRangeMapping[] | undefined; private _isUnstashed: boolean = false; private readonly _exchange: SessionExchange[] = []; private readonly _startTime = new Date(); @@ -187,26 +186,18 @@ export class Session { return this._exchange[this._exchange.length - 1]; } - get lastTextModelChanges() { - return this._lastTextModelChanges ?? []; - } - - set lastTextModelChanges(changes: readonly DetailedLineRangeMapping[]) { - this._lastTextModelChanges = changes; - } - get hasChangedText(): boolean { return !this.textModel0.equalsTextBuffer(this.textModelN.getTextBuffer()); } - asChangedText(): string | undefined { - if (!this._lastTextModelChanges || this._lastTextModelChanges.length === 0) { + asChangedText(changes: readonly LineRangeMapping[]): string | undefined { + if (changes.length === 0) { return undefined; } let startLine = Number.MAX_VALUE; let endLine = Number.MIN_VALUE; - for (const change of this._lastTextModelChanges) { + for (const change of changes) { startLine = Math.min(startLine, change.modified.startLineNumber); endLine = Math.max(endLine, change.modified.endLineNumberExclusive); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 4fa8470f957..9821f36e6a3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -12,7 +12,7 @@ import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; -import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { DetailedLineRangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { TextEdit } from 'vs/editor/common/languages'; import { ICursorStateComputer, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; @@ -133,7 +133,7 @@ export class PreviewStrategy extends EditModeStrategy { override async renderChanges(response: EditResponse): Promise { if (response.allLocalEdits.length > 0) { const allEditOperation = response.allLocalEdits.map(edits => edits.map(TextEdit.asEditOperation)); - this._widget.showEditsPreview(this._session.textModel0, allEditOperation, this._session.lastTextModelChanges); + await this._widget.showEditsPreview(this._session.textModel0, this._session.textModelN, allEditOperation); } else { this._widget.hideEditsPreview(); } @@ -327,9 +327,9 @@ export class LiveStrategy extends EditModeStrategy { } override async renderChanges(response: EditResponse) { - + const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); + this._updateSummaryMessage(diff?.changes ?? []); this._inlineDiffDecorations.update(); - this._updateSummaryMessage(); if (response.singleCreateFileEdit) { this._widget.showCreatePreview(response.singleCreateFileEdit.uri, await Promise.all(response.singleCreateFileEdit.edits)); @@ -344,9 +344,9 @@ export class LiveStrategy extends EditModeStrategy { } } - protected _updateSummaryMessage() { + protected _updateSummaryMessage(mappings: readonly LineRangeMapping[]) { let linesChanged = 0; - for (const change of this._session.lastTextModelChanges) { + for (const change of mappings) { linesChanged += change.changedLineCount; } let message: string; @@ -440,6 +440,7 @@ export class LivePreviewStrategy extends LiveStrategy { return; } + this._updateSummaryMessage(diff.changes); this._currentLineRangeGroups = groups; const handleDiff = () => { @@ -465,7 +466,6 @@ export class LivePreviewStrategy extends LiveStrategy { override async renderChanges(response: EditResponse) { - this._updateSummaryMessage(); await this._renderDiffZones(); if (response.singleCreateFileEdit) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index db86fea8ea0..a4223bb8d8d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -36,7 +36,6 @@ import { FileKind } from 'vs/platform/files/common/files'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { invertLineRange, lineRangeAsRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LineRange } from 'vs/editor/common/core/lineRange'; @@ -62,6 +61,7 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { editorForeground, inputBackground, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { CodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { Lazy } from 'vs/base/common/lazy'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -215,7 +215,8 @@ export class InlineChatWidget { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService ) { // input editor logic @@ -664,27 +665,29 @@ export class InlineChatWidget { // --- preview - showEditsPreview(textModelv0: ITextModel, allEdits: ISingleEditOperation[][], changes: readonly DetailedLineRangeMapping[]) { - if (changes.length === 0) { + async showEditsPreview(textModel0: ITextModel, textModelN: ITextModel, allEdits: ISingleEditOperation[][]) { + + const diff = await this._editorWorkerService.computeDiff(textModel0.uri, textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); + if (!diff || diff.changes.length === 0) { this.hideEditsPreview(); return; } this._elements.previewDiff.classList.remove('hidden'); - const languageSelection: ILanguageSelection = { languageId: textModelv0.getLanguageId(), onDidChange: Event.None }; - const modified = this._modelService.createModel(createTextBufferFactoryFromSnapshot(textModelv0.createSnapshot()), languageSelection, undefined, true); + const languageSelection: ILanguageSelection = { languageId: textModel0.getLanguageId(), onDidChange: Event.None }; + const modified = this._modelService.createModel(createTextBufferFactoryFromSnapshot(textModel0.createSnapshot()), languageSelection, undefined, true); for (const edits of allEdits) { modified.applyEdits(edits, false); } - this._previewDiffEditor.value.setModel({ original: textModelv0, modified }); + this._previewDiffEditor.value.setModel({ original: textModel0, modified }); // joined ranges - let originalLineRange = changes[0].original; - let modifiedLineRange = changes[0].modified; - for (let i = 1; i < changes.length; i++) { - originalLineRange = originalLineRange.join(changes[i].original); - modifiedLineRange = modifiedLineRange.join(changes[i].modified); + let originalLineRange = diff.changes[0].original; + let modifiedLineRange = diff.changes[0].modified; + for (let i = 1; i < diff.changes.length; i++) { + originalLineRange = originalLineRange.join(diff.changes[i].original); + modifiedLineRange = modifiedLineRange.join(diff.changes[i].modified); } // apply extra padding @@ -695,10 +698,10 @@ export class InlineChatWidget { const newEndLineModified = Math.min(modifiedLineRange.endLineNumberExclusive + pad, modified.getLineCount()); modifiedLineRange = new LineRange(modifiedLineRange.startLineNumber, newEndLineModified); - const newEndLineOriginal = Math.min(originalLineRange.endLineNumberExclusive + pad, textModelv0.getLineCount()); + const newEndLineOriginal = Math.min(originalLineRange.endLineNumberExclusive + pad, textModel0.getLineCount()); originalLineRange = new LineRange(originalLineRange.startLineNumber, newEndLineOriginal); - const hiddenOriginal = invertLineRange(originalLineRange, textModelv0); + const hiddenOriginal = invertLineRange(originalLineRange, textModel0); const hiddenModified = invertLineRange(modifiedLineRange, modified); this._previewDiffEditor.value.getOriginalEditor().setHiddenAreas(hiddenOriginal.map(lineRangeAsRange), 'diff-hidden'); this._previewDiffEditor.value.getModifiedEditor().setHiddenAreas(hiddenModified.map(lineRangeAsRange), 'diff-hidden'); 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 bb25fb4e308..822c5f8064c 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -173,7 +173,7 @@ suite('InteractiveChatController', function () { const run = ctrl.run({ message: 'Hello', autoSend: true }); await p; assert.ok(ctrl.getWidgetPosition() !== undefined); - ctrl.cancelSession(); + await ctrl.cancelSession(); await run; @@ -205,7 +205,7 @@ suite('InteractiveChatController', function () { assert.ok(session); assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 6)); - ctrl.cancelSession(); + await ctrl.cancelSession(); d.dispose(); }); @@ -235,7 +235,7 @@ suite('InteractiveChatController', function () { assert.ok(session); assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 6)); - ctrl.cancelSession(); + await ctrl.cancelSession(); d.dispose(); }); @@ -298,7 +298,7 @@ suite('InteractiveChatController', function () { assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 12)); - ctrl.cancelSession(); + await ctrl.cancelSession(); await r; });