Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.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 { CancellationToken } from '../../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../../base/common/codicons.js';
8
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
9
import { basename } from '../../../../../base/common/resources.js';
10
import { URI, UriComponents } from '../../../../../base/common/uri.js';
11
import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
12
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
13
import { Position } from '../../../../../editor/common/core/position.js';
14
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
15
import { isLocation, Location } from '../../../../../editor/common/languages.js';
16
import { ITextModel } from '../../../../../editor/common/model.js';
17
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
18
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
19
import { localize, localize2 } from '../../../../../nls.js';
20
import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
21
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
22
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
23
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
24
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
25
import { EditorActivation } from '../../../../../platform/editor/common/editor.js';
26
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
27
import { IEditorPane } from '../../../../common/editor.js';
28
import { IEditorService } from '../../../../services/editor/common/editorService.js';
29
import { isChatViewTitleActionContext } from '../../common/chatActions.js';
30
import { ChatContextKeys } from '../../common/chatContextKeys.js';
31
import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js';
32
import { IChatService } from '../../common/chatService.js';
33
import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js';
34
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
35
import { CHAT_CATEGORY } from '../actions/chatActions.js';
36
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
37
38
export abstract class EditingSessionAction extends Action2 {
39
40
constructor(opts: Readonly<IAction2Options>) {
41
super({
42
category: CHAT_CATEGORY,
43
...opts
44
});
45
}
46
47
run(accessor: ServicesAccessor, ...args: any[]) {
48
const context = getEditingSessionContext(accessor, args);
49
if (!context || !context.editingSession) {
50
return;
51
}
52
53
return this.runEditingSessionAction(accessor, context.editingSession, context.chatWidget, ...args);
54
}
55
56
abstract runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): any;
57
}
58
59
export function getEditingSessionContext(accessor: ServicesAccessor, args: any[]): { editingSession?: IChatEditingSession; chatWidget: IChatWidget } | undefined {
60
const arg0 = args.at(0);
61
const context = isChatViewTitleActionContext(arg0) ? arg0 : undefined;
62
63
const chatWidgetService = accessor.get(IChatWidgetService);
64
const chatEditingService = accessor.get(IChatEditingService);
65
let chatWidget = context ? chatWidgetService.getWidgetBySessionId(context.sessionId) : undefined;
66
if (!chatWidget) {
67
chatWidget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel).find(w => w.supportsChangingModes);
68
}
69
70
if (!chatWidget?.viewModel) {
71
return;
72
}
73
74
const chatSessionId = chatWidget.viewModel.model.sessionId;
75
const editingSession = chatEditingService.getEditingSession(chatSessionId);
76
77
if (!editingSession) {
78
return;
79
}
80
81
return { editingSession, chatWidget };
82
}
83
84
85
abstract class WorkingSetAction extends EditingSessionAction {
86
87
runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) {
88
89
const uris: URI[] = [];
90
if (URI.isUri(args[0])) {
91
uris.push(args[0]);
92
} else if (chatWidget) {
93
uris.push(...chatWidget.input.selectedElements);
94
}
95
if (!uris.length) {
96
return;
97
}
98
99
return this.runWorkingSetAction(accessor, editingSession, chatWidget, ...uris);
100
}
101
102
abstract runWorkingSetAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget | undefined, ...uris: URI[]): any;
103
}
104
105
registerAction2(class OpenFileInDiffAction extends WorkingSetAction {
106
constructor() {
107
super({
108
id: 'chatEditing.openFileInDiff',
109
title: localize2('open.fileInDiff', 'Open Changes in Diff Editor'),
110
icon: Codicon.diffSingle,
111
menu: [{
112
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
113
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
114
order: 2,
115
group: 'navigation'
116
}],
117
});
118
}
119
120
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, _chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
121
const editorService = accessor.get(IEditorService);
122
123
124
for (const uri of uris) {
125
126
let pane: IEditorPane | undefined = editorService.activeEditorPane;
127
if (!pane) {
128
pane = await editorService.openEditor({ resource: uri });
129
}
130
131
if (!pane) {
132
return;
133
}
134
135
const editedFile = currentEditingSession.getEntry(uri);
136
editedFile?.getEditorIntegration(pane).toggleDiff(undefined, true);
137
}
138
}
139
});
140
141
registerAction2(class AcceptAction extends WorkingSetAction {
142
constructor() {
143
super({
144
id: 'chatEditing.acceptFile',
145
title: localize2('accept.file', 'Keep'),
146
icon: Codicon.check,
147
precondition: ChatContextKeys.requestInProgress.negate(),
148
menu: [{
149
when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),
150
id: MenuId.MultiDiffEditorFileToolbar,
151
order: 0,
152
group: 'navigation',
153
}, {
154
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
155
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
156
order: 0,
157
group: 'navigation'
158
}],
159
});
160
}
161
162
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
163
await currentEditingSession.accept(...uris);
164
}
165
});
166
167
registerAction2(class DiscardAction extends WorkingSetAction {
168
constructor() {
169
super({
170
id: 'chatEditing.discardFile',
171
title: localize2('discard.file', 'Undo'),
172
icon: Codicon.discard,
173
precondition: ChatContextKeys.requestInProgress.negate(),
174
menu: [{
175
when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),
176
id: MenuId.MultiDiffEditorFileToolbar,
177
order: 2,
178
group: 'navigation',
179
}, {
180
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
181
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
182
order: 1,
183
group: 'navigation'
184
}],
185
});
186
}
187
188
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
189
await currentEditingSession.reject(...uris);
190
}
191
});
192
193
export class ChatEditingAcceptAllAction extends EditingSessionAction {
194
195
constructor() {
196
super({
197
id: 'chatEditing.acceptAllFiles',
198
title: localize('accept', 'Keep'),
199
icon: Codicon.check,
200
tooltip: localize('acceptAllEdits', 'Keep All Edits'),
201
precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey),
202
keybinding: {
203
primary: KeyMod.CtrlCmd | KeyCode.Enter,
204
when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput),
205
weight: KeybindingWeight.WorkbenchContrib,
206
},
207
menu: [
208
209
{
210
id: MenuId.ChatEditingWidgetToolbar,
211
group: 'navigation',
212
order: 0,
213
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey))
214
}
215
]
216
});
217
}
218
219
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) {
220
await editingSession.accept();
221
}
222
}
223
registerAction2(ChatEditingAcceptAllAction);
224
225
export class ChatEditingDiscardAllAction extends EditingSessionAction {
226
227
constructor() {
228
super({
229
id: 'chatEditing.discardAllFiles',
230
title: localize('discard', 'Undo'),
231
icon: Codicon.discard,
232
tooltip: localize('discardAllEdits', 'Undo All Edits'),
233
precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey),
234
menu: [
235
{
236
id: MenuId.ChatEditingWidgetToolbar,
237
group: 'navigation',
238
order: 1,
239
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), hasUndecidedChatEditingResourceContextKey)
240
}
241
],
242
keybinding: {
243
when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput, ChatContextKeys.inputHasText.negate()),
244
weight: KeybindingWeight.WorkbenchContrib,
245
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
246
},
247
});
248
}
249
250
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]) {
251
await discardAllEditsWithConfirmation(accessor, editingSession);
252
}
253
}
254
registerAction2(ChatEditingDiscardAllAction);
255
256
export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession): Promise<boolean> {
257
258
const dialogService = accessor.get(IDialogService);
259
260
// Ask for confirmation if there are any edits
261
const entries = currentEditingSession.entries.get();
262
if (entries.length > 0) {
263
const confirmation = await dialogService.confirm({
264
title: localize('chat.editing.discardAll.confirmation.title', "Undo all edits?"),
265
message: entries.length === 1
266
? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made in {0}. Do you want to proceed?", basename(entries[0].modifiedURI))
267
: localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made in {0} files. Do you want to proceed?", entries.length),
268
primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"),
269
type: 'info'
270
});
271
if (!confirmation.confirmed) {
272
return false;
273
}
274
}
275
276
await currentEditingSession.reject();
277
return true;
278
}
279
280
export class ChatEditingShowChangesAction extends EditingSessionAction {
281
static readonly ID = 'chatEditing.viewChanges';
282
static readonly LABEL = localize('chatEditing.viewChanges', 'View All Edits');
283
284
constructor() {
285
super({
286
id: ChatEditingShowChangesAction.ID,
287
title: { value: ChatEditingShowChangesAction.LABEL, original: ChatEditingShowChangesAction.LABEL },
288
tooltip: ChatEditingShowChangesAction.LABEL,
289
f1: true,
290
icon: Codicon.diffMultiple,
291
precondition: hasUndecidedChatEditingResourceContextKey,
292
menu: [
293
{
294
id: MenuId.ChatEditingWidgetToolbar,
295
group: 'navigation',
296
order: 4,
297
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey))
298
}
299
],
300
});
301
}
302
303
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise<void> {
304
await editingSession.show();
305
}
306
}
307
registerAction2(ChatEditingShowChangesAction);
308
309
async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise<void> {
310
const configurationService = accessor.get(IConfigurationService);
311
const dialogService = accessor.get(IDialogService);
312
const chatWidgetService = accessor.get(IChatWidgetService);
313
const widget = chatWidgetService.lastFocusedWidget;
314
const chatService = accessor.get(IChatService);
315
const chatModel = chatService.getSession(item.sessionId);
316
if (!chatModel) {
317
return;
318
}
319
320
const session = chatModel.editingSession;
321
if (!session) {
322
return;
323
}
324
325
const requestId = isRequestVM(item) ? item.id :
326
isResponseVM(item) ? item.requestId : undefined;
327
328
if (requestId) {
329
const chatRequests = chatModel.getRequests();
330
const itemIndex = chatRequests.findIndex(request => request.id === requestId);
331
const editsToUndo = chatRequests.length - itemIndex;
332
333
const requestsToRemove = chatRequests.slice(itemIndex);
334
const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));
335
const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
336
const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;
337
338
let message: string;
339
if (editsToUndo === 1) {
340
if (entriesModifiedInRequestsToRemove.length === 1) {
341
message = localize('chat.removeLast.confirmation.message2', "This will remove your last request and undo the edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
342
} else {
343
message = localize('chat.removeLast.confirmation.multipleEdits.message', "This will remove your last request and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
344
}
345
} else {
346
if (entriesModifiedInRequestsToRemove.length === 1) {
347
message = localize('chat.remove.confirmation.message2', "This will remove all subsequent requests and undo edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
348
} else {
349
message = localize('chat.remove.confirmation.multipleEdits.message', "This will remove all subsequent requests and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
350
}
351
}
352
353
const confirmation = shouldPrompt
354
? await dialogService.confirm({
355
title: editsToUndo === 1
356
? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")
357
: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),
358
message: message,
359
primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),
360
checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },
361
type: 'info'
362
})
363
: { confirmed: true };
364
365
if (!confirmation.confirmed) {
366
widget?.viewModel?.model.setCheckpoint(undefined);
367
return;
368
}
369
370
if (confirmation.checkboxChecked) {
371
await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);
372
}
373
374
// Restore the snapshot to what it was before the request(s) that we deleted
375
const snapshotRequestId = chatRequests[itemIndex].id;
376
await session.restoreSnapshot(snapshotRequestId, undefined);
377
}
378
}
379
380
registerAction2(class RemoveAction extends Action2 {
381
constructor() {
382
super({
383
id: 'workbench.action.chat.undoEdits',
384
title: localize2('chat.undoEdits.label', "Undo Requests"),
385
f1: false,
386
category: CHAT_CATEGORY,
387
icon: Codicon.discard,
388
keybinding: {
389
primary: KeyCode.Delete,
390
mac: {
391
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
392
},
393
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
394
weight: KeybindingWeight.WorkbenchContrib,
395
},
396
menu: [
397
{
398
id: MenuId.ChatMessageTitle,
399
group: 'navigation',
400
order: 2,
401
when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate(), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, false), ChatContextKeys.lockedToCodingAgent.negate()),
402
}
403
]
404
});
405
}
406
407
async run(accessor: ServicesAccessor, ...args: any[]) {
408
let item: ChatTreeItem | undefined = args[0];
409
const chatWidgetService = accessor.get(IChatWidgetService);
410
const configurationService = accessor.get(IConfigurationService);
411
const widget = chatWidgetService.lastFocusedWidget;
412
if (!isResponseVM(item) && !isRequestVM(item)) {
413
item = widget?.getFocus();
414
}
415
416
if (!item) {
417
return;
418
}
419
420
await restoreSnapshotWithConfirmation(accessor, item);
421
422
if (isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) {
423
widget?.focusInput();
424
widget?.input.setValue(item.messageText, false);
425
}
426
}
427
});
428
429
registerAction2(class RestoreCheckpointAction extends Action2 {
430
constructor() {
431
super({
432
id: 'workbench.action.chat.restoreCheckpoint',
433
title: localize2('chat.restoreCheckpoint.label', "Restore Checkpoint"),
434
tooltip: localize2('chat.restoreCheckpoint.tooltip', "Restores workspace and chat to this point"),
435
f1: false,
436
category: CHAT_CATEGORY,
437
keybinding: {
438
primary: KeyCode.Delete,
439
mac: {
440
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
441
},
442
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
443
weight: KeybindingWeight.WorkbenchContrib,
444
},
445
menu: [
446
{
447
id: MenuId.ChatMessageCheckpoint,
448
group: 'navigation',
449
order: 2,
450
when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.lockedToCodingAgent.negate())
451
}
452
]
453
});
454
}
455
456
async run(accessor: ServicesAccessor, ...args: any[]) {
457
let item: ChatTreeItem | undefined = args[0];
458
const chatWidgetService = accessor.get(IChatWidgetService);
459
const widget = chatWidgetService.lastFocusedWidget;
460
if (!isResponseVM(item) && !isRequestVM(item)) {
461
item = widget?.getFocus();
462
}
463
464
if (!item) {
465
return;
466
}
467
468
if (isRequestVM(item)) {
469
widget?.focusInput();
470
widget?.input.setValue(item.messageText, false);
471
}
472
473
widget?.viewModel?.model.setCheckpoint(item.id);
474
await restoreSnapshotWithConfirmation(accessor, item);
475
}
476
});
477
478
registerAction2(class RestoreLastCheckpoint extends Action2 {
479
constructor() {
480
super({
481
id: 'workbench.action.chat.restoreLastCheckpoint',
482
title: localize2('chat.restoreLastCheckpoint.label', "Restore to Last Checkpoint"),
483
f1: false,
484
category: CHAT_CATEGORY,
485
icon: Codicon.discard,
486
menu: [
487
{
488
id: MenuId.ChatMessageFooter,
489
group: 'navigation',
490
order: 1,
491
when: ContextKeyExpr.and(ContextKeyExpr.in(ChatContextKeys.itemId.key, ChatContextKeys.lastItemId.key), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true), ChatContextKeys.lockedToCodingAgent.negate()),
492
}
493
]
494
});
495
}
496
497
async run(accessor: ServicesAccessor, ...args: any[]) {
498
let item: ChatTreeItem | undefined = args[0];
499
const chatWidgetService = accessor.get(IChatWidgetService);
500
const chatService = accessor.get(IChatService);
501
const widget = chatWidgetService.lastFocusedWidget;
502
if (!isResponseVM(item) && !isRequestVM(item)) {
503
item = widget?.getFocus();
504
}
505
506
if (!item) {
507
return;
508
}
509
510
const chatModel = chatService.getSession(item.sessionId);
511
if (!chatModel) {
512
return;
513
}
514
515
const session = chatModel.editingSession;
516
if (!session) {
517
return;
518
}
519
520
await restoreSnapshotWithConfirmation(accessor, item);
521
522
if (isResponseVM(item)) {
523
widget?.viewModel?.model.setCheckpoint(item.requestId);
524
const request = chatModel.getRequests().find(request => request.id === item.requestId);
525
if (request) {
526
widget?.focusInput();
527
widget?.input.setValue(request.message.text, false);
528
}
529
}
530
}
531
});
532
533
registerAction2(class EditAction extends Action2 {
534
constructor() {
535
super({
536
id: 'workbench.action.chat.editRequests',
537
title: localize2('chat.editRequests.label', "Edit Request"),
538
f1: false,
539
category: CHAT_CATEGORY,
540
icon: Codicon.edit,
541
keybinding: {
542
primary: KeyCode.Enter,
543
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
544
weight: KeybindingWeight.WorkbenchContrib,
545
},
546
menu: [
547
{
548
id: MenuId.ChatMessageTitle,
549
group: 'navigation',
550
order: 2,
551
when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input')))
552
}
553
]
554
});
555
}
556
557
async run(accessor: ServicesAccessor, ...args: any[]) {
558
let item: ChatTreeItem | undefined = args[0];
559
const chatWidgetService = accessor.get(IChatWidgetService);
560
const widget = chatWidgetService.lastFocusedWidget;
561
if (!isResponseVM(item) && !isRequestVM(item)) {
562
item = widget?.getFocus();
563
}
564
565
if (!item) {
566
return;
567
}
568
569
if (isRequestVM(item)) {
570
widget?.startEditing(item.id);
571
}
572
}
573
});
574
575
registerAction2(class OpenWorkingSetHistoryAction extends Action2 {
576
577
static readonly id = 'chat.openFileUpdatedBySnapshot';
578
constructor() {
579
super({
580
id: OpenWorkingSetHistoryAction.id,
581
title: localize('chat.openFileUpdatedBySnapshot.label', "Open File"),
582
menu: [{
583
id: MenuId.ChatEditingCodeBlockContext,
584
group: 'navigation',
585
order: 0,
586
},]
587
});
588
}
589
590
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
591
const context: { sessionId: string; requestId: string; uri: URI; stopId: string | undefined } | undefined = args[0];
592
if (!context?.sessionId) {
593
return;
594
}
595
596
const editorService = accessor.get(IEditorService);
597
await editorService.openEditor({ resource: context.uri });
598
}
599
});
600
601
registerAction2(class OpenWorkingSetHistoryAction extends Action2 {
602
603
static readonly id = 'chat.openFileSnapshot';
604
constructor() {
605
super({
606
id: OpenWorkingSetHistoryAction.id,
607
title: localize('chat.openSnapshot.label', "Open File Snapshot"),
608
menu: [{
609
id: MenuId.ChatEditingCodeBlockContext,
610
group: 'navigation',
611
order: 1,
612
},]
613
});
614
}
615
616
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
617
const context: { sessionId: string; requestId: string; uri: URI; stopId: string | undefined } | undefined = args[0];
618
if (!context?.sessionId) {
619
return;
620
}
621
622
const chatService = accessor.get(IChatService);
623
const chatEditingService = accessor.get(IChatEditingService);
624
const editorService = accessor.get(IEditorService);
625
626
const chatModel = chatService.getSession(context.sessionId);
627
if (!chatModel) {
628
return;
629
}
630
631
const snapshot = chatEditingService.getEditingSession(chatModel.sessionId)?.getSnapshotUri(context.requestId, context.uri, context.stopId);
632
if (snapshot) {
633
const editor = await editorService.openEditor({ resource: snapshot, label: localize('chatEditing.snapshot', '{0} (Snapshot)', basename(context.uri)), options: { transient: true, activation: EditorActivation.ACTIVATE } });
634
if (isCodeEditor(editor)) {
635
editor.updateOptions({ readOnly: true });
636
}
637
}
638
}
639
});
640
641
registerAction2(class ResolveSymbolsContextAction extends EditingSessionAction {
642
constructor() {
643
super({
644
id: 'workbench.action.edits.addFilesFromReferences',
645
title: localize2('addFilesFromReferences', "Add Files From References"),
646
f1: false,
647
category: CHAT_CATEGORY,
648
menu: {
649
id: MenuId.ChatInputSymbolAttachmentContext,
650
group: 'navigation',
651
order: 1,
652
when: ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), EditorContextKeys.hasReferenceProvider)
653
}
654
});
655
}
656
657
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise<void> {
658
if (args.length === 0 || !isLocation(args[0])) {
659
return;
660
}
661
662
const textModelService = accessor.get(ITextModelService);
663
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
664
const symbol = args[0] as Location;
665
666
const modelReference = await textModelService.createModelReference(symbol.uri);
667
const textModel = modelReference.object.textEditorModel;
668
if (!textModel) {
669
return;
670
}
671
672
const position = new Position(symbol.range.startLineNumber, symbol.range.startColumn);
673
674
const [references, definitions, implementations] = await Promise.all([
675
this.getReferences(position, textModel, languageFeaturesService),
676
this.getDefinitions(position, textModel, languageFeaturesService),
677
this.getImplementations(position, textModel, languageFeaturesService)
678
]);
679
680
// Sort the references, definitions and implementations by
681
// how important it is that they make it into the working set as it has limited size
682
const attachments = [];
683
for (const reference of [...definitions, ...implementations, ...references]) {
684
attachments.push(chatWidget.attachmentModel.asFileVariableEntry(reference.uri));
685
}
686
687
chatWidget.attachmentModel.addContext(...attachments);
688
}
689
690
private async getReferences(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
691
const referenceProviders = languageFeaturesService.referenceProvider.all(textModel);
692
693
const references = await Promise.all(referenceProviders.map(async (referenceProvider) => {
694
return await referenceProvider.provideReferences(textModel, position, { includeDeclaration: true }, CancellationToken.None) ?? [];
695
}));
696
697
return references.flat();
698
}
699
700
private async getDefinitions(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
701
const definitionProviders = languageFeaturesService.definitionProvider.all(textModel);
702
703
const definitions = await Promise.all(definitionProviders.map(async (definitionProvider) => {
704
return await definitionProvider.provideDefinition(textModel, position, CancellationToken.None) ?? [];
705
}));
706
707
return definitions.flat();
708
}
709
710
private async getImplementations(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
711
const implementationProviders = languageFeaturesService.implementationProvider.all(textModel);
712
713
const implementations = await Promise.all(implementationProviders.map(async (implementationProvider) => {
714
return await implementationProvider.provideImplementation(textModel, position, CancellationToken.None) ?? [];
715
}));
716
717
return implementations.flat();
718
}
719
});
720
721
export class ViewPreviousEditsAction extends EditingSessionAction {
722
static readonly Id = 'chatEditing.viewPreviousEdits';
723
static readonly Label = localize('chatEditing.viewPreviousEdits', 'View Previous Edits');
724
725
constructor() {
726
super({
727
id: ViewPreviousEditsAction.Id,
728
title: { value: ViewPreviousEditsAction.Label, original: ViewPreviousEditsAction.Label },
729
tooltip: ViewPreviousEditsAction.Label,
730
f1: true,
731
icon: Codicon.diffMultiple,
732
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, hasUndecidedChatEditingResourceContextKey.negate()),
733
menu: [
734
{
735
id: MenuId.ChatEditingWidgetToolbar,
736
group: 'navigation',
737
order: 4,
738
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey.negate()))
739
}
740
],
741
});
742
}
743
744
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: any[]): Promise<void> {
745
await editingSession.show(true);
746
}
747
}
748
registerAction2(ViewPreviousEditsAction);
749
750
/**
751
* Workbench command to explore accepting working set changes from an extension. Executing
752
* the command will accept the changes for the provided resources across all edit sessions.
753
*/
754
CommandsRegistry.registerCommand('_chat.editSessions.accept', async (accessor: ServicesAccessor, resources: UriComponents[]) => {
755
if (resources.length === 0) {
756
return;
757
}
758
759
const uris = resources.map(resource => URI.revive(resource));
760
const chatEditingService = accessor.get(IChatEditingService);
761
for (const editingSession of chatEditingService.editingSessionsObs.get()) {
762
await editingSession.accept(...uris);
763
}
764
});
765
766