mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
debt - remove old inline chat world (#286503)
fixes https://github.com/microsoft/vscode/issues/282015
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -969,8 +969,6 @@ export interface IChatEditorLocationData {
|
||||
document: URI;
|
||||
selection: ISelection;
|
||||
wholeRange: IRange;
|
||||
close: () => void;
|
||||
delegateSessionResource: URI | undefined;
|
||||
}
|
||||
|
||||
export interface IChatNotebookLocationData {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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' });
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user