quickInputList.ts 22.7 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

B
Benjamin Pasero 已提交
6
import 'vs/css!./media/quickInput';
J
Joao Moreno 已提交
7
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
8
import * as dom from 'vs/base/browser/dom';
9
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
10
import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput';
11
import { IMatch } from 'vs/base/common/filters';
12
import { matchesFuzzyCodiconAware, parseCodicons } from 'vs/base/common/codicon';
13
import { compareAnything } from 'vs/base/common/comparers';
J
Joao Moreno 已提交
14
import { Emitter, Event } from 'vs/base/common/event';
C
Christof Marti 已提交
15 16 17
import { assign } from 'vs/base/common/objects';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
18 19
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
20
import { memoize } from 'vs/base/common/decorators';
21 22
import { range } from 'vs/base/common/arrays';
import * as platform from 'vs/base/common/platform';
C
Christof Marti 已提交
23 24
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
25
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
26
import { withNullAsUndefined } from 'vs/base/common/types';
27
import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput';
J
João Moreno 已提交
28
import { IListOptions, List, IListStyles, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
29
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
30
import { localize } from 'vs/nls';
31 32 33

const $ = dom.$;

34
interface IListElement {
35 36 37
	readonly index: number;
	readonly item: IQuickPickItem;
	readonly saneLabel: string;
38
	readonly saneAriaLabel: string;
39 40
	readonly saneDescription?: string;
	readonly saneDetail?: string;
41 42 43
	readonly labelHighlights?: IMatch[];
	readonly descriptionHighlights?: IMatch[];
	readonly detailHighlights?: IMatch[];
44 45 46
	readonly checked: boolean;
	readonly separator?: IQuickPickSeparator;
	readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
C
Christof Marti 已提交
47 48
}

C
Christof Marti 已提交
49
class ListElement implements IListElement, IDisposable {
50 51 52
	index!: number;
	item!: IQuickPickItem;
	saneLabel!: string;
53
	saneAriaLabel!: string;
C
Christof Marti 已提交
54 55
	saneDescription?: string;
	saneDetail?: string;
C
Christof Marti 已提交
56
	hidden = false;
57
	private readonly _onChecked = new Emitter<boolean>();
58
	onChecked = this._onChecked.event;
59
	_checked?: boolean;
60
	get checked() {
61
		return !!this._checked;
C
Christof Marti 已提交
62
	}
63 64 65 66
	set checked(value: boolean) {
		if (value !== this._checked) {
			this._checked = value;
			this._onChecked.fire(value);
C
Christof Marti 已提交
67 68
		}
	}
69
	separator?: IQuickPickSeparator;
70 71 72
	labelHighlights?: IMatch[];
	descriptionHighlights?: IMatch[];
	detailHighlights?: IMatch[];
73
	fireButtonTriggered!: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
C
Christof Marti 已提交
74

75
	constructor(init: IListElement) {
C
Christof Marti 已提交
76 77
		assign(this, init);
	}
C
Christof Marti 已提交
78 79 80 81

	dispose() {
		this._onChecked.dispose();
	}
82 83
}

84
interface IListElementTemplateData {
C
Christof Marti 已提交
85
	entry: HTMLDivElement;
86
	checkbox: HTMLInputElement;
87
	label: IconLabel;
88
	keybinding: KeybindingLabel;
89
	detail: HighlightedLabel;
C
Christof Marti 已提交
90
	separator: HTMLDivElement;
C
Christof Marti 已提交
91
	actionBar: ActionBar;
92
	element: ListElement;
C
Christof Marti 已提交
93 94
	toDisposeElement: IDisposable[];
	toDisposeTemplate: IDisposable[];
95 96
}

J
Joao Moreno 已提交
97
class ListElementRenderer implements IListRenderer<ListElement, IListElementTemplateData> {
98

99
	static readonly ID = 'listelement';
100 101

	get templateId() {
102
		return ListElementRenderer.ID;
103 104
	}

105 106
	renderTemplate(container: HTMLElement): IListElementTemplateData {
		const data: IListElementTemplateData = Object.create(null);
107 108
		data.toDisposeElement = [];
		data.toDisposeTemplate = [];
109

C
Christof Marti 已提交
110
		data.entry = dom.append(container, $('.quick-input-list-entry'));
111

112
		// Checkbox
C
Christof Marti 已提交
113
		const label = dom.append(data.entry, $('label.quick-input-list-label'));
114 115 116 117 118
		data.toDisposeTemplate.push(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => {
			if (!data.checkbox.offsetParent) { // If checkbox not visible:
				e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740
			}
		}));
119
		data.checkbox = <HTMLInputElement>dom.append(label, $('input.quick-input-list-checkbox'));
120
		data.checkbox.type = 'checkbox';
C
Christof Marti 已提交
121
		data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => {
122
			data.element.checked = data.checkbox.checked;
C
Christof Marti 已提交
123
		}));
