Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts
5251 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 { AsyncIterableProducer } from '../../../../../base/common/async.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { Codicon } from '../../../../../base/common/codicons.js';
9
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
10
import { Disposable, markAsSingleton } from '../../../../../base/common/lifecycle.js';
11
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
12
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
13
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
14
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
15
import { CopyAction } from '../../../../../editor/contrib/clipboard/browser/clipboard.js';
16
import { localize, localize2 } from '../../../../../nls.js';
17
import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
18
import { MenuEntryActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
19
import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../platform/actions/common/actions.js';
20
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
21
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
22
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
23
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
24
import { ILabelService } from '../../../../../platform/label/common/label.js';
25
import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js';
26
import { IWorkbenchContribution } from '../../../../common/contributions.js';
27
import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js';
28
import { IEditorService } from '../../../../services/editor/common/editorService.js';
29
import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js';
30
import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';
31
import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js';
32
import { reviewEdits } from '../../../inlineChat/browser/inlineChatController.js';
33
import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js';
34
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
35
import { ChatCopyKind, IChatService } from '../../common/chatService/chatService.js';
36
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
37
import { ChatAgentLocation } from '../../common/constants.js';
38
import { IChatCodeBlockContextProviderService, IChatWidgetService } from '../chat.js';
39
import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../widget/chatContentParts/codeBlockPart.js';
40
import { CHAT_CATEGORY } from './chatActions.js';
41
import { ApplyCodeBlockOperation, InsertCodeBlockOperation } from './codeBlockOperations.js';
42
43
const shellLangIds = [
44
'fish',
45
'ps1',
46
'pwsh',
47
'powershell',
48
'sh',
49
'shellscript',
50
'zsh'
51
];
52
53
export interface IChatCodeBlockActionContext extends ICodeBlockActionContext {
54
element: IChatResponseViewModel;
55
}
56
57
export function isCodeBlockActionContext(thing: unknown): thing is ICodeBlockActionContext {
58
return typeof thing === 'object' && thing !== null && 'code' in thing && 'element' in thing;
59
}
60
61
export function isCodeCompareBlockActionContext(thing: unknown): thing is ICodeCompareBlockActionContext {
62
return typeof thing === 'object' && thing !== null && 'element' in thing && 'diffEditor' in thing && 'toggleDiffViewMode' in thing;
63
}
64
65
function isResponseFiltered(context: ICodeBlockActionContext) {
66
return isResponseVM(context.element) && context.element.errorDetails?.responseIsFiltered;
67
}
68
69
abstract class ChatCodeBlockAction extends Action2 {
70
run(accessor: ServicesAccessor, ...args: unknown[]) {
71
let context = args[0];
72
if (!isCodeBlockActionContext(context)) {
73
const codeEditorService = accessor.get(ICodeEditorService);
74
const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();
75
if (!editor) {
76
return;
77
}
78
79
context = getContextFromEditor(editor, accessor);
80
if (!isCodeBlockActionContext(context)) {
81
return;
82
}
83
}
84
85
return this.runWithContext(accessor, context);
86
}
87
88
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89
abstract runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext): any;
90
}
91
92
const APPLY_IN_EDITOR_ID = 'workbench.action.chat.applyInEditor';
93
94
export class CodeBlockActionRendering extends Disposable implements IWorkbenchContribution {
95
96
static readonly ID = 'chat.codeBlockActionRendering';
97
98
constructor(
99
@IActionViewItemService actionViewItemService: IActionViewItemService,
100
@IInstantiationService instantiationService: IInstantiationService,
101
@ILabelService labelService: ILabelService,
102
) {
103
super();
104
105
const disposable = actionViewItemService.register(MenuId.ChatCodeBlock, APPLY_IN_EDITOR_ID, (action, options) => {
106
if (!(action instanceof MenuItemAction)) {
107
return undefined;
108
}
109
return instantiationService.createInstance(class extends MenuEntryActionViewItem {
110
protected override getTooltip(): string {
111
const context = this._context;
112
if (isCodeBlockActionContext(context) && context.codemapperUri) {
113
const label = labelService.getUriLabel(context.codemapperUri, { relative: true });
114
return localize('interactive.applyInEditorWithURL.label', "Apply to {0}", label);
115
}
116
return super.getTooltip();
117
}
118
override setActionContext(newContext: unknown): void {
119
super.setActionContext(newContext);
120
this.updateTooltip();
121
}
122
}, action, undefined);
123
});
124
125
// Reduces flicker a bit on reload/restart
126
markAsSingleton(disposable);
127
}
128
}
129
130
export function registerChatCodeBlockActions() {
131
registerAction2(class CopyCodeBlockAction extends Action2 {
132
constructor() {
133
super({
134
id: 'workbench.action.chat.copyCodeBlock',
135
title: localize2('interactive.copyCodeBlock.label', "Copy"),
136
f1: false,
137
category: CHAT_CATEGORY,
138
icon: Codicon.copy,
139
menu: {
140
id: MenuId.ChatCodeBlock,
141
group: 'navigation',
142
order: 30
143
}
144
});
145
}
146
147
run(accessor: ServicesAccessor, ...args: unknown[]) {
148
const context = args[0];
149
if (!isCodeBlockActionContext(context) || isResponseFiltered(context)) {
150
return;
151
}
152
153
const clipboardService = accessor.get(IClipboardService);
154
const aiEditTelemetryService = accessor.get(IAiEditTelemetryService);
155
clipboardService.writeText(context.code);
156
157
if (isResponseVM(context.element)) {
158
const chatService = accessor.get(IChatService);
159
const requestId = context.element.requestId;
160
const request = context.element.session.getItems().find(item => item.id === requestId && isRequestVM(item)) as IChatRequestViewModel | undefined;
161
chatService.notifyUserAction({
162
agentId: context.element.agent?.id,
163
command: context.element.slashCommand?.name,
164
sessionResource: context.element.sessionResource,
165
requestId: context.element.requestId,
166
result: context.element.result,
167
action: {
168
kind: 'copy',
169
codeBlockIndex: context.codeBlockIndex,
170
copyKind: ChatCopyKind.Toolbar,
171
copiedCharacters: context.code.length,
172
totalCharacters: context.code.length,
173
copiedText: context.code,
174
copiedLines: context.code.split('\n').length,
175
languageId: context.languageId,
176
totalLines: context.code.split('\n').length,
177
modelId: request?.modelId ?? ''
178
}
179
});
180
181
const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex);
182
aiEditTelemetryService.handleCodeAccepted({
183
acceptanceMethod: 'copyButton',
184
suggestionId: codeBlockInfo?.suggestionId,
185
editDeltaInfo: EditDeltaInfo.fromText(context.code),
186
feature: 'sideBarChat',
187
languageId: context.languageId,
188
modeId: context.element.model.request?.modeInfo?.modeId,
189
modelId: request?.modelId,
190
presentation: 'codeBlock',
191
applyCodeBlockSuggestionId: undefined,
192
source: undefined,
193
});
194
}
195
}
196
});
197
198
CopyAction?.addImplementation(50000, 'chat-codeblock', (accessor) => {
199
// get active code editor
200
const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
201
if (!editor) {
202
return false;
203
}
204
205
const editorModel = editor.getModel();
206
if (!editorModel) {
207
return false;
208
}
209
210
const context = getContextFromEditor(editor, accessor);
211
if (!context) {
212
return false;
213
}
214
215
const noSelection = editor.getSelections()?.length === 1 && editor.getSelection()?.isEmpty();
216
const copiedText = noSelection ?
217
editorModel.getValue() :
218
editor.getSelections()?.reduce((acc, selection) => acc + editorModel.getValueInRange(selection), '') ?? '';
219
const totalCharacters = editorModel.getValueLength();
220
221
// Report copy to extensions
222
const chatService = accessor.get(IChatService);
223
const aiEditTelemetryService = accessor.get(IAiEditTelemetryService);
224
const element = context.element as IChatResponseViewModel | undefined;
225
if (isResponseVM(element)) {
226
const requestId = element.requestId;
227
const request = element.session.getItems().find(item => item.id === requestId && isRequestVM(item)) as IChatRequestViewModel | undefined;
228
chatService.notifyUserAction({
229
agentId: element.agent?.id,
230
command: element.slashCommand?.name,
231
sessionResource: element.sessionResource,
232
requestId: element.requestId,
233
result: element.result,
234
action: {
235
kind: 'copy',
236
codeBlockIndex: context.codeBlockIndex,
237
copyKind: ChatCopyKind.Action,
238
copiedText,
239
copiedCharacters: copiedText.length,
240
totalCharacters,
241
languageId: context.languageId,
242
totalLines: context.code.split('\n').length,
243
copiedLines: copiedText.split('\n').length,
244
modelId: request?.modelId ?? ''
245
}
246
});
247
248
const codeBlockInfo = element.model.codeBlockInfos?.at(context.codeBlockIndex);
249
aiEditTelemetryService.handleCodeAccepted({
250
acceptanceMethod: 'copyManual',
251
suggestionId: codeBlockInfo?.suggestionId,
252
editDeltaInfo: EditDeltaInfo.fromText(copiedText),
253
feature: 'sideBarChat',
254
languageId: context.languageId,
255
modeId: element.model.request?.modeInfo?.modeId,
256
modelId: request?.modelId,
257
presentation: 'codeBlock',
258
applyCodeBlockSuggestionId: undefined,
259
source: undefined,
260
});
261
}
262
263
// Copy full cell if no selection, otherwise fall back on normal editor implementation
264
if (noSelection) {
265
accessor.get(IClipboardService).writeText(context.code);
266
return true;
267
}
268
269
return false;
270
});
271
272
registerAction2(class SmartApplyInEditorAction extends ChatCodeBlockAction {
273
274
private operation: ApplyCodeBlockOperation | undefined;
275
276
constructor() {
277
super({
278
id: APPLY_IN_EDITOR_ID,
279
title: localize2('interactive.applyInEditor.label', "Apply in Editor"),
280
precondition: ChatContextKeys.enabled,
281
f1: false,
282
category: CHAT_CATEGORY,
283
icon: Codicon.gitPullRequestGoToChanges,
284
285
menu: [
286
{
287
id: MenuId.ChatCodeBlock,
288
group: 'navigation',
289
when: ContextKeyExpr.and(
290
...shellLangIds.map(e => ContextKeyExpr.notEquals(EditorContextKeys.languageId.key, e))
291
),
292
order: 10
293
},
294
{
295
id: MenuId.ChatCodeBlock,
296
when: ContextKeyExpr.or(
297
...shellLangIds.map(e => ContextKeyExpr.equals(EditorContextKeys.languageId.key, e))
298
)
299
},
300
],
301
keybinding: {
302
when: ContextKeyExpr.or(ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate()), accessibleViewInCodeBlock),
303
primary: KeyMod.CtrlCmd | KeyCode.Enter,
304
mac: { primary: KeyMod.WinCtrl | KeyCode.Enter },
305
weight: KeybindingWeight.ExternalExtension + 1
306
},
307
});
308
}
309
310
override runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {
311
if (!this.operation) {
312
this.operation = accessor.get(IInstantiationService).createInstance(ApplyCodeBlockOperation);
313
}
314
return this.operation.run(context);
315
}
316
});
317
318
registerAction2(class InsertAtCursorAction extends ChatCodeBlockAction {
319
constructor() {
320
super({
321
id: 'workbench.action.chat.insertCodeBlock',
322
title: localize2('interactive.insertCodeBlock.label', "Insert At Cursor"),
323
precondition: ChatContextKeys.enabled,
324
f1: true,
325
category: CHAT_CATEGORY,
326
icon: Codicon.insert,
327
menu: [{
328
id: MenuId.ChatCodeBlock,
329
group: 'navigation',
330
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.location.notEqualsTo(ChatAgentLocation.Terminal)),
331
order: 20
332
}, {
333
id: MenuId.ChatCodeBlock,
334
group: 'navigation',
335
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Terminal)),
336
isHiddenByDefault: true,
337
order: 20
338
}],
339
keybinding: {
340
when: ContextKeyExpr.or(ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate()), accessibleViewInCodeBlock),
341
primary: KeyMod.CtrlCmd | KeyCode.Enter,
342
mac: { primary: KeyMod.WinCtrl | KeyCode.Enter },
343
weight: KeybindingWeight.ExternalExtension + 1
344
},
345
});
346
}
347
348
override runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {
349
const operation = accessor.get(IInstantiationService).createInstance(InsertCodeBlockOperation);
350
return operation.run(context);
351
}
352
});
353
354
registerAction2(class InsertIntoNewFileAction extends ChatCodeBlockAction {
355
constructor() {
356
super({
357
id: 'workbench.action.chat.insertIntoNewFile',
358
title: localize2('interactive.insertIntoNewFile.label', "Insert into New File"),
359
precondition: ChatContextKeys.enabled,
360
f1: true,
361
category: CHAT_CATEGORY,
362
icon: Codicon.newFile,
363
menu: {
364
id: MenuId.ChatCodeBlock,
365
group: 'navigation',
366
isHiddenByDefault: true,
367
order: 40,
368
}
369
});
370
}
371
372
override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {
373
if (isResponseFiltered(context)) {
374
// When run from command palette
375
return;
376
}
377
378
const editorService = accessor.get(IEditorService);
379
const chatService = accessor.get(IChatService);
380
const aiEditTelemetryService = accessor.get(IAiEditTelemetryService);
381
382
editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined } satisfies IUntitledTextResourceEditorInput);
383
384
if (isResponseVM(context.element)) {
385
const requestId = context.element.requestId;
386
const request = context.element.session.getItems().find(item => item.id === requestId && isRequestVM(item)) as IChatRequestViewModel | undefined;
387
chatService.notifyUserAction({
388
agentId: context.element.agent?.id,
389
command: context.element.slashCommand?.name,
390
sessionResource: context.element.sessionResource,
391
requestId: context.element.requestId,
392
result: context.element.result,
393
action: {
394
kind: 'insert',
395
codeBlockIndex: context.codeBlockIndex,
396
totalCharacters: context.code.length,
397
newFile: true,
398
totalLines: context.code.split('\n').length,
399
languageId: context.languageId,
400
modelId: request?.modelId ?? ''
401
}
402
});
403
404
const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex);
405
406
aiEditTelemetryService.handleCodeAccepted({
407
acceptanceMethod: 'insertInNewFile',
408
suggestionId: codeBlockInfo?.suggestionId,
409
editDeltaInfo: EditDeltaInfo.fromText(context.code),
410
feature: 'sideBarChat',
411
languageId: context.languageId,
412
modeId: context.element.model.request?.modeInfo?.modeId,
413
modelId: request?.modelId,
414
presentation: 'codeBlock',
415
applyCodeBlockSuggestionId: undefined,
416
source: undefined,
417
});
418
}
419
}
420
});
421
422
registerAction2(class RunInTerminalAction extends ChatCodeBlockAction {
423
constructor() {
424
super({
425
id: 'workbench.action.chat.runInTerminal',
426
title: localize2('interactive.runInTerminal.label', "Insert into Terminal"),
427
precondition: ChatContextKeys.enabled,
428
f1: true,
429
category: CHAT_CATEGORY,
430
icon: Codicon.terminal,
431
menu: [{
432
id: MenuId.ChatCodeBlock,
433
group: 'navigation',
434
when: ContextKeyExpr.and(
435
ChatContextKeys.inChatSession,
436
ContextKeyExpr.or(...shellLangIds.map(e => ContextKeyExpr.equals(EditorContextKeys.languageId.key, e)))
437
),
438
},
439
{
440
id: MenuId.ChatCodeBlock,
441
group: 'navigation',
442
isHiddenByDefault: true,
443
when: ContextKeyExpr.and(
444
ChatContextKeys.inChatSession,
445
...shellLangIds.map(e => ContextKeyExpr.notEquals(EditorContextKeys.languageId.key, e))
446
)
447
}],
448
keybinding: [{
449
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter,
450
mac: {
451
primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter
452
},
453
weight: KeybindingWeight.EditorContrib,
454
when: ContextKeyExpr.or(ChatContextKeys.inChatSession, accessibleViewInCodeBlock),
455
}]
456
});
457
}
458
459
override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) {
460
if (isResponseFiltered(context)) {
461
// When run from command palette
462
return;
463
}
464
465
const chatService = accessor.get(IChatService);
466
const terminalService = accessor.get(ITerminalService);
467
const editorService = accessor.get(IEditorService);
468
const terminalEditorService = accessor.get(ITerminalEditorService);
469
const terminalGroupService = accessor.get(ITerminalGroupService);
470
471
let terminal = await terminalService.getActiveOrCreateInstance({ acceptsInput: true });
472
473
// isFeatureTerminal = debug terminal or task terminal
474
if (terminal.xterm?.isStdinDisabled || terminal.shellLaunchConfig.isFeatureTerminal) {
475
terminal = await terminalService.createAndFocusTerminal({ location: TerminalLocation.Panel });
476
} else {
477
await terminalService.focusInstance(terminal);
478
}
479
480
if (terminal.target === TerminalLocation.Editor) {
481
const existingEditors = editorService.findEditors(terminal.resource);
482
terminalEditorService.openEditor(terminal, { viewColumn: existingEditors?.[0].groupId });
483
} else {
484
await terminalGroupService.showPanel(true);
485
}
486
487
terminal.runCommand(context.code, false);
488
489
if (isResponseVM(context.element)) {
490
chatService.notifyUserAction({
491
agentId: context.element.agent?.id,
492
command: context.element.slashCommand?.name,
493
sessionResource: context.element.sessionResource,
494
requestId: context.element.requestId,
495
result: context.element.result,
496
action: {
497
kind: 'runInTerminal',
498
codeBlockIndex: context.codeBlockIndex,
499
languageId: context.languageId,
500
}
501
});
502
}
503
}
504
});
505
506
function navigateCodeBlocks(accessor: ServicesAccessor, reverse?: boolean): void {
507
const codeEditorService = accessor.get(ICodeEditorService);
508
const chatWidgetService = accessor.get(IChatWidgetService);
509
const widget = chatWidgetService.lastFocusedWidget;
510
if (!widget) {
511
return;
512
}
513
514
const editor = codeEditorService.getFocusedCodeEditor();
515
const editorUri = editor?.getModel()?.uri;
516
const curCodeBlockInfo = editorUri ? widget.getCodeBlockInfoForEditor(editorUri) : undefined;
517
const focused = !widget.inputEditor.hasWidgetFocus() && widget.getFocus();
518
const focusedResponse = isResponseVM(focused) ? focused : undefined;
519
520
const elementId = curCodeBlockInfo?.elementId;
521
const element = elementId ? widget.viewModel?.getItems().find(item => item.id === elementId) : undefined;
522
const currentResponse = element ??
523
(focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item)));
524
if (!currentResponse || !isResponseVM(currentResponse)) {
525
return;
526
}
527
528
widget.reveal(currentResponse);
529
const responseCodeblocks = widget.getCodeBlockInfosForResponse(currentResponse);
530
const focusIdx = curCodeBlockInfo ?
531
(curCodeBlockInfo.codeBlockIndex + (reverse ? -1 : 1) + responseCodeblocks.length) % responseCodeblocks.length :
532
reverse ? responseCodeblocks.length - 1 : 0;
533
534
responseCodeblocks[focusIdx]?.focus();
535
}
536
537
registerAction2(class NextCodeBlockAction extends Action2 {
538
constructor() {
539
super({
540
id: 'workbench.action.chat.nextCodeBlock',
541
title: localize2('interactive.nextCodeBlock.label', "Next Code Block"),
542
keybinding: {
543
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown,
544
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, },
545
weight: KeybindingWeight.WorkbenchContrib,
546
when: ChatContextKeys.inChatSession,
547
},
548
precondition: ChatContextKeys.enabled,
549
f1: true,
550
category: CHAT_CATEGORY,
551
});
552
}
553
554
run(accessor: ServicesAccessor, ...args: unknown[]) {
555
navigateCodeBlocks(accessor);
556
}
557
});
558
559
registerAction2(class PreviousCodeBlockAction extends Action2 {
560
constructor() {
561
super({
562
id: 'workbench.action.chat.previousCodeBlock',
563
title: localize2('interactive.previousCodeBlock.label', "Previous Code Block"),
564
keybinding: {
565
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp,
566
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, },
567
weight: KeybindingWeight.WorkbenchContrib,
568
when: ChatContextKeys.inChatSession,
569
},
570
precondition: ChatContextKeys.enabled,
571
f1: true,
572
category: CHAT_CATEGORY,
573
});
574
}
575
576
run(accessor: ServicesAccessor, ...args: unknown[]) {
577
navigateCodeBlocks(accessor, true);
578
}
579
});
580
}
581
582
function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined {
583
const chatWidgetService = accessor.get(IChatWidgetService);
584
const chatCodeBlockContextProviderService = accessor.get(IChatCodeBlockContextProviderService);
585
const model = editor.getModel();
586
if (!model) {
587
return;
588
}
589
590
const widget = chatWidgetService.lastFocusedWidget;
591
const codeBlockInfo = widget?.getCodeBlockInfoForEditor(model.uri);
592
if (!codeBlockInfo) {
593
for (const provider of chatCodeBlockContextProviderService.providers) {
594
const context = provider.getCodeBlockContext(editor);
595
if (context) {
596
return context;
597
}
598
}
599
return;
600
}
601
602
const element = widget?.viewModel?.getItems().find(item => item.id === codeBlockInfo.elementId);
603
return {
604
element,
605
codeBlockIndex: codeBlockInfo.codeBlockIndex,
606
code: editor.getValue(),
607
languageId: editor.getModel()!.getLanguageId(),
608
codemapperUri: codeBlockInfo.codemapperUri,
609
chatSessionResource: codeBlockInfo.chatSessionResource,
610
};
611
}
612
613
export function registerChatCodeCompareBlockActions() {
614
615
abstract class ChatCompareCodeBlockAction extends Action2 {
616
run(accessor: ServicesAccessor, ...args: unknown[]) {
617
const context = args[0];
618
if (!isCodeCompareBlockActionContext(context)) {
619
return;
620
// TODO@jrieken derive context
621
}
622
623
return this.runWithContext(accessor, context);
624
}
625
626
// eslint-disable-next-line @typescript-eslint/no-explicit-any
627
abstract runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): any;
628
}
629
630
registerAction2(class ApplyEditsCompareBlockAction extends ChatCompareCodeBlockAction {
631
constructor() {
632
super({
633
id: 'workbench.action.chat.applyCompareEdits',
634
title: localize2('interactive.compare.apply', "Apply Edits"),
635
f1: false,
636
category: CHAT_CATEGORY,
637
icon: Codicon.gitPullRequestGoToChanges,
638
precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, ChatContextKeys.editApplied.negate(), EditorContextKeys.readOnly.negate()),
639
menu: {
640
id: MenuId.ChatCompareBlock,
641
group: 'navigation',
642
order: 10,
643
when: EditorContextKeys.readOnly.negate(),
644
}
645
});
646
}
647
648
// eslint-disable-next-line @typescript-eslint/no-explicit-any
649
async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<any> {
650
651
const instaService = accessor.get(IInstantiationService);
652
const editorService = accessor.get(ICodeEditorService);
653
654
const item = context.edit;
655
const response = context.element;
656
657
if (item.state?.applied) {
658
// already applied
659
return false;
660
}
661
662
if (!response.response.value.includes(item)) {
663
// bogous item
664
return false;
665
}
666
667
const firstEdit = item.edits[0]?.[0];
668
if (!firstEdit) {
669
return false;
670
}
671
const textEdits = AsyncIterableProducer.fromArray(item.edits);
672
673
const editorToApply = await editorService.openCodeEditor({ resource: item.uri }, null);
674
if (editorToApply) {
675
editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber);
676
instaService.invokeFunction(reviewEdits, editorToApply, textEdits, CancellationToken.None, undefined);
677
response.setEditApplied(item, 1);
678
return true;
679
}
680
return false;
681
}
682
});
683
684
registerAction2(class DiscardEditsCompareBlockAction extends ChatCompareCodeBlockAction {
685
constructor() {
686
super({
687
id: 'workbench.action.chat.discardCompareEdits',
688
title: localize2('interactive.compare.discard', "Discard Edits"),
689
f1: false,
690
category: CHAT_CATEGORY,
691
icon: Codicon.trash,
692
precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, ChatContextKeys.editApplied.negate(), EditorContextKeys.readOnly.negate()),
693
menu: {
694
id: MenuId.ChatCompareBlock,
695
group: 'navigation',
696
order: 11,
697
when: EditorContextKeys.readOnly.negate(),
698
}
699
});
700
}
701
702
// eslint-disable-next-line @typescript-eslint/no-explicit-any
703
async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<any> {
704
const instaService = accessor.get(IInstantiationService);
705
const editor = instaService.createInstance(DefaultChatTextEditor);
706
editor.discard(context.element, context.edit);
707
}
708
});
709
710
registerAction2(class ToggleDiffViewModeAction extends ChatCompareCodeBlockAction {
711
constructor() {
712
super({
713
id: 'workbench.action.chat.toggleCompareBlockDiffViewMode',
714
title: localize2('interactive.compare.toggleDiffViewMode', "Toggle Inline/Side-by-Side Diff"),
715
f1: false,
716
category: CHAT_CATEGORY,
717
icon: Codicon.diffSingle,
718
toggled: {
719
condition: EditorContextKeys.diffEditorInlineMode,
720
icon: Codicon.diff,
721
},
722
menu: {
723
id: MenuId.ChatCompareBlock,
724
group: 'navigation',
725
order: 1,
726
}
727
});
728
}
729
730
runWithContext(_accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): void {
731
context.toggleDiffViewMode();
732
}
733
});
734
735
registerAction2(class OpenCompareBlockInDiffEditor extends ChatCompareCodeBlockAction {
736
constructor() {
737
super({
738
id: 'workbench.action.chat.openCompareBlockInDiffEditor',
739
title: localize2('interactive.compare.openInDiffEditor', "Open in Diff Editor"),
740
f1: false,
741
category: CHAT_CATEGORY,
742
icon: Codicon.goToFile,
743
menu: {
744
id: MenuId.ChatCompareBlock,
745
group: 'navigation',
746
order: 2,
747
}
748
});
749
}
750
751
async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<void> {
752
const editorService = accessor.get(IEditorService);
753
const model = context.diffEditor.getModel();
754
if (!model) {
755
return;
756
}
757
758
await editorService.openEditor({
759
original: { resource: model.original.uri },
760
modified: { resource: model.modified.uri },
761
});
762
}
763
});
764
}
765
766