debt - remove old inline chat world (#286503)

fixes https://github.com/microsoft/vscode/issues/282015
This commit is contained in:
Johannes Rieken
2026-01-08 11:12:31 +01:00
committed by GitHub
parent f76928590d
commit 74195430ca
22 changed files with 82 additions and 5378 deletions

View File

@@ -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<void> {
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);

View File

@@ -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) {

View File

@@ -969,8 +969,6 @@ export interface IChatEditorLocationData {
document: URI;
selection: ISelection;
wholeRange: IRange;
close: () => void;
delegateSessionResource: URI | undefined;
}
export interface IChatNotebookLocationData {

View File

@@ -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;
}

View File

@@ -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<IWorkbenchContributionsRegistry>(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());

View File

@@ -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
);
}
}

View File

@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise<void> {
if (this._keep) {
await ctrl.acceptSession();
} else {

View File

@@ -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 = `<notebook>${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 `<notebook>${editor.getId()}#${uri}`;
}
throw illegalState('Expected notebook editor');
}
}));
this._store.add(sessionService.onWillStartSession(newSessionEditor => {
const candidate = CellUri.parse(newSessionEditor.getModel().uri);
if (!candidate) {

View File

@@ -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<this>();
readonly onDidChange: Event<this> = 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<string, number>();
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<boolean> {
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<boolean>;
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<RawHunk, RawHunkData>();
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;
}

View File

@@ -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>('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<IActiveCodeEditor>;
readonly onDidMoveSession: Event<IInlineChatSessionEvent>;
readonly onDidStashSession: Event<IInlineChatSessionEvent>;
readonly onDidEndSession: Event<IInlineChatSessionEndEvent>;
createSession(editor: IActiveCodeEditor, options: { wholeRange?: IRange; session?: Session; headless?: boolean }, token: CancellationToken): Promise<Session | undefined>;
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<this>;
dispose(): void;
createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise<IInlineChatSession2>;
getSession2(uri: URI): IInlineChatSession2 | undefined;
createSession(editor: ICodeEditor): IInlineChatSession2;
getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined;
getSessionBySessionUri(uri: URI): IInlineChatSession2 | undefined;
readonly onDidChangeSessions: Event<this>;
}
export async function moveToPanelChat(accessor: ServicesAccessor, model: IChatModel | undefined, resend: boolean) {

View File

@@ -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<IInlineChatSession2>();
private readonly _onWillStartSession = this._store.add(new Emitter<IActiveCodeEditor>());
readonly onWillStartSession: Event<IActiveCodeEditor> = this._onWillStartSession.event;
private readonly _onDidMoveSession = this._store.add(new Emitter<IInlineChatSessionEvent>());
readonly onDidMoveSession: Event<IInlineChatSessionEvent> = this._onDidMoveSession.event;
private readonly _onDidEndSession = this._store.add(new Emitter<IInlineChatSessionEndEvent>());
readonly onDidEndSession: Event<IInlineChatSessionEndEvent> = this._onDidEndSession.event;
private readonly _onDidStashSession = this._store.add(new Emitter<IInlineChatSessionEvent>());
readonly onDidStashSession: Event<IInlineChatSessionEvent> = this._onDidStashSession.event;
private readonly _sessions = new Map<string, SessionData>();
private readonly _keyComputers = new Map<string, ISessionKeyComputer>();
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<Session | undefined> {
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<TelemetryData, TelemetryDataClassification>('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<IInlineChatSession2>();
private readonly _onDidChangeSessions = this._store.add(new Emitter<this>());
readonly onDidChangeSessions: Event<this> = this._onDidChangeSessions.event;
constructor(
@IChatService private readonly _chatService: IChatService
) { }
async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise<IInlineChatSession2> {
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;
}

View File

@@ -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<void>());
protected readonly _onDidDiscard = this._store.add(new Emitter<void>());
private readonly _ctxCurrentChangeHasDiff: IContextKey<boolean>;
private readonly _ctxCurrentChangeShowsDiff: IContextKey<boolean>;
private readonly _progressiveEditingDecorations: IEditorDecorationsCollection;
private readonly _lensActionsFactory: ConflictActionsFactory;
private _editCount: number = 0;
private readonly _hunkData = new Map<HunkInformation, HunkDisplayData>();
readonly onDidAccept: Event<void> = this._onDidAccept.event;
readonly onDidDiscard: Event<void> = 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<void> {
return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore, metadata);
}
async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise<void> {
// add decorations once per line that got edited
const progress = new Progress<IValidEditOperation[]>(edits => {
const newLines = new Set<number>();
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<IValidEditOperation[]> | undefined, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise<void> {
// 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<void> {
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;
}

View File

@@ -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<HunkAccessibleDiffViewer>());
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<number>;
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();
}
}

View File

@@ -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<string>;
}
export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdit, progress?: IProgress<IValidEditOperation[]>, 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<string>();
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
};
}

View File

@@ -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: {

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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<IEditorProgressService>() {
override show(total: unknown, delay?: unknown): IProgressRunner {
return {
total() { },
worked(value) { },
done() { },
};
}
}],
[IChatEditingService, new class extends mock<IChatEditingService>() {
override editingSessionsObs: IObservable<readonly IChatEditingSession[]> = constObservable([]);
}],
[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
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<IAccessibleViewService>() {
override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {
return null;
}
}],
[IQuickChatService, new class extends mock<IQuickChatService>() { }],
[IConfigurationService, new TestConfigurationService()],
[IViewDescriptorService, new class extends mock<IViewDescriptorService>() {
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<IChatRequestModel>() {
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' });
});
});

View File

@@ -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);
});
});

View File

@@ -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),