124

125
		// Rows
126 127 128
		const rows = dom.append(label, $('.quick-input-list-rows'));
		const row1 = dom.append(rows, $('.quick-input-list-row'));
		const row2 = dom.append(rows, $('.quick-input-list-row'));
129 130

		// Label
131
		data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportCodicons: true });
132

133 134 135 136
		// Keybinding
		const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding'));
		data.keybinding = new KeybindingLabel(keybindingContainer, platform.OS);

137
		// Detail
138
		const detailContainer = dom.append(row2, $('.quick-input-list-label-meta'));
S
Sandeep Somavarapu 已提交
139
		data.detail = new HighlightedLabel(detailContainer, true);
140

C
Christof Marti 已提交
141 142 143
		// Separator
		data.separator = dom.append(data.entry, $('.quick-input-list-separator'));

C
Christof Marti 已提交
144
		// Actions
C
Christof Marti 已提交
145
		data.actionBar = new ActionBar(data.entry);
C
Christof Marti 已提交
146 147 148
		data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar');
		data.toDisposeTemplate.push(data.actionBar);

149 150 151
		return data;
	}

152
	renderElement(element: ListElement, index: number, data: IListElementTemplateData): void {
C
Christof Marti 已提交
153
		data.toDisposeElement = dispose(data.toDisposeElement);
154
		data.element = element;
155 156
		data.checkbox.checked = element.checked;
		data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked));
157 158 159 160 161 162

		const { labelHighlights, descriptionHighlights, detailHighlights } = element;

		// Label
		const options: IIconLabelValueOptions = Object.create(null);
		options.matches = labelHighlights || [];
C
Christof Marti 已提交
163
		options.descriptionTitle = element.saneDescription;
164
		options.descriptionMatches = descriptionHighlights || [];
C
Christof Marti 已提交
165
		options.extraClasses = element.item.iconClasses;
166
		options.italic = element.item.italic;
167
		options.strikethrough = element.item.strikethrough;
B
Benjamin Pasero 已提交
168
		data.label.setLabel(element.saneLabel, element.saneDescription, options);
169

170 171 172
		// Keybinding
		data.keybinding.set(element.item.keybinding);

173
		// Meta
C
Christof Marti 已提交
174
		data.detail.set(element.saneDetail, detailHighlights);
C
Christof Marti 已提交
175

C
Christof Marti 已提交
176 177 178
		// Separator
		if (element.separator && element.separator.label) {
			data.separator.textContent = element.separator.label;
M
Matt Bierner 已提交
179
			data.separator.style.display = '';
C
Christof Marti 已提交
180 181 182
		} else {
			data.separator.style.display = 'none';
		}
C
Christof Marti 已提交
183
		if (element.separator) {
C
Christof Marti 已提交
184 185 186 187 188
			dom.addClass(data.entry, 'quick-input-list-separator-border');
		} else {
			dom.removeClass(data.entry, 'quick-input-list-separator-border');
		}

C
Christof Marti 已提交
189 190
		// Actions
		data.actionBar.clear();
