Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.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 { DeferredPromise, isThenable } from '../../../../../base/common/async.js';
7
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
8
import { Codicon } from '../../../../../base/common/codicons.js';
9
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
10
import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
11
import { Schemas } from '../../../../../base/common/network.js';
12
import { autorun, observableValue } from '../../../../../base/common/observable.js';
13
import { ThemeIcon } from '../../../../../base/common/themables.js';
14
import { isObject } from '../../../../../base/common/types.js';
15
import { URI } from '../../../../../base/common/uri.js';
16
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
17
import { Range } from '../../../../../editor/common/core/range.js';
18
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
19
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
20
import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js';
21
import { localize, localize2 } from '../../../../../nls.js';
22
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
23
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
24
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
25
import { IFileService } from '../../../../../platform/files/common/files.js';
26
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
27
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
28
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
29
import { IListService } from '../../../../../platform/list/browser/listService.js';
30
import { ILogService } from '../../../../../platform/log/common/log.js';
31
import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js';
32
import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
33
import { resolveCommandsContext } from '../../../../browser/parts/editor/editorCommandsContext.js';
34
import { ResourceContextKey } from '../../../../common/contextkeys.js';
35
import { EditorResourceAccessor, isEditorCommandsContext, SideBySideEditor } from '../../../../common/editor.js';
36
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
37
import { IEditorService } from '../../../../services/editor/common/editorService.js';
38
import { IViewsService } from '../../../../services/views/common/viewsService.js';
39
import { ExplorerFolderContext } from '../../../files/common/files.js';
40
import { AnythingQuickAccessProvider } from '../../../search/browser/anythingQuickAccess.js';
41
import { isSearchTreeFileMatch, isSearchTreeMatch } from '../../../search/browser/searchTreeModel/searchTreeCommon.js';
42
import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../search/browser/symbolsQuickAccess.js';
43
import { SearchContext } from '../../../search/common/constants.js';
44
import { ChatContextKeys } from '../../common/chatContextKeys.js';
45
import { IChatRequestVariableEntry, OmittedState } from '../../common/chatVariableEntries.js';
46
import { ChatAgentLocation } from '../../common/constants.js';
47
import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js';
48
import { IChatContextPickerItem, IChatContextPickService, IChatContextValueItem, isChatContextPickerPickItem } from '../chatContextPickService.js';
49
import { isQuickChat } from '../chatWidget.js';
50
import { resizeImage } from '../imageUtils.js';
51
import { registerPromptActions } from '../promptSyntax/promptFileActions.js';
52
import { CHAT_CATEGORY } from './chatActions.js';
53
54
export function registerChatContextActions() {
55
registerAction2(AttachContextAction);
56
registerAction2(AttachFileToChatAction);
57
registerAction2(AttachFolderToChatAction);
58
registerAction2(AttachSelectionToChatAction);
59
registerAction2(AttachSearchResultAction);
60
registerPromptActions();
61
}
62
63
async function withChatView(accessor: ServicesAccessor): Promise<IChatWidget | undefined> {
64
const viewsService = accessor.get(IViewsService);
65
const chatWidgetService = accessor.get(IChatWidgetService);
66
67
const lastFocusedWidget = chatWidgetService.lastFocusedWidget;
68
if (!lastFocusedWidget || lastFocusedWidget.location === ChatAgentLocation.Panel) {
69
return showChatView(viewsService); // only show chat view if we either have no chat view or its located in view container
70
}
71
return lastFocusedWidget;
72
}
73
74
abstract class AttachResourceAction extends Action2 {
75
76
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
77
const instaService = accessor.get(IInstantiationService);
78
const widget = await instaService.invokeFunction(withChatView);
79
if (!widget) {
80
return;
81
}
82
return instaService.invokeFunction(this.runWithWidget.bind(this), widget, ...args);
83
}
84
85
abstract runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: any[]): Promise<void>;
86
87
protected _getResources(accessor: ServicesAccessor, ...args: any[]): URI[] {
88
const editorService = accessor.get(IEditorService);
89
90
const contexts = isEditorCommandsContext(args[1]) ? this._getEditorResources(accessor, args) : Array.isArray(args[1]) ? args[1] : [args[0]];
91
const files = [];
92
for (const context of contexts) {
93
let uri;
94
if (URI.isUri(context)) {
95
uri = context;
96
} else if (isSearchTreeFileMatch(context)) {
97
uri = context.resource;
98
} else if (isSearchTreeMatch(context)) {
99
uri = context.parent().resource;
100
} else if (!context && editorService.activeTextEditorControl) {
101
uri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
102
}
103
104
if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) {
105
files.push(uri);
106
}
107
}
108
109
return files;
110
}
111
112
private _getEditorResources(accessor: ServicesAccessor, ...args: any[]): URI[] {
113
const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService));
114
115
return resolvedContext.groupedEditors
116
.flatMap(groupedEditor => groupedEditor.editors)
117
.map(editor => EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }))
118
.filter(uri => uri !== undefined);
119
}
120
}
121
122
class AttachFileToChatAction extends AttachResourceAction {
123
124
static readonly ID = 'workbench.action.chat.attachFile';
125
126
constructor() {
127
super({
128
id: AttachFileToChatAction.ID,
129
title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"),
130
category: CHAT_CATEGORY,
131
precondition: ChatContextKeys.enabled,
132
f1: true,
133
menu: [{
134
id: MenuId.SearchContext,
135
group: 'z_chat',
136
order: 1,
137
when: ContextKeyExpr.and(ChatContextKeys.enabled, SearchContext.FileMatchOrMatchFocusKey, SearchContext.SearchResultHeaderFocused.negate()),
138
}, {
139
id: MenuId.ExplorerContext,
140
group: '5_chat',
141
order: 1,
142
when: ContextKeyExpr.and(
143
ChatContextKeys.enabled,
144
ExplorerFolderContext.negate(),
145
ContextKeyExpr.or(
146
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
147
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)
148
)
149
),
150
}, {
151
id: MenuId.EditorTitleContext,
152
group: '2_chat',
153
order: 1,
154
when: ContextKeyExpr.and(
155
ChatContextKeys.enabled,
156
ContextKeyExpr.or(
157
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
158
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)
159
)
160
),
161
}, {
162
id: MenuId.EditorContext,
163
group: '1_chat',
164
order: 2,
165
when: ContextKeyExpr.and(
166
ChatContextKeys.enabled,
167
ContextKeyExpr.or(
168
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
169
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote),
170
ResourceContextKey.Scheme.isEqualTo(Schemas.untitled),
171
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData)
172
)
173
)
174
}]
175
});
176
}
177
178
override async runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: any[]): Promise<void> {
179
const files = this._getResources(accessor, ...args);
180
if (!files.length) {
181
return;
182
}
183
if (widget) {
184
widget.focusInput();
185
for (const file of files) {
186
widget.attachmentModel.addFile(file);
187
}
188
}
189
}
190
}
191
192
class AttachFolderToChatAction extends AttachResourceAction {
193
194
static readonly ID = 'workbench.action.chat.attachFolder';
195
196
constructor() {
197
super({
198
id: AttachFolderToChatAction.ID,
199
title: localize2('workbench.action.chat.attachFolder.label', "Add Folder to Chat"),
200
category: CHAT_CATEGORY,
201
f1: false,
202
menu: {
203
id: MenuId.ExplorerContext,
204
group: '5_chat',
205
order: 1,
206
when: ContextKeyExpr.and(
207
ChatContextKeys.enabled,
208
ExplorerFolderContext,
209
ContextKeyExpr.or(
210
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
211
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)
212
)
213
)
214
}
215
});
216
}
217
218
override async runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: any[]): Promise<void> {
219
const folders = this._getResources(accessor, ...args);
220
if (!folders.length) {
221
return;
222
}
223
if (widget) {
224
widget.focusInput();
225
for (const folder of folders) {
226
widget.attachmentModel.addFolder(folder);
227
}
228
}
229
}
230
}
231
232
class AttachSelectionToChatAction extends Action2 {
233
234
static readonly ID = 'workbench.action.chat.attachSelection';
235
236
constructor() {
237
super({
238
id: AttachSelectionToChatAction.ID,
239
title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"),
240
category: CHAT_CATEGORY,
241
f1: true,
242
precondition: ChatContextKeys.enabled,
243
menu: {
244
id: MenuId.EditorContext,
245
group: '1_chat',
246
order: 1,
247
when: ContextKeyExpr.and(
248
ChatContextKeys.enabled,
249
EditorContextKeys.hasNonEmptySelection,
250
ContextKeyExpr.or(
251
ResourceContextKey.Scheme.isEqualTo(Schemas.file),
252
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote),
253
ResourceContextKey.Scheme.isEqualTo(Schemas.untitled),
254
ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData)
255
)
256
)
257
}
258
});
259
}
260
261
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
262
const editorService = accessor.get(IEditorService);
263
264
const widget = await accessor.get(IInstantiationService).invokeFunction(withChatView);
265
if (!widget) {
266
return;
267
}
268
269
const [_, matches] = args;
270
// If we have search matches, it means this is coming from the search widget
271
if (matches && matches.length > 0) {
272
const uris = new Map<URI, Range | undefined>();
273
for (const match of matches) {
274
if (isSearchTreeFileMatch(match)) {
275
uris.set(match.resource, undefined);
276
} else {
277
const context = { uri: match._parent.resource, range: match._range };
278
const range = uris.get(context.uri);
279
if (!range ||
280
range.startLineNumber !== context.range.startLineNumber && range.endLineNumber !== context.range.endLineNumber) {
281
uris.set(context.uri, context.range);
282
widget.attachmentModel.addFile(context.uri, context.range);
283
}
284
}
285
}
286
// Add the root files for all of the ones that didn't have a match
287
for (const uri of uris) {
288
const [resource, range] = uri;
289
if (!range) {
290
widget.attachmentModel.addFile(resource);
291
}
292
}
293
} else {
294
const activeEditor = editorService.activeTextEditorControl;
295
const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
296
if (activeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) {
297
const selection = activeEditor.getSelection();
298
if (selection) {
299
widget.focusInput();
300
const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection;
301
widget.attachmentModel.addFile(activeUri, range);
302
}
303
}
304
}
305
}
306
}
307
308
export class AttachSearchResultAction extends Action2 {
309
310
private static readonly Name = 'searchResults';
311
312
constructor() {
313
super({
314
id: 'workbench.action.chat.insertSearchResults',
315
title: localize2('chat.insertSearchResults', 'Add Search Results to Chat'),
316
category: CHAT_CATEGORY,
317
f1: false,
318
menu: [{
319
id: MenuId.SearchContext,
320
group: 'z_chat',
321
order: 3,
322
when: ContextKeyExpr.and(
323
ChatContextKeys.enabled,
324
SearchContext.SearchResultHeaderFocused),
325
}]
326
});
327
}
328
async run(accessor: ServicesAccessor) {
329
const logService = accessor.get(ILogService);
330
const widget = await accessor.get(IInstantiationService).invokeFunction(withChatView);
331
332
if (!widget) {
333
logService.trace('InsertSearchResultAction: no chat view available');
334
return;
335
}
336
337
const editor = widget.inputEditor;
338
const originalRange = editor.getSelection() ?? editor.getModel()?.getFullModelRange().collapseToEnd();
339
340
if (!originalRange) {
341
logService.trace('InsertSearchResultAction: no selection');
342
return;
343
}
344
345
let insertText = `#${AttachSearchResultAction.Name}`;
346
const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startLineNumber + insertText.length);
347
// check character before the start of the range. If it's not a space, add a space
348
const model = editor.getModel();
349
if (model && model.getValueInRange(new Range(originalRange.startLineNumber, originalRange.startColumn - 1, originalRange.startLineNumber, originalRange.startColumn)) !== ' ') {
350
insertText = ' ' + insertText;
351
}
352
const success = editor.executeEdits('chatInsertSearch', [{ range: varRange, text: insertText + ' ' }]);
353
if (!success) {
354
logService.trace(`InsertSearchResultAction: failed to insert "${insertText}"`);
355
return;
356
}
357
}
358
}
359
360
/** This is our type */
361
interface IContextPickItemItem extends IQuickPickItem {
362
kind: 'contextPick';
363
item: IChatContextValueItem | IChatContextPickerItem;
364
}
365
366
/** These are the types we get from "platform QP" */
367
type IQuickPickServicePickItem = IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickPickItemWithResource;
368
369
function isIContextPickItemItem(obj: unknown): obj is IContextPickItemItem {
370
return (
371
isObject(obj)
372
&& typeof (<IContextPickItemItem>obj).kind === 'string'
373
&& (<IContextPickItemItem>obj).kind === 'contextPick'
374
);
375
}
376
377
function isIGotoSymbolQuickPickItem(obj: unknown): obj is IGotoSymbolQuickPickItem {
378
return (
379
isObject(obj)
380
&& typeof (obj as IGotoSymbolQuickPickItem).symbolName === 'string'
381
&& !!(obj as IGotoSymbolQuickPickItem).uri
382
&& !!(obj as IGotoSymbolQuickPickItem).range);
383
}
384
385
function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource {
386
return (
387
isObject(obj)
388
&& URI.isUri((obj as IQuickPickItemWithResource).resource));
389
}
390
391
392
export class AttachContextAction extends Action2 {
393
394
constructor() {
395
super({
396
id: 'workbench.action.chat.attachContext',
397
title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."),
398
icon: Codicon.attach,
399
category: CHAT_CATEGORY,
400
keybinding: {
401
when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel)),
402
primary: KeyMod.CtrlCmd | KeyCode.Slash,
403
weight: KeybindingWeight.EditorContrib
404
},
405
menu: {
406
when: ContextKeyExpr.and(
407
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel),
408
ChatContextKeys.lockedToCodingAgent.negate()
409
),
410
id: MenuId.ChatInputAttachmentToolbar,
411
group: 'navigation',
412
order: 3
413
},
414
});
415
}
416
417
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
418
419
const instantiationService = accessor.get(IInstantiationService);
420
const widgetService = accessor.get(IChatWidgetService);
421
const contextKeyService = accessor.get(IContextKeyService);
422
const keybindingService = accessor.get(IKeybindingService);
423
const contextPickService = accessor.get(IChatContextPickService);
424
425
const context: { widget?: IChatWidget; placeholder?: string } | undefined = args[0];
426
const widget = context?.widget ?? widgetService.lastFocusedWidget;
427
if (!widget) {
428
return;
429
}
430
431
const quickPickItems: IContextPickItemItem[] = [];
432
433
for (const item of contextPickService.items) {
434
435
if (item.isEnabled && !await item.isEnabled(widget)) {
436
continue;
437
}
438
439
quickPickItems.push({
440
kind: 'contextPick',
441
item,
442
label: item.label,
443
iconClass: ThemeIcon.asClassName(item.icon),
444
keybinding: item.commandId ? keybindingService.lookupKeybinding(item.commandId, contextKeyService) : undefined,
445
});
446
}
447
448
instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, context?.placeholder);
449
}
450
451
private _show(accessor: ServicesAccessor, widget: IChatWidget, additionPicks: IContextPickItemItem[] | undefined, placeholder?: string) {
452
const quickInputService = accessor.get(IQuickInputService);
453
const quickChatService = accessor.get(IQuickChatService);
454
const instantiationService = accessor.get(IInstantiationService);
455
const commandService = accessor.get(ICommandService);
456
457
const providerOptions: AnythingQuickAccessProviderRunOptions = {
458
additionPicks,
459
handleAccept: async (item: IQuickPickServicePickItem | IContextPickItemItem, isBackgroundAccept: boolean) => {
460
461
if (isIContextPickItemItem(item)) {
462
463
let isDone = true;
464
if (item.item.type === 'valuePick') {
465
this._handleContextPick(item.item, widget);
466
467
} else if (item.item.type === 'pickerPick') {
468
isDone = await this._handleContextPickerItem(quickInputService, commandService, item.item, widget);
469
}
470
471
if (!isDone) {
472
// restart picker when sub-picker didn't return anything
473
instantiationService.invokeFunction(this._show.bind(this), widget, additionPicks, placeholder);
474
return;
475
}
476
477
} else {
478
instantiationService.invokeFunction(this._handleQPPick.bind(this), widget, isBackgroundAccept, item);
479
}
480
if (isQuickChat(widget)) {
481
quickChatService.open();
482
}
483
}
484
};
485
486
quickInputService.quickAccess.show('', {
487
enabledProviderPrefixes: [
488
AnythingQuickAccessProvider.PREFIX,
489
SymbolsQuickAccessProvider.PREFIX,
490
AbstractGotoSymbolQuickAccessProvider.PREFIX
491
],
492
placeholder: placeholder ?? localize('chatContext.attach.placeholder', 'Search attachments'),
493
providerOptions,
494
});
495
}
496
497
private async _handleQPPick(accessor: ServicesAccessor, widget: IChatWidget, isInBackground: boolean, pick: IQuickPickServicePickItem) {
498
const fileService = accessor.get(IFileService);
499
const textModelService = accessor.get(ITextModelService);
500
501
const toAttach: IChatRequestVariableEntry[] = [];
502
503
if (isIQuickPickItemWithResource(pick) && pick.resource) {
504
if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) {
505
// checks if the file is an image
506
if (URI.isUri(pick.resource)) {
507
// read the image and attach a new file context.
508
const readFile = await fileService.readFile(pick.resource);
509
const resizedImage = await resizeImage(readFile.value.buffer);
510
toAttach.push({
511
id: pick.resource.toString(),
512
name: pick.label,
513
fullName: pick.label,
514
value: resizedImage,
515
kind: 'image',
516
references: [{ reference: pick.resource, kind: 'reference' }]
517
});
518
}
519
} else {
520
let omittedState = OmittedState.NotOmitted;
521
try {
522
const createdModel = await textModelService.createModelReference(pick.resource);
523
createdModel.dispose();
524
} catch {
525
omittedState = OmittedState.Full;
526
}
527
528
toAttach.push({
529
kind: 'file',
530
id: pick.resource.toString(),
531
value: pick.resource,
532
name: pick.label,
533
omittedState
534
});
535
}
536
} else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) {
537
toAttach.push({
538
kind: 'generic',
539
id: JSON.stringify({ uri: pick.uri, range: pick.range.decoration }),
540
value: { uri: pick.uri, range: pick.range.decoration },
541
fullName: pick.label,
542
name: pick.symbolName!,
543
});
544
}
545
546
547
widget.attachmentModel.addContext(...toAttach);
548
549
if (!isInBackground) {
550
// Set focus back into the input once the user is done attaching items
551
// so that the user can start typing their message
552
widget.focusInput();
553
}
554
}
555
556
private async _handleContextPick(item: IChatContextValueItem, widget: IChatWidget) {
557
558
const value = await item.asAttachment(widget);
559
if (Array.isArray(value)) {
560
widget.attachmentModel.addContext(...value);
561
} else if (value) {
562
widget.attachmentModel.addContext(value);
563
}
564
}
565
566
private async _handleContextPickerItem(quickInputService: IQuickInputService, commandService: ICommandService, item: IChatContextPickerItem, widget: IChatWidget): Promise<boolean> {
567
568
const pickerConfig = item.asPicker(widget);
569
570
const store = new DisposableStore();
571
572
const goBackItem: IQuickPickItem = {
573
label: localize('goBack', 'Go back ↩'),
574
alwaysShow: true
575
};
576
const configureItem = pickerConfig.configure ? {
577
label: pickerConfig.configure.label,
578
commandId: pickerConfig.configure.commandId,
579
alwaysShow: true
580
} : undefined;
581
const extraPicks: QuickPickItem[] = [{ type: 'separator' }];
582
if (configureItem) {
583
extraPicks.push(configureItem);
584
}
585
extraPicks.push(goBackItem);
586
587
const qp = store.add(quickInputService.createQuickPick({ useSeparators: true }));
588
589
const cts = new CancellationTokenSource();
590
store.add(qp.onDidHide(() => cts.cancel()));
591
store.add(toDisposable(() => cts.dispose(true)));
592
593
qp.placeholder = pickerConfig.placeholder;
594
qp.matchOnDescription = true;
595
qp.matchOnDetail = true;
596
// qp.ignoreFocusOut = true;
597
qp.canAcceptInBackground = true;
598
qp.busy = true;
599
qp.show();
600
601
if (isThenable(pickerConfig.picks)) {
602
const items = await (pickerConfig.picks.then(value => {
603
return ([] as QuickPickItem[]).concat(value, extraPicks);
604
}));
605
606
qp.items = items;
607
qp.busy = false;
608
} else {
609
const query = observableValue<string>('attachContext.query', qp.value);
610
store.add(qp.onDidChangeValue(() => query.set(qp.value, undefined)));
611
612
const picksObservable = pickerConfig.picks(query, cts.token);
613
store.add(autorun(reader => {
614
const { busy, picks } = picksObservable.read(reader);
615
qp.items = ([] as QuickPickItem[]).concat(picks, extraPicks);
616
qp.busy = busy;
617
}));
618
}
619
620
if (cts.token.isCancellationRequested) {
621
return true; // picker got hidden already
622
}
623
624
const defer = new DeferredPromise<boolean>();
625
const addPromises: Promise<void>[] = [];
626
627
store.add(qp.onDidAccept(e => {
628
const [selected] = qp.selectedItems;
629
if (isChatContextPickerPickItem(selected)) {
630
const attachment = selected.asAttachment();
631
if (isThenable(attachment)) {
632
addPromises.push(attachment.then(v => widget.attachmentModel.addContext(v)));
633
} else {
634
widget.attachmentModel.addContext(attachment);
635
}
636
}
637
if (selected === goBackItem) {
638
defer.complete(false);
639
}
640
if (selected === configureItem) {
641
defer.complete(true);
642
commandService.executeCommand(configureItem.commandId);
643
}
644
if (!e.inBackground) {
645
defer.complete(true);
646
}
647
}));
648
649
store.add(qp.onDidHide(() => {
650
defer.complete(true);
651
}));
652
653
try {
654
const result = await defer.p;
655
qp.busy = true; // if still visible
656
await Promise.all(addPromises);
657
return result;
658
} finally {
659
store.dispose();
660
}
661
}
662
}
663
664