Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/fixtures/codeMapper/quickInput.ts
13399 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 * as dom from '../../../base/browser/dom.js';
7
import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js';
8
import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js';
9
import { Button, IButtonStyles } from '../../../base/browser/ui/button/button.js';
10
import { CountBadge, ICountBadgeStyles } from '../../../base/browser/ui/countBadge/countBadge.js';
11
import { IHoverDelegate, IHoverDelegateOptions } from '../../../base/browser/ui/hover/hoverDelegate.js';
12
import { IInputBoxStyles } from '../../../base/browser/ui/inputbox/inputBox.js';
13
import { IKeybindingLabelStyles } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
14
import { IListStyles } from '../../../base/browser/ui/list/listWidget.js';
15
import { IProgressBarStyles, ProgressBar } from '../../../base/browser/ui/progressbar/progressbar.js';
16
import { IToggleStyles, Toggle } from '../../../base/browser/ui/toggle/toggle.js';
17
import { equals } from '../../../base/common/arrays.js';
18
import { TimeoutTimer } from '../../../base/common/async.js';
19
import { Codicon } from '../../../base/common/codicons.js';
20
import { Emitter, Event, EventBufferer } from '../../../base/common/event.js';
21
import { KeyCode } from '../../../base/common/keyCodes.js';
22
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
23
import { isIOS } from '../../../base/common/platform.js';
24
import Severity from '../../../base/common/severity.js';
25
import { ThemeIcon } from '../../../base/common/themables.js';
26
import './media/quickInput.css';
27
import { localize } from '../../../nls.js';
28
import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputButtonLocation, QuickInputHideReason, QuickInputType, QuickPickFocus } from '../common/quickInput.js';
29
import { QuickInputBox } from './quickInputBox.js';
30
import { quickInputButtonToAction, renderQuickInputDescription } from './quickInputUtils.js';
31
import { IConfigurationService } from '../../configuration/common/configuration.js';
32
import { IHoverService, WorkbenchHoverDelegate } from '../../hover/browser/hover.js';
33
import { QuickInputTree } from './quickInputTree.js';
34
import type { IHoverOptions } from '../../../base/browser/ui/hover/hover.js';
35
import { ContextKeyExpr, RawContextKey } from '../../contextkey/common/contextkey.js';
36
37
export const inQuickInputContextKeyValue = 'inQuickInput';
38
export const InQuickInputContextKey = new RawContextKey<boolean>(inQuickInputContextKeyValue, false, localize('inQuickInput', "Whether keyboard focus is inside the quick input control"));
39
export const inQuickInputContext = ContextKeyExpr.has(inQuickInputContextKeyValue);
40
41
export const quickInputAlignmentContextKeyValue = 'quickInputAlignment';
42
export const QuickInputAlignmentContextKey = new RawContextKey<'top' | 'center' | undefined>(quickInputAlignmentContextKeyValue, 'top', localize('quickInputAlignment', "The alignment of the quick input"));
43
44
export const quickInputTypeContextKeyValue = 'quickInputType';
45
export const QuickInputTypeContextKey = new RawContextKey<QuickInputType>(quickInputTypeContextKeyValue, undefined, localize('quickInputType', "The type of the currently visible quick input"));
46
47
export const endOfQuickInputBoxContextKeyValue = 'cursorAtEndOfQuickInputBox';
48
export const EndOfQuickInputBoxContextKey = new RawContextKey<boolean>(endOfQuickInputBoxContextKeyValue, false, localize('cursorAtEndOfQuickInputBox', "Whether the cursor in the quick input is at the end of the input box"));
49
export const endOfQuickInputBoxContext = ContextKeyExpr.has(endOfQuickInputBoxContextKeyValue);
50
51
export interface IQuickInputOptions {
52
idPrefix: string;
53
container: HTMLElement;
54
ignoreFocusOut(): boolean;
55
backKeybindingLabel(): string | undefined;
56
setContextKey(id?: string): void;
57
linkOpenerDelegate(content: string): void;
58
returnFocus(): void;
59
/**
60
* @todo With IHover in vs/editor, can we depend on the service directly
61
* instead of passing it through a hover delegate?
62
*/
63
hoverDelegate: IHoverDelegate;
64
styles: IQuickInputStyles;
65
}
66
67
export interface IQuickInputStyles {
68
readonly widget: IQuickInputWidgetStyles;
69
readonly inputBox: IInputBoxStyles;
70
readonly toggle: IToggleStyles;
71
readonly countBadge: ICountBadgeStyles;
72
readonly button: IButtonStyles;
73
readonly progressBar: IProgressBarStyles;
74
readonly keybindingLabel: IKeybindingLabelStyles;
75
readonly list: IListStyles;
76
readonly pickerGroup: { pickerGroupBorder: string | undefined; pickerGroupForeground: string | undefined };
77
}
78
79
export interface IQuickInputWidgetStyles {
80
readonly quickInputBackground: string | undefined;
81
readonly quickInputForeground: string | undefined;
82
readonly quickInputTitleBackground: string | undefined;
83
readonly widgetBorder: string | undefined;
84
readonly widgetShadow: string | undefined;
85
}
86
87
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
88
89
export const backButton = {
90
iconClass: ThemeIcon.asClassName(Codicon.quickInputBack),
91
tooltip: localize('quickInput.back', "Back"),
92
handle: -1 // TODO
93
};
94
95
export interface QuickInputUI {
96
container: HTMLElement;
97
styleSheet: HTMLStyleElement;
98
leftActionBar: ActionBar;
99
titleBar: HTMLElement;
100
title: HTMLElement;
101
description1: HTMLElement;
102
description2: HTMLElement;
103
widget: HTMLElement;
104
rightActionBar: ActionBar;
105
inlineActionBar: ActionBar;
106
checkAll: HTMLInputElement;
107
inputContainer: HTMLElement;
108
filterContainer: HTMLElement;
109
inputBox: QuickInputBox;
110
visibleCountContainer: HTMLElement;
111
visibleCount: CountBadge;
112
countContainer: HTMLElement;
113
count: CountBadge;
114
okContainer: HTMLElement;
115
ok: Button;
116
message: HTMLElement;
117
customButtonContainer: HTMLElement;
118
customButton: Button;
119
progressBar: ProgressBar;
120
list: QuickInputTree;
121
onDidAccept: Event<void>;
122
onDidCustom: Event<void>;
123
onDidTriggerButton: Event<IQuickInputButton>;
124
ignoreFocusOut: boolean;
125
keyMods: Writeable<IKeyMods>;
126
show(controller: QuickInput): void;
127
setVisibilities(visibilities: Visibilities): void;
128
setEnabled(enabled: boolean): void;
129
setContextKey(contextKey?: string): void;
130
linkOpenerDelegate(content: string): void;
131
hide(): void;
132
}
133
134
export type Visibilities = {
135
title?: boolean;
136
description?: boolean;
137
checkAll?: boolean;
138
inputBox?: boolean;
139
checkBox?: boolean;
140
visibleCount?: boolean;
141
count?: boolean;
142
message?: boolean;
143
list?: boolean;
144
ok?: boolean;
145
customButton?: boolean;
146
progressBar?: boolean;
147
};
148
149
abstract class QuickInput extends Disposable implements IQuickInput {
150
protected static readonly noPromptMessage = localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel");
151
152
private _title: string | undefined;
153
private _description: string | undefined;
154
private _widget: HTMLElement | undefined;
155
private _widgetUpdated = false;
156
private _steps: number | undefined;
157
private _totalSteps: number | undefined;
158
protected visible = false;
159
private _enabled = true;
160
private _contextKey: string | undefined;
161
private _busy = false;
162
private _ignoreFocusOut = false;
163
private _leftButtons: IQuickInputButton[] = [];
164
private _rightButtons: IQuickInputButton[] = [];
165
private _inlineButtons: IQuickInputButton[] = [];
166
private buttonsUpdated = false;
167
private _toggles: IQuickInputToggle[] = [];
168
private togglesUpdated = false;
169
protected noValidationMessage = QuickInput.noPromptMessage;
170
private _validationMessage: string | undefined;
171
private _lastValidationMessage: string | undefined;
172
private _severity: Severity = Severity.Ignore;
173
private _lastSeverity: Severity | undefined;
174
private readonly onDidTriggerButtonEmitter = this._register(new Emitter<IQuickInputButton>());
175
private readonly onDidHideEmitter = this._register(new Emitter<IQuickInputHideEvent>());
176
private readonly onWillHideEmitter = this._register(new Emitter<IQuickInputHideEvent>());
177
private readonly onDisposeEmitter = this._register(new Emitter<void>());
178
179
protected readonly visibleDisposables = this._register(new DisposableStore());
180
181
private busyDelay: TimeoutTimer | undefined;
182
183
abstract type: QuickInputType;
184
185
constructor(
186
protected ui: QuickInputUI
187
) {
188
super();
189
}
190
191
get title() {
192
return this._title;
193
}
194
195
set title(title: string | undefined) {
196
this._title = title;
197
this.update();
198
}
199
200
get description() {
201
return this._description;
202
}
203
204
set description(description: string | undefined) {
205
this._description = description;
206
this.update();
207
}
208
209
get widget() {
210
return this._widget;
211
}
212
213
set widget(widget: unknown | undefined) {
214
if (!(dom.isHTMLElement(widget))) {
215
return;
216
}
217
if (this._widget !== widget) {
218
this._widget = widget;
219
this._widgetUpdated = true;
220
this.update();
221
}
222
}
223
224
get step() {
225
return this._steps;
226
}
227
228
set step(step: number | undefined) {
229
this._steps = step;
230
this.update();
231
}
232
233
get totalSteps() {
234
return this._totalSteps;
235
}
236
237
set totalSteps(totalSteps: number | undefined) {
238
this._totalSteps = totalSteps;
239
this.update();
240
}
241
242
get enabled() {
243
return this._enabled;
244
}
245
246
set enabled(enabled: boolean) {
247
this._enabled = enabled;
248
this.update();
249
}
250
251
get contextKey() {
252
return this._contextKey;
253
}
254
255
set contextKey(contextKey: string | undefined) {
256
this._contextKey = contextKey;
257
this.update();
258
}
259
260
get busy() {
261
return this._busy;
262
}
263
264
set busy(busy: boolean) {
265
this._busy = busy;
266
this.update();
267
}
268
269
get ignoreFocusOut() {
270
return this._ignoreFocusOut;
271
}
272
273
set ignoreFocusOut(ignoreFocusOut: boolean) {
274
const shouldUpdate = this._ignoreFocusOut !== ignoreFocusOut && !isIOS;
275
this._ignoreFocusOut = ignoreFocusOut && !isIOS;
276
if (shouldUpdate) {
277
this.update();
278
}
279
}
280
281
protected get titleButtons() {
282
return this._leftButtons.length
283
? [...this._leftButtons, this._rightButtons]
284
: this._rightButtons;
285
}
286
287
get buttons() {
288
return [
289
...this._leftButtons,
290
...this._rightButtons,
291
...this._inlineButtons
292
];
293
}
294
295
set buttons(buttons: IQuickInputButton[]) {
296
this._leftButtons = buttons.filter(b => b === backButton);
297
this._rightButtons = buttons.filter(b => b !== backButton && b.location !== QuickInputButtonLocation.Inline);
298
this._inlineButtons = buttons.filter(b => b.location === QuickInputButtonLocation.Inline);
299
this.buttonsUpdated = true;
300
this.update();
301
}
302
303
get toggles() {
304
return this._toggles;
305
}
306
307
set toggles(toggles: IQuickInputToggle[]) {
308
this._toggles = toggles ?? [];
309
this.togglesUpdated = true;
310
this.update();
311
}
312
313
get validationMessage() {
314
return this._validationMessage;
315
}
316
317
set validationMessage(validationMessage: string | undefined) {
318
this._validationMessage = validationMessage;
319
this.update();
320
}
321
322
get severity() {
323
return this._severity;
324
}
325
326
set severity(severity: Severity) {
327
this._severity = severity;
328
this.update();
329
}
330
331
readonly onDidTriggerButton = this.onDidTriggerButtonEmitter.event;
332
333
show(): void {
334
if (this.visible) {
335
return;
336
}
337
this.visibleDisposables.add(
338
this.ui.onDidTriggerButton(button => {
339
if (this.buttons.indexOf(button) !== -1) {
340
this.onDidTriggerButtonEmitter.fire(button);
341
}
342
}),
343
);
344
this.ui.show(this);
345
346
// update properties in the controller that get reset in the ui.show() call
347
this.visible = true;
348
// This ensures the message/prompt gets rendered
349
this._lastValidationMessage = undefined;
350
// This ensures the input box has the right severity applied
351
this._lastSeverity = undefined;
352
if (this.buttons.length) {
353
// if there are buttons, the ui.show() clears them out of the UI so we should
354
// rerender them.
355
this.buttonsUpdated = true;
356
}
357
if (this.toggles.length) {
358
// if there are toggles, the ui.show() clears them out of the UI so we should
359
// rerender them.
360
this.togglesUpdated = true;
361
}
362
363
this.update();
364
}
365
366
hide(): void {
367
if (!this.visible) {
368
return;
369
}
370
this.ui.hide();
371
}
372
373
didHide(reason = QuickInputHideReason.Other): void {
374
this.visible = false;
375
this.visibleDisposables.clear();
376
this.onDidHideEmitter.fire({ reason });
377
}
378
379
readonly onDidHide = this.onDidHideEmitter.event;
380
381
willHide(reason = QuickInputHideReason.Other): void {
382
this.onWillHideEmitter.fire({ reason });
383
}
384
readonly onWillHide = this.onWillHideEmitter.event;
385
386
protected update() {
387
if (!this.visible) {
388
return;
389
}
390
const title = this.getTitle();
391
if (title && this.ui.title.textContent !== title) {
392
this.ui.title.textContent = title;
393
} else if (!title && this.ui.title.innerHTML !== '&nbsp;') {
394
this.ui.title.innerText = '\u00a0';
395
}
396
const description = this.getDescription();
397
if (this.ui.description1.textContent !== description) {
398
this.ui.description1.textContent = description;
399
}
400
if (this.ui.description2.textContent !== description) {
401
this.ui.description2.textContent = description;
402
}
403
if (this._widgetUpdated) {
404
this._widgetUpdated = false;
405
if (this._widget) {
406
dom.reset(this.ui.widget, this._widget);
407
} else {
408
dom.reset(this.ui.widget);
409
}
410
}
411
if (this.busy && !this.busyDelay) {
412
this.busyDelay = new TimeoutTimer();
413
this.busyDelay.setIfNotSet(() => {
414
if (this.visible) {
415
this.ui.progressBar.infinite();
416
}
417
}, 800);
418
}
419
if (!this.busy && this.busyDelay) {
420
this.ui.progressBar.stop();
421
this.busyDelay.cancel();
422
this.busyDelay = undefined;
423
}
424
if (this.buttonsUpdated) {
425
this.buttonsUpdated = false;
426
this.ui.leftActionBar.clear();
427
const leftButtons = this._leftButtons
428
.map((button, index) => quickInputButtonToAction(
429
button,
430
`id-${index}`,
431
async () => this.onDidTriggerButtonEmitter.fire(button)
432
));
433
this.ui.leftActionBar.push(leftButtons, { icon: true, label: false });
434
this.ui.rightActionBar.clear();
435
const rightButtons = this._rightButtons
436
.map((button, index) => quickInputButtonToAction(
437
button,
438
`id-${index}`,
439
async () => this.onDidTriggerButtonEmitter.fire(button)
440
));
441
this.ui.rightActionBar.push(rightButtons, { icon: true, label: false });
442
this.ui.inlineActionBar.clear();
443
const inlineButtons = this._inlineButtons
444
.map((button, index) => quickInputButtonToAction(
445
button,
446
`id-${index}`,
447
async () => this.onDidTriggerButtonEmitter.fire(button)
448
));
449
this.ui.inlineActionBar.push(inlineButtons, { icon: true, label: false });
450
}
451
if (this.togglesUpdated) {
452
this.togglesUpdated = false;
453
// HACK: Filter out toggles here that are not concrete Toggle objects. This is to workaround
454
// a layering issue as quick input's interface is in common but Toggle is in browser and
455
// it requires a HTMLElement on its interface
456
const concreteToggles = this.toggles?.filter(opts => opts instanceof Toggle) as Toggle[] ?? [];
457
this.ui.inputBox.toggles = concreteToggles;
458
}
459
this.ui.ignoreFocusOut = this.ignoreFocusOut;
460
this.ui.setEnabled(this.enabled);
461
this.ui.setContextKey(this.contextKey);
462
463
const validationMessage = this.validationMessage || this.noValidationMessage;
464
if (this._lastValidationMessage !== validationMessage) {
465
this._lastValidationMessage = validationMessage;
466
dom.reset(this.ui.message);
467
renderQuickInputDescription(validationMessage, this.ui.message, {
468
callback: (content) => {
469
this.ui.linkOpenerDelegate(content);
470
},
471
disposables: this.visibleDisposables,
472
});
473
}
474
if (this._lastSeverity !== this.severity) {
475
this._lastSeverity = this.severity;
476
this.showMessageDecoration(this.severity);
477
}
478
}
479
480
private getTitle() {
481
if (this.title && this.step) {
482
return `${this.title} (${this.getSteps()})`;
483
}
484
if (this.title) {
485
return this.title;
486
}
487
if (this.step) {
488
return this.getSteps();
489
}
490
return '';
491
}
492
493
private getDescription() {
494
return this.description || '';
495
}
496
497
private getSteps() {
498
if (this.step && this.totalSteps) {
499
return localize('quickInput.steps', "{0}/{1}", this.step, this.totalSteps);
500
}
501
if (this.step) {
502
return String(this.step);
503
}
504
return '';
505
}
506
507
protected showMessageDecoration(severity: Severity) {
508
this.ui.inputBox.showDecoration(severity);
509
if (severity !== Severity.Ignore) {
510
const styles = this.ui.inputBox.stylesForType(severity);
511
this.ui.message.style.color = styles.foreground ? `${styles.foreground}` : '';
512
this.ui.message.style.backgroundColor = styles.background ? `${styles.background}` : '';
513
this.ui.message.style.border = styles.border ? `1px solid ${styles.border}` : '';
514
this.ui.message.style.marginBottom = '-2px';
515
} else {
516
this.ui.message.style.color = '';
517
this.ui.message.style.backgroundColor = '';
518
this.ui.message.style.border = '';
519
this.ui.message.style.marginBottom = '';
520
}
521
}
522
523
readonly onDispose = this.onDisposeEmitter.event;
524
525
override dispose(): void {
526
this.hide();
527
this.onDisposeEmitter.fire();
528
529
super.dispose();
530
}
531
}
532
533
export class QuickPick<T extends IQuickPickItem, O extends { useSeparators: boolean } = { useSeparators: false }> extends QuickInput implements IQuickPick<T, O> {
534
535
private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results.");
536
537
private _value = '';
538
private _ariaLabel: string | undefined;
539
private _placeholder: string | undefined;
540
private readonly onDidChangeValueEmitter = this._register(new Emitter<string>());
541
private readonly onWillAcceptEmitter = this._register(new Emitter<IQuickPickWillAcceptEvent>());
542
private readonly onDidAcceptEmitter = this._register(new Emitter<IQuickPickDidAcceptEvent>());
543
private readonly onDidCustomEmitter = this._register(new Emitter<void>());
544
private _items: O extends { useSeparators: true } ? Array<T | IQuickPickSeparator> : Array<T> = [];
545
private itemsUpdated = false;
546
private _canSelectMany = false;
547
private _canAcceptInBackground = false;
548
private _matchOnDescription = false;
549
private _matchOnDetail = false;
550
private _matchOnLabel = true;
551
private _matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy';
552
private _sortByLabel = true;
553
private _keepScrollPosition = false;
554
private _itemActivation = ItemActivation.FIRST;
555
private _activeItems: T[] = [];
556
private activeItemsUpdated = false;
557
private activeItemsToConfirm: T[] | null = [];
558
private readonly onDidChangeActiveEmitter = this._register(new Emitter<T[]>());
559
private _selectedItems: T[] = [];
560
private selectedItemsUpdated = false;
561
private selectedItemsToConfirm: T[] | null = [];
562
private readonly onDidChangeSelectionEmitter = this._register(new Emitter<T[]>());
563
private readonly onDidTriggerItemButtonEmitter = this._register(new Emitter<IQuickPickItemButtonEvent<T>>());
564
private readonly onDidTriggerSeparatorButtonEmitter = this._register(new Emitter<IQuickPickSeparatorButtonEvent>());
565
private _valueSelection: Readonly<[number, number]> | undefined;
566
private valueSelectionUpdated = true;
567
private _ok: boolean | 'default' = 'default';
568
private _okLabel: string | undefined;
569
private _customButton = false;
570
private _customButtonLabel: string | undefined;
571
private _customButtonHover: string | undefined;
572
private _quickNavigate: IQuickNavigateConfiguration | undefined;
573
private _hideInput: boolean | undefined;
574
private _hideCountBadge: boolean | undefined;
575
private _hideCheckAll: boolean | undefined;
576
private _focusEventBufferer = new EventBufferer();
577
578
readonly type = QuickInputType.QuickPick;
579
580
get quickNavigate() {
581
return this._quickNavigate;
582
}
583
584
set quickNavigate(quickNavigate: IQuickNavigateConfiguration | undefined) {
585
this._quickNavigate = quickNavigate;
586
this.update();
587
}
588
589
get value() {
590
return this._value;
591
}
592
593
set value(value: string) {
594
this.doSetValue(value);
595
}
596
597
private doSetValue(value: string, skipUpdate?: boolean): void {
598
if (this._value !== value) {
599
this._value = value;
600
if (!skipUpdate) {
601
this.update();
602
}
603
if (this.visible) {
604
const didFilter = this.ui.list.filter(this.filterValue(this._value));
605
if (didFilter) {
606
this.trySelectFirst();
607
}
608
}
609
this.onDidChangeValueEmitter.fire(this._value);
610
}
611
}
612
613
filterValue = (value: string) => value;
614
615
set ariaLabel(ariaLabel: string | undefined) {
616
this._ariaLabel = ariaLabel;
617
this.update();
618
}
619
620
get ariaLabel() {
621
return this._ariaLabel;
622
}
623
624
get placeholder() {
625
return this._placeholder;
626
}
627
628
set placeholder(placeholder: string | undefined) {
629
this._placeholder = placeholder;
630
this.update();
631
}
632
633
onDidChangeValue = this.onDidChangeValueEmitter.event;
634
635
onWillAccept = this.onWillAcceptEmitter.event;
636
onDidAccept = this.onDidAcceptEmitter.event;
637
638
onDidCustom = this.onDidCustomEmitter.event;
639
640
get items() {
641
return this._items;
642
}
643
644
get scrollTop() {
645
return this.ui.list.scrollTop;
646
}
647
648
private set scrollTop(scrollTop: number) {
649
this.ui.list.scrollTop = scrollTop;
650
}
651
652
set items(items: O extends { useSeparators: true } ? Array<T | IQuickPickSeparator> : Array<T>) {
653
this._items = items;
654
this.itemsUpdated = true;
655
this.update();
656
}
657
658
get canSelectMany() {
659
return this._canSelectMany;
660
}
661
662
set canSelectMany(canSelectMany: boolean) {
663
this._canSelectMany = canSelectMany;
664
this.update();
665
}
666
667
get canAcceptInBackground() {
668
return this._canAcceptInBackground;
669
}
670
671
set canAcceptInBackground(canAcceptInBackground: boolean) {
672
this._canAcceptInBackground = canAcceptInBackground;
673
}
674
675
get matchOnDescription() {
676
return this._matchOnDescription;
677
}
678
679
set matchOnDescription(matchOnDescription: boolean) {
680
this._matchOnDescription = matchOnDescription;
681
this.update();
682
}
683
684
get matchOnDetail() {
685
return this._matchOnDetail;
686
}
687
688
set matchOnDetail(matchOnDetail: boolean) {
689
this._matchOnDetail = matchOnDetail;
690
this.update();
691
}
692
693
get matchOnLabel() {
694
return this._matchOnLabel;
695
}
696
697
set matchOnLabel(matchOnLabel: boolean) {
698
this._matchOnLabel = matchOnLabel;
699
this.update();
700
}
701
702
get matchOnLabelMode() {
703
return this._matchOnLabelMode;
704
}
705
706
set matchOnLabelMode(matchOnLabelMode: 'fuzzy' | 'contiguous') {
707
this._matchOnLabelMode = matchOnLabelMode;
708
this.update();
709
}
710
711
get sortByLabel() {
712
return this._sortByLabel;
713
}
714
715
set sortByLabel(sortByLabel: boolean) {
716
this._sortByLabel = sortByLabel;
717
this.update();
718
}
719
720
get keepScrollPosition() {
721
return this._keepScrollPosition;
722
}
723
724
set keepScrollPosition(keepScrollPosition: boolean) {
725
this._keepScrollPosition = keepScrollPosition;
726
}
727
728
get itemActivation() {
729
return this._itemActivation;
730
}
731
732
set itemActivation(itemActivation: ItemActivation) {
733
this._itemActivation = itemActivation;
734
}
735
736
get activeItems() {
737
return this._activeItems;
738
}
739
740
set activeItems(activeItems: T[]) {
741
this._activeItems = activeItems;
742
this.activeItemsUpdated = true;
743
this.update();
744
}
745
746
onDidChangeActive = this.onDidChangeActiveEmitter.event;
747
748
get selectedItems() {
749
return this._selectedItems;
750
}
751
752
set selectedItems(selectedItems: T[]) {
753
this._selectedItems = selectedItems;
754
this.selectedItemsUpdated = true;
755
this.update();
756
}
757
758
get keyMods() {
759
if (this._quickNavigate) {
760
// Disable keyMods when quick navigate is enabled
761
// because in this model the interaction is purely
762
// keyboard driven and Ctrl/Alt are typically
763
// pressed and hold during this interaction.
764
return NO_KEY_MODS;
765
}
766
return this.ui.keyMods;
767
}
768
769
get valueSelection() {
770
const selection = this.ui.inputBox.getSelection();
771
if (!selection) {
772
return undefined;
773
}
774
return [selection.start, selection.end];
775
}
776
777
set valueSelection(valueSelection: Readonly<[number, number]> | undefined) {
778
this._valueSelection = valueSelection;
779
this.valueSelectionUpdated = true;
780
this.update();
781
}
782
783
get customButton() {
784
return this._customButton;
785
}
786
787
set customButton(showCustomButton: boolean) {
788
this._customButton = showCustomButton;
789
this.update();
790
}
791
792
get customLabel() {
793
return this._customButtonLabel;
794
}
795
796
set customLabel(label: string | undefined) {
797
this._customButtonLabel = label;
798
this.update();
799
}
800
801
get customHover() {
802
return this._customButtonHover;
803
}
804
805
set customHover(hover: string | undefined) {
806
this._customButtonHover = hover;
807
this.update();
808
}
809
810
get ok() {
811
return this._ok;
812
}
813
814
set ok(showOkButton: boolean | 'default') {
815
this._ok = showOkButton;
816
this.update();
817
}
818
819
get okLabel() {
820
return this._okLabel ?? localize('ok', "OK");
821
}
822
823
set okLabel(okLabel: string | undefined) {
824
this._okLabel = okLabel;
825
this.update();
826
}
827
828
inputHasFocus(): boolean {
829
return this.visible ? this.ui.inputBox.hasFocus() : false;
830
}
831
832
focusOnInput() {
833
this.ui.inputBox.setFocus();
834
}
835
836
get hideInput() {
837
return !!this._hideInput;
838
}
839
840
set hideInput(hideInput: boolean) {
841
this._hideInput = hideInput;
842
this.update();
843
}
844
845
get hideCountBadge() {
846
return !!this._hideCountBadge;
847
}
848
849
set hideCountBadge(hideCountBadge: boolean) {
850
this._hideCountBadge = hideCountBadge;
851
this.update();
852
}
853
854
get hideCheckAll() {
855
return !!this._hideCheckAll;
856
}
857
858
set hideCheckAll(hideCheckAll: boolean) {
859
this._hideCheckAll = hideCheckAll;
860
this.update();
861
}
862
863
onDidChangeSelection = this.onDidChangeSelectionEmitter.event;
864
865
onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event;
866
867
onDidTriggerSeparatorButton = this.onDidTriggerSeparatorButtonEmitter.event;
868
869
private trySelectFirst() {
870
if (!this.canSelectMany) {
871
this.ui.list.focus(QuickPickFocus.First);
872
}
873
}
874
875
override show() {
876
if (!this.visible) {
877
this.visibleDisposables.add(
878
this.ui.inputBox.onDidChange(value => {
879
this.doSetValue(value, true /* skip update since this originates from the UI */);
880
}));
881
this.visibleDisposables.add(this.ui.onDidAccept(() => {
882
if (this.canSelectMany) {
883
// if there are no checked elements, it means that an onDidChangeSelection never fired to overwrite
884
// `_selectedItems`. In that case, we should emit one with an empty array to ensure that
885
// `.selectedItems` is up to date.
886
if (!this.ui.list.getCheckedElements().length) {
887
this._selectedItems = [];
888
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
889
}
890
} else if (this.activeItems[0]) {
891
// For single-select, we set `selectedItems` to the item that was accepted.
892
this._selectedItems = [this.activeItems[0]];
893
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
894
}
895
this.handleAccept(false);
896
}));
897
this.visibleDisposables.add(this.ui.onDidCustom(() => {
898
this.onDidCustomEmitter.fire();
899
}));
900
this.visibleDisposables.add(this._focusEventBufferer.wrapEvent(
901
this.ui.list.onDidChangeFocus,
902
// Only fire the last event
903
(_, e) => e
904
)(focusedItems => {
905
if (this.activeItemsUpdated) {
906
return; // Expect another event.
907
}
908
if (this.activeItemsToConfirm !== this._activeItems && equals(focusedItems, this._activeItems, (a, b) => a === b)) {
909
return;
910
}
911
this._activeItems = focusedItems as T[];
912
this.onDidChangeActiveEmitter.fire(focusedItems as T[]);
913
}));
914
this.visibleDisposables.add(this.ui.list.onDidChangeSelection(({ items: selectedItems, event }) => {
915
if (this.canSelectMany && !selectedItems.some(i => i.pickable === false)) {
916
if (selectedItems.length) {
917
this.ui.list.setSelectedElements([]);
918
}
919
return;
920
}
921
if (this.selectedItemsToConfirm !== this._selectedItems && equals(selectedItems, this._selectedItems, (a, b) => a === b)) {
922
return;
923
}
924
this._selectedItems = selectedItems as T[];
925
this.onDidChangeSelectionEmitter.fire(selectedItems as T[]);
926
if (selectedItems.length) {
927
this.handleAccept(dom.isMouseEvent(event) && event.button === 1 /* mouse middle click */);
928
}
929
}));
930
this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => {
931
if (!this.canSelectMany || !this.visible) {
932
return;
933
}
934
if (this.selectedItemsToConfirm !== this._selectedItems && equals(checkedItems, this._selectedItems, (a, b) => a === b)) {
935
return;
936
}
937
this._selectedItems = checkedItems as T[];
938
this.onDidChangeSelectionEmitter.fire(checkedItems as T[]);
939
}));
940
this.visibleDisposables.add(this.ui.list.onButtonTriggered(event => this.onDidTriggerItemButtonEmitter.fire(event as IQuickPickItemButtonEvent<T>)));
941
this.visibleDisposables.add(this.ui.list.onSeparatorButtonTriggered(event => this.onDidTriggerSeparatorButtonEmitter.fire(event)));
942
this.visibleDisposables.add(this.registerQuickNavigation());
943
this.valueSelectionUpdated = true;
944
}
945
super.show(); // TODO: Why have show() bubble up while update() trickles down?
946
}
947
948
private handleAccept(inBackground: boolean): void {
949
950
// Figure out veto via `onWillAccept` event
951
let veto = false;
952
this.onWillAcceptEmitter.fire({ veto: () => veto = true });
953
954
// Continue with `onDidAccept` if no veto
955
if (!veto) {
956
this.onDidAcceptEmitter.fire({ inBackground });
957
}
958
}
959
960
private registerQuickNavigation() {
961
return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => {
962
if (this.canSelectMany || !this._quickNavigate) {
963
return;
964
}
965
966
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
967
const keyCode = keyboardEvent.keyCode;
968
969
// Select element when keys are pressed that signal it
970
const quickNavKeys = this._quickNavigate.keybindings;
971
const wasTriggerKeyPressed = quickNavKeys.some(k => {
972
const chords = k.getChords();
973
if (chords.length > 1) {
974
return false;
975
}
976
977
if (chords[0].shiftKey && keyCode === KeyCode.Shift) {
978
if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
979
return false; // this is an optimistic check for the shift key being used to navigate back in quick input
980
}
981
982
return true;
983
}
984
985
if (chords[0].altKey && keyCode === KeyCode.Alt) {
986
return true;
987
}
988
989
if (chords[0].ctrlKey && keyCode === KeyCode.Ctrl) {
990
return true;
991
}
992
993
if (chords[0].metaKey && keyCode === KeyCode.Meta) {
994
return true;
995
}
996
997
return false;
998
});
999
1000
if (wasTriggerKeyPressed) {
1001
if (this.activeItems[0]) {
1002
this._selectedItems = [this.activeItems[0]];
1003
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
1004
this.handleAccept(false);
1005
}
1006
// Unset quick navigate after press. It is only valid once
1007
// and should not result in any behaviour change afterwards
1008
// if the picker remains open because there was no active item
1009
this._quickNavigate = undefined;
1010
}
1011
});
1012
}
1013
1014
protected override update() {
1015
if (!this.visible) {
1016
return;
1017
}
1018
// store the scrollTop before it is reset
1019
const scrollTopBefore = this.keepScrollPosition ? this.scrollTop : 0;
1020
const hasDescription = !!this.description;
1021
const visibilities: Visibilities = {
1022
title: !!this.title || !!this.step || !!this.titleButtons.length,
1023
description: hasDescription,
1024
checkAll: this.canSelectMany && !this._hideCheckAll,
1025
checkBox: this.canSelectMany,
1026
inputBox: !this._hideInput,
1027
progressBar: !this._hideInput || hasDescription,
1028
visibleCount: true,
1029
count: this.canSelectMany && !this._hideCountBadge,
1030
ok: this.ok === 'default' ? this.canSelectMany : this.ok,
1031
list: true,
1032
message: !!this.validationMessage,
1033
customButton: this.customButton
1034
};
1035
this.ui.setVisibilities(visibilities);
1036
super.update();
1037
if (this.ui.inputBox.value !== this.value) {
1038
this.ui.inputBox.value = this.value;
1039
}
1040
if (this.valueSelectionUpdated) {
1041
this.valueSelectionUpdated = false;
1042
this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] });
1043
}
1044
if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
1045
this.ui.inputBox.placeholder = (this.placeholder || '');
1046
}
1047
1048
let ariaLabel = this.ariaLabel;
1049
// Only set aria label to the input box placeholder if we actually have an input box.
1050
if (!ariaLabel && visibilities.inputBox) {
1051
ariaLabel = this.placeholder || QuickPick.DEFAULT_ARIA_LABEL;
1052
// If we have a title, include it in the aria label.
1053
if (this.title) {
1054
ariaLabel += ` - ${this.title}`;
1055
}
1056
}
1057
if (this.ui.list.ariaLabel !== ariaLabel) {
1058
this.ui.list.ariaLabel = ariaLabel ?? null;
1059
}
1060
this.ui.list.matchOnDescription = this.matchOnDescription;
1061
this.ui.list.matchOnDetail = this.matchOnDetail;
1062
this.ui.list.matchOnLabel = this.matchOnLabel;
1063
this.ui.list.matchOnLabelMode = this.matchOnLabelMode;
1064
this.ui.list.sortByLabel = this.sortByLabel;
1065
if (this.itemsUpdated) {
1066
this.itemsUpdated = false;
1067
this._focusEventBufferer.bufferEvents(() => {
1068
this.ui.list.setElements(this.items);
1069
// We want focus to exist in the list if there are items so that space can be used to toggle
1070
this.ui.list.shouldLoop = !this.canSelectMany;
1071
this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
1072
switch (this._itemActivation) {
1073
case ItemActivation.NONE:
1074
this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
1075
break;
1076
case ItemActivation.SECOND:
1077
this.ui.list.focus(QuickPickFocus.Second);
1078
this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
1079
break;
1080
case ItemActivation.LAST:
1081
this.ui.list.focus(QuickPickFocus.Last);
1082
this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
1083
break;
1084
default:
1085
this.trySelectFirst();
1086
break;
1087
}
1088
});
1089
}
1090
if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) {
1091
if (this.canSelectMany) {
1092
this.ui.list.clearFocus();
1093
} else {
1094
this.trySelectFirst();
1095
}
1096
}
1097
if (this.activeItemsUpdated) {
1098
this.activeItemsUpdated = false;
1099
this.activeItemsToConfirm = this._activeItems;
1100
this.ui.list.setFocusedElements(this.activeItems);
1101
if (this.activeItemsToConfirm === this._activeItems) {
1102
this.activeItemsToConfirm = null;
1103
}
1104
}
1105
if (this.selectedItemsUpdated) {
1106
this.selectedItemsUpdated = false;
1107
this.selectedItemsToConfirm = this._selectedItems;
1108
if (this.canSelectMany) {
1109
this.ui.list.setCheckedElements(this.selectedItems);
1110
} else {
1111
this.ui.list.setSelectedElements(this.selectedItems);
1112
}
1113
if (this.selectedItemsToConfirm === this._selectedItems) {
1114
this.selectedItemsToConfirm = null;
1115
}
1116
}
1117
this.ui.ok.label = this.okLabel || '';
1118
this.ui.customButton.label = this.customLabel || '';
1119
this.ui.customButton.element.title = this.customHover || '';
1120
if (!visibilities.inputBox) {
1121
// we need to move focus into the tree to detect keybindings
1122
// properly when the input box is not visible (quick nav)
1123
this.ui.list.domFocus();
1124
1125
// Focus the first element in the list if multiselect is enabled
1126
if (this.canSelectMany) {
1127
this.ui.list.focus(QuickPickFocus.First);
1128
}
1129
}
1130
1131
// Set the scroll position to what it was before updating the items
1132
if (this.keepScrollPosition) {
1133
this.scrollTop = scrollTopBefore;
1134
}
1135
}
1136
1137
focus(focus: QuickPickFocus): void {
1138
this.ui.list.focus(focus);
1139
// To allow things like space to check/uncheck items
1140
if (this.canSelectMany) {
1141
this.ui.list.domFocus();
1142
}
1143
}
1144
1145
accept(inBackground?: boolean | undefined): void {
1146
if (inBackground && !this._canAcceptInBackground) {
1147
return; // needs to be enabled
1148
}
1149
1150
if (this.activeItems[0]) {
1151
this._selectedItems = [this.activeItems[0]];
1152
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
1153
this.handleAccept(inBackground ?? false);
1154
}
1155
}
1156
}
1157
1158
export class InputBox extends QuickInput implements IInputBox {
1159
private _value = '';
1160
private _valueSelection: Readonly<[number, number]> | undefined;
1161
private valueSelectionUpdated = true;
1162
private _placeholder: string | undefined;
1163
private _password = false;
1164
private _prompt: string | undefined;
1165
private readonly onDidValueChangeEmitter = this._register(new Emitter<string>());
1166
private readonly onDidAcceptEmitter = this._register(new Emitter<void>());
1167
1168
readonly type = QuickInputType.InputBox;
1169
1170
get value() {
1171
return this._value;
1172
}
1173
1174
set value(value: string) {
1175
this._value = value || '';
1176
this.update();
1177
}
1178
1179
get valueSelection() {
1180
const selection = this.ui.inputBox.getSelection();
1181
if (!selection) {
1182
return undefined;
1183
}
1184
return [selection.start, selection.end];
1185
}
1186
1187
set valueSelection(valueSelection: Readonly<[number, number]> | undefined) {
1188
this._valueSelection = valueSelection;
1189
this.valueSelectionUpdated = true;
1190
this.update();
1191
}
1192
1193
get placeholder() {
1194
return this._placeholder;
1195
}
1196
1197
set placeholder(placeholder: string | undefined) {
1198
this._placeholder = placeholder;
1199
this.update();
1200
}
1201
1202
get password() {
1203
return this._password;
1204
}
1205
1206
set password(password: boolean) {
1207
this._password = password;
1208
this.update();
1209
}
1210
1211
get prompt() {
1212
return this._prompt;
1213
}
1214
1215
set prompt(prompt: string | undefined) {
1216
this._prompt = prompt;
1217
this.noValidationMessage = prompt
1218
? localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", prompt)
1219
: QuickInput.noPromptMessage;
1220
this.update();
1221
}
1222
1223
readonly onDidChangeValue = this.onDidValueChangeEmitter.event;
1224
1225
readonly onDidAccept = this.onDidAcceptEmitter.event;
1226
1227
override show() {
1228
if (!this.visible) {
1229
this.visibleDisposables.add(
1230
this.ui.inputBox.onDidChange(value => {
1231
if (value === this.value) {
1232
return;
1233
}
1234
this._value = value;
1235
this.onDidValueChangeEmitter.fire(value);
1236
}));
1237
this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire()));
1238
this.valueSelectionUpdated = true;
1239
}
1240
super.show();
1241
}
1242
1243
protected override update() {
1244
if (!this.visible) {
1245
return;
1246
}
1247
1248
this.ui.container.classList.remove('hidden-input');
1249
const visibilities: Visibilities = {
1250
title: !!this.title || !!this.step || !!this.titleButtons.length,
1251
description: !!this.description || !!this.step,
1252
inputBox: true,
1253
message: true,
1254
progressBar: true
1255
};
1256
1257
this.ui.setVisibilities(visibilities);
1258
super.update();
1259
if (this.ui.inputBox.value !== this.value) {
1260
this.ui.inputBox.value = this.value;
1261
}
1262
if (this.valueSelectionUpdated) {
1263
this.valueSelectionUpdated = false;
1264
this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] });
1265
}
1266
if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
1267
this.ui.inputBox.placeholder = (this.placeholder || '');
1268
}
1269
if (this.ui.inputBox.password !== this.password) {
1270
this.ui.inputBox.password = this.password;
1271
}
1272
}
1273
}
1274
1275
export class QuickWidget extends QuickInput implements IQuickWidget {
1276
readonly type = QuickInputType.QuickWidget;
1277
1278
protected override update() {
1279
if (!this.visible) {
1280
return;
1281
}
1282
1283
const visibilities: Visibilities = {
1284
title: !!this.title || !!this.step || !!this.titleButtons.length,
1285
description: !!this.description || !!this.step
1286
};
1287
1288
this.ui.setVisibilities(visibilities);
1289
super.update();
1290
}
1291
}
1292
1293
export class QuickInputHoverDelegate extends WorkbenchHoverDelegate {
1294
1295
constructor(
1296
@IConfigurationService configurationService: IConfigurationService,
1297
@IHoverService hoverService: IHoverService
1298
) {
1299
super('element', undefined, (options) => this.getOverrideOptions(options), configurationService, hoverService);
1300
}
1301
1302
private getOverrideOptions(options: IHoverDelegateOptions): Partial<IHoverOptions> {
1303
// Only show the hover hint if the content is of a decent size
1304
const showHoverHint = (
1305
dom.isHTMLElement(options.content)
1306
? options.content.textContent ?? ''
1307
: typeof options.content === 'string'
1308
? options.content
1309
: options.content.value
1310
).includes('\n');
1311
1312
return {
1313
persistence: {
1314
hideOnKeyDown: false,
1315
},
1316
appearance: {
1317
showHoverHint,
1318
skipFadeInAnimation: true,
1319
},
1320
};
1321
}
1322
}
1323
1324