C
Christof Marti 已提交
191 192 193
		const buttons = element.item.buttons;
		if (buttons && buttons.length) {
			data.actionBar.push(buttons.map((button, index) => {
194 195 196 197 198
				let cssClasses = button.iconClass || (button.iconPath ? getIconClass(button.iconPath) : undefined);
				if (button.alwaysVisible) {
					cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible';
				}
				const action = new Action(`id-${index}`, '', cssClasses, true, () => {
C
Christof Marti 已提交
199 200 201 202
					element.fireButtonTriggered({
						button,
						item: element.item
					});
203
					return Promise.resolve();
C
Christof Marti 已提交
204
				});
205
				action.tooltip = button.tooltip || '';
C
Christof Marti 已提交
206 207
				return action;
			}), { icon: true, label: false });
C
Christof Marti 已提交
208 209 210
			dom.addClass(data.entry, 'has-actions');
		} else {
			dom.removeClass(data.entry, 'has-actions');
C
Christof Marti 已提交
211
		}
212 213
	}

C
Christof Marti 已提交
214 215
	disposeElement(element: ListElement, index: number, data: IListElementTemplateData): void {
		data.toDisposeElement = dispose(data.toDisposeElement);
J
Joao Moreno 已提交
216 217
	}

218
	disposeTemplate(data: IListElementTemplateData): void {
C
Christof Marti 已提交
219 220
		data.toDisposeElement = dispose(data.toDisposeElement);
		data.toDisposeTemplate = dispose(data.toDisposeTemplate);
221 222 223
	}
}

J
Joao Moreno 已提交
224
class ListElementDelegate implements IListVirtualDelegate<ListElement> {
225

226
	getHeight(element: ListElement): number {
C
Christof Marti 已提交
227
		return element.saneDetail ? 44 : 22;
228 229
	}

230 231
	getTemplateId(element: ListElement): string {
		return ListElementRenderer.ID;
232 233 234
	}
}

B
Benjamin Pasero 已提交
235 236 237 238 239 240 241 242 243 244
export enum QuickInputListFocus {
	First = 1,
	Second,
	Last,
	Next,
	Previous,
	NextPage,
	PreviousPage
}

245
export class QuickInputList {
246

247
	readonly id: string;
C
Christof Marti 已提交
248
	private container: HTMLElement;
249
	private list: List<ListElement>;
250
	private inputElements: Array<IQuickPickItem | IQuickPickSeparator> = [];
251
	private elements: ListElement[] = [];
252
	private elementsToIndexes = new Map<IQuickPickItem, number>();
253 254
	matchOnDescription = false;
	matchOnDetail = false;
A
Alex Ross 已提交
255
	matchOnLabel = true;
256
	sortByLabel = true;
257
	private readonly _onChangedAllVisibleChecked = new Emitter<boolean>();
258
	onChangedAllVisibleChecked: Event<boolean> = this._onChangedAllVisibleChecked.event;
259
	private readonly _onChangedCheckedCount = new Emitter<number>();
260
	onChangedCheckedCount: Event<number> = this._onChangedCheckedCount.event;
261
	private readonly _onChangedVisibleCount = new Emitter<number>();
262
	onChangedVisibleCount: Event<number> = this._onChangedVisibleCount.event;
263
	private readonly _onChangedCheckedElements = new Emitter<IQuickPickItem[]>();
264
	onChangedCheckedElements: Event<IQuickPickItem[]> = this._onChangedCheckedElements.event;
265
	private readonly _onButtonTriggered = new Emitter<IQuickPickItemButtonEvent<IQuickPickItem>>();
C
Christof Marti 已提交
266
	onButtonTriggered = this._onButtonTriggered.event;
B
Benjamin Pasero 已提交
267 268
	private readonly _onKeyDown = new Emitter<StandardKeyboardEvent>();
	onKeyDown: Event<StandardKeyboardEvent> = this._onKeyDown.event;
269
	private readonly _onLeave = new Emitter<void>();
270
	onLeave: Event<void> = this._onLeave.event;
271
	private _fireCheckedEvents = true;
272 273
	private elementDisposables: IDisposable[] = [];
	private disposables: IDisposable[] = [];
274 275 276

