Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts
5245 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../../../base/common/codicons.js';6import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';7import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';8import { EditorAction2 } from '../../../../editor/browser/editorExtensions.js';9import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js';10import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';11import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';12import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js';13import { 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, CTX_HOVER_MODE } from '../common/inlineChat.js';14import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js';15import { localize, localize2 } from '../../../../nls.js';16import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js';17import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';18import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';19import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';20import { IEditorService } from '../../../services/editor/common/editorService.js';21import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';22import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js';23import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';24import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';25import { ILogService } from '../../../../platform/log/common/log.js';26import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';272829CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start');30CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES);313233export const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.'));3435// some gymnastics to enable hold for speech without moving the StartSessionAction into the electron-layer3637export interface IHoldForSpeech {38(accessor: ServicesAccessor, controller: InlineChatController, source: Action2): void;39}40let _holdForSpeech: IHoldForSpeech | undefined = undefined;41export function setHoldForSpeech(holdForSpeech: IHoldForSpeech) {42_holdForSpeech = holdForSpeech;43}4445const inlineChatContextKey = ContextKeyExpr.and(46ContextKeyExpr.or(CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_V2_ENABLED),47CTX_INLINE_CHAT_POSSIBLE,48EditorContextKeys.writable,49EditorContextKeys.editorSimpleInput.negate()50);5152export class StartSessionAction extends Action2 {5354constructor() {55super({56id: ACTION_START,57title: localize2('run', 'Open Inline Chat'),58shortTitle: localize2('runShort', 'Inline Chat'),59category: AbstractInlineChatAction.category,60f1: true,61precondition: inlineChatContextKey,62keybinding: {63when: EditorContextKeys.focus,64weight: KeybindingWeight.WorkbenchContrib,65primary: KeyMod.CtrlCmd | KeyCode.KeyI66},67icon: START_INLINE_CHAT,68menu: [{69id: MenuId.EditorContext,70group: '1_chat',71order: 3,72when: inlineChatContextKey73}, {74id: MenuId.ChatTitleBarMenu,75group: 'a_open',76order: 3,77}, {78id: MenuId.ChatEditorInlineGutter,79group: '1_chat',80order: 1,81}, {82id: MenuId.InlineChatEditorAffordance,83group: '1_chat',84order: 1,85when: EditorContextKeys.hasNonEmptySelection86}]87});88}89override run(accessor: ServicesAccessor, ...args: unknown[]): any {9091const codeEditorService = accessor.get(ICodeEditorService);92const editor = codeEditorService.getActiveCodeEditor();93if (!editor || editor.isSimpleWidget) {94// well, at least we tried...95return;96}979899// precondition does hold100return editor.invokeWithinContext((editorAccessor) => {101const kbService = editorAccessor.get(IContextKeyService);102const logService = editorAccessor.get(ILogService);103const enabled = kbService.contextMatchesRules(this.desc.precondition ?? undefined);104if (!enabled) {105logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize());106return;107}108return this._runEditorCommand(editorAccessor, editor, ...args);109});110}111112private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) {113114const ctrl = InlineChatController.get(editor);115if (!ctrl) {116return;117}118119if (_holdForSpeech) {120accessor.get(IInstantiationService).invokeFunction(_holdForSpeech, ctrl, this);121}122123let options: InlineChatRunOptions | undefined;124const arg = args[0];125if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {126options = arg;127}128await InlineChatController.get(editor)?.run({ ...options });129}130}131132export class FocusInlineChat extends EditorAction2 {133134constructor() {135super({136id: 'inlineChat.focus',137title: localize2('focus', "Focus Input"),138f1: true,139category: AbstractInlineChatAction.category,140precondition: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_FOCUSED.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()),141keybinding: [{142weight: KeybindingWeight.EditorCore + 10, // win against core_command143when: ContextKeyExpr.and(CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.isEqualTo('above'), EditorContextKeys.isEmbeddedDiffEditor.negate()),144primary: KeyMod.CtrlCmd | KeyCode.DownArrow,145}, {146weight: KeybindingWeight.EditorCore + 10, // win against core_command147when: ContextKeyExpr.and(CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.isEqualTo('below'), EditorContextKeys.isEmbeddedDiffEditor.negate()),148primary: KeyMod.CtrlCmd | KeyCode.UpArrow,149}]150});151}152153override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) {154InlineChatController.get(editor)?.focus();155}156}157158//#region --- VERSION 2159export abstract class AbstractInlineChatAction extends EditorAction2 {160161static readonly category = localize2('cat', "Inline Chat");162163constructor(desc: IAction2Options) {164const massageMenu = (menu: IAction2Options['menu'] | undefined) => {165if (Array.isArray(menu)) {166for (const entry of menu) {167entry.when = ContextKeyExpr.and(CTX_INLINE_CHAT_V2_ENABLED, entry.when);168}169} else if (menu) {170menu.when = ContextKeyExpr.and(CTX_INLINE_CHAT_V2_ENABLED, menu.when);171}172};173if (Array.isArray(desc.menu)) {174massageMenu(desc.menu);175} else {176massageMenu(desc.menu);177}178179super({180...desc,181category: AbstractInlineChatAction.category,182precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_V2_ENABLED, desc.precondition)183});184}185186override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) {187const editorService = accessor.get(IEditorService);188const logService = accessor.get(ILogService);189190let ctrl = InlineChatController.get(editor);191if (!ctrl) {192const { activeTextEditorControl } = editorService;193if (isCodeEditor(activeTextEditorControl)) {194editor = activeTextEditorControl;195} else if (isDiffEditor(activeTextEditorControl)) {196editor = activeTextEditorControl.getModifiedEditor();197}198ctrl = InlineChatController.get(editor);199}200201if (!ctrl) {202logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri);203return;204}205206if (editor instanceof EmbeddedCodeEditorWidget) {207editor = editor.getParentEditor();208}209if (!ctrl) {210for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) {211if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) {212if (diffEditor instanceof EmbeddedDiffEditorWidget) {213this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args);214}215}216}217return;218}219this.runInlineChatCommand(accessor, ctrl, editor, ..._args);220}221222abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ...args: unknown[]): void;223}224225class KeepOrUndoSessionAction extends AbstractInlineChatAction {226227constructor(private readonly _keep: boolean, desc: IAction2Options) {228super(desc);229}230231override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise<void> {232if (this._keep) {233await ctrl.acceptSession();234} else {235await ctrl.rejectSession();236}237if (editor.hasModel()) {238editor.setSelection(editor.getSelection().collapseToStart());239}240}241}242243export class KeepSessionAction2 extends KeepOrUndoSessionAction {244constructor() {245super(true, {246id: 'inlineChat2.keep',247title: localize2('Keep', "Keep"),248f1: true,249icon: Codicon.check,250precondition: ContextKeyExpr.and(251CTX_INLINE_CHAT_VISIBLE,252ctxHasRequestInProgress.negate(),253ctxHasEditorModification,254),255keybinding: [{256when: ContextKeyExpr.and(ChatContextKeys.inputHasFocus, ChatContextKeys.inputHasText.negate()),257weight: KeybindingWeight.WorkbenchContrib,258primary: KeyCode.Enter259}, {260weight: KeybindingWeight.WorkbenchContrib + 10,261primary: KeyMod.CtrlCmd | KeyCode.Enter262}],263menu: [{264id: MenuId.ChatEditorInlineExecute,265group: 'navigation',266order: 4,267when: ContextKeyExpr.and(268ctxHasRequestInProgress.negate(),269ctxHasEditorModification,270ChatContextKeys.inputHasText.toNegated()271),272}]273});274}275}276277export class UndoSessionAction2 extends KeepOrUndoSessionAction {278279constructor() {280super(false, {281id: 'inlineChat2.undo',282title: localize2('undo', "Undo"),283f1: true,284icon: Codicon.discard,285precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_HOVER_MODE),286keybinding: [{287when: ContextKeyExpr.or(288ContextKeyExpr.and(EditorContextKeys.focus, ctxHasEditorModification.negate()),289ChatContextKeys.inputHasFocus,290),291weight: KeybindingWeight.WorkbenchContrib + 1,292primary: KeyCode.Escape,293}],294menu: [{295id: MenuId.ChatEditorInlineExecute,296group: 'navigation',297order: 100,298when: ContextKeyExpr.and(299CTX_HOVER_MODE,300ctxHasRequestInProgress.negate(),301ctxHasEditorModification,302)303}]304});305}306}307308export class UndoAndCloseSessionAction2 extends KeepOrUndoSessionAction {309310constructor() {311super(false, {312id: 'inlineChat2.close',313title: localize2('close2', "Close"),314f1: true,315icon: Codicon.close,316precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_HOVER_MODE.negate()),317keybinding: [{318when: ContextKeyExpr.or(319ContextKeyExpr.and(EditorContextKeys.focus, ctxHasEditorModification.negate()),320ChatContextKeys.inputHasFocus,321),322weight: KeybindingWeight.WorkbenchContrib + 1,323primary: KeyCode.Escape,324}],325menu: [{326id: MenuId.ChatEditorInlineExecute,327group: 'navigation',328order: 100,329when: ContextKeyExpr.or(330CTX_HOVER_MODE.negate(),331ContextKeyExpr.and(CTX_HOVER_MODE, ctxHasEditorModification.negate(), ctxHasRequestInProgress.negate())332)333}]334});335}336}337338339