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