	constructor(
		private parent: HTMLElement,
277
		id: string,
278
		options: IQuickInputOptions,
279
	) {
280
		this.id = id;
281 282
		this.container = dom.append(this.parent, $('.quick-input-list'));
		const delegate = new ListElementDelegate();
283
		const accessibilityProvider = new QuickInputAccessibilityProvider();
284
		this.list = options.createList('QuickInput', this.container, delegate, [new ListElementRenderer()], {
285
			identityProvider: { getId: element => element.saneLabel },
286
			setRowLineHeight: false,
287
			multipleSelectionSupport: false,
C
Christof Marti 已提交
288
			horizontalScrolling: false,
289
			accessibilityProvider
290
		} as IListOptions<ListElement>);
291
		this.list.getHTMLElement().id = id;
292 293
		this.disposables.push(this.list);
		this.disposables.push(this.list.onKeyDown(e => {
C
Christof Marti 已提交
294
			const event = new StandardKeyboardEvent(e);
295 296 297 298
			switch (event.keyCode) {
				case KeyCode.Space:
					this.toggleCheckbox();
					break;
299 300 301 302 303
				case KeyCode.KEY_A:
					if (platform.isMacintosh ? e.metaKey : e.ctrlKey) {
						this.list.setFocus(range(this.list.length));
					}
					break;
304
				case KeyCode.UpArrow:
305 306 307 308 309 310 311 312
					const focus1 = this.list.getFocus();
					if (focus1.length === 1 && focus1[0] === 0) {
						this._onLeave.fire();
					}
					break;
				case KeyCode.DownArrow:
					const focus2 = this.list.getFocus();
					if (focus2.length === 1 && focus2[0] === this.list.length - 1) {
313 314 315
						this._onLeave.fire();
					}
					break;
C
Christof Marti 已提交
316
			}
B
Benjamin Pasero 已提交
317 318

			this._onKeyDown.fire(event);
C
Christof Marti 已提交
319
		}));
320
		this.disposables.push(this.list.onMouseDown(e => {
321 322 323 324 325
			if (e.browserEvent.button !== 2) {
				// Works around / fixes #64350.
				e.browserEvent.preventDefault();
			}
		}));
326
		this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => {
327 328 329 330
			if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox.
				this._onLeave.fire();
			}
		}));
C
Christof Marti 已提交
331 332 333
		this.disposables.push(this.list.onMouseMiddleClick(e => {
			this._onLeave.fire();
		}));
334 335 336 337 338 339 340 341 342 343 344 345
		this.disposables.push(this.list.onContextMenu(e => {
			if (typeof e.index === 'number') {
				e.browserEvent.preventDefault();

				// we want to treat a context menu event as
				// a gesture to open the item at the index
				// since we do not have any context menu
				// this enables for example macOS to Ctrl-
				// click on an item to open it.
				this.list.setSelection([e.index]);
			}
		}));
C
Christof Marti 已提交
346 347 348 349 350 351 352
		this.disposables.push(
			this._onChangedAllVisibleChecked,
			this._onChangedCheckedCount,
			this._onChangedVisibleCount,
			this._onChangedCheckedElements,
			this._onButtonTriggered,
			this._onLeave,
B
Benjamin Pasero 已提交
353
			this._onKeyDown
C
Christof Marti 已提交
354
		);
355 356
	}

357
	@memoize
358
	get onDidChangeFocus() {
359
		return Event.map(this.list.onDidChangeFocus, e => e.elements.map(e => e.item));
C
Christof Marti 已提交
360 361
	}

362
	@memoize
363
	get onDidChangeSelection() {
364
		return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent }));
365 366
	}

367
	getAllVisibleChecked() {
368
		return this.allVisibleChecked(this.elements, false);
369 370
	}

371
	private allVisibleChecked(elements: ListElement[], whenNoneVisible = true) {
372 373
		for (let i = 0, n = elements.length; i < n; i++) {
			const element = elements[i];
374 375 376 377 378 379
			if (!element.hidden) {
				if (!element.checked) {
					return false;
				} else {
					whenNoneVisible = true;
				}
380 381
			}
		}
382
		return whenNoneVisible;
C
Christof Marti 已提交
383 384
	}

