diff --git a/src/vs/workbench/api/common/extHostInlineChat.ts b/src/vs/workbench/api/common/extHostInlineChat.ts index c6e4266df75..47ec20ccea2 100644 --- a/src/vs/workbench/api/common/extHostInlineChat.ts +++ b/src/vs/workbench/api/common/extHostInlineChat.ts @@ -151,6 +151,7 @@ export class ExtHostInteractiveEditor implements ExtHostInlineChatShape { wholeRange: typeConvert.Range.to(request.wholeRange), attempt: request.attempt, live: request.live, + previewDocument: this._documents.getDocument(URI.revive(request.previewDocument)), withIntentDetection: request.withIntentDetection, }; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index be5046ad137..d0ff5ed48e5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -559,6 +559,7 @@ export class InlineChatController implements IEditorContribution { selection: this._editor.getSelection(), wholeRange: this._session.wholeRange.trackedInitialRange, live: this._session.editMode !== EditMode.Preview, // TODO@jrieken let extension know what document is used for previewing + previewDocument: this._session.textModelN.uri, withIntentDetection: options.withIntentDetection ?? true /* use intent detection by default */, }; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 6784574fc21..e711ffb0fbe 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -110,7 +110,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { textModelN = store.add(this._modelService.createModel( createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), { languageId: textModel.getLanguageId(), onDidChange: Event.None }, - targetUri.with({ scheme: Schemas.inMemory, query: new URLSearchParams({ id, 'inline-chat-textModelN': '' }).toString() }), true + 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 @@ -122,7 +122,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const textModel0 = store.add(this._modelService.createModel( createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), { languageId: textModel.getLanguageId(), onDidChange: Event.None }, - targetUri.with({ scheme: Schemas.inMemory, query: new URLSearchParams({ id, 'inline-chat-textModel0': '' }).toString() }), true + targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModel0': '' }).toString() }), true )); // untitled documents are special diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 4c347325e64..fdc930567c1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -192,7 +192,9 @@ export class PreviewStrategy extends EditModeStrategy { } override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise { - return this._makeChanges(edits, obs, opts, undefined); + 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 { @@ -202,7 +204,7 @@ export class PreviewStrategy extends EditModeStrategy { override async renderChanges(response: ReplyResponse): Promise { if (response.allLocalEdits.length > 0) { - await this._zone.widget.showEditsPreview(this._session.textModel0, this._session.textModelN); + this._zone.widget.showEditsPreview(this._session.hunkData, this._session.textModel0, this._session.textModelN); } else { this._zone.widget.hideEditsPreview(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index ae291d9d462..14a780fd04a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -41,7 +41,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { ExpansionState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { ExpansionState, HunkData } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; @@ -54,7 +54,6 @@ import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions import { MenuId } from 'vs/platform/actions/common/actions'; import { editorForeground, inputBackground, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { Lazy } from 'vs/base/common/lazy'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ILogService } from 'vs/platform/log/common/log'; @@ -246,7 +245,6 @@ export class InlineChatWidget { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ILogService private readonly _logService: ILogService, @ITextModelService private readonly _textModelResolverService: ITextModelService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @@ -749,36 +747,32 @@ export class InlineChatWidget { // --- preview - async showEditsPreview(textModel0: ITextModel, textModelN: ITextModel) { + showEditsPreview(hunks: HunkData, textModel0: ITextModel, textModelN: ITextModel) { - this._elements.previewDiff.classList.remove('hidden'); - - const diff = await this._editorWorkerService.computeDiff(textModel0.uri, textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); - if (!diff || diff.changes.length === 0) { + if (hunks.size === 0) { this.hideEditsPreview(); return; } + this._elements.previewDiff.classList.remove('hidden'); + this._previewDiffEditor.value.setModel({ original: textModel0, modified: textModelN }); // joined ranges - 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); + let originalLineRange: LineRange | undefined; + let modifiedLineRange: LineRange | undefined; + for (const item of hunks.getInfo()) { + const [first0] = item.getRanges0(); + const [firstN] = item.getRangesN(); + + originalLineRange = !originalLineRange ? LineRange.fromRangeInclusive(first0) : originalLineRange.join(LineRange.fromRangeInclusive(first0)); + modifiedLineRange = !modifiedLineRange ? LineRange.fromRangeInclusive(firstN) : modifiedLineRange.join(LineRange.fromRangeInclusive(firstN)); } - // apply extra padding - const pad = 3; - const newStartLine = Math.max(1, originalLineRange.startLineNumber - pad); - modifiedLineRange = new LineRange(newStartLine, modifiedLineRange.endLineNumberExclusive); - originalLineRange = new LineRange(newStartLine, originalLineRange.endLineNumberExclusive); - - const newEndLineModified = Math.min(modifiedLineRange.endLineNumberExclusive + pad, textModelN.getLineCount()); - modifiedLineRange = new LineRange(modifiedLineRange.startLineNumber, newEndLineModified); - const newEndLineOriginal = Math.min(originalLineRange.endLineNumberExclusive + pad, textModel0.getLineCount()); - originalLineRange = new LineRange(originalLineRange.startLineNumber, newEndLineOriginal); + if (!originalLineRange || !modifiedLineRange) { + this.hideEditsPreview(); + return; + } const hiddenOriginal = invertLineRange(originalLineRange, textModel0); const hiddenModified = invertLineRange(modifiedLineRange, textModelN); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 1b5712c36e2..bf8c26d3a7d 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -21,6 +21,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBackground, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ExtensionsMigration, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; +import { URI } from 'vs/base/common/uri'; export interface IInlineChatSlashCommand { command: string; @@ -45,6 +46,7 @@ export interface IInlineChatRequest { attempt: number; requestId: string; live: boolean; + previewDocument: URI; withIntentDetection: boolean; } 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 8efc9619cc9..cb1260e7a0f 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -36,12 +36,13 @@ import { IInlineChatSavingService } from '../../browser/inlineChatSavingService' import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; -import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TestWorkerService } from './testWorkerService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +import { Schemas } from 'vs/base/common/network'; suite('InteractiveChatController', function () { class TestController extends InlineChatController { @@ -68,7 +69,7 @@ suite('InteractiveChatController', function () { setTimeout(() => { d.dispose(); - reject(`timeout, \nWANTED ${states.join('>')}, \nGOT ${actual.join('>')}`); + reject(new Error(`timeout, \nEXPECTED: ${states.join('>')}, \nACTUAL : ${actual.join('>')}`)); }, 1000); }); } @@ -105,6 +106,7 @@ suite('InteractiveChatController', function () { configurationService = new TestConfigurationService(); configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); + configurationService.setUserConfiguration('inlineChat', { mode: 'livePreview' }); configurationService.setUserConfiguration('editor', {}); const serviceCollection = new ServiceCollection( @@ -477,4 +479,49 @@ suite('InteractiveChatController', function () { }); + test('context has correct preview document', async function () { + + const requests: IInlineChatRequest[] = []; + + store.add(inlineChatService.addProvider({ + debugName: 'Unit Test', + label: 'Unit Test', + prepareInlineChatSession() { + return { + id: Math.random() + }; + }, + provideResponse(_session, request) { + requests.push(request); + return undefined; + } + })); + + async function makeRequest() { + const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); + const r = ctrl.run({ message: 'Hello', autoSend: true }); + await p; + await ctrl.cancelSession(); + await r; + } + + // manual edits -> finish + ctrl = instaService.createInstance(TestController, editor); + + configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Live }); + await makeRequest(); + + configurationService.setUserConfiguration('inlineChat', { mode: EditMode.LivePreview }); + await makeRequest(); + + configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Preview }); + await makeRequest(); + + assert.strictEqual(requests.length, 3); + + assert.strictEqual(requests[0].previewDocument.toString(), model.uri.toString()); // live + assert.strictEqual(requests[1].previewDocument.toString(), model.uri.toString()); // live preview + assert.strictEqual(requests[2].previewDocument.scheme, Schemas.vscode); // preview + assert.strictEqual(requests[2].previewDocument.authority, 'inline-chat'); + }); }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts index d14a84bed36..829d4d0c4a5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController.ts @@ -307,6 +307,7 @@ export class NotebookCellChatController extends Disposable { selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, live: true, + previewDocument: editor.getModel().uri, withIntentDetection: true, // TODO: don't hard code but allow in corresponding UI to run without intent detection? }; diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index e233f52fa5c..3b3e6e53c31 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -33,6 +33,7 @@ declare module 'vscode' { wholeRange: Range; attempt: number; live: boolean; + previewDocument: TextDocument | undefined; withIntentDetection: boolean; }