From 74195430ca252eda61ca1d4c90f4b16d3c7987fd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 8 Jan 2026 11:12:31 +0100 Subject: [PATCH] debt - remove old inline chat world (#286503) fixes https://github.com/microsoft/vscode/issues/282015 --- .../browser/actions/chatExecuteActions.ts | 62 - .../chatEditingEditorContextKeys.ts | 2 +- .../chat/common/chatService/chatService.ts | 2 - .../emptyTextEditorHint.ts | 8 +- .../browser/inlineChat.contribution.ts | 22 +- .../browser/inlineChatAccessibleView.ts | 45 - .../inlineChat/browser/inlineChatActions.ts | 424 +----- .../browser/inlineChatController.ts | 1201 +---------------- .../inlineChat/browser/inlineChatNotebook.ts | 49 - .../inlineChat/browser/inlineChatSession.ts | 646 --------- .../browser/inlineChatSessionService.ts | 40 +- .../browser/inlineChatSessionServiceImpl.ts | 321 +---- .../browser/inlineChatStrategies.ts | 591 -------- .../inlineChat/browser/inlineChatWidget.ts | 138 +- .../contrib/inlineChat/browser/utils.ts | 95 -- .../electron-browser/inlineChatActions.ts | 4 +- ..._should_be_easier_to_undo_esc__7537.1.snap | 13 - ..._should_be_easier_to_undo_esc__7537.2.snap | 6 - .../test/browser/inlineChatController.test.ts | 1101 --------------- .../test/browser/inlineChatSession.test.ts | 598 -------- .../test/browser/inlineChatStrategies.test.ts | 75 - .../chat/browser/terminalChatActions.ts | 17 +- 22 files changed, 82 insertions(+), 5378 deletions(-) delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/browser/utils.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.1.snap delete mode 100644 src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.2.snap delete mode 100644 src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts delete mode 100644 src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 38f5e3b2938..dc52a099eb2 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -208,67 +208,6 @@ export class ChatSubmitAction extends SubmitAction { } } -export class ChatDelegateToEditSessionAction extends Action2 { - static readonly ID = 'workbench.action.chat.delegateToEditSession'; - - constructor() { - super({ - id: ChatDelegateToEditSessionAction.ID, - title: localize2('interactive.submit.panel.label', "Send to Edit Session"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.commentDiscussion, - keybinding: { - when: ContextKeyExpr.and( - ChatContextKeys.inChatInput, - ChatContextKeys.withinEditSessionDiff, - ), - primary: KeyCode.Enter, - weight: KeybindingWeight.EditorContrib - }, - menu: [ - { - id: MenuId.ChatExecute, - order: 4, - when: ContextKeyExpr.and( - whenNotInProgress, - ChatContextKeys.withinEditSessionDiff, - ), - group: 'navigation', - } - ] - }); - } - - override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - const context = args[0] as IChatExecuteActionContext | undefined; - const widgetService = accessor.get(IChatWidgetService); - const inlineWidget = context?.widget ?? widgetService.lastFocusedWidget; - const locationData = inlineWidget?.locationData; - - if (inlineWidget && locationData?.type === ChatAgentLocation.EditorInline && locationData.delegateSessionResource) { - const sessionWidget = widgetService.getWidgetBySessionResource(locationData.delegateSessionResource); - - if (sessionWidget) { - await widgetService.reveal(sessionWidget); - sessionWidget.attachmentModel.addContext({ - id: 'vscode.delegate.inline', - kind: 'file', - modelDescription: `User's chat context`, - name: 'delegate-inline', - value: { range: locationData.wholeRange, uri: locationData.document }, - }); - sessionWidget.acceptInput(inlineWidget.getInput(), { - noCommandDetection: true, - enableImplicitContext: false, - }); - - inlineWidget.setInput(''); - locationData.close(); - } - } - } -} export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode'; @@ -807,7 +746,6 @@ export class CancelEdit extends Action2 { export function registerChatExecuteActions() { registerAction2(ChatSubmitAction); - registerAction2(ChatDelegateToEditSessionAction); registerAction2(ChatEditingSessionSubmitAction); registerAction2(SubmitWithoutDispatchingAction); registerAction2(CancelAction); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts index f08d332b625..2bf6ec47bb0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts @@ -157,7 +157,7 @@ export class ObservableEditorSession { @IInlineChatSessionService inlineChatService: IInlineChatSessionService ) { - const inlineSessionObs = observableFromEvent(this, inlineChatService.onDidChangeSessions, () => inlineChatService.getSession2(uri)); + const inlineSessionObs = observableFromEvent(this, inlineChatService.onDidChangeSessions, () => inlineChatService.getSessionByTextModel(uri)); const sessionObs = chatEditingService.editingSessionsObs.map((value, r) => { for (const session of value) { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 9301acecf2b..bf39345b325 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -969,8 +969,6 @@ export interface IChatEditorLocationData { document: URI; selection: ISelection; wholeRange: IRange; - close: () => void; - delegateSessionResource: URI | undefined; } export interface IChatNotebookLocationData { diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index 7656f9306ff..1e42e3c27a0 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -69,10 +69,8 @@ export class EmptyTextEditorHintContribution extends Disposable implements IEdit this.textHintContentWidget?.dispose(); } })); - this._register(inlineChatSessionService.onDidEndSession(e => { - if (this.editor === e.editor) { - this.update(); - } + this._register(inlineChatSessionService.onDidChangeSessions(() => { + this.update(); })); } @@ -92,7 +90,7 @@ export class EmptyTextEditorHintContribution extends Disposable implements IEdit return false; } - if (this.inlineChatSessionService.getSession(this.editor, model.uri)) { + if (this.inlineChatSessionService.getSessionByTextModel(model.uri)) { return false; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 33033f1aff0..a983857b40d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -5,15 +5,14 @@ import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { InlineChatController, InlineChatController1, InlineChatController2 } from './inlineChatController.js'; +import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; -import { InlineChatAccessibleView } from './inlineChatAccessibleView.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatEscapeToolContribution, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -23,8 +22,7 @@ import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; -registerEditorContribution(InlineChatController2.ID, InlineChatController2, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerEditorContribution(INLINE_CHAT_ID, InlineChatController1, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors +registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors registerAction2(InlineChatActions.KeepSessionAction2); @@ -87,26 +85,12 @@ MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem // --- actions --- registerAction2(InlineChatActions.StartSessionAction); -registerAction2(InlineChatActions.CloseAction); -registerAction2(InlineChatActions.ConfigureInlineChatAction); -registerAction2(InlineChatActions.UnstashSessionAction); -registerAction2(InlineChatActions.DiscardHunkAction); -registerAction2(InlineChatActions.RerunAction); -registerAction2(InlineChatActions.MoveToNextHunk); -registerAction2(InlineChatActions.MoveToPreviousHunk); - -registerAction2(InlineChatActions.ArrowOutUpAction); -registerAction2(InlineChatActions.ArrowOutDownAction); registerAction2(InlineChatActions.FocusInlineChat); -registerAction2(InlineChatActions.ViewInChatAction); -registerAction2(InlineChatActions.ToggleDiffForChange); -registerAction2(InlineChatActions.AcceptChanges); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(InlineChatEscapeToolContribution.Id, InlineChatEscapeToolContribution, WorkbenchPhase.AfterRestored); -AccessibleViewRegistry.register(new InlineChatAccessibleView()); AccessibleViewRegistry.register(new InlineChatAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts deleted file mode 100644 index cfea2d516c1..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { InlineChatController } from './inlineChatController.js'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from '../common/inlineChat.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; -import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; - -export class InlineChatAccessibleView implements IAccessibleViewImplementation { - readonly priority = 100; - readonly name = 'inlineChat'; - readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED); - readonly type = AccessibleViewType.View; - getProvider(accessor: ServicesAccessor) { - const codeEditorService = accessor.get(ICodeEditorService); - - const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); - if (!editor) { - return; - } - const controller = InlineChatController.get(editor); - if (!controller) { - return; - } - const responseContent = controller.widget.responseContent; - if (!responseContent) { - return; - } - return new AccessibleContentProvider( - AccessibleViewProviderId.InlineChat, - { type: AccessibleViewType.View }, - () => renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true }), - () => controller.focus(), - AccessibilityVerbositySettingId.InlineChat - ); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 5dbbfdfe02e..91c874e70ab 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -10,8 +10,8 @@ import { EditorAction2 } from '../../../../editor/browser/editorExtensions.js'; import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { InlineChatController, InlineChatController1, InlineChatController2, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, MENU_INLINE_CHAT_SIDE, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED } from '../common/inlineChat.js'; +import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -23,12 +23,8 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; -import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IChatService } from '../../chat/common/chatService/chatService.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; -import { HunkInformation } from './inlineChatSession.js'; -import { IChatWidgetService } from '../../chat/browser/chat.js'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); @@ -60,7 +56,7 @@ export class StartSessionAction extends Action2 { super({ id: ACTION_START, title: localize2('run', 'Open Inline Chat'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, f1: true, precondition: inlineChatContextKey, keybinding: { @@ -134,7 +130,7 @@ export class FocusInlineChat extends EditorAction2 { id: 'inlineChat.focus', title: localize2('focus', "Focus Input"), f1: true, - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_FOCUSED.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), keybinding: [{ weight: KeybindingWeight.EditorCore + 10, // win against core_command @@ -153,406 +149,8 @@ export class FocusInlineChat extends EditorAction2 { } } -//#region --- VERSION 1 - -export class UnstashSessionAction extends EditorAction2 { - constructor() { - super({ - id: 'inlineChat.unstash', - title: localize2('unstash', "Resume Last Dismissed Inline Chat"), - category: AbstractInline1ChatAction.category, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_STASHED_SESSION, EditorContextKeys.writable), - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyZ, - } - }); - } - - override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) { - const ctrl = InlineChatController1.get(editor); - if (ctrl) { - const session = ctrl.unstashLastSession(); - if (session) { - ctrl.run({ - existingSession: session, - }); - } - } - } -} - -export abstract class AbstractInline1ChatAction extends EditorAction2 { - - static readonly category = localize2('cat', "Inline Chat"); - - constructor(desc: IAction2Options) { - - const massageMenu = (menu: IAction2Options['menu'] | undefined) => { - if (Array.isArray(menu)) { - for (const entry of menu) { - entry.when = ContextKeyExpr.and(CTX_INLINE_CHAT_V1_ENABLED, entry.when); - } - } else if (menu) { - menu.when = ContextKeyExpr.and(CTX_INLINE_CHAT_V1_ENABLED, menu.when); - } - }; - if (Array.isArray(desc.menu)) { - massageMenu(desc.menu); - } else { - massageMenu(desc.menu); - } - - super({ - ...desc, - category: AbstractInline1ChatAction.category, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_V1_ENABLED, desc.precondition) - }); - } - - override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) { - const editorService = accessor.get(IEditorService); - const logService = accessor.get(ILogService); - - let ctrl = InlineChatController1.get(editor); - if (!ctrl) { - const { activeTextEditorControl } = editorService; - if (isCodeEditor(activeTextEditorControl)) { - editor = activeTextEditorControl; - } else if (isDiffEditor(activeTextEditorControl)) { - editor = activeTextEditorControl.getModifiedEditor(); - } - ctrl = InlineChatController1.get(editor); - } - - if (!ctrl) { - logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); - return; - } - - if (editor instanceof EmbeddedCodeEditorWidget) { - editor = editor.getParentEditor(); - } - if (!ctrl) { - for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { - if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { - if (diffEditor instanceof EmbeddedDiffEditorWidget) { - this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args); - } - } - } - return; - } - this.runInlineChatCommand(accessor, ctrl, editor, ..._args); - } - - abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, editor: ICodeEditor, ...args: unknown[]): void; -} - -export class ArrowOutUpAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: 'inlineChat.arrowOutUp', - title: localize('arrowUp', 'Cursor Up'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - keybinding: { - weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.UpArrow - } - }); - } - - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): void { - ctrl.arrowOut(true); - } -} - -export class ArrowOutDownAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: 'inlineChat.arrowOutDown', - title: localize('arrowDown', 'Cursor Down'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_LAST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - keybinding: { - weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.DownArrow - } - }); - } - - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): void { - ctrl.arrowOut(false); - } -} - -export class AcceptChanges extends AbstractInline1ChatAction { - - constructor() { - super({ - id: ACTION_ACCEPT_CHANGES, - title: localize2('apply1', "Accept Changes"), - shortTitle: localize('apply2', 'Accept'), - icon: Codicon.check, - f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE), - keybinding: [{ - weight: KeybindingWeight.WorkbenchContrib + 10, - primary: KeyMod.CtrlCmd | KeyCode.Enter, - }], - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) - ), - }, { - id: MENU_INLINE_CHAT_ZONE, - group: 'navigation', - order: 1, - }] - }); - } - - override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, hunk?: HunkInformation | any): Promise { - ctrl.acceptHunk(hunk); - } -} - -export class DiscardHunkAction extends AbstractInline1ChatAction { - - constructor() { - super({ - id: ACTION_DISCARD_CHANGES, - title: localize('discard', 'Discard'), - icon: Codicon.chromeClose, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: [{ - id: MENU_INLINE_CHAT_ZONE, - group: 'navigation', - order: 2 - }], - keybinding: { - weight: KeybindingWeight.EditorContrib, - primary: KeyCode.Escape, - when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) - } - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, hunk?: HunkInformation | any): Promise { - return ctrl.discardHunk(hunk); - } -} - -export class RerunAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: ACTION_REGENERATE_RESPONSE, - title: localize2('chat.rerun.label', "Rerun Request"), - shortTitle: localize('rerun', 'Rerun'), - f1: false, - icon: Codicon.refresh, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 5, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.None) - ) - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyR - } - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): Promise { - const chatService = accessor.get(IChatService); - const chatWidgetService = accessor.get(IChatWidgetService); - const model = ctrl.chatWidget.viewModel?.model; - if (!model) { - return; - } - - const lastRequest = model.getRequests().at(-1); - if (lastRequest) { - const widget = chatWidgetService.getWidgetBySessionResource(model.sessionResource); - await chatService.resendRequest(lastRequest, { - noCommandDetection: false, - attempt: lastRequest.attempt + 1, - location: ctrl.chatWidget.location, - userSelectedModelId: widget?.input.currentLanguageModel - }); - } - } -} - -export class CloseAction extends AbstractInline1ChatAction { - - constructor() { - super({ - id: 'inlineChat.close', - title: localize('close', 'Close'), - icon: Codicon.close, - precondition: CTX_INLINE_CHAT_VISIBLE, - keybinding: { - weight: KeybindingWeight.EditorContrib + 1, - primary: KeyCode.Escape, - }, - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() - }, { - id: MENU_INLINE_CHAT_SIDE, - group: 'navigation', - when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.None) - }] - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): Promise { - ctrl.cancelSession(); - } -} - -export class ConfigureInlineChatAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: 'inlineChat.configure', - title: localize2('configure', 'Configure Inline Chat'), - icon: Codicon.settingsGear, - precondition: CTX_INLINE_CHAT_VISIBLE, - f1: true, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'zzz', - order: 5 - } - }); - } - - async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): Promise { - accessor.get(IPreferencesService).openSettings({ query: 'inlineChat' }); - } -} - -export class MoveToNextHunk extends AbstractInline1ChatAction { - - constructor() { - super({ - id: 'inlineChat.moveToNextHunk', - title: localize2('moveToNextHunk', 'Move to Next Change'), - precondition: CTX_INLINE_CHAT_VISIBLE, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.F7 - } - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, editor: ICodeEditor, ...args: unknown[]): void { - ctrl.moveHunk(true); - } -} - -export class MoveToPreviousHunk extends AbstractInline1ChatAction { - - constructor() { - super({ - id: 'inlineChat.moveToPreviousHunk', - title: localize2('moveToPreviousHunk', 'Move to Previous Change'), - f1: true, - precondition: CTX_INLINE_CHAT_VISIBLE, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Shift | KeyCode.F7 - } - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, editor: ICodeEditor, ...args: unknown[]): void { - ctrl.moveHunk(false); - } -} - -export class ViewInChatAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: ACTION_VIEW_IN_CHAT, - title: localize('viewInChat', 'View in Chat'), - icon: Codicon.chatSparkle, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'more', - order: 1, - when: CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.Messages) - }, { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() - ) - }], - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - when: ChatContextKeys.inChatInput - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]) { - return ctrl.viewInChat(); - } -} - -export class ToggleDiffForChange extends AbstractInline1ChatAction { - - constructor() { - super({ - id: ACTION_TOGGLE_DIFF, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - title: localize2('showChanges', 'Toggle Changes'), - icon: Codicon.diffSingle, - toggled: { - condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, - }, - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'zzz', - order: 1, - }, { - id: MENU_INLINE_CHAT_ZONE, - group: 'navigation', - when: CTX_INLINE_CHAT_CHANGE_HAS_DIFF, - order: 2 - }] - }); - } - - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, hunkInfo: HunkInformation | any): void { - ctrl.toggleDiff(hunkInfo); - } -} - -//#endregion - - //#region --- VERSION 2 -abstract class AbstractInline2ChatAction extends EditorAction2 { +export abstract class AbstractInlineChatAction extends EditorAction2 { static readonly category = localize2('cat', "Inline Chat"); @@ -574,7 +172,7 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { super({ ...desc, - category: AbstractInline2ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_V2_ENABLED, desc.precondition) }); } @@ -583,7 +181,7 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { const editorService = accessor.get(IEditorService); const logService = accessor.get(ILogService); - let ctrl = InlineChatController2.get(editor); + let ctrl = InlineChatController.get(editor); if (!ctrl) { const { activeTextEditorControl } = editorService; if (isCodeEditor(activeTextEditorControl)) { @@ -591,7 +189,7 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { } else if (isDiffEditor(activeTextEditorControl)) { editor = activeTextEditorControl.getModifiedEditor(); } - ctrl = InlineChatController2.get(editor); + ctrl = InlineChatController.get(editor); } if (!ctrl) { @@ -615,16 +213,16 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { this.runInlineChatCommand(accessor, ctrl, editor, ..._args); } - abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: unknown[]): void; + abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ...args: unknown[]): void; } -class KeepOrUndoSessionAction extends AbstractInline2ChatAction { +class KeepOrUndoSessionAction extends AbstractInlineChatAction { constructor(private readonly _keep: boolean, desc: IAction2Options) { super(desc); } - override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ..._args: unknown[]): Promise { + override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise { if (this._keep) { await ctrl.acceptSession(); } else { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 9592a5c93f2..b47d6950711 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -4,55 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; -import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { Barrier, DeferredPromise, Queue, raceCancellation } from '../../../../base/common/async.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; +import { raceCancellation } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; -import { MovingAverage } from '../../../../base/common/numbers.js'; import { autorun, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, waitForState } 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'; import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ISelection, Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { ISelection, Selection } from '../../../../editor/common/core/selection.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { TextEdit, VersionedExtensionId } from '../../../../editor/common/languages.js'; -import { ITextModel, IValidEditOperation } from '../../../../editor/common/model.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; +import { TextEdit } from '../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../editor/common/model.js'; import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js'; -import { DefaultModelSHA1Computer } from '../../../../editor/common/services/modelService.js'; import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; -import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; -import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; -import { IChatEditingSession, ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; -import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/model/chatModel.js'; +import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; +import { ChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js'; @@ -63,33 +50,11 @@ import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../. import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; -import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; -import { IInlineChatSession2, IInlineChatSessionService, moveToPanelChat } from './inlineChatSessionService.js'; -import { InlineChatError } from './inlineChatSessionServiceImpl.js'; -import { HunkAction, IEditObserver, IInlineChatMetadata, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; +import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -export const enum State { - CREATE_SESSION = 'CREATE_SESSION', - INIT_UI = 'INIT_UI', - WAIT_FOR_INPUT = 'WAIT_FOR_INPUT', - SHOW_REQUEST = 'SHOW_REQUEST', - PAUSE = 'PAUSE', - CANCEL = 'CANCEL', - ACCEPT = 'DONE', -} - -const enum Message { - NONE = 0, - ACCEPT_SESSION = 1 << 0, - CANCEL_SESSION = 1 << 1, - PAUSE_SESSION = 1 << 2, - CANCEL_REQUEST = 1 << 3, - CANCEL_INPUT = 1 << 4, - ACCEPT_INPUT = 1 << 5, -} export abstract class InlineChatRunOptions { @@ -98,7 +63,6 @@ export abstract class InlineChatRunOptions { message?: string; attachments?: URI[]; autoSend?: boolean; - existingSession?: Session; position?: IPosition; modelSelector?: ILanguageModelChatSelector; blockOnResponse?: boolean; @@ -109,14 +73,13 @@ export abstract class InlineChatRunOptions { return false; } - const { initialSelection, initialRange, message, autoSend, position, existingSession, attachments, modelSelector, blockOnResponse } = options; + const { initialSelection, initialRange, message, autoSend, position, attachments, modelSelector, blockOnResponse } = options; if ( typeof message !== 'undefined' && typeof message !== 'string' || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' || typeof initialRange !== 'undefined' && !Range.isIRange(initialRange) || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) || typeof position !== 'undefined' && !Position.isIPosition(position) - || typeof existingSession !== 'undefined' && !(existingSession instanceof Session) || typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI)) || typeof modelSelector !== 'undefined' && !isILanguageModelChatSelector(modelSelector) || typeof blockOnResponse !== 'undefined' && typeof blockOnResponse !== 'boolean' @@ -128,1127 +91,17 @@ export abstract class InlineChatRunOptions { } } -export class InlineChatController implements IEditorContribution { - - static ID = 'editor.contrib.inlineChatController'; - - static get(editor: ICodeEditor) { - return editor.getContribution(InlineChatController.ID); - } - - private readonly _delegate: InlineChatController2; - - constructor( - editor: ICodeEditor, - ) { - this._delegate = InlineChatController2.get(editor)!; - } - - dispose(): void { - - } - - get isActive(): boolean { - return this._delegate.isActive; - } - - async run(arg?: InlineChatRunOptions): Promise { - return this._delegate.run(arg); - } - - focus() { - return this._delegate.focus(); - } - - get widget(): EditorBasedInlineChatWidget { - return this._delegate.widget; - } - - getWidgetPosition() { - return this._delegate.getWidgetPosition(); - } - - acceptSession() { - return this._delegate.acceptSession(); - } -} - // TODO@jrieken THIS should be shared with the code in MainThreadEditors function getEditorId(editor: ICodeEditor, model: ITextModel): string { return `${editor.getId()},${model.id}`; } -/** - * @deprecated - */ -export class InlineChatController1 implements IEditorContribution { +export class InlineChatController implements IEditorContribution { - static get(editor: ICodeEditor) { - return editor.getContribution(INLINE_CHAT_ID); - } + static readonly ID = 'editor.contrib.inlineChatController'; - private _isDisposed: boolean = false; - private readonly _store = new DisposableStore(); - - private readonly _ui: Lazy; - - private readonly _ctxVisible: IContextKey; - private readonly _ctxEditing: IContextKey; - private readonly _ctxResponseType: IContextKey; - private readonly _ctxRequestInProgress: IContextKey; - - private readonly _ctxResponse: IContextKey; - - private readonly _messages = this._store.add(new Emitter()); - protected readonly _onDidEnterState = this._store.add(new Emitter()); - - get chatWidget() { - return this._ui.value.widget.chatWidget; - } - - private readonly _sessionStore = this._store.add(new DisposableStore()); - private readonly _stashedSession = this._store.add(new MutableDisposable()); - private _delegateSession?: IChatEditingSession; - - private _session?: Session; - private _strategy?: LiveStrategy; - - constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @ILogService private readonly _logService: ILogService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IDialogService private readonly _dialogService: IDialogService, - @IContextKeyService contextKeyService: IContextKeyService, - @IChatService private readonly _chatService: IChatService, - @IEditorService private readonly _editorService: IEditorService, - @INotebookEditorService notebookEditorService: INotebookEditorService, - @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, - @IFileService private readonly _fileService: IFileService, - @IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService - ) { - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxEditing = CTX_INLINE_CHAT_EDITING.bindTo(contextKeyService); - this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService); - this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); - - this._ctxResponse = ChatContextKeys.isResponse.bindTo(contextKeyService); - ChatContextKeys.responseHasError.bindTo(contextKeyService); - - this._ui = new Lazy(() => { - - const location: IChatWidgetLocationOptions = { - location: ChatAgentLocation.EditorInline, - resolveData: () => { - assertType(this._editor.hasModel()); - assertType(this._session); - return { - type: ChatAgentLocation.EditorInline, - id: getEditorId(this._editor, this._session.textModelN), - selection: this._editor.getSelection(), - document: this._session.textModelN.uri, - wholeRange: this._session?.wholeRange.trackedInitialRange, - close: () => this.cancelSession(), - delegateSessionResource: this._delegateSession?.chatSessionResource, - }; - } - }; - - // inline chat in notebooks - // check if this editor is part of a notebook editor - // and iff so, use the notebook location but keep the resolveData - // talk about editor data - const notebookEditor = notebookEditorService.getNotebookForPossibleCell(this._editor); - if (!!notebookEditor) { - location.location = ChatAgentLocation.Notebook; - } - - const clear = async () => { - const r = this.joinCurrentRun(); - this.cancelSession(); - await r; - this.run(); - }; - const zone = _instaService.createInstance(InlineChatZoneWidget, location, undefined, { editor: this._editor, notebookEditor }, clear); - this._store.add(zone); - - return zone; - }); - - this._store.add(this._editor.onDidChangeModel(async e => { - if (this._session || !e.newModelUrl) { - return; - } - - const existingSession = this._inlineChatSessionService.getSession(this._editor, e.newModelUrl); - if (!existingSession) { - return; - } - - this._log('session RESUMING after model change', e); - await this.run({ existingSession }); - })); - - this._store.add(this._inlineChatSessionService.onDidEndSession(e => { - if (e.session === this._session && e.endedByExternalCause) { - this._log('session ENDED by external cause'); - this.acceptSession(); - } - })); - - this._store.add(this._inlineChatSessionService.onDidMoveSession(async e => { - if (e.editor === this._editor) { - this._log('session RESUMING after move', e); - await this.run({ existingSession: e.session }); - } - })); - - this._log(`NEW controller`); - } - - dispose(): void { - if (this._currentRun) { - this._messages.fire(this._session?.chatModel.hasRequests - ? Message.PAUSE_SESSION - : Message.CANCEL_SESSION); - } - this._store.dispose(); - this._isDisposed = true; - this._log('DISPOSED controller'); - } - - private _log(message: string | Error, ...more: unknown[]): void { - if (message instanceof Error) { - this._logService.error(message, ...more); - } else { - this._logService.trace(`[IE] (editor:${this._editor.getId()}) ${message}`, ...more); - } - } - - get widget(): EditorBasedInlineChatWidget { - return this._ui.value.widget; - } - - getId(): string { - return INLINE_CHAT_ID; - } - - getWidgetPosition(): Position | undefined { - return this._ui.value.position; - } - - private _currentRun?: Promise; - - async run(options: InlineChatRunOptions | undefined = {}): Promise { - - let lastState: State | undefined; - const d = this._onDidEnterState.event(e => lastState = e); - - try { - this.acceptSession(); - if (this._currentRun) { - await this._currentRun; - } - if (options.initialSelection) { - this._editor.setSelection(options.initialSelection); - } - this._stashedSession.clear(); - this._currentRun = this._nextState(State.CREATE_SESSION, options); - await this._currentRun; - - } catch (error) { - // this should not happen but when it does make sure to tear down the UI and everything - this._log('error during run', error); - onUnexpectedError(error); - if (this._session) { - this._inlineChatSessionService.releaseSession(this._session); - } - this[State.PAUSE](); - - } finally { - this._currentRun = undefined; - d.dispose(); - } - - return lastState !== State.CANCEL; - } - - // ---- state machine - - protected async _nextState(state: State, options: InlineChatRunOptions): Promise { - let nextState: State | void = state; - while (nextState && !this._isDisposed) { - this._log('setState to ', nextState); - const p: State | Promise | Promise = this[nextState](options); - this._onDidEnterState.fire(nextState); - nextState = await p; - } - } - - private async [State.CREATE_SESSION](options: InlineChatRunOptions): Promise { - assertType(this._session === undefined); - assertType(this._editor.hasModel()); - - let session: Session | undefined = options.existingSession; - - let initPosition: Position | undefined; - if (options.position) { - initPosition = Position.lift(options.position).delta(-1); - delete options.position; - } - - const widgetPosition = this._showWidget(session?.headless, true, initPosition); - - // this._updatePlaceholder(); - let errorMessage = localize('create.fail', "Failed to start editor chat"); - - if (!session) { - const createSessionCts = new CancellationTokenSource(); - const msgListener = Event.once(this._messages.event)(m => { - this._log('state=_createSession) message received', m); - if (m === Message.ACCEPT_INPUT) { - // user accepted the input before having a session - options.autoSend = true; - this._ui.value.widget.updateInfo(localize('welcome.2', "Getting ready...")); - } else { - createSessionCts.cancel(); - } - }); - - try { - session = await this._inlineChatSessionService.createSession( - this._editor, - { wholeRange: options.initialRange }, - createSessionCts.token - ); - } catch (error) { - // Inline chat errors are from the provider and have their error messages shown to the user - if (error instanceof InlineChatError || error?.name === InlineChatError.code) { - errorMessage = error.message; - } - } - - createSessionCts.dispose(); - msgListener.dispose(); - - if (createSessionCts.token.isCancellationRequested) { - if (session) { - this._inlineChatSessionService.releaseSession(session); - } - return State.CANCEL; - } - } - - delete options.initialRange; - delete options.existingSession; - - if (!session) { - MessageController.get(this._editor)?.showMessage(errorMessage, widgetPosition); - this._log('Failed to start editor chat'); - return State.CANCEL; - } - - // create a new strategy - this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless); - - this._session = session; - return State.INIT_UI; - } - - private async [State.INIT_UI](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - - // hide/cancel inline completions when invoking IE - InlineCompletionsController.get(this._editor)?.reject(); - - this._sessionStore.clear(); - - const wholeRangeDecoration = this._editor.createDecorationsCollection(); - const handleWholeRangeChange = () => { - const newDecorations = this._strategy?.getWholeRangeDecoration() ?? []; - wholeRangeDecoration.set(newDecorations); - - this._ctxEditing.set(!this._session?.wholeRange.trackedInitialRange.isEmpty()); - }; - this._sessionStore.add(toDisposable(() => { - wholeRangeDecoration.clear(); - this._ctxEditing.reset(); - })); - this._sessionStore.add(this._session.wholeRange.onDidChange(handleWholeRangeChange)); - handleWholeRangeChange(); - - this._ui.value.widget.setChatModel(this._session.chatModel); - this._updatePlaceholder(); - - const isModelEmpty = !this._session.chatModel.hasRequests; - this._ui.value.widget.updateToolbar(true); - this._ui.value.widget.toggleStatus(!isModelEmpty); - this._showWidget(this._session.headless, isModelEmpty); - - this._sessionStore.add(this._editor.onDidChangeModel((e) => { - const msg = this._session?.chatModel.hasRequests - ? Message.PAUSE_SESSION // pause when switching models/tabs and when having a previous exchange - : Message.CANCEL_SESSION; - this._log('model changed, pause or cancel session', msg, e); - this._messages.fire(msg); - })); - - const filePartOfEditSessions = this._chatService.editingSessions.filter(session => - session.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified && e.modifiedURI.toString() === this._session!.textModelN.uri.toString()) - ); - - const withinEditSession = filePartOfEditSessions.find(session => - session.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified && e.hasModificationAt({ - range: this._session!.wholeRange.trackedInitialRange, - uri: this._session!.textModelN.uri - })) - ); - - const chatWidget = this._ui.value.widget.chatWidget; - this._delegateSession = withinEditSession || filePartOfEditSessions[0]; - chatWidget.input.setIsWithinEditSession(!!withinEditSession, filePartOfEditSessions.length > 0); - - this._sessionStore.add(this._editor.onDidChangeModelContent(e => { - - - if (this._session?.hunkData.ignoreTextModelNChanges || this._ui.value.widget.hasFocus()) { - return; - } - - const wholeRange = this._session!.wholeRange; - let shouldFinishSession = false; - if (this._configurationService.getValue(InlineChatConfigKeys.FinishOnType)) { - for (const { range } of e.changes) { - shouldFinishSession = !Range.areIntersectingOrTouching(range, wholeRange.value); - } - } - - this._session!.recordExternalEditOccurred(shouldFinishSession); - - if (shouldFinishSession) { - this._log('text changed outside of whole range, FINISH session'); - this.acceptSession(); - } - })); - - this._sessionStore.add(this._session.chatModel.onDidChange(async e => { - if (e.kind === 'removeRequest') { - // TODO@jrieken there is still some work left for when a request "in the middle" - // is removed. We will undo all changes till that point but not remove those - // later request - await this._session!.undoChangesUntil(e.requestId); - } - })); - - // apply edits from completed requests that haven't been applied yet - const editState = this._createChatTextEditGroupState(); - let didEdit = false; - for (const request of this._session.chatModel.getRequests()) { - if (!request.response || request.response.result?.errorDetails) { - // done when seeing the first request that is still pending (no response). - break; - } - for (const part of request.response.response.value) { - if (part.kind !== 'textEditGroup' || !isEqual(part.uri, this._session.textModelN.uri)) { - continue; - } - if (part.state?.applied) { - continue; - } - for (const edit of part.edits) { - this._makeChanges(edit, undefined, !didEdit); - didEdit = true; - } - part.state ??= editState; - } - } - if (didEdit) { - const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); - this._session.wholeRange.fixup(diff?.changes ?? []); - await this._session.hunkData.recompute(editState, diff); - - this._updateCtxResponseType(); - } - options.position = await this._strategy.renderChanges(); - - if (this._session.chatModel.requestInProgress.get()) { - return State.SHOW_REQUEST; - } else { - return State.WAIT_FOR_INPUT; - } - } - - private async [State.WAIT_FOR_INPUT](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - - this._updatePlaceholder(); - - if (options.message) { - this._updateInput(options.message); - aria.alert(options.message); - delete options.message; - this._showWidget(this._session.headless, false); - } - - let message = Message.NONE; - let request: IChatRequestModel | undefined; - - const barrier = new Barrier(); - const store = new DisposableStore(); - store.add(this._session.chatModel.onDidChange(e => { - if (e.kind === 'addRequest') { - request = e.request; - message = Message.ACCEPT_INPUT; - barrier.open(); - } - })); - store.add(this._strategy.onDidAccept(() => this.acceptSession())); - store.add(this._strategy.onDidDiscard(() => this.cancelSession())); - store.add(this.chatWidget.onDidHide(() => this.cancelSession())); - store.add(Event.once(this._messages.event)(m => { - this._log('state=_waitForInput) message received', m); - message = m; - barrier.open(); - })); - - if (options.attachments) { - await Promise.all(options.attachments.map(async attachment => { - await this._ui.value.widget.chatWidget.attachmentModel.addFile(attachment); - })); - delete options.attachments; - } - if (options.autoSend) { - delete options.autoSend; - this._showWidget(this._session.headless, false); - this._ui.value.widget.chatWidget.acceptInput(); - } - - await barrier.wait(); - store.dispose(); - - - if (message & (Message.CANCEL_INPUT | Message.CANCEL_SESSION)) { - return State.CANCEL; - } - - if (message & Message.PAUSE_SESSION) { - return State.PAUSE; - } - - if (message & Message.ACCEPT_SESSION) { - this._ui.value.widget.selectAll(); - return State.ACCEPT; - } - - if (!request?.message.text) { - return State.WAIT_FOR_INPUT; - } - - - return State.SHOW_REQUEST; - } - - - private async [State.SHOW_REQUEST](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - assertType(this._session.chatModel.requestInProgress.get()); - - this._ctxRequestInProgress.set(true); - - const { chatModel } = this._session; - const request = chatModel.lastRequest; - - assertType(request); - assertType(request.response); - - this._showWidget(this._session.headless, false); - this._ui.value.widget.selectAll(); - this._ui.value.widget.updateInfo(''); - this._ui.value.widget.toggleStatus(true); - - const { response } = request; - const responsePromise = new DeferredPromise(); - - const store = new DisposableStore(); - - const progressiveEditsCts = store.add(new CancellationTokenSource()); - const progressiveEditsAvgDuration = new MovingAverage(); - const progressiveEditsClock = StopWatch.create(); - const progressiveEditsQueue = new Queue(); - - // disable typing and squiggles while streaming a reply - const origDeco = this._editor.getOption(EditorOption.renderValidationDecorations); - this._editor.updateOptions({ - renderValidationDecorations: 'off' - }); - store.add(toDisposable(() => { - this._editor.updateOptions({ - renderValidationDecorations: origDeco - }); - })); - - - let next: State.WAIT_FOR_INPUT | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT = State.WAIT_FOR_INPUT; - store.add(Event.once(this._messages.event)(message => { - this._log('state=_makeRequest) message received', message); - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource); - if (message & Message.CANCEL_SESSION) { - next = State.CANCEL; - } else if (message & Message.PAUSE_SESSION) { - next = State.PAUSE; - } else if (message & Message.ACCEPT_SESSION) { - next = State.ACCEPT; - } - })); - - store.add(chatModel.onDidChange(async e => { - if (e.kind === 'removeRequest' && e.requestId === request.id) { - progressiveEditsCts.cancel(); - responsePromise.complete(); - if (e.reason === ChatRequestRemovalReason.Resend) { - next = State.SHOW_REQUEST; - } else { - next = State.CANCEL; - } - return; - } - if (e.kind === 'move') { - assertType(this._session); - const log: typeof this._log = (msg: string, ...args: unknown[]) => this._log('state=_showRequest) moving inline chat', msg, ...args); - - log('move was requested', e.target, e.range); - - // if there's already a tab open for targetUri, show it and move inline chat to that tab - // otherwise, open the tab to the side - const initialSelection = Selection.fromRange(Range.lift(e.range), SelectionDirection.LTR); - const editorPane = await this._editorService.openEditor({ resource: e.target, options: { selection: initialSelection } }, SIDE_GROUP); - - if (!editorPane) { - log('opening editor failed'); - return; - } - - const newEditor = editorPane.getControl(); - if (!isCodeEditor(newEditor) || !newEditor.hasModel()) { - log('new editor is either missing or not a code editor or does not have a model'); - return; - } - - if (this._inlineChatSessionService.getSession(newEditor, e.target)) { - log('new editor ALREADY has a session'); - return; - } - - const newSession = await this._inlineChatSessionService.createSession( - newEditor, - { - session: this._session, - }, - CancellationToken.None); // TODO@ulugbekna: add proper cancellation? - - - InlineChatController1.get(newEditor)?.run({ existingSession: newSession }); - - next = State.CANCEL; - responsePromise.complete(); - - return; - } - })); - - // cancel the request when the user types - store.add(this._ui.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource); - })); - - let lastLength = 0; - let isFirstChange = true; - - const editState = this._createChatTextEditGroupState(); - let localEditGroup: IChatTextEditGroup | undefined; - - // apply edits - const handleResponse = () => { - - this._updateCtxResponseType(); - - if (!localEditGroup) { - localEditGroup = response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); - } - - if (localEditGroup) { - - localEditGroup.state ??= editState; - - const edits = localEditGroup.edits; - const newEdits = edits.slice(lastLength); - if (newEdits.length > 0) { - - this._log(`${this._session?.textModelN.uri.toString()} received ${newEdits.length} edits`); - - // NEW changes - lastLength = edits.length; - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); - - progressiveEditsQueue.queue(async () => { - - const startThen = this._session!.wholeRange.value.getStartPosition(); - - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - for (const edits of newEdits) { - await this._makeChanges(edits, { - duration: progressiveEditsAvgDuration.value, - token: progressiveEditsCts.token - }, isFirstChange); - - isFirstChange = false; - } - - // reshow the widget if the start position changed or shows at the wrong position - const startNow = this._session!.wholeRange.value.getStartPosition(); - if (!startNow.equals(startThen) || !this._ui.value.position?.equals(startNow)) { - this._showWidget(this._session!.headless, false, startNow.delta(-1)); - } - }); - } - } - - if (response.isCanceled) { - progressiveEditsCts.cancel(); - responsePromise.complete(); - - } else if (response.isComplete) { - responsePromise.complete(); - } - }; - store.add(response.onDidChange(handleResponse)); - handleResponse(); - - // (1) we must wait for the request to finish - // (2) we must wait for all edits that came in via progress to complete - await responsePromise.p; - await progressiveEditsQueue.whenIdle(); - - if (response.result?.errorDetails && !response.result.errorDetails.responseIsFiltered) { - await this._session.undoChangesUntil(response.requestId); - } - - store.dispose(); - - const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); - this._session.wholeRange.fixup(diff?.changes ?? []); - await this._session.hunkData.recompute(editState, diff); - - this._ctxRequestInProgress.set(false); - - - let newPosition: Position | undefined; - - if (response.result?.errorDetails) { - // error -> no message, errors are shown with the request - alert(response.result.errorDetails.message); - } else if (response.response.value.length === 0) { - // empty -> show message - const status = localize('empty', "No results, please refine your input and try again"); - this._ui.value.widget.updateStatus(status, { classes: ['warn'] }); - alert(status); - } else { - // real response -> no message - this._ui.value.widget.updateStatus(''); - alert(localize('responseWasEmpty', "Response was empty")); - } - - const position = await this._strategy.renderChanges(); - if (position) { - // if the selection doesn't start far off we keep the widget at its current position - // because it makes reading this nicer - const selection = this._editor.getSelection(); - if (selection?.containsPosition(position)) { - if (position.lineNumber - selection.startLineNumber > 8) { - newPosition = position; - } - } else { - newPosition = position; - } - } - this._showWidget(this._session.headless, false, newPosition); - - return next; - } - - private async[State.PAUSE]() { - - this._resetWidget(); - - this._strategy?.dispose?.(); - this._session = undefined; - } - - private async[State.ACCEPT]() { - assertType(this._session); - assertType(this._strategy); - this._sessionStore.clear(); - - try { - await this._strategy.apply(); - } catch (err) { - this._dialogService.error(localize('err.apply', "Failed to apply changes.", toErrorMessage(err))); - this._log('FAILED to apply changes'); - this._log(err); - } - - this._resetWidget(); - this._inlineChatSessionService.releaseSession(this._session); - - - this._strategy?.dispose(); - this._strategy = undefined; - this._session = undefined; - } - - private async[State.CANCEL]() { - - this._resetWidget(); - - if (this._session) { - // assertType(this._session); - assertType(this._strategy); - this._sessionStore.clear(); - - // only stash sessions that were not unstashed, not "empty", and not interacted with - const shouldStash = !this._session.isUnstashed && this._session.chatModel.hasRequests && this._session.hunkData.size === this._session.hunkData.pending; - let undoCancelEdits: IValidEditOperation[] = []; - try { - undoCancelEdits = this._strategy.cancel(); - } catch (err) { - this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err))); - this._log('FAILED to discard changes'); - this._log(err); - } - - this._stashedSession.clear(); - if (shouldStash) { - this._stashedSession.value = this._inlineChatSessionService.stashSession(this._session, this._editor, undoCancelEdits); - } else { - this._inlineChatSessionService.releaseSession(this._session); - } - } - - - this._strategy?.dispose(); - this._strategy = undefined; - this._session = undefined; - } - - // ---- - - private _showWidget(headless: boolean = false, initialRender: boolean = false, position?: Position) { - assertType(this._editor.hasModel()); - this._ctxVisible.set(true); - - let widgetPosition: Position; - if (position) { - // explicit position wins - widgetPosition = position; - } else if (this._ui.rawValue?.position) { - // already showing - special case of line 1 - if (this._ui.rawValue?.position.lineNumber === 1) { - widgetPosition = this._ui.rawValue?.position.delta(-1); - } else { - widgetPosition = this._ui.rawValue?.position; - } - } else { - // default to ABOVE the selection - widgetPosition = this._editor.getSelection().getStartPosition().delta(-1); - } - - if (this._session && !position && (this._session.hasChangedText || this._session.chatModel.hasRequests)) { - widgetPosition = this._session.wholeRange.trackedInitialRange.getStartPosition().delta(-1); - } - - if (initialRender && (this._editor.getOption(EditorOption.stickyScroll)).enabled) { - this._editor.revealLine(widgetPosition.lineNumber); // do NOT substract `this._editor.getOption(EditorOption.stickyScroll).maxLineCount` because the editor already does that - } - - if (!headless) { - if (this._ui.rawValue?.position) { - this._ui.value.updatePositionAndHeight(widgetPosition); - } else { - this._ui.value.show(widgetPosition); - } - } - - return widgetPosition; - } - - private _resetWidget() { - - this._sessionStore.clear(); - this._ctxVisible.reset(); - - this._ui.rawValue?.hide(); - - // Return focus to the editor only if the current focus is within the editor widget - if (this._editor.hasWidgetFocus()) { - this._editor.focus(); - } - } - - private _updateCtxResponseType(): void { - - if (!this._session) { - this._ctxResponseType.set(InlineChatResponseType.None); - return; - } - - const hasLocalEdit = (response: IResponse): boolean => { - return response.value.some(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); - }; - - let responseType = InlineChatResponseType.None; - for (const request of this._session.chatModel.getRequests()) { - if (!request.response) { - continue; - } - responseType = InlineChatResponseType.Messages; - if (hasLocalEdit(request.response.response)) { - responseType = InlineChatResponseType.MessagesAndEdits; - break; // no need to check further - } - } - this._ctxResponseType.set(responseType); - this._ctxResponse.set(responseType !== InlineChatResponseType.None); - } - - private _createChatTextEditGroupState(): IChatTextEditGroupState { - assertType(this._session); - - const sha1 = new DefaultModelSHA1Computer(); - const textModel0Sha1 = sha1.canComputeSHA1(this._session.textModel0) - ? sha1.computeSHA1(this._session.textModel0) - : generateUuid(); - - return { - sha1: textModel0Sha1, - applied: 0 - }; - } - - private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) { - assertType(this._session); - assertType(this._strategy); - - const moreMinimalEdits = await raceCancellation(this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits), opts?.token || CancellationToken.None); - this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.agent.extensionId, edits, moreMinimalEdits); - - if (moreMinimalEdits?.length === 0) { - // nothing left to do - return; - } - - const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits; - const editOperations = actualEdits.map(TextEdit.asEditOperation); - - const editsObserver: IEditObserver = { - start: () => this._session!.hunkData.ignoreTextModelNChanges = true, - stop: () => this._session!.hunkData.ignoreTextModelNChanges = false, - }; - - const metadata = this._getMetadata(); - if (opts) { - await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore, metadata); - } else { - await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore, metadata); - } - } - - private _getMetadata(): IInlineChatMetadata { - const lastRequest = this._session?.chatModel.lastRequest; - return { - extensionId: VersionedExtensionId.tryCreate(this._session?.agent.extensionId.value, this._session?.agent.extensionVersion), - modelId: lastRequest?.modelId, - requestId: lastRequest?.id, - }; - } - - private _updatePlaceholder(): void { - this._ui.value.widget.placeholder = this._session?.agent.description ?? localize('askOrEditInContext', 'Ask or edit in context'); - } - - private _updateInput(text: string, selectAll = true): void { - - this._ui.value.widget.chatWidget.setInput(text); - if (selectAll) { - const newSelection = new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1); - this._ui.value.widget.chatWidget.inputEditor.setSelection(newSelection); - } - } - - // ---- controller API - - arrowOut(up: boolean): void { - if (this._ui.value.position && this._editor.hasModel()) { - const { column } = this._editor.getPosition(); - const { lineNumber } = this._ui.value.position; - const newLine = up ? lineNumber : lineNumber + 1; - this._editor.setPosition({ lineNumber: newLine, column }); - this._editor.focus(); - } - } - - focus(): void { - this._ui.value.widget.focus(); - } - - async viewInChat() { - if (!this._strategy || !this._session) { - return; - } - - let someApplied = false; - let lastEdit: IChatTextEditGroup | undefined; - - const uri = this._editor.getModel()?.uri; - const requests = this._session.chatModel.getRequests(); - for (const request of requests) { - if (!request.response) { - continue; - } - for (const part of request.response.response.value) { - if (part.kind === 'textEditGroup' && isEqual(part.uri, uri)) { - // fully or partially applied edits - someApplied = someApplied || Boolean(part.state?.applied); - lastEdit = part; - part.edits = []; - part.state = undefined; - } - } - } - - const doEdits = this._strategy.cancel(); - - if (someApplied) { - assertType(lastEdit); - lastEdit.edits = [doEdits]; - } - - await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel, false); - - this.cancelSession(); - } - - acceptSession(): void { - const response = this._session?.chatModel.getRequests().at(-1)?.response; - if (response) { - this._chatService.notifyUserAction({ - sessionResource: response.session.sessionResource, - requestId: response.requestId, - agentId: response.agent?.id, - command: response.slashCommand?.name, - result: response.result, - action: { - kind: 'inlineChat', - action: 'accepted' - } - }); - } - this._messages.fire(Message.ACCEPT_SESSION); - } - - acceptHunk(hunkInfo?: HunkInformation) { - return this._strategy?.performHunkAction(hunkInfo, HunkAction.Accept); - } - - discardHunk(hunkInfo?: HunkInformation) { - return this._strategy?.performHunkAction(hunkInfo, HunkAction.Discard); - } - - toggleDiff(hunkInfo?: HunkInformation) { - return this._strategy?.performHunkAction(hunkInfo, HunkAction.ToggleDiff); - } - - moveHunk(next: boolean) { - this.focus(); - this._strategy?.performHunkAction(undefined, next ? HunkAction.MoveNext : HunkAction.MovePrev); - } - - async cancelSession() { - const response = this._session?.chatModel.lastRequest?.response; - if (response) { - this._chatService.notifyUserAction({ - sessionResource: response.session.sessionResource, - requestId: response.requestId, - agentId: response.agent?.id, - command: response.slashCommand?.name, - result: response.result, - action: { - kind: 'inlineChat', - action: 'discarded' - } - }); - } - - this._resetWidget(); - this._messages.fire(Message.CANCEL_SESSION); - } - - reportIssue() { - const response = this._session?.chatModel.lastRequest?.response; - if (response) { - this._chatService.notifyUserAction({ - sessionResource: response.session.sessionResource, - requestId: response.requestId, - agentId: response.agent?.id, - command: response.slashCommand?.name, - result: response.result, - action: { kind: 'bug' } - }); - } - } - - unstashLastSession(): Session | undefined { - const result = this._stashedSession.value?.unstash(); - return result; - } - - joinCurrentRun(): Promise | undefined { - return this._currentRun; - } - - get isActive() { - return Boolean(this._currentRun); - } - - async createImageAttachment(attachment: URI): Promise { - if (attachment.scheme === Schemas.file) { - if (await this._fileService.canHandleResource(attachment)) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); - } - } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { - const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None); - if (extractedImages) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); - } - } - - return undefined; - } -} - -export class InlineChatController2 implements IEditorContribution { - - static readonly ID = 'editor.contrib.inlineChatController2'; - - static get(editor: ICodeEditor): InlineChatController2 | undefined { - return editor.getContribution(InlineChatController2.ID) ?? undefined; + static get(editor: ICodeEditor): InlineChatController | undefined { + return editor.getContribution(InlineChatController.ID) ?? undefined; } private readonly _store = new DisposableStore(); @@ -1279,7 +132,6 @@ export class InlineChatController2 implements IEditorContribution { @IEditorService private readonly _editorService: IEditorService, @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, - @IChatService chatService: IChatService, ) { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); @@ -1301,14 +153,7 @@ export class InlineChatController2 implements IEditorContribution { id: getEditorId(this._editor, this._editor.getModel()), selection: this._editor.getSelection(), document, - wholeRange, - close: () => { /* TODO@jrieken */ }, - delegateSessionResource: chatService.editingSessions.find(session => - session.entries.get().some(e => e.hasModificationAt({ - range: wholeRange, - uri: document - })) - )?.chatSessionResource, + wholeRange }; } }; @@ -1368,7 +213,7 @@ export class InlineChatController2 implements IEditorContribution { this._currentSession = derived(r => { sessionsSignal.read(r); const model = editorObs.model.read(r); - const session = model && _inlineChatSessionService.getSession2(model.uri); + const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri); return session ?? undefined; }); @@ -1394,7 +239,7 @@ export class InlineChatController2 implements IEditorContribution { let foundOne = false; for (const editor of codeEditorService.listCodeEditors()) { - if (Boolean(InlineChatController2.get(editor)?._isActiveController.read(undefined))) { + if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) { foundOne = true; break; } @@ -1573,7 +418,7 @@ export class InlineChatController2 implements IEditorContribution { const uri = this._editor.getModel().uri; - const existingSession = this._inlineChatSessionService.getSession2(uri); + const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri); if (existingSession) { await existingSession.editingSession.accept(); existingSession.dispose(); @@ -1581,7 +426,7 @@ export class InlineChatController2 implements IEditorContribution { this._isActiveController.set(true, undefined); - const session = await this._inlineChatSessionService.createSession2(this._editor, uri, CancellationToken.None); + const session = this._inlineChatSessionService.createSession(this._editor); // ADD diagnostics const entries: IChatRequestVariableEntry[] = []; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index 95c38b1b78e..539e8197ee0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -3,9 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { illegalState } from '../../../../base/common/errors.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InlineChatController } from './inlineChatController.js'; @@ -13,8 +11,6 @@ import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri } from '../../notebook/common/notebookCommon.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { NotebookTextDiffEditor } from '../../notebook/browser/diff/notebookDiffEditor.js'; -import { NotebookMultiTextDiffEditor } from '../../notebook/browser/diff/notebookMultiDiffEditor.js'; export class InlineChatNotebookContribution { @@ -26,51 +22,6 @@ export class InlineChatNotebookContribution { @INotebookEditorService notebookEditorService: INotebookEditorService, ) { - this._store.add(sessionService.registerSessionKeyComputer(Schemas.vscodeNotebookCell, { - getComparisonKey: (editor, uri) => { - const data = CellUri.parse(uri); - if (!data) { - throw illegalState('Expected notebook cell uri'); - } - let fallback: string | undefined; - for (const notebookEditor of notebookEditorService.listNotebookEditors()) { - if (notebookEditor.hasModel() && isEqual(notebookEditor.textModel.uri, data.notebook)) { - - const candidate = `${notebookEditor.getId()}#${uri}`; - - if (!fallback) { - fallback = candidate; - } - - // find the code editor in the list of cell-code editors - if (notebookEditor.codeEditors.find((tuple) => tuple[1] === editor)) { - return candidate; - } - - // // reveal cell and try to find code editor again - // const cell = notebookEditor.getCellByHandle(data.handle); - // if (cell) { - // notebookEditor.revealInViewAtTop(cell); - // if (notebookEditor.codeEditors.find((tuple) => tuple[1] === editor)) { - // return candidate; - // } - // } - } - } - - if (fallback) { - return fallback; - } - - const activeEditor = editorService.activeEditorPane; - if (activeEditor && (activeEditor.getId() === NotebookTextDiffEditor.ID || activeEditor.getId() === NotebookMultiTextDiffEditor.ID)) { - return `${editor.getId()}#${uri}`; - } - - throw illegalState('Expected notebook editor'); - } - })); - this._store.add(sessionService.onWillStartSession(newSessionEditor => { const candidate = CellUri.parse(newSessionEditor.getModel().uri); if (!candidate) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts deleted file mode 100644 index 72752f91792..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ /dev/null @@ -1,646 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from '../../../../base/common/uri.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../common/inlineChat.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; -import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; -import { IInlineChatSessionService } from './inlineChatSessionService.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { coalesceInPlace } from '../../../../base/common/arrays.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IChatModel, IChatRequestModel, IChatTextEditGroupState } from '../../chat/common/model/chatModel.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { IChatAgent } from '../../chat/common/participants/chatAgents.js'; -import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; - - -export type TelemetryData = { - extension: string; - rounds: string; - undos: string; - unstashed: number; - edits: number; - finishedByEdit: boolean; - startTime: string; - endTime: string; - acceptedHunks: number; - discardedHunks: number; - responseTypes: string; -}; - -export type TelemetryDataClassification = { - owner: 'jrieken'; - comment: 'Data about an interaction editor session'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension providing the data' }; - rounds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of request that were made' }; - undos: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Requests that have been undone' }; - edits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits happen while the session was active' }; - unstashed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How often did this session become stashed and resumed' }; - finishedByEdit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits cause the session to terminate' }; - startTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session started' }; - endTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session ended' }; - acceptedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of accepted hunks' }; - discardedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of discarded hunks' }; - responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; -}; - - -export class SessionWholeRange { - - private static readonly _options: IModelDecorationOptions = ModelDecorationOptions.register({ description: 'inlineChat/session/wholeRange' }); - - private readonly _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - - private _decorationIds: string[] = []; - - constructor(private readonly _textModel: ITextModel, wholeRange: IRange) { - this._decorationIds = _textModel.deltaDecorations([], [{ range: wholeRange, options: SessionWholeRange._options }]); - } - - dispose() { - this._onDidChange.dispose(); - if (!this._textModel.isDisposed()) { - this._textModel.deltaDecorations(this._decorationIds, []); - } - } - - fixup(changes: readonly DetailedLineRangeMapping[]): void { - const newDeco: IModelDeltaDecoration[] = []; - for (const { modified } of changes) { - const modifiedRange = this._textModel.validateRange(modified.isEmpty - ? new Range(modified.startLineNumber, 1, modified.startLineNumber, Number.MAX_SAFE_INTEGER) - : new Range(modified.startLineNumber, 1, modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER)); - - newDeco.push({ range: modifiedRange, options: SessionWholeRange._options }); - } - const [first, ...rest] = this._decorationIds; // first is the original whole range - const newIds = this._textModel.deltaDecorations(rest, newDeco); - this._decorationIds = [first].concat(newIds); - this._onDidChange.fire(this); - } - - get trackedInitialRange(): Range { - const [first] = this._decorationIds; - return this._textModel.getDecorationRange(first) ?? new Range(1, 1, 1, 1); - } - - get value(): Range { - let result: Range | undefined; - for (const id of this._decorationIds) { - const range = this._textModel.getDecorationRange(id); - if (range) { - if (!result) { - result = range; - } else { - result = Range.plusRange(result, range); - } - } - } - return result!; - } -} - -export class Session { - - private _isUnstashed: boolean = false; - private readonly _startTime = new Date(); - private readonly _teldata: TelemetryData; - - private readonly _versionByRequest = new Map(); - - constructor( - readonly headless: boolean, - /** - * The URI of the document which is being EditorEdit - */ - readonly targetUri: URI, - /** - * A copy of the document at the time the session was started - */ - readonly textModel0: ITextModel, - /** - * The model of the editor - */ - readonly textModelN: ITextModel, - readonly agent: IChatAgent, - readonly wholeRange: SessionWholeRange, - readonly hunkData: HunkData, - readonly chatModel: IChatModel, - versionsByRequest?: [string, number][], // DEBT? this is needed when a chat model is "reused" for a new chat session - ) { - - this._teldata = { - extension: ExtensionIdentifier.toKey(agent.extensionId), - startTime: this._startTime.toISOString(), - endTime: this._startTime.toISOString(), - edits: 0, - finishedByEdit: false, - rounds: '', - undos: '', - unstashed: 0, - acceptedHunks: 0, - discardedHunks: 0, - responseTypes: '' - }; - if (versionsByRequest) { - this._versionByRequest = new Map(versionsByRequest); - } - } - - get isUnstashed(): boolean { - return this._isUnstashed; - } - - markUnstashed() { - this._teldata.unstashed! += 1; - this._isUnstashed = true; - } - - markModelVersion(request: IChatRequestModel) { - this._versionByRequest.set(request.id, this.textModelN.getAlternativeVersionId()); - } - - get versionsByRequest() { - return Array.from(this._versionByRequest); - } - - async undoChangesUntil(requestId: string): Promise { - - const targetAltVersion = this._versionByRequest.get(requestId); - if (targetAltVersion === undefined) { - return false; - } - // undo till this point - this.hunkData.ignoreTextModelNChanges = true; - try { - while (targetAltVersion < this.textModelN.getAlternativeVersionId() && this.textModelN.canUndo()) { - await this.textModelN.undo(); - } - } finally { - this.hunkData.ignoreTextModelNChanges = false; - } - return true; - } - - get hasChangedText(): boolean { - return !this.textModel0.equalsTextBuffer(this.textModelN.getTextBuffer()); - } - - 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 changes) { - startLine = Math.min(startLine, change.modified.startLineNumber); - endLine = Math.max(endLine, change.modified.endLineNumberExclusive); - } - - return this.textModelN.getValueInRange(new Range(startLine, 1, endLine, Number.MAX_VALUE)); - } - - recordExternalEditOccurred(didFinish: boolean) { - this._teldata.edits += 1; - this._teldata.finishedByEdit = didFinish; - } - - asTelemetryData(): TelemetryData { - - for (const item of this.hunkData.getInfo()) { - switch (item.getState()) { - case HunkState.Accepted: - this._teldata.acceptedHunks += 1; - break; - case HunkState.Rejected: - this._teldata.discardedHunks += 1; - break; - } - } - - this._teldata.endTime = new Date().toISOString(); - return this._teldata; - } -} - - -export class StashedSession { - - private readonly _listener: IDisposable; - private readonly _ctxHasStashedSession: IContextKey; - private _session: Session | undefined; - - constructor( - editor: ICodeEditor, - session: Session, - private readonly _undoCancelEdits: IValidEditOperation[], - @IContextKeyService contextKeyService: IContextKeyService, - @IInlineChatSessionService private readonly _sessionService: IInlineChatSessionService, - @ILogService private readonly _logService: ILogService - ) { - this._ctxHasStashedSession = CTX_INLINE_CHAT_HAS_STASHED_SESSION.bindTo(contextKeyService); - - // keep session for a little bit, only release when user continues to work (type, move cursor, etc.) - this._session = session; - this._ctxHasStashedSession.set(true); - this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel, editor.onDidBlurEditorWidget))(() => { - this._session = undefined; - this._sessionService.releaseSession(session); - this._ctxHasStashedSession.reset(); - }); - } - - dispose() { - this._listener.dispose(); - this._ctxHasStashedSession.reset(); - if (this._session) { - this._sessionService.releaseSession(this._session); - } - } - - unstash(): Session | undefined { - if (!this._session) { - return undefined; - } - this._listener.dispose(); - const result = this._session; - result.markUnstashed(); - result.hunkData.ignoreTextModelNChanges = true; - result.textModelN.pushEditOperations(null, this._undoCancelEdits, () => null); - result.hunkData.ignoreTextModelNChanges = false; - this._session = undefined; - this._logService.debug('[IE] Unstashed session'); - return result; - } -} - -// --- - -function lineRangeAsRange(lineRange: LineRange, model: ITextModel): Range { - return lineRange.isEmpty - ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, Number.MAX_SAFE_INTEGER) - : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER); -} - -export class HunkData { - - private static readonly _HUNK_TRACKED_RANGE = ModelDecorationOptions.register({ - description: 'inline-chat-hunk-tracked-range', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - }); - - private static readonly _HUNK_THRESHOLD = 8; - - private readonly _store = new DisposableStore(); - private readonly _data = new Map(); - private _ignoreChanges: boolean = false; - - constructor( - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - private readonly _textModel0: ITextModel, - private readonly _textModelN: ITextModel, - ) { - - this._store.add(_textModelN.onDidChangeContent(e => { - if (!this._ignoreChanges) { - this._mirrorChanges(e); - } - })); - } - - dispose(): void { - if (!this._textModelN.isDisposed()) { - this._textModelN.changeDecorations(accessor => { - for (const { textModelNDecorations } of this._data.values()) { - textModelNDecorations.forEach(accessor.removeDecoration, accessor); - } - }); - } - if (!this._textModel0.isDisposed()) { - this._textModel0.changeDecorations(accessor => { - for (const { textModel0Decorations } of this._data.values()) { - textModel0Decorations.forEach(accessor.removeDecoration, accessor); - } - }); - } - this._data.clear(); - this._store.dispose(); - } - - set ignoreTextModelNChanges(value: boolean) { - this._ignoreChanges = value; - } - - get ignoreTextModelNChanges(): boolean { - return this._ignoreChanges; - } - - private _mirrorChanges(event: IModelContentChangedEvent) { - - // mirror textModelN changes to textModel0 execept for those that - // overlap with a hunk - - type HunkRangePair = { rangeN: Range; range0: Range; markAccepted: () => void }; - const hunkRanges: HunkRangePair[] = []; - - const ranges0: Range[] = []; - - for (const entry of this._data.values()) { - - if (entry.state === HunkState.Pending) { - // pending means the hunk's changes aren't "sync'd" yet - for (let i = 1; i < entry.textModelNDecorations.length; i++) { - const rangeN = this._textModelN.getDecorationRange(entry.textModelNDecorations[i]); - const range0 = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]); - if (rangeN && range0) { - hunkRanges.push({ - rangeN, range0, - markAccepted: () => entry.state = HunkState.Accepted - }); - } - } - - } else if (entry.state === HunkState.Accepted) { - // accepted means the hunk's changes are also in textModel0 - for (let i = 1; i < entry.textModel0Decorations.length; i++) { - const range = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]); - if (range) { - ranges0.push(range); - } - } - } - } - - hunkRanges.sort((a, b) => Range.compareRangesUsingStarts(a.rangeN, b.rangeN)); - ranges0.sort(Range.compareRangesUsingStarts); - - const edits: IIdentifiedSingleEditOperation[] = []; - - for (const change of event.changes) { - - let isOverlapping = false; - - let pendingChangesLen = 0; - - for (const entry of hunkRanges) { - if (entry.rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) { - // pending hunk _before_ this change. When projecting into textModel0 we need to - // subtract that. Because diffing is relaxed it might include changes that are not - // actual insertions/deletions. Therefore we need to take the length of the original - // range into account. - pendingChangesLen += this._textModelN.getValueLengthInRange(entry.rangeN); - pendingChangesLen -= this._textModel0.getValueLengthInRange(entry.range0); - - } else if (Range.areIntersectingOrTouching(entry.rangeN, change.range)) { - // an edit overlaps with a (pending) hunk. We take this as a signal - // to mark the hunk as accepted and to ignore the edit. The range of the hunk - // will be up-to-date because of decorations created for them - entry.markAccepted(); - isOverlapping = true; - break; - - } else { - // hunks past this change aren't relevant - break; - } - } - - if (isOverlapping) { - // hunk overlaps, it grew - continue; - } - - const offset0 = change.rangeOffset - pendingChangesLen; - const start0 = this._textModel0.getPositionAt(offset0); - - let acceptedChangesLen = 0; - for (const range of ranges0) { - if (range.getEndPosition().isBefore(start0)) { - // accepted hunk _before_ this projected change. When projecting into textModel0 - // we need to add that - acceptedChangesLen += this._textModel0.getValueLengthInRange(range); - } - } - - const start = this._textModel0.getPositionAt(offset0 + acceptedChangesLen); - const end = this._textModel0.getPositionAt(offset0 + acceptedChangesLen + change.rangeLength); - edits.push(EditOperation.replace(Range.fromPositions(start, end), change.text)); - } - - this._textModel0.pushEditOperations(null, edits, () => null); - } - - async recompute(editState: IChatTextEditGroupState, diff?: IDocumentDiff | null) { - - diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); - - let mergedChanges: DetailedLineRangeMapping[] = []; - - if (diff && diff.changes.length > 0) { - // merge changes neighboring changes - mergedChanges = [diff.changes[0]]; - for (let i = 1; i < diff.changes.length; i++) { - const lastChange = mergedChanges[mergedChanges.length - 1]; - const thisChange = diff.changes[i]; - if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) { - mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping( - lastChange.original.join(thisChange.original), - lastChange.modified.join(thisChange.modified), - (lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? []) - ); - } else { - mergedChanges.push(thisChange); - } - } - } - - const hunks = mergedChanges.map(change => new RawHunk(change.original, change.modified, change.innerChanges ?? [])); - - editState.applied = hunks.length; - - this._textModelN.changeDecorations(accessorN => { - - this._textModel0.changeDecorations(accessor0 => { - - // clean up old decorations - for (const { textModelNDecorations, textModel0Decorations } of this._data.values()) { - textModelNDecorations.forEach(accessorN.removeDecoration, accessorN); - textModel0Decorations.forEach(accessor0.removeDecoration, accessor0); - } - - this._data.clear(); - - // add new decorations - for (const hunk of hunks) { - - const textModelNDecorations: string[] = []; - const textModel0Decorations: string[] = []; - - textModelNDecorations.push(accessorN.addDecoration(lineRangeAsRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(lineRangeAsRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); - - for (const change of hunk.changes) { - textModelNDecorations.push(accessorN.addDecoration(change.modifiedRange, HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(change.originalRange, HunkData._HUNK_TRACKED_RANGE)); - } - - this._data.set(hunk, { - editState, - textModelNDecorations, - textModel0Decorations, - state: HunkState.Pending - }); - } - }); - }); - } - - get size(): number { - return this._data.size; - } - - get pending(): number { - return Iterable.reduce(this._data.values(), (r, { state }) => r + (state === HunkState.Pending ? 1 : 0), 0); - } - - private _discardEdits(item: HunkInformation): ISingleEditOperation[] { - const edits: ISingleEditOperation[] = []; - const rangesN = item.getRangesN(); - const ranges0 = item.getRanges0(); - for (let i = 1; i < rangesN.length; i++) { - const modifiedRange = rangesN[i]; - - const originalValue = this._textModel0.getValueInRange(ranges0[i]); - edits.push(EditOperation.replace(modifiedRange, originalValue)); - } - return edits; - } - - discardAll() { - const edits: ISingleEditOperation[][] = []; - for (const item of this.getInfo()) { - if (item.getState() === HunkState.Pending) { - edits.push(this._discardEdits(item)); - } - } - const undoEdits: IValidEditOperation[][] = []; - this._textModelN.pushEditOperations(null, edits.flat(), (_undoEdits) => { - undoEdits.push(_undoEdits); - return null; - }); - return undoEdits.flat(); - } - - getInfo(): HunkInformation[] { - - const result: HunkInformation[] = []; - - for (const [hunk, data] of this._data.entries()) { - const item: HunkInformation = { - getState: () => { - return data.state; - }, - isInsertion: () => { - return hunk.original.isEmpty; - }, - getRangesN: () => { - const ranges = data.textModelNDecorations.map(id => this._textModelN.getDecorationRange(id)); - coalesceInPlace(ranges); - return ranges; - }, - getRanges0: () => { - const ranges = data.textModel0Decorations.map(id => this._textModel0.getDecorationRange(id)); - coalesceInPlace(ranges); - return ranges; - }, - discardChanges: () => { - // DISCARD: replace modified range with original value. The modified range is retrieved from a decoration - // which was created above so that typing in the editor keeps discard working. - if (data.state === HunkState.Pending) { - const edits = this._discardEdits(item); - this._textModelN.pushEditOperations(null, edits, () => null); - data.state = HunkState.Rejected; - if (data.editState.applied > 0) { - data.editState.applied -= 1; - } - } - }, - acceptChanges: () => { - // ACCEPT: replace original range with modified value. The modified value is retrieved from the model via - // its decoration and the original range is retrieved from the hunk. - if (data.state === HunkState.Pending) { - const edits: ISingleEditOperation[] = []; - const rangesN = item.getRangesN(); - const ranges0 = item.getRanges0(); - for (let i = 1; i < ranges0.length; i++) { - const originalRange = ranges0[i]; - const modifiedValue = this._textModelN.getValueInRange(rangesN[i]); - edits.push(EditOperation.replace(originalRange, modifiedValue)); - } - this._textModel0.pushEditOperations(null, edits, () => null); - data.state = HunkState.Accepted; - } - } - }; - result.push(item); - } - - return result; - } -} - -class RawHunk { - constructor( - readonly original: LineRange, - readonly modified: LineRange, - readonly changes: RangeMapping[] - ) { } -} - -type RawHunkData = { - textModelNDecorations: string[]; - textModel0Decorations: string[]; - state: HunkState; - editState: IChatTextEditGroupState; -}; - -export const enum HunkState { - Pending = 0, - Accepted = 1, - Rejected = 2 -} - -export interface HunkInformation { - /** - * The first element [0] is the whole modified range and subsequent elements are word-level changes - */ - getRangesN(): Range[]; - - getRanges0(): Range[]; - - isInsertion(): boolean; - - discardChanges(): void; - - /** - * Accept the hunk. Applies the corresponding edits into textModel0 - */ - acceptChanges(): void; - - getState(): HunkState; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 8396def2c6b..22a55a85fcc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -2,38 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../editor/common/core/position.js'; -import { IRange } from '../../../../editor/common/core/range.js'; import { Selection } from '../../../../editor/common/core/selection.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ChatViewPaneTarget, IChatWidgetService } from '../../chat/browser/chat.js'; import { IChatEditingSession } from '../../chat/common/editing/chatEditingService.js'; import { IChatModel, IChatModelInputState, IChatRequestModel } from '../../chat/common/model/chatModel.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; -import { Session, StashedSession } from './inlineChatSession.js'; -export interface ISessionKeyComputer { - getComparisonKey(editor: ICodeEditor, uri: URI): string; -} export const IInlineChatSessionService = createDecorator('IInlineChatSessionService'); -export interface IInlineChatSessionEvent { - readonly editor: ICodeEditor; - readonly session: Session; -} - -export interface IInlineChatSessionEndEvent extends IInlineChatSessionEvent { - readonly endedByExternalCause: boolean; -} - export interface IInlineChatSession2 { readonly initialPosition: Position; readonly initialSelection: Selection; @@ -47,30 +30,13 @@ export interface IInlineChatSessionService { _serviceBrand: undefined; readonly onWillStartSession: Event; - readonly onDidMoveSession: Event; - readonly onDidStashSession: Event; - readonly onDidEndSession: Event; - - createSession(editor: IActiveCodeEditor, options: { wholeRange?: IRange; session?: Session; headless?: boolean }, token: CancellationToken): Promise; - - moveSession(session: Session, newEditor: ICodeEditor): void; - - getCodeEditor(session: Session): ICodeEditor; - - getSession(editor: ICodeEditor, uri: URI): Session | undefined; - - releaseSession(session: Session): void; - - stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession; - - registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable; + readonly onDidChangeSessions: Event; dispose(): void; - createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise; - getSession2(uri: URI): IInlineChatSession2 | undefined; + createSession(editor: ICodeEditor): IInlineChatSession2; + getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined; getSessionBySessionUri(uri: URI): IInlineChatSession2 | undefined; - readonly onDidChangeSessions: Event; } export async function moveToPanelChat(accessor: ServicesAccessor, model: IChatModel | undefined, resend: boolean) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 2d1e049319b..797c4ef4566 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -2,25 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; -import { Schemas } from '../../../../base/common/network.js'; import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; -import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { IActiveCodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; -import { createTextBufferFactoryFromSnapshot } from '../../../../editor/common/model/textModel.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -30,12 +19,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { ILogService } from '../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; -import { IChatWidgetService } from '../../chat/browser/chat.js'; import { IChatAgentService } from '../../chat/common/participants/chatAgents.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; @@ -43,15 +27,7 @@ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js'; import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js'; -import { askInPanelChat, IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js'; - - -type SessionData = { - editor: ICodeEditor; - session: Session; - store: IDisposable; -}; +import { askInPanelChat, IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; export class InlineChatError extends Error { static readonly code = 'InlineChatError'; @@ -61,301 +37,46 @@ export class InlineChatError extends Error { } } - export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; private readonly _store = new DisposableStore(); + private readonly _sessions = new ResourceMap(); private readonly _onWillStartSession = this._store.add(new Emitter()); readonly onWillStartSession: Event = this._onWillStartSession.event; - private readonly _onDidMoveSession = this._store.add(new Emitter()); - readonly onDidMoveSession: Event = this._onDidMoveSession.event; - - private readonly _onDidEndSession = this._store.add(new Emitter()); - readonly onDidEndSession: Event = this._onDidEndSession.event; - - private readonly _onDidStashSession = this._store.add(new Emitter()); - readonly onDidStashSession: Event = this._onDidStashSession.event; - - private readonly _sessions = new Map(); - private readonly _keyComputers = new Map(); - - constructor( - @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IModelService private readonly _modelService: IModelService, - @ITextModelService private readonly _textModelService: ITextModelService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @ILogService private readonly _logService: ILogService, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IEditorService private readonly _editorService: IEditorService, - @ITextFileService private readonly _textFileService: ITextFileService, - @ILanguageService private readonly _languageService: ILanguageService, - @IChatService private readonly _chatService: IChatService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - ) { - - } - - dispose() { - this._store.dispose(); - this._sessions.forEach(x => x.store.dispose()); - this._sessions.clear(); - } - - async createSession(editor: IActiveCodeEditor, options: { headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise { - - const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline); - - if (!agent) { - this._logService.trace('[IE] NO agent found'); - return undefined; - } - - this._onWillStartSession.fire(editor); - - const textModel = editor.getModel(); - const selection = editor.getSelection(); - - const store = new DisposableStore(); - this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); - - const chatModelRef = options.session ? undefined : this._chatService.startSession(ChatAgentLocation.EditorInline); - const chatModel = options.session?.chatModel ?? chatModelRef?.object; - if (!chatModel) { - this._logService.trace('[IE] NO chatModel found'); - chatModelRef?.dispose(); - return undefined; - } - if (chatModelRef) { - store.add(chatModelRef); - } - - const lastResponseListener = store.add(new MutableDisposable()); - store.add(chatModel.onDidChange(e => { - if (e.kind !== 'addRequest' || !e.request.response) { - return; - } - - const { response } = e.request; - - session.markModelVersion(e.request); - lastResponseListener.value = response.onDidChange(() => { - - if (!response.isComplete) { - return; - } - - lastResponseListener.clear(); // ONCE - - // special handling for untitled files - for (const part of response.response.value) { - if (part.kind !== 'textEditGroup' || part.uri.scheme !== Schemas.untitled || isEqual(part.uri, session.textModelN.uri)) { - continue; - } - const langSelection = this._languageService.createByFilepathOrFirstLine(part.uri, undefined); - const untitledTextModel = this._textFileService.untitled.create({ - associatedResource: part.uri, - languageId: langSelection.languageId - }); - untitledTextModel.resolve(); - this._textModelService.createModelReference(part.uri).then(ref => { - store.add(ref); - }); - } - - }); - })); - - store.add(this._chatAgentService.onDidChangeAgents(e => { - if (e === undefined && (!this._chatAgentService.getAgent(agent.id) || !this._chatAgentService.getActivatedAgents().map(agent => agent.id).includes(agent.id))) { - this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`); - this._releaseSession(session, true); - } - })); - - const id = generateUuid(); - const targetUri = textModel.uri; - - // 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( - createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), - { languageId: textModel.getLanguageId(), onDidChange: Event.None }, - targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModel0': '' }).toString() }), true - )); - - // untitled documents are special and we are releasing their session when their last editor closes - if (targetUri.scheme === Schemas.untitled) { - store.add(this._editorService.onDidCloseEditor(() => { - if (!this._editorService.isOpened({ resource: targetUri, typeId: UntitledTextEditorInput.ID, editorId: DEFAULT_EDITOR_ASSOCIATION.id })) { - this._releaseSession(session, true); - } - })); - } - - let wholeRange = options.wholeRange; - if (!wholeRange) { - wholeRange = new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn); - } - - if (token.isCancellationRequested) { - store.dispose(); - return undefined; - } - - const session = new Session( - options.headless ?? false, - targetUri, - textModel0, - textModelN, - agent, - store.add(new SessionWholeRange(textModelN, wholeRange)), - store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), - chatModel, - options.session?.versionsByRequest, - ); - - // store: key -> session - const key = this._key(editor, session.targetUri); - if (this._sessions.has(key)) { - store.dispose(); - throw new Error(`Session already stored for ${key}`); - } - this._sessions.set(key, { session, editor, store }); - return session; - } - - moveSession(session: Session, target: ICodeEditor): void { - const newKey = this._key(target, session.targetUri); - const existing = this._sessions.get(newKey); - if (existing) { - if (existing.session !== session) { - throw new Error(`Cannot move session because the target editor already/still has one`); - } else { - // noop - return; - } - } - - let found = false; - for (const [oldKey, data] of this._sessions) { - if (data.session === session) { - found = true; - this._sessions.delete(oldKey); - this._sessions.set(newKey, { ...data, editor: target }); - this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`); - this._onDidMoveSession.fire({ session, editor: target }); - break; - } - } - if (!found) { - throw new Error(`Cannot move session because it is not stored`); - } - } - - releaseSession(session: Session): void { - this._releaseSession(session, false); - } - - private _releaseSession(session: Session, byServer: boolean): void { - - let tuple: [string, SessionData] | undefined; - - // cleanup - for (const candidate of this._sessions) { - if (candidate[1].session === session) { - // if (value.session === session) { - tuple = candidate; - break; - } - } - - if (!tuple) { - // double remove - return; - } - - this._telemetryService.publicLog2('interactiveEditor/session', session.asTelemetryData()); - - const [key, value] = tuple; - this._sessions.delete(key); - this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`); - - this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer }); - value.store.dispose(); - } - - stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession { - const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits); - this._onDidStashSession.fire({ editor, session }); - this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`); - return result; - } - - getCodeEditor(session: Session): ICodeEditor { - for (const [, data] of this._sessions) { - if (data.session === session) { - return data.editor; - } - } - throw new Error('session not found'); - } - - getSession(editor: ICodeEditor, uri: URI): Session | undefined { - const key = this._key(editor, uri); - return this._sessions.get(key)?.session; - } - - private _key(editor: ICodeEditor, uri: URI): string { - const item = this._keyComputers.get(uri.scheme); - return item - ? item.getComparisonKey(editor, uri) - : `${editor.getId()}@${uri.toString()}`; - - } - - registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable { - this._keyComputers.set(scheme, value); - return toDisposable(() => this._keyComputers.delete(scheme)); - } - - // ---- NEW - - private readonly _sessions2 = new ResourceMap(); - private readonly _onDidChangeSessions = this._store.add(new Emitter()); readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + constructor( + @IChatService private readonly _chatService: IChatService + ) { } - async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise { + dispose() { + this._store.dispose(); + } - assertType(editor.hasModel()); - if (this._sessions2.has(uri)) { + createSession(editor: IActiveCodeEditor): IInlineChatSession2 { + const uri = editor.getModel().uri; + + if (this._sessions.has(uri)) { throw new Error('Session already exists'); } - this._onWillStartSession.fire(editor as IActiveCodeEditor); + this._onWillStartSession.fire(editor); const chatModelRef = this._chatService.startSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); const chatModel = chatModelRef.object; chatModel.startEditingSession(false); - const widget = this._chatWidgetService.getWidgetBySessionResource(chatModel.sessionResource); - await widget?.attachmentModel.addFile(uri); - const store = new DisposableStore(); store.add(toDisposable(() => { this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource); chatModel.editingSession?.reject(); - this._sessions2.delete(uri); + this._sessions.delete(uri); this._onDidChangeSessions.fire(this); })); store.add(chatModelRef); @@ -405,16 +126,16 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { editingSession: chatModel.editingSession!, dispose: store.dispose.bind(store) }; - this._sessions2.set(uri, result); + this._sessions.set(uri, result); this._onDidChangeSessions.fire(this); return result; } - getSession2(uri: URI): IInlineChatSession2 | undefined { - let result = this._sessions2.get(uri); + getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined { + let result = this._sessions.get(uri); if (!result) { // no direct session, try to find an editing session which has a file entry for the uri - for (const [_, candidate] of this._sessions2) { + for (const [_, candidate] of this._sessions) { const entry = candidate.editingSession.getEntry(uri); if (entry) { result = candidate; @@ -426,7 +147,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined { - for (const session of this._sessions2.values()) { + for (const session of this._sessions.values()) { if (isEqual(session.chatModel.sessionResource, sessionResource)) { return session; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts deleted file mode 100644 index e273a6f436b..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ /dev/null @@ -1,591 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { WindowIntervalTimer } from '../../../../base/browser/dom.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { themeColorFromId, ThemeIcon } from '../../../../base/common/themables.js'; -import { ICodeEditor, IViewZone, IViewZoneChangeAccessor } from '../../../../editor/browser/editorBrowser.js'; -import { StableEditorScrollState } from '../../../../editor/browser/stableEditorScroll.js'; -import { LineSource, RenderOptions, renderLines } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; -import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { Progress } from '../../../../platform/progress/common/progress.js'; -import { SaveReason } from '../../../common/editor.js'; -import { countWords } from '../../chat/common/model/chatWordCounter.js'; -import { HunkInformation, Session, HunkState } from './inlineChatSession.js'; -import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from '../common/inlineChat.js'; -import { assertType } from '../../../../base/common/types.js'; -import { performAsyncTextEdit, asProgressiveEdit } from './utils.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { IUntitledTextEditorModel } from '../../../services/untitled/common/untitledTextEditorModel.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { DefaultChatTextEditor } from '../../chat/browser/widget/chatContentParts/codeBlockPart.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { ConflictActionsFactory, IContentWidgetAction } from '../../mergeEditor/browser/view/conflictActions.js'; -import { observableValue } from '../../../../base/common/observable.js'; -import { IMenuService, MenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel/inlineDecorations.js'; -import { EditSources } from '../../../../editor/common/textModelEditSource.js'; -import { VersionedExtensionId } from '../../../../editor/common/languages.js'; - -export interface IEditObserver { - start(): void; - stop(): void; -} - -export const enum HunkAction { - Accept, - Discard, - MoveNext, - MovePrev, - ToggleDiff -} - -export class LiveStrategy { - - private readonly _decoInsertedText = ModelDecorationOptions.register({ - description: 'inline-modified-line', - className: 'inline-chat-inserted-range-linehighlight', - isWholeLine: true, - overviewRuler: { - position: OverviewRulerLane.Full, - color: themeColorFromId(overviewRulerInlineChatDiffInserted), - }, - minimap: { - position: MinimapPosition.Inline, - color: themeColorFromId(minimapInlineChatDiffInserted), - } - }); - - private readonly _decoInsertedTextRange = ModelDecorationOptions.register({ - description: 'inline-chat-inserted-range-linehighlight', - className: 'inline-chat-inserted-range', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - }); - - protected readonly _store = new DisposableStore(); - protected readonly _onDidAccept = this._store.add(new Emitter()); - protected readonly _onDidDiscard = this._store.add(new Emitter()); - private readonly _ctxCurrentChangeHasDiff: IContextKey; - private readonly _ctxCurrentChangeShowsDiff: IContextKey; - private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; - private readonly _lensActionsFactory: ConflictActionsFactory; - private _editCount: number = 0; - private readonly _hunkData = new Map(); - - readonly onDidAccept: Event = this._onDidAccept.event; - readonly onDidDiscard: Event = this._onDidDiscard.event; - - constructor( - protected readonly _session: Session, - protected readonly _editor: ICodeEditor, - protected readonly _zone: InlineChatZoneWidget, - private readonly _showOverlayToolbar: boolean, - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, - // @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - // @IConfigurationService private readonly _configService: IConfigurationService, - @IMenuService private readonly _menuService: IMenuService, - @IContextKeyService private readonly _contextService: IContextKeyService, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInstantiationService protected readonly _instaService: IInstantiationService - ) { - 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._lensActionsFactory = this._store.add(new ConflictActionsFactory(this._editor)); - } - - dispose(): void { - this._resetDiff(); - this._store.dispose(); - } - - private _resetDiff(): void { - this._ctxCurrentChangeHasDiff.reset(); - this._ctxCurrentChangeShowsDiff.reset(); - this._zone.widget.updateStatus(''); - this._progressiveEditingDecorations.clear(); - - - for (const data of this._hunkData.values()) { - data.remove(); - } - } - - async apply() { - this._resetDiff(); - if (this._editCount > 0) { - this._editor.pushUndoStop(); - } - await this._doApplyChanges(true); - } - - cancel() { - this._resetDiff(); - return this._session.hunkData.discardAll(); - } - - async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise { - return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore, metadata); - } - - async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise { - - // add decorations once per line that got edited - const progress = new Progress(edits => { - - const newLines = new Set(); - for (const edit of edits) { - LineRange.fromRange(edit.range).forEach(line => newLines.add(line)); - } - const existingRanges = this._progressiveEditingDecorations.getRanges().map(LineRange.fromRange); - for (const existingRange of existingRanges) { - existingRange.forEach(line => newLines.delete(line)); - } - const newDecorations: IModelDeltaDecoration[] = []; - for (const line of newLines) { - newDecorations.push({ range: new Range(line, 1, line, Number.MAX_VALUE), options: this._decoInsertedText }); - } - - this._progressiveEditingDecorations.append(newDecorations); - }); - return this._makeChanges(edits, obs, opts, progress, undoStopBefore, metadata); - } - - private async _makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions | undefined, progress: Progress | undefined, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise { - - // push undo stop before first edit - if (undoStopBefore) { - this._editor.pushUndoStop(); - } - - this._editCount++; - const editSource = EditSources.inlineChatApplyEdit({ - modelId: metadata.modelId, - extensionId: metadata.extensionId, - requestId: metadata.requestId, - sessionId: undefined, - languageId: this._session.textModelN.getLanguageId(), - }); - - if (opts) { - // ASYNC - const durationInSec = opts.duration / 1000; - for (const edit of edits) { - const wordCount = countWords(edit.text ?? ''); - const speed = wordCount / durationInSec; - // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); - const asyncEdit = asProgressiveEdit(new WindowIntervalTimer(this._zone.domNode), edit, speed, opts.token); - await performAsyncTextEdit(this._session.textModelN, asyncEdit, progress, obs, editSource); - } - - } else { - // SYNC - obs.start(); - this._session.textModelN.pushEditOperations(null, edits, (undoEdits) => { - progress?.report(undoEdits); - return null; - }, undefined, editSource); - obs.stop(); - } - } - - performHunkAction(hunk: HunkInformation | undefined, action: HunkAction) { - const displayData = this._findDisplayData(hunk); - - if (!displayData) { - // no hunks (left or not yet) found, make sure to - // finish the sessions - if (action === HunkAction.Accept) { - this._onDidAccept.fire(); - } else if (action === HunkAction.Discard) { - this._onDidDiscard.fire(); - } - return; - } - - if (action === HunkAction.Accept) { - displayData.acceptHunk(); - } else if (action === HunkAction.Discard) { - displayData.discardHunk(); - } else if (action === HunkAction.MoveNext) { - displayData.move(true); - } else if (action === HunkAction.MovePrev) { - displayData.move(false); - } else if (action === HunkAction.ToggleDiff) { - displayData.toggleDiff?.(); - } - } - - private _findDisplayData(hunkInfo?: HunkInformation) { - let result: HunkDisplayData | undefined; - if (hunkInfo) { - // use context hunk (from tool/buttonbar) - result = this._hunkData.get(hunkInfo); - } - - if (!result && this._zone.position) { - // find nearest from zone position - const zoneLine = this._zone.position.lineNumber; - let distance: number = Number.MAX_SAFE_INTEGER; - for (const candidate of this._hunkData.values()) { - if (candidate.hunk.getState() !== HunkState.Pending) { - continue; - } - const hunkRanges = candidate.hunk.getRangesN(); - if (hunkRanges.length === 0) { - // bogous hunk - continue; - } - const myDistance = zoneLine <= hunkRanges[0].startLineNumber - ? hunkRanges[0].startLineNumber - zoneLine - : zoneLine - hunkRanges[0].endLineNumber; - - if (myDistance < distance) { - distance = myDistance; - result = candidate; - } - } - } - - if (!result) { - // fallback: first hunk that is pending - result = Iterable.first(Iterable.filter(this._hunkData.values(), candidate => candidate.hunk.getState() === HunkState.Pending)); - } - return result; - } - - async renderChanges() { - - this._progressiveEditingDecorations.clear(); - - const renderHunks = () => { - - let widgetData: HunkDisplayData | undefined; - - changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - - const keysNow = new Set(this._hunkData.keys()); - widgetData = undefined; - - for (const hunkData of this._session.hunkData.getInfo()) { - - keysNow.delete(hunkData); - - const hunkRanges = hunkData.getRangesN(); - let data = this._hunkData.get(hunkData); - if (!data) { - // first time -> create decoration - const decorationIds: string[] = []; - for (let i = 0; i < hunkRanges.length; i++) { - decorationIds.push(decorationsAccessor.addDecoration(hunkRanges[i], i === 0 - ? this._decoInsertedText - : this._decoInsertedTextRange) - ); - } - - const acceptHunk = () => { - hunkData.acceptChanges(); - renderHunks(); - }; - - const discardHunk = () => { - hunkData.discardChanges(); - renderHunks(); - }; - - // original view zone - const mightContainNonBasicASCII = this._session.textModel0.mightContainNonBasicASCII(); - const mightContainRTL = this._session.textModel0.mightContainRTL(); - const renderOptions = RenderOptions.fromEditor(this._editor); - const originalRange = hunkData.getRanges0()[0]; - const source = new LineSource( - LineRange.fromRangeInclusive(originalRange).mapToLineArray(l => this._session.textModel0.tokenization.getLineTokens(l)), - [], - mightContainNonBasicASCII, - mightContainRTL, - ); - const domNode = document.createElement('div'); - domNode.className = 'inline-chat-original-zone2'; - const result = renderLines(source, renderOptions, [new InlineDecoration(new Range(originalRange.startLineNumber, 1, originalRange.startLineNumber, 1), '', InlineDecorationType.Regular)], domNode); - const viewZoneData: IViewZone = { - afterLineNumber: -1, - heightInLines: result.heightInLines, - domNode, - ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 - }; - - const toggleDiff = () => { - const scrollState = StableEditorScrollState.capture(this._editor); - changeDecorationsAndViewZones(this._editor, (_decorationsAccessor, viewZoneAccessor) => { - assertType(data); - if (!data.diffViewZoneId) { - const [hunkRange] = hunkData.getRangesN(); - viewZoneData.afterLineNumber = hunkRange.startLineNumber - 1; - data.diffViewZoneId = viewZoneAccessor.addZone(viewZoneData); - } else { - viewZoneAccessor.removeZone(data.diffViewZoneId!); - data.diffViewZoneId = undefined; - } - }); - this._ctxCurrentChangeShowsDiff.set(typeof data?.diffViewZoneId === 'string'); - scrollState.restore(this._editor); - }; - - - let lensActions: DisposableStore | undefined; - const lensActionsViewZoneIds: string[] = []; - - if (this._showOverlayToolbar && hunkData.getState() === HunkState.Pending) { - - lensActions = new DisposableStore(); - - const menu = this._menuService.createMenu(MENU_INLINE_CHAT_ZONE, this._contextService); - const makeActions = () => { - const actions: IContentWidgetAction[] = []; - const tuples = menu.getActions({ arg: hunkData }); - for (const [, group] of tuples) { - for (const item of group) { - if (item instanceof MenuItemAction) { - - let text = item.label; - - if (item.id === ACTION_TOGGLE_DIFF) { - text = item.checked ? 'Hide Changes' : 'Show Changes'; - } else if (ThemeIcon.isThemeIcon(item.item.icon)) { - text = `$(${item.item.icon.id}) ${text}`; - } - - actions.push({ - text, - tooltip: item.tooltip, - action: async () => item.run(), - }); - } - } - } - return actions; - }; - - const obs = observableValue(this, makeActions()); - lensActions.add(menu.onDidChange(() => obs.set(makeActions(), undefined))); - lensActions.add(menu); - - lensActions.add(this._lensActionsFactory.createWidget(viewZoneAccessor, - hunkRanges[0].startLineNumber - 1, - obs, - lensActionsViewZoneIds - )); - } - - const remove = () => { - changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - assertType(data); - for (const decorationId of data.decorationIds) { - decorationsAccessor.removeDecoration(decorationId); - } - if (data.diffViewZoneId) { - viewZoneAccessor.removeZone(data.diffViewZoneId!); - } - data.decorationIds = []; - data.diffViewZoneId = undefined; - - data.lensActionsViewZoneIds?.forEach(viewZoneAccessor.removeZone); - data.lensActionsViewZoneIds = undefined; - }); - - lensActions?.dispose(); - }; - - const move = (next: boolean) => { - const keys = Array.from(this._hunkData.keys()); - const idx = keys.indexOf(hunkData); - const nextIdx = (idx + (next ? 1 : -1) + keys.length) % keys.length; - if (nextIdx !== idx) { - const nextData = this._hunkData.get(keys[nextIdx])!; - this._zone.updatePositionAndHeight(nextData?.position); - renderHunks(); - } - }; - - const zoneLineNumber = this._zone.position?.lineNumber ?? this._editor.getPosition()!.lineNumber; - const myDistance = zoneLineNumber <= hunkRanges[0].startLineNumber - ? hunkRanges[0].startLineNumber - zoneLineNumber - : zoneLineNumber - hunkRanges[0].endLineNumber; - - data = { - hunk: hunkData, - decorationIds, - diffViewZoneId: '', - diffViewZone: viewZoneData, - lensActionsViewZoneIds, - distance: myDistance, - position: hunkRanges[0].getStartPosition().delta(-1), - acceptHunk, - discardHunk, - toggleDiff: !hunkData.isInsertion() ? toggleDiff : undefined, - remove, - move, - }; - - this._hunkData.set(hunkData, data); - - } else if (hunkData.getState() !== HunkState.Pending) { - data.remove(); - - } else { - // update distance and position based on modifiedRange-decoration - const zoneLineNumber = this._zone.position?.lineNumber ?? this._editor.getPosition()!.lineNumber; - const modifiedRangeNow = hunkRanges[0]; - data.position = modifiedRangeNow.getStartPosition().delta(-1); - data.distance = zoneLineNumber <= modifiedRangeNow.startLineNumber - ? modifiedRangeNow.startLineNumber - zoneLineNumber - : zoneLineNumber - modifiedRangeNow.endLineNumber; - } - - if (hunkData.getState() === HunkState.Pending && (!widgetData || data.distance < widgetData.distance)) { - widgetData = data; - } - } - - for (const key of keysNow) { - const data = this._hunkData.get(key); - if (data) { - this._hunkData.delete(key); - data.remove(); - } - } - }); - - if (widgetData) { - this._zone.reveal(widgetData.position); - - // const mode = this._configService.getValue<'on' | 'off' | 'auto'>(InlineChatConfigKeys.AccessibleDiffView); - // if (mode === 'on' || mode === 'auto' && this._accessibilityService.isScreenReaderOptimized()) { - // this._zone.widget.showAccessibleHunk(this._session, widgetData.hunk); - // } - - this._ctxCurrentChangeHasDiff.set(Boolean(widgetData.toggleDiff)); - - } else if (this._hunkData.size > 0) { - // everything accepted or rejected - let oneAccepted = false; - for (const hunkData of this._session.hunkData.getInfo()) { - if (hunkData.getState() === HunkState.Accepted) { - oneAccepted = true; - break; - } - } - if (oneAccepted) { - this._onDidAccept.fire(); - } else { - this._onDidDiscard.fire(); - } - } - - return widgetData; - }; - - return renderHunks()?.position; - } - - getWholeRangeDecoration(): IModelDeltaDecoration[] { - // don't render the blue in live mode - return []; - } - - private async _doApplyChanges(ignoreLocal: boolean): Promise { - - const untitledModels: IUntitledTextEditorModel[] = []; - - const editor = this._instaService.createInstance(DefaultChatTextEditor); - - - for (const request of this._session.chatModel.getRequests()) { - - if (!request.response?.response) { - continue; - } - - for (const item of request.response.response.value) { - if (item.kind !== 'textEditGroup') { - continue; - } - if (ignoreLocal && isEqual(item.uri, this._session.textModelN.uri)) { - continue; - } - - await editor.apply(request.response, item, undefined); - - if (item.uri.scheme === Schemas.untitled) { - const untitled = this._textFileService.untitled.get(item.uri); - if (untitled) { - untitledModels.push(untitled); - } - } - } - } - - for (const untitledModel of untitledModels) { - if (!untitledModel.isDisposed()) { - await untitledModel.resolve(); - await untitledModel.save({ reason: SaveReason.EXPLICIT }); - } - } - } -} - -export interface ProgressingEditsOptions { - duration: number; - token: CancellationToken; -} - -type HunkDisplayData = { - - decorationIds: string[]; - - diffViewZoneId: string | undefined; - diffViewZone: IViewZone; - - lensActionsViewZoneIds?: string[]; - - distance: number; - position: Position; - acceptHunk: () => void; - discardHunk: () => void; - toggleDiff?: () => any; - remove(): void; - move: (next: boolean) => void; - - hunk: HunkInformation; -}; - -function changeDecorationsAndViewZones(editor: ICodeEditor, callback: (accessor: IModelDecorationsChangeAccessor, viewZoneAccessor: IViewZoneChangeAccessor) => void): void { - editor.changeDecorations(decorationsAccessor => { - editor.changeViewZones(viewZoneAccessor => { - callback(decorationsAccessor, viewZoneAccessor); - }); - }); -} - -export interface IInlineChatMetadata { - modelId: string | undefined; - extensionId: VersionedExtensionId | undefined; - requestId: string | undefined; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 051fe25b923..c866191d832 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -10,18 +10,12 @@ import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/icon import { IAction } from '../../../../base/common/actions.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from '../../../../editor/browser/widget/diffEditor/components/accessibleDiffViewer.js'; -import { EditorOption, IComputedEditorOptions } from '../../../../editor/common/config/editorOptions.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; import { Selection } from '../../../../editor/common/core/selection.js'; -import { DetailedLineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; -import { ICodeEditorViewState, ScrollType } from '../../../../editor/common/editorCommon.js'; +import { ICodeEditorViewState } from '../../../../editor/common/editorCommon.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../nls.js'; @@ -56,7 +50,6 @@ import { ChatMode } from '../../chat/common/chatModes.js'; import { ChatAgentVoteDirection, IChatService } from '../../chat/common/chatService/chatService.js'; import { isResponseVM } from '../../chat/common/model/chatViewModel.js'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground, inlineChatForeground } from '../common/inlineChat.js'; -import { HunkInformation, Session } from './inlineChatSession.js'; import './media/inlineChat.css'; export interface InlineChatWidgetViewState { @@ -532,12 +525,9 @@ const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); export class EditorBasedInlineChatWidget extends InlineChatWidget { - private readonly _accessibleViewer = this._store.add(new MutableDisposable()); - - constructor( location: IChatWidgetLocationOptions, - private readonly _parentEditor: ICodeEditor, + parentEditor: ICodeEditor, options: IInlineChatWidgetConstructionOptions, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @@ -552,7 +542,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IChatEntitlementService chatEntitlementService: IChatEntitlementService, @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, ) { - const overflowWidgetsNode = layoutService.getContainer(getWindow(_parentEditor.getContainerDomNode())).appendChild($('.inline-chat-overflow.monaco-editor')); + const overflowWidgetsNode = layoutService.getContainer(getWindow(parentEditor.getContainerDomNode())).appendChild($('.inline-chat-overflow.monaco-editor')); super(location, { ...options, chatWidgetViewOptions: { @@ -568,24 +558,10 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { // --- layout - override get contentHeight(): number { - let result = super.contentHeight; - - if (this._accessibleViewer.value) { - result += this._accessibleViewer.value.height + 8 /* padding */; - } - - return result; - } protected override _doLayout(dimension: Dimension): void { - let newHeight = dimension.height; - - if (this._accessibleViewer.value) { - this._accessibleViewer.value.width = dimension.width - 12; - newHeight -= this._accessibleViewer.value.height + 8; - } + const newHeight = dimension.height; super._doLayout(dimension.with(undefined, newHeight)); @@ -594,110 +570,8 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { } override reset() { - this._accessibleViewer.clear(); this.chatWidget.setInput(); super.reset(); } - // --- accessible viewer - - showAccessibleHunk(session: Session, hunkData: HunkInformation): void { - - this._elements.accessibleViewer.classList.remove('hidden'); - this._accessibleViewer.clear(); - - this._accessibleViewer.value = this._instantiationService.createInstance(HunkAccessibleDiffViewer, - this._elements.accessibleViewer, - session, - hunkData, - new AccessibleHunk(this._parentEditor, session, hunkData) - ); - - this._onDidChangeHeight.fire(); - } -} - -class HunkAccessibleDiffViewer extends AccessibleDiffViewer { - - readonly height: number; - - set width(value: number) { - this._width2.set(value, undefined); - } - - private readonly _width2: ISettableObservable; - - constructor( - parentNode: HTMLElement, - session: Session, - hunk: HunkInformation, - models: IAccessibleDiffViewerModel, - @IInstantiationService instantiationService: IInstantiationService, - ) { - const width = observableValue('width', 0); - const diff = observableValue('diff', HunkAccessibleDiffViewer._asMapping(hunk)); - const diffs = derived(r => [diff.read(r)]); - const lines = Math.min(10, 8 + diff.get().changedLineCount); - const height = models.getModifiedOptions().get(EditorOption.lineHeight) * lines; - - super(parentNode, constObservable(true), () => { }, constObservable(false), width, constObservable(height), diffs, models, instantiationService); - - this.height = height; - this._width2 = width; - - this._store.add(session.textModelN.onDidChangeContent(() => { - diff.set(HunkAccessibleDiffViewer._asMapping(hunk), undefined); - })); - } - - private static _asMapping(hunk: HunkInformation): DetailedLineRangeMapping { - const ranges0 = hunk.getRanges0(); - const rangesN = hunk.getRangesN(); - const originalLineRange = LineRange.fromRangeInclusive(ranges0[0]); - const modifiedLineRange = LineRange.fromRangeInclusive(rangesN[0]); - const innerChanges: RangeMapping[] = []; - for (let i = 1; i < ranges0.length; i++) { - innerChanges.push(new RangeMapping(ranges0[i], rangesN[i])); - } - return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, innerChanges); - } - -} - -class AccessibleHunk implements IAccessibleDiffViewerModel { - - constructor( - private readonly _editor: ICodeEditor, - private readonly _session: Session, - private readonly _hunk: HunkInformation - ) { } - - getOriginalModel(): ITextModel { - return this._session.textModel0; - } - getModifiedModel(): ITextModel { - return this._session.textModelN; - } - getOriginalOptions(): IComputedEditorOptions { - return this._editor.getOptions(); - } - getModifiedOptions(): IComputedEditorOptions { - return this._editor.getOptions(); - } - originalReveal(range: Range): void { - // throw new Error('Method not implemented.'); - } - modifiedReveal(range?: Range | undefined): void { - this._editor.revealRangeInCenterIfOutsideViewport(range || this._hunk.getRangesN()[0], ScrollType.Smooth); - } - modifiedSetSelection(range: Range): void { - // this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth); - // this._editor.setSelection(range); - } - modifiedFocus(): void { - this._editor.focus(); - } - getModifiedPosition(): Position | undefined { - return this._hunk.getRangesN()[0].getStartPosition(); - } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/utils.ts b/src/vs/workbench/contrib/inlineChat/browser/utils.ts deleted file mode 100644 index 45af959a2ae..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/utils.ts +++ /dev/null @@ -1,95 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { EditOperation } from '../../../../editor/common/core/editOperation.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IIdentifiedSingleEditOperation, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { IEditObserver } from './inlineChatStrategies.js'; -import { IProgress } from '../../../../platform/progress/common/progress.js'; -import { IntervalTimer, AsyncIterableSource } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { getNWords } from '../../chat/common/model/chatWordCounter.js'; -import { TextModelEditSource } from '../../../../editor/common/textModelEditSource.js'; - - - -// --- async edit - -export interface AsyncTextEdit { - readonly range: IRange; - readonly newText: AsyncIterable; -} - -export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdit, progress?: IProgress, obs?: IEditObserver, editSource?: TextModelEditSource) { - - const [id] = model.deltaDecorations([], [{ - range: edit.range, - options: { - description: 'asyncTextEdit', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - } - }]); - - let first = true; - for await (const part of edit.newText) { - - if (model.isDisposed()) { - break; - } - - const range = model.getDecorationRange(id); - if (!range) { - throw new Error('FAILED to perform async replace edit because the anchor decoration was removed'); - } - - const edit = first - ? EditOperation.replace(range, part) // first edit needs to override the "anchor" - : EditOperation.insert(range.getEndPosition(), part); - obs?.start(); - - model.pushEditOperations(null, [edit], (undoEdits) => { - progress?.report(undoEdits); - return null; - }, undefined, editSource); - - obs?.stop(); - first = false; - } -} - -export function asProgressiveEdit(interval: IntervalTimer, edit: IIdentifiedSingleEditOperation, wordsPerSec: number, token: CancellationToken): AsyncTextEdit { - - wordsPerSec = Math.max(30, wordsPerSec); - - const stream = new AsyncIterableSource(); - let newText = edit.text ?? ''; - - interval.cancelAndSet(() => { - if (token.isCancellationRequested) { - return; - } - const r = getNWords(newText, 1); - stream.emitOne(r.value); - newText = newText.substring(r.value.length); - if (r.isFullString) { - interval.cancel(); - stream.resolve(); - d.dispose(); - } - - }, 1000 / wordsPerSec); - - // cancel ASAP - const d = token.onCancellationRequested(() => { - interval.cancel(); - stream.resolve(); - d.dispose(); - }); - - return { - range: edit.range, - newText: stream.asyncIterable - }; -} diff --git a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts index 914993f57ae..0a9c91d1859 100644 --- a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts @@ -8,7 +8,7 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { InlineChatController } from '../browser/inlineChatController.js'; -import { AbstractInline1ChatAction, setHoldForSpeech } from '../browser/inlineChatActions.js'; +import { AbstractInlineChatAction, setHoldForSpeech } from '../browser/inlineChatActions.js'; import { disposableTimeout } from '../../../../base/common/async.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -27,7 +27,7 @@ export class HoldToSpeak extends EditorAction2 { constructor() { super({ id: 'inlineChat.holdForSpeech', - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and(HasSpeechProvider, CTX_INLINE_CHAT_VISIBLE), title: localize2('holdForSpeech', "Hold for Speech"), keybinding: { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.1.snap b/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.1.snap deleted file mode 100644 index a0379e041b9..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.1.snap +++ /dev/null @@ -1,13 +0,0 @@ -export function fib(n) { - if (n <= 0) return 0; - if (n === 1) return 0; - if (n === 2) return 1; - - let a = 0, b = 1, c; - for (let i = 3; i <= n; i++) { - c = a + b; - a = b; - b = c; - } - return b; -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.2.snap b/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.2.snap deleted file mode 100644 index 3d44a421300..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.2.snap +++ /dev/null @@ -1,6 +0,0 @@ -export function fib(n) { - if (n <= 0) return 0; - if (n === 1) return 0; - if (n === 2) return 1; - return fib(n - 1) + fib(n - 2); -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts deleted file mode 100644 index e404b130280..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ /dev/null @@ -1,1101 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { equals } from '../../../../../base/common/arrays.js'; -import { DeferredPromise, raceCancellation, timeout } from '../../../../../base/common/async.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { constObservable, IObservable } from '../../../../../base/common/observable.js'; -import { assertType } from '../../../../../base/common/types.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { mock } from '../../../../../base/test/common/mock.js'; -import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; -import { IActiveCodeEditor, ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { IDiffProviderFactoryService } from '../../../../../editor/browser/widget/diffEditor/diffProviderFactoryService.js'; -import { EditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { EndOfLineSequence, ITextModel } from '../../../../../editor/common/model.js'; -import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js'; -import { TestCommandService } from '../../../../../editor/test/browser/editorTestServices.js'; -import { instantiateTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js'; -import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { IEditorProgressService, IProgressRunner } from '../../../../../platform/progress/common/progress.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IView, IViewDescriptorService } from '../../../../common/views.js'; -import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; -import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { TestChatEntitlementService, TestContextService, TestExtensionService } from '../../../../test/common/workbenchTestServices.js'; -import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from '../../../chat/browser/chat.js'; -import { ChatContextService, IChatContextService } from '../../../chat/browser/contextContrib/chatContextService.js'; -import { ChatInputBoxContentProvider } from '../../../chat/browser/widget/input/editor/chatEditorInputContentProvider.js'; -import { ChatLayoutService } from '../../../chat/browser/widget/chatLayoutService.js'; -import { ChatVariablesService } from '../../../chat/browser/attachments/chatVariables.js'; -import { ChatWidget } from '../../../chat/browser/widget/chatWidget.js'; -import { ChatWidgetService } from '../../../chat/browser/widget/chatWidgetService.js'; -import { ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/participants/chatAgents.js'; -import { IChatEditingService, IChatEditingSession } from '../../../chat/common/editing/chatEditingService.js'; -import { IChatLayoutService } from '../../../chat/common/widget/chatLayoutService.js'; -import { IChatModeService } from '../../../chat/common/chatModes.js'; -import { IChatProgress, IChatService } from '../../../chat/common/chatService/chatService.js'; -import { ChatService } from '../../../chat/common/chatService/chatServiceImpl.js'; -import { ChatSlashCommandService, IChatSlashCommandService } from '../../../chat/common/participants/chatSlashCommands.js'; -import { IChatTodo, IChatTodoListService } from '../../../chat/common/tools/chatTodoListService.js'; -import { ChatTransferService, IChatTransferService } from '../../../chat/common/model/chatTransferService.js'; -import { IChatVariablesService } from '../../../chat/common/attachments/chatVariables.js'; -import { IChatResponseViewModel } from '../../../chat/common/model/chatViewModel.js'; -import { ChatWidgetHistoryService, IChatWidgetHistoryService } from '../../../chat/common/widget/chatWidgetHistoryService.js'; -import { ChatAgentLocation, ChatModeKind } from '../../../chat/common/constants.js'; -import { ILanguageModelsService, LanguageModelsService } from '../../../chat/common/languageModels.js'; -import { ILanguageModelToolsService } from '../../../chat/common/tools/languageModelToolsService.js'; -import { PromptsType } from '../../../chat/common/promptSyntax/promptTypes.js'; -import { IPromptPath, IPromptsService } from '../../../chat/common/promptSyntax/service/promptsService.js'; -import { MockChatModeService } from '../../../chat/test/common/mockChatModeService.js'; -import { MockLanguageModelToolsService } from '../../../chat/test/common/tools/mockLanguageModelToolsService.js'; -import { IMcpService } from '../../../mcp/common/mcpTypes.js'; -import { TestMcpService } from '../../../mcp/test/common/testMcpService.js'; -import { INotebookEditorService } from '../../../notebook/browser/services/notebookEditorService.js'; -import { RerunAction } from '../../browser/inlineChatActions.js'; -import { InlineChatController1, State } from '../../browser/inlineChatController.js'; -import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; -import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; -import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; -import { TestWorkerService } from './testWorkerService.js'; -import { MockChatSessionsService } from '../../../chat/test/common/mockChatSessionsService.js'; -import { IChatSessionsService } from '../../../chat/common/chatSessionsService.js'; -import { IAgentSessionsService } from '../../../chat/browser/agentSessions/agentSessionsService.js'; -import { IAgentSessionsModel } from '../../../chat/browser/agentSessions/agentSessionsModel.js'; - -suite('InlineChatController', function () { - - const agentData = { - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - // id: 'testEditorAgent', - name: 'testEditorAgent', - isDefault: true, - locations: [ChatAgentLocation.EditorInline], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - }; - - class TestController extends InlineChatController1 { - - static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT]; - static INIT_SEQUENCE_AUTO_SEND: readonly State[] = [...this.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]; - - - readonly onDidChangeState: Event = this._onDidEnterState.event; - - readonly states: readonly State[] = []; - - awaitStates(states: readonly State[]): Promise { - const actual: State[] = []; - - return new Promise((resolve, reject) => { - const d = this.onDidChangeState(state => { - actual.push(state); - if (equals(states, actual)) { - d.dispose(); - resolve(undefined); - } - }); - - setTimeout(() => { - d.dispose(); - resolve(`[${states.join(',')}] <> [${actual.join(',')}]`); - }, 1000); - }); - } - } - - const store = new DisposableStore(); - let configurationService: TestConfigurationService; - let editor: IActiveCodeEditor; - let model: ITextModel; - let ctrl: TestController; - let contextKeyService: MockContextKeyService; - let chatService: IChatService; - let chatAgentService: IChatAgentService; - let inlineChatSessionService: IInlineChatSessionService; - let instaService: TestInstantiationService; - - let chatWidget: IChatWidget; - - setup(function () { - - const serviceCollection = new ServiceCollection( - [IConfigurationService, new TestConfigurationService()], - [IChatVariablesService, new SyncDescriptor(ChatVariablesService)], - [ILogService, new NullLogService()], - [ITelemetryService, NullTelemetryService], - [IHoverService, NullHoverService], - [IExtensionService, new TestExtensionService()], - [IContextKeyService, new MockContextKeyService()], - [IViewsService, new class extends TestViewsService { - override async openView(id: string, focus?: boolean | undefined): Promise { - // eslint-disable-next-line local/code-no-any-casts - return { widget: chatWidget ?? null } as any; - } - }()], - [IWorkspaceContextService, new TestContextService()], - [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], - [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], - [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], - [IChatTransferService, new SyncDescriptor(ChatTransferService)], - [IChatService, new SyncDescriptor(ChatService)], - [IMcpService, new TestMcpService()], - [IChatAgentNameService, new class extends mock() { - override getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { - return false; - } - }], - [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], - [IContextKeyService, contextKeyService], - [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], - [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], - [ICommandService, new SyncDescriptor(TestCommandService)], - [IChatEditingService, new class extends mock() { - override editingSessionsObs: IObservable = constObservable([]); - }], - [IEditorProgressService, new class extends mock() { - override show(total: unknown, delay?: unknown): IProgressRunner { - return { - total() { }, - worked(value) { }, - done() { }, - }; - } - }], - [IChatAccessibilityService, new class extends mock() { - override acceptResponse(widget: ChatWidget, container: HTMLElement, response: IChatResponseViewModel | undefined, requestId: URI | undefined): void { } - override acceptRequest(): URI | undefined { return undefined; } - override acceptElicitation(): void { } - }], - [IAccessibleViewService, new class extends mock() { - override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { - return null; - } - }], - [IConfigurationService, configurationService], - [IViewDescriptorService, new class extends mock() { - override onDidChangeLocation = Event.None; - }], - [INotebookEditorService, new class extends mock() { - override listNotebookEditors() { return []; } - override getNotebookForPossibleCell(editor: ICodeEditor) { - return undefined; - } - }], - [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()], - [ILanguageModelsService, new SyncDescriptor(LanguageModelsService)], - [ITextModelService, new SyncDescriptor(TextModelResolverService)], - [ILanguageModelToolsService, new SyncDescriptor(MockLanguageModelToolsService)], - [IPromptsService, new class extends mock() { - override async listPromptFiles(type: PromptsType, token: CancellationToken): Promise { - return []; - } - }], - [IChatEntitlementService, new class extends mock() { }], - [IChatModeService, new SyncDescriptor(MockChatModeService)], - [IChatLayoutService, new SyncDescriptor(ChatLayoutService)], - [IQuickChatService, new class extends mock() { }], - [IChatTodoListService, new class extends mock() { - override onDidUpdateTodos = Event.None; - override getTodos(sessionResource: URI): IChatTodo[] { return []; } - override setTodos(sessionResource: URI, todos: IChatTodo[]): void { } - }], - [IChatEntitlementService, new SyncDescriptor(TestChatEntitlementService)], - [IChatSessionsService, new SyncDescriptor(MockChatSessionsService)], - [IAgentSessionsService, new class extends mock() { - override get model(): IAgentSessionsModel { - return { - onWillResolve: Event.None, - onDidResolve: Event.None, - onDidChangeSessions: Event.None, - sessions: [], - resolve: async () => { }, - getSession: (resource: URI) => undefined, - } as IAgentSessionsModel; - } - }], - ); - - instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); - - configurationService = instaService.get(IConfigurationService) as TestConfigurationService; - configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); - - configurationService.setUserConfiguration('editor', {}); - - contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; - chatService = instaService.get(IChatService); - chatAgentService = instaService.get(IChatAgentService); - - inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); - - store.add(instaService.get(ILanguageModelsService) as LanguageModelsService); - store.add(instaService.get(IEditorWorkerService) as TestWorkerService); - - store.add(instaService.createInstance(ChatInputBoxContentProvider)); - - model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); - model.setEOL(EndOfLineSequence.LF); - editor = store.add(instantiateTestCodeEditor(instaService, model)); - - instaService.set(IChatContextService, store.add(instaService.createInstance(ChatContextService))); - - store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { - async invoke(request, progress, history, token) { - progress([{ - kind: 'textEdit', - uri: model.uri, - edits: [{ - range: new Range(1, 1, 1, 1), - text: request.message - }] - }]); - return {}; - }, - })); - - }); - - teardown(async function () { - store.clear(); - ctrl?.dispose(); - await chatService.waitForModelDisposals(); - }); - - // TODO@jrieken re-enable, looks like List/ChatWidget is leaking - // ensureNoDisposablesAreLeakedInTestSuite(); - - test('creation, not showing anything', function () { - ctrl = instaService.createInstance(TestController, editor); - assert.ok(ctrl); - assert.strictEqual(ctrl.getWidgetPosition(), undefined); - }); - - test('run (show/hide)', async function () { - ctrl = instaService.createInstance(TestController, editor); - const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); - const run = ctrl.run({ message: 'Hello', autoSend: true }); - assert.strictEqual(await actualStates, undefined); - assert.ok(ctrl.getWidgetPosition() !== undefined); - await ctrl.cancelSession(); - - await run; - - assert.ok(ctrl.getWidgetPosition() === undefined); - }); - - test('wholeRange does not expand to whole lines, editor selection default', async function () { - - editor.setSelection(new Range(1, 1, 1, 3)); - ctrl = instaService.createInstance(TestController, editor); - - ctrl.run({}); - await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3)); - - await ctrl.cancelSession(); - }); - - test('typing outside of wholeRange finishes session', async function () { - - configurationService.setUserConfiguration(InlineChatConfigKeys.FinishOnType, true); - - ctrl = instaService.createInstance(TestController, editor); - const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - - assert.strictEqual(await actualStates, undefined); - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 11 /* line length */)); - - editor.setSelection(new Range(2, 1, 2, 1)); - editor.trigger('test', 'type', { text: 'a' }); - - assert.strictEqual(await ctrl.awaitStates([State.ACCEPT]), undefined); - await r; - }); - - test('\'whole range\' isn\'t updated for edits outside whole range #4346', async function () { - - editor.setSelection(new Range(3, 1, 3, 3)); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ - kind: 'textEdit', - uri: editor.getModel().uri, - edits: [{ - range: new Range(1, 1, 1, 1), // EDIT happens outside of whole range - text: `${request.message}\n${request.message}` - }] - }]); - - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates(TestController.INIT_SEQUENCE); - const r = ctrl.run({ message: 'GENGEN', autoSend: false }); - - assert.strictEqual(await p, undefined); - - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial - - ctrl.chatWidget.setInput('GENGEN'); - ctrl.chatWidget.acceptInput(); - assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]), undefined); - - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3)); - - await ctrl.cancelSession(); - await r; - }); - - test('Stuck inline chat widget #211', async function () { - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - return new Promise(() => { }); - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - - assert.strictEqual(await p, undefined); - - ctrl.acceptSession(); - - await r; - assert.strictEqual(ctrl.getWidgetPosition(), undefined); - }); - - test('[Bug] Inline Chat\'s streaming pushed broken iterations to the undo stack #2403', async function () { - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }]); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }]); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] }]); - - return {}; - }, - })); - - const valueThen = editor.getModel().getValue(); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - assert.strictEqual(await p, undefined); - ctrl.acceptSession(); - await r; - - assert.strictEqual(editor.getModel().getValue(), 'Hello1\nHello2\n'); - - editor.getModel().undo(); - assert.strictEqual(editor.getModel().getValue(), valueThen); - }); - - - - test.skip('UI is streaming edits minutes after the response is finished #3345', async function () { - - - return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => { - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - const text = '${CSI}#a\n${CSI}#b\n${CSI}#c\n'; - - await timeout(10); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text }] }]); - - await timeout(10); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }]); - - throw new Error('Too long'); - }, - })); - - - // let modelChangeCounter = 0; - // store.add(editor.getModel().onDidChangeContent(() => { modelChangeCounter++; })); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - assert.strictEqual(await p, undefined); - - // assert.ok(modelChangeCounter > 0, modelChangeCounter.toString()); // some changes have been made - // const modelChangeCounterNow = modelChangeCounter; - - assert.ok(!editor.getModel().getValue().includes('DONE')); - await timeout(10); - - // assert.strictEqual(modelChangeCounterNow, modelChangeCounter); - assert.ok(!editor.getModel().getValue().includes('DONE')); - - await ctrl.cancelSession(); - await r; - }); - }); - - test('escape doesn\'t remove code added from inline editor chat #3523 1/2', async function () { - - - // NO manual edits -> cancel - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.ok(model.getValue().includes('GENERATED')); - ctrl.cancelSession(); - await r; - assert.ok(!model.getValue().includes('GENERATED')); - - }); - - test('escape doesn\'t remove code added from inline editor chat #3523, 2/2', async function () { - - // manual edits -> finish - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.ok(model.getValue().includes('GENERATED')); - - editor.executeEdits('test', [EditOperation.insert(model.getFullModelRange().getEndPosition(), 'MANUAL')]); - - ctrl.acceptSession(); - await r; - assert.ok(model.getValue().includes('GENERATED')); - assert.ok(model.getValue().includes('MANUAL')); - - }); - - test('cancel while applying streamed edits should close the widget', async function () { - - const workerService = instaService.get(IEditorWorkerService) as TestWorkerService; - const originalCompute = workerService.computeMoreMinimalEdits.bind(workerService); - const editsBarrier = new DeferredPromise(); - let computeInvoked = false; - workerService.computeMoreMinimalEdits = async (resource, edits, pretty) => { - computeInvoked = true; - await editsBarrier.p; - return originalCompute(resource, edits, pretty); - }; - store.add({ dispose: () => { workerService.computeMoreMinimalEdits = originalCompute; } }); - - const progressBarrier = new DeferredPromise(); - store.add(chatAgentService.registerDynamicAgent({ - id: 'pendingEditsAgent', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message }] }]); - await progressBarrier.p; - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const states = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - const run = ctrl.run({ message: 'BLOCK', autoSend: true }); - assert.strictEqual(await states, undefined); - assert.ok(computeInvoked); - - ctrl.cancelSession(); - assert.strictEqual(await states, undefined); - - await run; - }); - - test('re-run should discard pending edits', async function () { - - let count = 1; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]); - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const rerun = new RerunAction(); - - model.setValue(''); - - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'PROMPT_', autoSend: true }); - assert.strictEqual(await p, undefined); - - - assert.strictEqual(model.getValue(), 'PROMPT_1'); - - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'PROMPT_2'); - ctrl.acceptSession(); - await r; - }); - - test('Retry undoes all changes, not just those from the request#5736', async function () { - - const text = [ - 'eins-', - 'zwei-', - 'drei-' - ]; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]); - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const rerun = new RerunAction(); - - model.setValue(''); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: '1', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'eins-'); - - // REQUEST 2 - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.chatWidget.setInput('1'); - await ctrl.chatWidget.acceptInput(); - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'zwei-eins-'); - - // REQUEST 2 - RERUN - const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - assert.strictEqual(await p3, undefined); - - assert.strictEqual(model.getValue(), 'drei-eins-'); - - ctrl.acceptSession(); - await r; - - }); - - test('moving inline chat to another model undoes changes', async function () { - const text = [ - 'eins\n', - 'zwei\n' - ]; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]); - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: '1', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); - - const targetModel = chatService.startSession(ChatAgentLocation.EditorInline)!; - store.add(targetModel); - chatWidget = new class extends mock() { - override get viewModel() { - // eslint-disable-next-line local/code-no-any-casts - return { model: targetModel.object } as any; - } - override focusResponseItem() { } - }; - - const r = ctrl.joinCurrentRun(); - await ctrl.viewInChat(); - - assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n'); - await r; - }); - - test('moving inline chat to another model undoes changes (2 requests)', async function () { - const text = [ - 'eins\n', - 'zwei\n' - ]; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]); - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: '1', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); - - // REQUEST 2 - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.chatWidget.setInput('1'); - await ctrl.chatWidget.acceptInput(); - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n'); - - const targetModel = chatService.startSession(ChatAgentLocation.EditorInline)!; - store.add(targetModel); - chatWidget = new class extends mock() { - override get viewModel() { - // eslint-disable-next-line local/code-no-any-casts - return { model: targetModel.object } as any; - } - override focusResponseItem() { } - }; - - const r = ctrl.joinCurrentRun(); - - await ctrl.viewInChat(); - - assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n'); - - await r; - }); - - // TODO@jrieken https://github.com/microsoft/vscode/issues/251429 - test.skip('Clicking "re-run without /doc" while a request is in progress closes the widget #5997', async function () { - - model.setValue(''); - - let count = 0; - const commandDetection: (boolean | undefined)[] = []; - - const onDidInvoke = new Emitter(); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - queueMicrotask(() => onDidInvoke.fire()); - commandDetection.push(request.enableCommandDetection); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]); - - if (count === 1) { - // FIRST call waits for cancellation - await raceCancellation(new Promise(() => { }), token); - } else { - await timeout(10); - } - - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - // const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - const p = Event.toPromise(onDidInvoke.event); - ctrl.run({ message: 'Hello-', autoSend: true }); - - await p; - - // assert.strictEqual(await p, undefined); - - // resend pending request without command detection - const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); - assertType(request); - const p2 = Event.toPromise(onDidInvoke.event); - const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.EditorInline }); - - await p2; - assert.strictEqual(await p3, undefined); - - assert.deepStrictEqual(commandDetection, [true, false]); - assert.strictEqual(model.getValue(), 'Hello-1'); - }); - - test('Re-run without after request is done', async function () { - - model.setValue(''); - - let count = 0; - const commandDetection: (boolean | undefined)[] = []; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - commandDetection.push(request.enableCommandDetection); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]); - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: 'Hello-', autoSend: true }); - assert.strictEqual(await p, undefined); - - // resend pending request without command detection - const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); - assertType(request); - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.EditorInline }); - - assert.strictEqual(await p2, undefined); - - assert.deepStrictEqual(commandDetection, [true, false]); - assert.strictEqual(model.getValue(), 'Hello-1'); - }); - - - test('Inline: Pressing Rerun request while the response streams breaks the response #5442', async function () { - - model.setValue('two\none\n'); - - const attempts: (number | undefined)[] = []; - - const deferred = new DeferredPromise(); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - attempts.push(request.attempt); - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: `TRY:${request.attempt}\n` }] }]); - await raceCancellation(deferred.p, token); - deferred.complete(); - await timeout(10); - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - ctrl.run({ message: 'Hello-', autoSend: true }); - assert.strictEqual(await p, undefined); - await timeout(10); - assert.deepStrictEqual(attempts, [0]); - - // RERUN (cancel, undo, redo) - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const rerun = new RerunAction(); - await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - assert.strictEqual(await p2, undefined); - - assert.deepStrictEqual(attempts, [0, 1]); - - assert.strictEqual(model.getValue(), 'TRY:1\ntwo\none\n'); - - }); - - test('Stopping/cancelling a request should NOT undo its changes', async function () { - - model.setValue('World'); - - const deferred = new DeferredPromise(); - let progress: ((parts: IChatProgress[]) => void) | undefined; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, _progress, history, token) { - - progress = _progress; - await deferred.p; - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - ctrl.run({ message: 'Hello', autoSend: true }); - await timeout(10); - assert.strictEqual(await p, undefined); - - assertType(progress); - - const modelChange = new Promise(resolve => model.onDidChangeContent(() => resolve())); - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]); - - await modelChange; - assert.strictEqual(model.getValue(), 'HelloWorld'); // first word has been streamed - - const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]); - chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionResource); - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'HelloWorld'); // CANCEL just stops the request and progressive typing but doesn't undo - - }); - - test('Apply Edits from existing session w/ edits', async function () { - - model.setValue(''); - - const newSession = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(newSession); - - await (await chatService.sendRequest(newSession.chatModel.sessionResource, 'Existing', { location: ChatAgentLocation.EditorInline }))?.responseCreatedPromise; - - assert.strictEqual(newSession.chatModel.requestInProgress.get(), true); - - const response = newSession.chatModel.lastRequest?.response; - assertType(response); - - await new Promise(resolve => { - if (response.isComplete) { - resolve(undefined); - } - const d = response.onDidChange(() => { - if (response.isComplete) { - d.dispose(); - resolve(undefined); - } - }); - }); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE]); - ctrl.run({ existingSession: newSession }); - - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'Existing'); - - }); - - test('Undo on error (2 rounds)', async function () { - - return runWithFakedTimers({}, async () => { - - - store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { - async invoke(request, progress, history, token) { - - progress([{ - kind: 'textEdit', - uri: model.uri, - edits: [{ - range: new Range(1, 1, 1, 1), - text: request.message - }] - }]); - - if (request.message === 'two') { - await timeout(100); // give edit a chance - return { - errorDetails: { message: 'FAILED' } - }; - } - return {}; - }, - })); - - model.setValue(''); - - // ROUND 1 - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ autoSend: true, message: 'one' }); - assert.strictEqual(await p, undefined); - assert.strictEqual(model.getValue(), 'one'); - - - // ROUND 2 - - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const values = new Set(); - store.add(model.onDidChangeContent(() => values.add(model.getValue()))); - ctrl.chatWidget.acceptInput('two'); // WILL Trigger a failure - assert.strictEqual(await p2, undefined); - assert.strictEqual(model.getValue(), 'one'); // undone - assert.ok(values.has('twoone')); // we had but the change got undone - }); - }); - - test('Inline chat "discard" button does not always appear if response is stopped #228030', async function () { - - model.setValue('World'); - - const deferred = new DeferredPromise(); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]); - await deferred.p; - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - ctrl.run({ message: 'Hello', autoSend: true }); - - - assert.strictEqual(await p, undefined); - - const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]); - chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionResource); - assert.strictEqual(await p2, undefined); - - - const value = contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_RESPONSE_TYPE.key); - assert.notStrictEqual(value, InlineChatResponseType.None); - }); - - test('Restore doesn\'t edit on errored result', async function () { - return runWithFakedTimers({ useFakeTimers: true }, async () => { - - const model2 = store.add(instaService.get(IModelService).createModel('ABC', null)); - - model.setValue('World'); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello1' }] }]); - await timeout(100); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello2' }] }]); - await timeout(100); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello3' }] }]); - await timeout(100); - - return { - errorDetails: { message: 'FAILED' } - }; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: 'Hello', autoSend: true }); - - assert.strictEqual(await p, undefined); - - const p2 = ctrl.awaitStates([State.PAUSE]); - editor.setModel(model2); - assert.strictEqual(await p2, undefined); - - const p3 = ctrl.awaitStates([...TestController.INIT_SEQUENCE]); - editor.setModel(model); - assert.strictEqual(await p3, undefined); - - assert.strictEqual(model.getValue(), 'World'); - }); - }); -}); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts deleted file mode 100644 index 46d1a03ad10..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ /dev/null @@ -1,598 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import assert from 'assert'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { IObservable, constObservable } from '../../../../../base/common/observable.js'; -import { assertType } from '../../../../../base/common/types.js'; -import { mock } from '../../../../../base/test/common/mock.js'; -import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IActiveCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { IDiffProviderFactoryService } from '../../../../../editor/browser/widget/diffEditor/diffProviderFactoryService.js'; -import { EditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { Position } from '../../../../../editor/common/core/position.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js'; -import { TestCommandService } from '../../../../../editor/test/browser/editorTestServices.js'; -import { instantiateTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js'; -import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { IEditorProgressService, IProgressRunner } from '../../../../../platform/progress/common/progress.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IViewDescriptorService } from '../../../../common/views.js'; -import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { TestContextService, TestExtensionService } from '../../../../test/common/workbenchTestServices.js'; -import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatAccessibilityService, IChatWidgetService, IQuickChatService } from '../../../chat/browser/chat.js'; -import { ChatSessionsService } from '../../../chat/browser/chatSessions/chatSessions.contribution.js'; -import { ChatVariablesService } from '../../../chat/browser/attachments/chatVariables.js'; -import { ChatWidget } from '../../../chat/browser/widget/chatWidget.js'; -import { ChatAgentService, IChatAgentService } from '../../../chat/common/participants/chatAgents.js'; -import { IChatEditingService, IChatEditingSession } from '../../../chat/common/editing/chatEditingService.js'; -import { IChatRequestModel } from '../../../chat/common/model/chatModel.js'; -import { IChatService } from '../../../chat/common/chatService/chatService.js'; -import { ChatService } from '../../../chat/common/chatService/chatServiceImpl.js'; -import { IChatSessionsService } from '../../../chat/common/chatSessionsService.js'; -import { ChatSlashCommandService, IChatSlashCommandService } from '../../../chat/common/participants/chatSlashCommands.js'; -import { ChatTransferService, IChatTransferService } from '../../../chat/common/model/chatTransferService.js'; -import { IChatVariablesService } from '../../../chat/common/attachments/chatVariables.js'; -import { IChatResponseViewModel } from '../../../chat/common/model/chatViewModel.js'; -import { ChatWidgetHistoryService, IChatWidgetHistoryService } from '../../../chat/common/widget/chatWidgetHistoryService.js'; -import { ChatAgentLocation, ChatModeKind } from '../../../chat/common/constants.js'; -import { ILanguageModelsService } from '../../../chat/common/languageModels.js'; -import { ILanguageModelToolsService } from '../../../chat/common/tools/languageModelToolsService.js'; -import { NullLanguageModelsService } from '../../../chat/test/common/languageModels.js'; -import { MockLanguageModelToolsService } from '../../../chat/test/common/tools/mockLanguageModelToolsService.js'; -import { IMcpService } from '../../../mcp/common/mcpTypes.js'; -import { TestMcpService } from '../../../mcp/test/common/testMcpService.js'; -import { HunkState } from '../../browser/inlineChatSession.js'; -import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; -import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; -import { TestWorkerService } from './testWorkerService.js'; -import { ChatWidgetService } from '../../../chat/browser/widget/chatWidgetService.js'; -import { URI } from '../../../../../base/common/uri.js'; - -suite('InlineChatSession', function () { - - const store = new DisposableStore(); - let editor: IActiveCodeEditor; - let model: ITextModel; - let instaService: TestInstantiationService; - - let inlineChatSessionService: IInlineChatSessionService; - - setup(function () { - const contextKeyService = new MockContextKeyService(); - - - const serviceCollection = new ServiceCollection( - [IConfigurationService, new TestConfigurationService()], - [IChatVariablesService, new SyncDescriptor(ChatVariablesService)], - [ILogService, new NullLogService()], - [ITelemetryService, NullTelemetryService], - [IExtensionService, new TestExtensionService()], - [IContextKeyService, new MockContextKeyService()], - [IViewsService, new TestExtensionService()], - [IWorkspaceContextService, new TestContextService()], - [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], - [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], - [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], - [IChatTransferService, new SyncDescriptor(ChatTransferService)], - [IChatSessionsService, new SyncDescriptor(ChatSessionsService)], - [IChatService, new SyncDescriptor(ChatService)], - [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], - [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IContextKeyService, contextKeyService], - [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], - [ILanguageModelsService, new SyncDescriptor(NullLanguageModelsService)], - [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], - [ICommandService, new SyncDescriptor(TestCommandService)], - [ILanguageModelToolsService, new MockLanguageModelToolsService()], - [IMcpService, new TestMcpService()], - [IEditorProgressService, new class extends mock() { - override show(total: unknown, delay?: unknown): IProgressRunner { - return { - total() { }, - worked(value) { }, - done() { }, - }; - } - }], - [IChatEditingService, new class extends mock() { - override editingSessionsObs: IObservable = constObservable([]); - }], - [IChatAccessibilityService, new class extends mock() { - override acceptResponse(chatWidget: ChatWidget, container: HTMLElement, response: IChatResponseViewModel | undefined, requestId: URI | undefined): void { } - override acceptRequest(): URI | undefined { return undefined; } - override acceptElicitation(): void { } - }], - [IAccessibleViewService, new class extends mock() { - override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { - return null; - } - }], - [IQuickChatService, new class extends mock() { }], - [IConfigurationService, new TestConfigurationService()], - [IViewDescriptorService, new class extends mock() { - override onDidChangeLocation = Event.None; - }], - [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()] - ); - - - - instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); - inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); - store.add(instaService.get(IChatSessionsService) as ChatSessionsService); // Needs to be disposed in between test runs to clear extensionPoint contribution - store.add(instaService.get(IChatService) as ChatService); - - instaService.get(IChatAgentService).registerDynamicAgent({ - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - id: 'testAgent', - name: 'testAgent', - isDefault: true, - locations: [ChatAgentLocation.EditorInline], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - }, { - async invoke() { - return {}; - } - }); - - - store.add(instaService.get(IEditorWorkerService) as TestWorkerService); - model = store.add(instaService.get(IModelService).createModel('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven', null)); - editor = store.add(instantiateTestCodeEditor(instaService, model)); - }); - - teardown(async function () { - store.clear(); - await instaService.get(IChatService).waitForModelDisposals(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - async function makeEditAsAi(edit: EditOperation | EditOperation[]) { - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assertType(session); - session.hunkData.ignoreTextModelNChanges = true; - try { - editor.executeEdits('test', Array.isArray(edit) ? edit : [edit]); - } finally { - session.hunkData.ignoreTextModelNChanges = false; - } - await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' }); - } - - function makeEdit(edit: EditOperation | EditOperation[]) { - editor.executeEdits('test', Array.isArray(edit) ? edit : [edit]); - } - - test('Create, release', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, info', async function () { - - const decorationCountThen = model.getAllDecorations().length; - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - assert.ok(session.textModelN === model); - - await makeEditAsAi(EditOperation.insert(new Position(1, 1), 'AI_EDIT\n')); - - - assert.strictEqual(session.hunkData.size, 1); - let [hunk] = session.hunkData.getInfo(); - assertType(hunk); - - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - assert.strictEqual(hunk.getState(), HunkState.Pending); - assert.ok(hunk.getRangesN()[0].equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 8 })); - - await makeEditAsAi(EditOperation.insert(new Position(1, 3), 'foobar')); - [hunk] = session.hunkData.getInfo(); - assert.ok(hunk.getRangesN()[0].equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 14 })); - - inlineChatSessionService.releaseSession(session); - - assert.strictEqual(model.getAllDecorations().length, decorationCountThen); // no leaked decorations! - }); - - test('HunkData, accept', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - for (const hunk of session.hunkData.getInfo()) { - assertType(hunk); - assert.strictEqual(hunk.getState(), HunkState.Pending); - hunk.acceptChanges(); - assert.strictEqual(hunk.getState(), HunkState.Accepted); - } - - assert.strictEqual(session.textModel0.getValue(), session.textModelN.getValue()); - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, reject', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - for (const hunk of session.hunkData.getInfo()) { - assertType(hunk); - assert.strictEqual(hunk.getState(), HunkState.Pending); - hunk.discardChanges(); - assert.strictEqual(hunk.getState(), HunkState.Rejected); - } - - assert.strictEqual(session.textModel0.getValue(), session.textModelN.getValue()); - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, N rounds', async function () { - - model.setValue('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven\ntwelwe\nthirteen\nfourteen\nfifteen\nsixteen\nseventeen\neighteen\nnineteen\n'); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - assert.ok(session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - assert.strictEqual(session.hunkData.size, 0); - - // ROUND #1 - await makeEditAsAi([ - EditOperation.insert(new Position(1, 1), 'AI1'), - EditOperation.insert(new Position(4, 1), 'AI2'), - EditOperation.insert(new Position(19, 1), 'AI3') - ]); - - assert.strictEqual(session.hunkData.size, 2); // AI1, AI2 are merged into one hunk, AI3 is a separate hunk - - let [first, second] = session.hunkData.getInfo(); - - assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI1')); - assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI2')); - assert.ok(model.getValueInRange(second.getRangesN()[0]).includes('AI3')); - - assert.ok(!session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI1')); - assert.ok(!session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI2')); - assert.ok(!session.textModel0.getValueInRange(second.getRangesN()[0]).includes('AI3')); - - first.acceptChanges(); - assert.ok(session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI1')); - assert.ok(session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI2')); - assert.ok(!session.textModel0.getValueInRange(second.getRangesN()[0]).includes('AI3')); - - - // ROUND #2 - await makeEditAsAi([ - EditOperation.insert(new Position(7, 1), 'AI4'), - ]); - assert.strictEqual(session.hunkData.size, 2); - - [first, second] = session.hunkData.getInfo(); - assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI4')); // the new hunk (in line-order) - assert.ok(model.getValueInRange(second.getRangesN()[0]).includes('AI3')); // the previous hunk remains - - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, (mirror) edit before', async function () { - - const lines = ['one', 'two', 'three']; - model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - makeEdit([EditOperation.replace(new Range(1, 1, 1, 4), 'ONE')]); - assert.strictEqual(session.textModelN.getValue(), ['ONE', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['ONE', 'two', 'three'].join('\n')); - }); - - test('HunkData, (mirror) edit after', async function () { - - const lines = ['one', 'two', 'three', 'four', 'five']; - model.setValue(lines.join('\n')); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 1); - const [hunk] = session.hunkData.getInfo(); - - makeEdit([EditOperation.insert(new Position(1, 1), 'USER1')]); - assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'four', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'three', 'four', 'five'].join('\n')); - - makeEdit([EditOperation.insert(new Position(5, 1), 'USER2')]); - assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'three', 'USER2four', 'five'].join('\n')); - - hunk.acceptChanges(); - assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n')); - }); - - test('HunkData, (mirror) edit inside ', async function () { - - const lines = ['one', 'two', 'three']; - model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - makeEdit([EditOperation.replace(new Range(3, 4, 3, 7), 'wwaaassss')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI wwaaassss HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three'].join('\n')); - }); - - test('HunkData, (mirror) edit after dicard ', async function () { - - const lines = ['one', 'two', 'three']; - model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - assert.strictEqual(session.hunkData.size, 1); - const [hunk] = session.hunkData.getInfo(); - hunk.discardChanges(); - assert.strictEqual(session.textModelN.getValue(), lines.join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - makeEdit([EditOperation.replace(new Range(3, 4, 3, 6), '3333')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'thr3333'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'thr3333'].join('\n')); - }); - - test('HunkData, (mirror) edit after, multi turn', async function () { - - const lines = ['one', 'two', 'three', 'four', 'five']; - model.setValue(lines.join('\n')); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 1); - - makeEdit([EditOperation.insert(new Position(5, 1), 'FOO')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - await makeEditAsAi([EditOperation.insert(new Position(2, 4), ' zwei')]); - assert.strictEqual(session.hunkData.size, 1); - - assert.strictEqual(session.textModelN.getValue(), ['one', 'two zwei', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - makeEdit([EditOperation.replace(new Range(6, 3, 6, 5), 'vefivefi')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two zwei', 'AI_EDIT', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - }); - - test('HunkData, (mirror) edit after, multi turn 2', async function () { - - const lines = ['one', 'two', 'three', 'four', 'five']; - model.setValue(lines.join('\n')); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 1); - - makeEdit([EditOperation.insert(new Position(5, 1), 'FOO')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - await makeEditAsAi([EditOperation.insert(new Position(2, 4), 'zwei')]); - assert.strictEqual(session.hunkData.size, 1); - - assert.strictEqual(session.textModelN.getValue(), ['one', 'twozwei', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - makeEdit([EditOperation.replace(new Range(6, 3, 6, 5), 'vefivefi')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'twozwei', 'AI_EDIT', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - - session.hunkData.getInfo()[0].acceptChanges(); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - - makeEdit([EditOperation.replace(new Range(1, 1, 1, 1), 'done')]); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - }); - - test('HunkData, accept, discardAll', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - const textModeNNow = session.textModelN.getValue(); - - session.hunkData.getInfo()[0].acceptChanges(); - assert.strictEqual(textModeNNow, session.textModelN.getValue()); - - session.hunkData.discardAll(); // all remaining - assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven'); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, discardAll return undo edits', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - const textModeNNow = session.textModelN.getValue(); - - session.hunkData.getInfo()[0].acceptChanges(); - assert.strictEqual(textModeNNow, session.textModelN.getValue()); - - const undoEdits = session.hunkData.discardAll(); // all remaining - assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven'); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - - // undo the discards - session.textModelN.pushEditOperations(null, undoEdits, () => null); - assert.strictEqual(textModeNNow, session.textModelN.getValue()); - - inlineChatSessionService.releaseSession(session); - }); - - test('Pressing Escape after inline chat errored with "response filtered" leaves document dirty #7764', async function () { - - const origValue = `class Foo { - private onError(error: string): void { - if (/The request timed out|The network connection was lost/i.test(error)) { - return; - } - - error = error.replace(/See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information'); - - this.notificationService.notify({ - severity: Severity.Error, - message: error, - source: nls.localize('update service', "Update Service"), - }); - } -}`; - model.setValue(origValue); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - const fakeRequest = new class extends mock() { - override get id() { return 'one'; } - }; - session.markModelVersion(fakeRequest); - - assert.strictEqual(editor.getModel().getLineCount(), 15); - - await makeEditAsAi([EditOperation.replace(new Range(7, 1, 7, Number.MAX_SAFE_INTEGER), `error = error.replace( - /See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, - 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information' - );`)]); - - assert.strictEqual(editor.getModel().getLineCount(), 18); - - // called when a response errors out - await session.undoChangesUntil(fakeRequest.id); - await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' }, undefined); - - assert.strictEqual(editor.getModel().getValue(), origValue); - - session.hunkData.discardAll(); // called when dimissing the session - assert.strictEqual(editor.getModel().getValue(), origValue); - }); - - test('Apply Code\'s preview should be easier to undo/esc #7537', async function () { - model.setValue(`export function fib(n) { - if (n <= 0) return 0; - if (n === 1) return 0; - if (n === 2) return 1; - return fib(n - 1) + fib(n - 2); -}`); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.replace(new Range(5, 1, 6, Number.MAX_SAFE_INTEGER), ` - let a = 0, b = 1, c; - for (let i = 3; i <= n; i++) { - c = a + b; - a = b; - b = c; - } - return b; -}`)]); - - assert.strictEqual(session.hunkData.size, 1); - assert.strictEqual(session.hunkData.pending, 1); - assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Pending)); - - await assertSnapshot(editor.getModel().getValue(), { name: '1' }); - - await model.undo(); - await assertSnapshot(editor.getModel().getValue(), { name: '2' }); - - // overlapping edits (even UNDO) mark edits as accepted - assert.strictEqual(session.hunkData.size, 1); - assert.strictEqual(session.hunkData.pending, 0); - assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Accepted)); - - // no further change when discarding - session.hunkData.discardAll(); // CANCEL - await assertSnapshot(editor.getModel().getValue(), { name: '2' }); - }); - -}); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts deleted file mode 100644 index df51a99ed0d..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { IntervalTimer } from '../../../../../base/common/async.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { asProgressiveEdit } from '../../browser/utils.js'; -import assert from 'assert'; - - -suite('AsyncEdit', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('asProgressiveEdit', async () => { - const interval = new IntervalTimer(); - const edit = { - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - text: 'Hello, world!' - }; - - const cts = new CancellationTokenSource(); - const result = asProgressiveEdit(interval, edit, 5, cts.token); - - // Verify the range - assert.deepStrictEqual(result.range, edit.range); - - const iter = result.newText[Symbol.asyncIterator](); - - // Verify the newText - const a = await iter.next(); - assert.strictEqual(a.value, 'Hello,'); - assert.strictEqual(a.done, false); - - // Verify the next word - const b = await iter.next(); - assert.strictEqual(b.value, ' world!'); - assert.strictEqual(b.done, false); - - const c = await iter.next(); - assert.strictEqual(c.value, undefined); - assert.strictEqual(c.done, true); - - cts.dispose(); - }); - - test('asProgressiveEdit - cancellation', async () => { - const interval = new IntervalTimer(); - const edit = { - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - text: 'Hello, world!' - }; - - const cts = new CancellationTokenSource(); - const result = asProgressiveEdit(interval, edit, 5, cts.token); - - // Verify the range - assert.deepStrictEqual(result.range, edit.range); - - const iter = result.newText[Symbol.asyncIterator](); - - // Verify the newText - const a = await iter.next(); - assert.strictEqual(a.value, 'Hello,'); - assert.strictEqual(a.done, false); - - cts.dispose(true); - - const c = await iter.next(); - assert.strictEqual(c.value, undefined); - assert.strictEqual(c.done, true); - }); -}); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 897e9d8c88d..558af12eda9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -15,7 +15,7 @@ import { ChatContextKeys } from '../../../chat/common/actions/chatContextKeys.js import { IChatService } from '../../../chat/common/chatService/chatService.js'; import { LocalChatSessionUri } from '../../../chat/common/model/chatUri.js'; import { ChatAgentLocation, ChatConfiguration } from '../../../chat/common/constants.js'; -import { AbstractInline1ChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; + import { isDetachedTerminalInstance, ITerminalChatService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; import { registerActiveXtermAction } from '../../../terminal/browser/terminalActions.js'; import { TerminalContextMenuGroup } from '../../../terminal/browser/terminalMenus.js'; @@ -32,6 +32,7 @@ import { IPreferencesService, IOpenSettingsOptions } from '../../../../services/ import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; import { TerminalChatAgentToolsSettingId } from '../../chatAgentTools/common/terminalChatAgentToolsConfiguration.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { AbstractInlineChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; registerActiveXtermAction({ id: TerminalChatCommandId.Start, @@ -86,7 +87,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.Close, title: localize2('closeChat', 'Close'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, keybinding: { primary: KeyCode.Escape, when: ContextKeyExpr.and( @@ -119,7 +120,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.RunCommand, title: localize2('runCommand', 'Run Chat Command'), shortTitle: localize2('run', 'Run'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -152,7 +153,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.RunFirstCommand, title: localize2('runFirstCommand', 'Run First Chat Command'), shortTitle: localize2('runFirst', 'Run First'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -184,7 +185,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.InsertCommand, title: localize2('insertCommand', 'Insert Chat Command'), shortTitle: localize2('insert', 'Insert'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, icon: Codicon.insert, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, @@ -218,7 +219,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.InsertFirstCommand, title: localize2('insertFirstCommand', 'Insert First Chat Command'), shortTitle: localize2('insertFirst', 'Insert First'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -251,7 +252,7 @@ registerActiveXtermAction({ title: localize2('chat.rerun.label', "Rerun Request"), f1: false, icon: Codicon.refresh, - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -293,7 +294,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.ViewInChat, title: localize2('viewInChat', 'View in Chat'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),