385
	getCheckedCount() {
386 387 388 389 390 391 392 393
		let count = 0;
		const elements = this.elements;
		for (let i = 0, n = elements.length; i < n; i++) {
			if (elements[i].checked) {
				count++;
			}
		}
		return count;
C
Christof Marti 已提交
394 395
	}

396 397 398 399 400 401 402 403 404 405 406
	getVisibleCount() {
		let count = 0;
		const elements = this.elements;
		for (let i = 0, n = elements.length; i < n; i++) {
			if (!elements[i].hidden) {
				count++;
			}
		}
		return count;
	}

407
	setAllVisibleChecked(checked: boolean) {
408 409 410 411 412 413 414 415 416 417 418
		try {
			this._fireCheckedEvents = false;
			this.elements.forEach(element => {
				if (!element.hidden) {
					element.checked = checked;
				}
			});
		} finally {
			this._fireCheckedEvents = true;
			this.fireCheckedEvents();
		}
C
Christof Marti 已提交
419 420
	}

421
	setElements(inputElements: Array<IQuickPickItem | IQuickPickSeparator>): void {
422
		this.elementDisposables = dispose(this.elementDisposables);
C
Christof Marti 已提交
423
		const fireButtonTriggered = (event: IQuickPickItemButtonEvent<IQuickPickItem>) => this.fireButtonTriggered(event);
C
Christof Marti 已提交
424 425 426 427
		this.inputElements = inputElements;
		this.elements = inputElements.reduce((result, item, index) => {
			if (item.type !== 'separator') {
				const previous = index && inputElements[index - 1];
428 429 430 431 432 433 434 435
				const saneLabel = item.label && item.label.replace(/\r?\n/g, ' ');
				const saneDescription = item.description && item.description.replace(/\r?\n/g, ' ');
				const saneDetail = item.detail && item.detail.replace(/\r?\n/g, ' ');
				const saneAriaLabel = item.ariaLabel || [saneLabel, saneDescription, saneDetail]
					.map(s => s && parseCodicons(s).text)
					.filter(s => !!s)
					.join(', ');

C
Christof Marti 已提交
436 437 438
				result.push(new ListElement({
					index,
					item,
439
					saneLabel,
440
					saneAriaLabel,
441 442
					saneDescription,
					saneDetail,
443 444 445
					labelHighlights: item.highlights?.label,
					descriptionHighlights: item.highlights?.description,
					detailHighlights: item.highlights?.detail,
C
Christof Marti 已提交
446 447 448 449 450 451 452
					checked: false,
					separator: previous && previous.type === 'separator' ? previous : undefined,
					fireButtonTriggered
				}));
			}
			return result;
		}, [] as ListElement[]);
C
Christof Marti 已提交
453
		this.elementDisposables.push(...this.elements);
454
		this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents())));
455 456 457 458 459

		this.elementsToIndexes = this.elements.reduce((map, element, index) => {
			map.set(element.item, index);
			return map;
		}, new Map<IQuickPickItem, number>());
460
		this.list.splice(0, this.list.length); // Clear focus and selection first, sending the events when the list is empty.
461
		this.list.splice(0, this.list.length, this.elements);
462
		this._onChangedVisibleCount.fire(this.elements.length);
463 464
	}

465 466 467 468
	getElementsCount(): number {
		return this.inputElements.length;
	}

469 470
	getFocusedElements() {
		return this.list.getFocusedElements()
471 472 473
			.map(e => e.item);
	}

474 475 476
	setFocusedElements(items: IQuickPickItem[]) {
		this.list.setFocus(items
			.filter(item => this.elementsToIndexes.has(item))
477
			.map(item => this.elementsToIndexes.get(item)!));
A
Alex Ross 已提交
478
		if (items.length > 0) {
479 480 481 482
			const focused = this.list.getFocus()[0];
			if (typeof focused === 'number') {
				this.list.reveal(focused);
			}
A
Alex Ross 已提交
483
		}
484 485
	}

486 487 488 489
	getActiveDescendant() {
		return this.list.getHTMLElement().getAttribute('aria-activedescendant');
	}

490 491 492 493 494
	getSelectedElements() {
		return this.list.getSelectedElements()
			.map(e => e.item);
	}

