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