495 496 497
	setSelectedElements(items: IQuickPickItem[]) {
		this.list.setSelection(items
			.filter(item => this.elementsToIndexes.has(item))
498
			.map(item => this.elementsToIndexes.get(item)!));
499 500
	}

501 502
	getCheckedElements() {
		return this.elements.filter(e => e.checked)
503 504 505
			.map(e => e.item);
	}

506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
	setCheckedElements(items: IQuickPickItem[]) {
		try {
			this._fireCheckedEvents = false;
			const checked = new Set();
			for (const item of items) {
				checked.add(item);
			}
			for (const element of this.elements) {
				element.checked = checked.has(element.item);
			}
		} finally {
			this._fireCheckedEvents = true;
			this.fireCheckedEvents();
		}
	}

C
Christof Marti 已提交
522
	set enabled(value: boolean) {
523
		this.list.getHTMLElement().style.pointerEvents = value ? '' : 'none';
C
Christof Marti 已提交
524 525
	}

B
Benjamin Pasero 已提交
526
	focus(what: QuickInputListFocus): void {
527 528 529 530
		if (!this.list.length) {
			return;
		}

531
		if (what === QuickInputListFocus.Next && this.list.getFocus()[0] === this.list.length - 1) {
B
Benjamin Pasero 已提交
532
			what = QuickInputListFocus.First;
533
		}
B
Benjamin Pasero 已提交
534

535
		if (what === QuickInputListFocus.Previous && this.list.getFocus()[0] === 0) {
B
Benjamin Pasero 已提交
536
			what = QuickInputListFocus.Last;
537
		}
B
Benjamin Pasero 已提交
538

B
Benjamin Pasero 已提交
539 540 541 542
		if (what === QuickInputListFocus.Second && this.list.length < 2) {
			what = QuickInputListFocus.First;
		}

B
Benjamin Pasero 已提交
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
		switch (what) {
			case QuickInputListFocus.First:
				this.list.focusFirst();
				break;
			case QuickInputListFocus.Second:
				this.list.focusNth(1);
				break;
			case QuickInputListFocus.Last:
				this.list.focusLast();
				break;
			case QuickInputListFocus.Next:
				this.list.focusNext();
				break;
			case QuickInputListFocus.Previous:
				this.list.focusPrevious();
				break;
			case QuickInputListFocus.NextPage:
				this.list.focusNextPage();
				break;
			case QuickInputListFocus.PreviousPage:
				this.list.focusPreviousPage();
				break;
		}

B
Benjamin Pasero 已提交
567
		const focused = this.list.getFocus()[0];
568
		if (typeof focused === 'number') {
B
Benjamin Pasero 已提交
569 570
			this.list.reveal(focused);
		}
571 572
	}

573 574 575 576
	clearFocus() {
		this.list.setFocus([]);
	}

577 578 579 580
	domFocus() {
		this.list.domFocus();
	}

C
Christof Marti 已提交
581 582
	layout(maxHeight?: number): void {
		this.list.getHTMLElement().style.maxHeight = maxHeight ? `calc(${Math.floor(maxHeight / 44) * 44}px)` : '';
583 584 585
		this.list.layout();
	}

586
	filter(query: string): boolean {
587
		if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
588
			this.list.layout();
589
			return false;
A
Alex Ross 已提交
590
		}
591 592 593
		query = query.trim();

		// Reset filtering
594
		if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
595 596 597 598 599
			this.elements.forEach(element => {
				element.labelHighlights = undefined;
				element.descriptionHighlights = undefined;
				element.detailHighlights = undefined;
				element.hidden = false;
C
Christof Marti 已提交
600 601
				const previous = element.index && this.inputElements[element.index - 1];
				element.separator = previous && previous.type === 'separator' ? previous : undefined;
602 603 604
			});
		}

605
		// Filter by value (since we support codicons, use codicon aware fuzzy matching)
A
Alex Ross 已提交
606
		else {
607
			this.elements.forEach(element => {
608 609 610
				const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneLabel))) : undefined;
				const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDescription || ''))) : undefined;
				const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDetail || ''))) : undefined;
611

C
Christof Marti 已提交
612
				if (labelHighlights || descriptionHighlights || detailHighlights) {
613 614 615 616 617 618 619 620
					element.labelHighlights = labelHighlights;
					element.descriptionHighlights = descriptionHighlights;
					element.detailHighlights = detailHighlights;
					element.hidden = false;
				} else {
					element.labelHighlights = undefined;
					element.descriptionHighlights = undefined;
					element.detailHighlights = undefined;
C
Christof Marti 已提交
621
					element.hidden = !element.item.alwaysShow;
622
				}
C
Christof Marti 已提交
623
				element.separator = undefined;
624 625 626
			});
		}

627 628
		const shownElements = this.elements.filter(element => !element.hidden);

629
		// Sort by value
630
		if (this.sortByLabel && query) {
C
Christof Marti 已提交
631 632 633 634 635
			const normalizedSearchValue = query.toLowerCase();
			shownElements.sort((a, b) => {
				return compareEntries(a, b, normalizedSearchValue);
			});
		}
636

637 638 639 640
		this.elementsToIndexes = shownElements.reduce((map, element, index) => {
			map.set(element.item, index);
			return map;
		}, new Map<IQuickPickItem, number>());
641
		this.list.splice(0, this.list.length, shownElements);
642
		this.list.setFocus([]);
C
Christof Marti 已提交
643
		this.list.layout();
644

645
		this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
646
		this._onChangedVisibleCount.fire(shownElements.length);
647 648

		return true;
649
	}
C
Christof Marti 已提交
650 651

	toggleCheckbox() {
652 653 654 655 656 657 658 659 660 661
		try {
			this._fireCheckedEvents = false;
			const elements = this.list.getFocusedElements();
			const allChecked = this.allVisibleChecked(elements);
			for (const element of elements) {
				element.checked = !allChecked;
			}
		} finally {
			this._fireCheckedEvents = true;
			this.fireCheckedEvents();
C
Christof Marti 已提交
662 663 664
		}
	}

665
	display(display: boolean) {
666
		this.container.style.display = display ? '' : 'none';
667 668
	}

669 670 671 672
	isDisplayed() {
		return this.container.style.display !== 'none';
	}

673 674 675 676 677
	dispose() {
		this.elementDisposables = dispose(this.elementDisposables);
		this.disposables = dispose(this.disposables);
	}

678 679
	private fireCheckedEvents() {
		if (this._fireCheckedEvents) {
680 681 682
			this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
			this._onChangedCheckedCount.fire(this.getCheckedCount());
			this._onChangedCheckedElements.fire(this.getCheckedElements());
683 684
		}
	}
C
Christof Marti 已提交
685 686 687 688

	private fireButtonTriggered(event: IQuickPickItemButtonEvent<IQuickPickItem>) {
		this._onButtonTriggered.fire(event);
	}
689 690 691 692

	style(styles: IListStyles) {
		this.list.style(styles);
	}
693 694
}

695
function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number {
696 697 698 699 700 701 702 703 704 705 706

	const labelHighlightsA = elementA.labelHighlights || [];
	const labelHighlightsB = elementB.labelHighlights || [];
	if (labelHighlightsA.length && !labelHighlightsB.length) {
		return -1;
	}

	if (!labelHighlightsA.length && labelHighlightsB.length) {
		return 1;
	}

707 708 709 710
	if (labelHighlightsA.length === 0 && labelHighlightsB.length === 0) {
		return 0;
	}

C
Christof Marti 已提交
711
	return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor);
712
}
713

J
João Moreno 已提交
714 715
class QuickInputAccessibilityProvider implements IListAccessibilityProvider<ListElement> {

716 717 718 719
	getWidgetAriaLabel(): string {
		return localize('quickInput', "Quick Input");
	}

720 721 722
	getAriaLabel(element: ListElement): string | null {
		return element.saneAriaLabel;
	}
J
João Moreno 已提交
723

724 725 726 727
	getWidgetRole() {
		return 'listbox';
	}

J
João Moreno 已提交
728 729 730
	getRole() {
		return 'option';
	}
731
}