listWidget.ts 31.8 KB
Newer Older
J
Joao Moreno 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import 'vs/css!./list';
J
Joao Moreno 已提交
7
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
8
import { isNumber } from 'vs/base/common/types';
J
Joao Moreno 已提交
9
import { range, firstIndex } from 'vs/base/common/arrays';
J
Joao Moreno 已提交
10
import { memoize } from 'vs/base/common/decorators';
J
Joao Moreno 已提交
11
import * as DOM from 'vs/base/browser/dom';
J
Joao Moreno 已提交
12
import * as platform from 'vs/base/common/platform';
J
Joao Moreno 已提交
13
import { Gesture } from 'vs/base/browser/touch';
14 15
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
J
Joao Moreno 已提交
16
import Event, { Emitter, EventBufferer, chain, mapEvent, anyEvent } from 'vs/base/common/event';
J
Joao Moreno 已提交
17
import { domEvent } from 'vs/base/browser/event';
J
Joao Moreno 已提交
18
import { IDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list';
J
Joao Moreno 已提交
19
import { ListView, IListViewOptions } from './listView';
20 21
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
J
Joao Moreno 已提交
22
import { ISpliceable } from 'vs/base/common/sequence';
J
Joao Moreno 已提交
23

J
Joao Moreno 已提交
24 25 26 27
export interface IIdentityProvider<T> {
	(element: T): string;
}

28 29 30 31 32
class CombinedSpliceable<T> implements ISpliceable<T> {

	constructor(private spliceables: ISpliceable<T>[]) { }

	splice(start: number, deleteCount: number, elements: T[]): void {
J
Joao Moreno 已提交
33 34 35
		for (const spliceable of this.spliceables) {
			spliceable.splice(start, deleteCount, elements);
		}
36 37 38
	}
}

J
Joao Moreno 已提交
39 40 41 42
interface ITraitChangeEvent {
	indexes: number[];
}

J
Joao Moreno 已提交
43
type ITraitTemplateData = HTMLElement;
J
Joao Moreno 已提交
44

J
Joao Moreno 已提交
45
interface IRenderedContainer {
J
Joao Moreno 已提交
46 47
	templateData: ITraitTemplateData;
	index: number;
J
Joao Moreno 已提交
48 49
}

M
Matt Bierner 已提交
50
class TraitRenderer<T> implements IRenderer<T, ITraitTemplateData>
J
Joao Moreno 已提交
51
{
J
Joao Moreno 已提交
52
	private renderedElements: IRenderedContainer[] = [];
J
Joao Moreno 已提交
53 54

	constructor(private trait: Trait<T>) { }
J
Joao Moreno 已提交
55

J
Joao Moreno 已提交
56
	get templateId(): string {
J
Joao Moreno 已提交
57 58 59 60
		return `template:${this.trait.trait}`;
	}

	renderTemplate(container: HTMLElement): ITraitTemplateData {
J
Joao Moreno 已提交
61
		return container;
J
Joao Moreno 已提交
62 63 64
	}

	renderElement(element: T, index: number, templateData: ITraitTemplateData): void {
J
Joao Moreno 已提交
65 66 67 68 69 70 71 72 73 74
		const renderedElementIndex = firstIndex(this.renderedElements, el => el.templateData === templateData);

		if (renderedElementIndex >= 0) {
			const rendered = this.renderedElements[renderedElementIndex];
			this.trait.unrender(templateData);
			rendered.index = index;
		} else {
			const rendered = { index, templateData };
			this.renderedElements.push(rendered);
		}
J
Joao Moreno 已提交
75

J
Joao Moreno 已提交
76
		this.trait.renderIndex(index, templateData);
J
Joao Moreno 已提交
77 78
	}

J
Joao Moreno 已提交
79 80 81 82 83 84 85 86 87 88 89 90 91
	splice(start: number, deleteCount: number, insertCount: number): void {
		const rendered: IRenderedContainer[] = [];

		for (let i = 0; i < this.renderedElements.length; i++) {
			const renderedElement = this.renderedElements[i];

			if (renderedElement.index < start) {
				rendered.push(renderedElement);
			} else if (renderedElement.index >= start + deleteCount) {
				rendered.push({
					index: renderedElement.index + insertCount - deleteCount,
					templateData: renderedElement.templateData
				});
J
Joao Moreno 已提交
92 93
			}
		}
J
Joao Moreno 已提交
94 95

		this.renderedElements = rendered;
J
Joao Moreno 已提交
96 97
	}

J
Joao Moreno 已提交
98 99 100 101
	renderIndexes(indexes: number[]): void {
		for (const { index, templateData } of this.renderedElements) {
			if (indexes.indexOf(index) > -1) {
				this.trait.renderIndex(index, templateData);
J
Joao Moreno 已提交
102 103
			}
		}
J
Joao Moreno 已提交
104 105
	}

J
Joao Moreno 已提交
106
	disposeTemplate(templateData: ITraitTemplateData): void {
J
Joao Moreno 已提交
107 108 109 110 111 112 113
		const index = firstIndex(this.renderedElements, el => el.templateData === templateData);

		if (index < 0) {
			return;
		}

		this.renderedElements.splice(index, 1);
J
Joao Moreno 已提交
114 115 116
	}
}

117
class Trait<T> implements ISpliceable<boolean>, IDisposable {
J
Joao Moreno 已提交
118

J
Joao Moreno 已提交
119 120 121
	/**
	 * Sorted indexes which have this trait.
	 */
J
Joao Moreno 已提交
122
	private indexes: number[];
J
Joao Moreno 已提交
123

J
Joao Moreno 已提交
124
	private _onChange = new Emitter<ITraitChangeEvent>();
J
Joao Moreno 已提交
125 126 127 128 129
	get onChange(): Event<ITraitChangeEvent> { return this._onChange.event; }

	get trait(): string { return this._trait; }

	@memoize
M
Matt Bierner 已提交
130 131
	get renderer(): TraitRenderer<T> {
		return new TraitRenderer<T>(this);
J
Joao Moreno 已提交
132
	}
J
Joao Moreno 已提交
133

J
Joao Moreno 已提交
134 135 136 137
	constructor(private _trait: string) {
		this.indexes = [];
	}

J
Joao Moreno 已提交
138 139
	splice(start: number, deleteCount: number, elements: boolean[]): void {
		const diff = elements.length - deleteCount;
J
Joao Moreno 已提交
140
		const end = start + deleteCount;
J
Joao Moreno 已提交
141 142 143 144 145
		const indexes = [
			...this.indexes.filter(i => i < start),
			...elements.reduce((r, hasTrait, i) => hasTrait ? [...r, i + start] : r, []),
			...this.indexes.filter(i => i >= end).map(i => i + diff)
		];
J
Joao Moreno 已提交
146

J
Joao Moreno 已提交
147
		this.renderer.splice(start, deleteCount, elements.length);
J
Joao Moreno 已提交
148
		this.set(indexes);
J
Joao Moreno 已提交
149 150
	}

J
Joao Moreno 已提交
151
	renderIndex(index: number, container: HTMLElement): void {
A
Alex Dima 已提交
152
		DOM.toggleClass(container, this._trait, this.contains(index));
J
Joao Moreno 已提交
153 154
	}

J
Joao Moreno 已提交
155 156 157 158
	unrender(container: HTMLElement): void {
		DOM.removeClass(container, this._trait);
	}

J
Joao Moreno 已提交
159 160 161 162 163 164
	/**
	 * Sets the indexes which should have this trait.
	 *
	 * @param indexes Indexes which should have this trait.
	 * @return The old indexes which had this trait.
	 */
J
Joao Moreno 已提交
165
	set(indexes: number[]): number[] {
J
Joao Moreno 已提交
166 167
		const result = this.indexes;
		this.indexes = indexes;
J
Joao Moreno 已提交
168 169 170 171

		const toRender = disjunction(result, indexes);
		this.renderer.renderIndexes(toRender);

J
Joao Moreno 已提交
172
		this._onChange.fire({ indexes });
J
Joao Moreno 已提交
173
		return result;
J
Joao Moreno 已提交
174 175
	}

J
Joao Moreno 已提交
176 177 178 179
	get(): number[] {
		return this.indexes;
	}

J
Joao Moreno 已提交
180 181
	contains(index: number): boolean {
		return this.indexes.some(i => i === index);
J
Joao Moreno 已提交
182 183
	}

J
Joao Moreno 已提交
184 185 186 187
	dispose() {
		this.indexes = null;
		this._onChange = dispose(this._onChange);
	}
J
Joao Moreno 已提交
188 189
}

A
Alex Dima 已提交
190 191
class FocusTrait<T> extends Trait<T> {

J
Joao Moreno 已提交
192 193 194
	constructor(
		private getDomId: IIdentityProvider<number>
	) {
A
Alex Dima 已提交
195 196 197
		super('focused');
	}

J
Joao Moreno 已提交
198 199
	renderIndex(index: number, container: HTMLElement): void {
		super.renderIndex(index, container);
J
João Moreno 已提交
200
		container.setAttribute('role', 'treeitem');
J
Joao Moreno 已提交
201
		container.setAttribute('id', this.getDomId(index));
A
Alex Dima 已提交
202 203 204
	}
}

205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
class Aria<T> implements IRenderer<T, HTMLElement>, ISpliceable<T> {

	private length = 0;

	get templateId(): string {
		return 'aria';
	}

	splice(start: number, deleteCount: number, elements: T[]): void {
		this.length += elements.length - deleteCount;
	}

	renderTemplate(container: HTMLElement): HTMLElement {
		return container;
	}

	renderElement(element: T, index: number, container: HTMLElement): void {
		container.setAttribute('aria-setsize', `${this.length}`);
		container.setAttribute('aria-posinset', `${index + 1}`);
	}

	disposeTemplate(container: HTMLElement): void {
		// noop
	}
}

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
/**
 * The TraitSpliceable is used as a util class to be able
 * to preserve traits across splice calls, given an identity
 * provider.
 */
class TraitSpliceable<T> implements ISpliceable<T> {

	constructor(
		private trait: Trait<T>,
		private view: ListView<T>,
		private getId?: IIdentityProvider<T>
	) { }

	splice(start: number, deleteCount: number, elements: T[]): void {
		if (!this.getId) {
			return this.trait.splice(start, deleteCount, elements.map(e => false));
		}

		const pastElementsWithTrait = this.trait.get().map(i => this.getId(this.view.element(i)));
		const elementsWithTrait = elements.map(e => pastElementsWithTrait.indexOf(this.getId(e)) > -1);

		this.trait.splice(start, deleteCount, elementsWithTrait);
	}
}

J
Joao Moreno 已提交
256 257 258 259
function isInputElement(e: HTMLElement): boolean {
	return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
}

260
class KeyboardController<T> implements IDisposable {
261

262
	private disposables: IDisposable[];
J
Joao Moreno 已提交
263 264 265

	constructor(
		private list: List<T>,
J
Joao Moreno 已提交
266 267
		private view: ListView<T>,
		options: IListOptions<T>
J
Joao Moreno 已提交
268
	) {
J
Joao Moreno 已提交
269
		const multipleSelectionSupport = !(options.multipleSelectionSupport === false);
270 271
		this.disposables = [];

J
Joao Moreno 已提交
272
		const onKeyDown = chain(domEvent(view.domNode, 'keydown'))
J
Joao Moreno 已提交
273 274
			.filter(e => !isInputElement(e.target as HTMLElement))
			.map(e => new StandardKeyboardEvent(e));
J
Joao Moreno 已提交
275 276 277 278 279 280

		onKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUpArrow, this, this.disposables);
		onKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDownArrow, this, this.disposables);
J
Joao Moreno 已提交
281
		onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
J
Joao Moreno 已提交
282 283 284 285

		if (multipleSelectionSupport) {
			onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KEY_A).on(this.onCtrlA, this, this.disposables);
		}
J
Joao Moreno 已提交
286 287
	}

288 289 290
	private onEnter(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
J
Joao Moreno 已提交
291
		this.list.setSelection(this.list.getFocus());
J
Joao Moreno 已提交
292
		this.list.open(this.list.getFocus());
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
	}

	private onUpArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
		this.list.focusPrevious();
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

	private onDownArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
		this.list.focusNext();
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

	private onPageUpArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
		this.list.focusPreviousPage();
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

	private onPageDownArrow(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
		this.list.focusNextPage();
		this.list.reveal(this.list.getFocus()[0]);
		this.view.domNode.focus();
	}

J
Joao Moreno 已提交
327 328 329 330 331 332 333 334 335 336 337 338 339 340
	private onCtrlA(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
		this.list.setSelection(range(this.list.length));
		this.view.domNode.focus();
	}

	private onEscape(e: StandardKeyboardEvent): void {
		e.preventDefault();
		e.stopPropagation();
		this.list.setSelection([]);
		this.view.domNode.focus();
	}

J
Joao Moreno 已提交
341
	dispose() {
342
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
343 344 345
	}
}

J
Joao Moreno 已提交
346 347
function isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
	return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
J
Joao Moreno 已提交
348 349
}

J
Joao Moreno 已提交
350 351
function isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
	return event.browserEvent.shiftKey;
J
Joao Moreno 已提交
352 353
}

J
Joao Moreno 已提交
354
function isSelectionChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
J
Joao Moreno 已提交
355 356 357
	return isSelectionSingleChangeEvent(event) || isSelectionRangeChangeEvent(event);
}

358
class MouseController<T> implements IDisposable {
J
Joao Moreno 已提交
359

J
Joao Moreno 已提交
360
	private multipleSelectionSupport: boolean;
361
	private didJustPressContextMenuKey: boolean = false;
J
Joao Moreno 已提交
362
	private disposables: IDisposable[] = [];
363

364
	@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
365
		const fromKeydown = chain(domEvent(this.view.domNode, 'keydown'))
366
			.map(e => new StandardKeyboardEvent(e))
367 368 369 370 371 372 373 374 375 376 377 378
			.filter(e => this.didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
			.filter(e => { e.preventDefault(); e.stopPropagation(); return false; })
			.event as Event<any>;

		const fromKeyup = chain(domEvent(this.view.domNode, 'keyup'))
			.filter(() => {
				const didJustPressContextMenuKey = this.didJustPressContextMenuKey;
				this.didJustPressContextMenuKey = false;
				return didJustPressContextMenuKey;
			})
			.filter(() => this.list.getFocus().length > 0)
			.map(() => {
379 380 381 382 383 384 385 386
				const index = this.list.getFocus()[0];
				const element = this.view.element(index);
				const anchor = this.view.domElement(index);
				return { index, element, anchor };
			})
			.filter(({ anchor }) => !!anchor)
			.event;

J
Joao Moreno 已提交
387
		const fromMouse = chain(this.view.onContextMenu)
388
			.filter(() => !this.didJustPressContextMenuKey)
J
Joao Moreno 已提交
389
			.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY } }))
390 391
			.event;

392
		return anyEvent<IListContextMenuEvent<T>>(fromKeydown, fromKeyup, fromMouse);
393 394
	}

395 396
	constructor(
		private list: List<T>,
397
		private view: ListView<T>,
J
Joao Moreno 已提交
398
		private options: IListOptions<T> = {}
399
	) {
J
Joao Moreno 已提交
400 401
		this.multipleSelectionSupport = options.multipleSelectionSupport !== false;

J
Joao Moreno 已提交
402 403 404 405 406
		view.onMouseDown(this.onMouseDown, this, this.disposables);
		view.onMouseClick(this.onPointer, this, this.disposables);
		view.onMouseDblClick(this.onDoubleClick, this, this.disposables);
		view.onTouchStart(this.onMouseDown, this, this.disposables);
		view.onTap(this.onPointer, this, this.disposables);
407
		Gesture.addTarget(view.domNode);
408 409
	}

J
Joao Moreno 已提交
410
	private onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
J
Joao Moreno 已提交
411
		if (this.options.focusOnMouseDown === false) {
J
Joao Moreno 已提交
412 413
			e.browserEvent.preventDefault();
			e.browserEvent.stopPropagation();
J
Joao Moreno 已提交
414
		} else if (document.activeElement !== e.browserEvent.target) {
J
Joao Moreno 已提交
415 416
			this.view.domNode.focus();
		}
J
Joao Moreno 已提交
417 418 419 420

		let reference = this.list.getFocus()[0];
		reference = reference === undefined ? this.list.getSelection()[0] : reference;

J
Joao Moreno 已提交
421
		if (this.multipleSelectionSupport && isSelectionRangeChangeEvent(e)) {
J
Joao Moreno 已提交
422 423 424 425 426 427
			return this.changeSelection(e, reference);
		}

		const focus = e.index;
		this.list.setFocus([focus]);

J
Joao Moreno 已提交
428
		if (this.multipleSelectionSupport && isSelectionChangeEvent(e)) {
J
Joao Moreno 已提交
429 430
			return this.changeSelection(e, reference);
		}
431 432 433 434 435

		if (this.options.selectOnMouseDown) {
			this.list.setSelection([focus]);
			this.list.open([focus]);
		}
436 437
	}

J
Joao Moreno 已提交
438
	private onPointer(e: IListMouseEvent<T>): void {
J
Joao Moreno 已提交
439
		if (this.multipleSelectionSupport && isSelectionChangeEvent(e)) {
J
Joao Moreno 已提交
440 441
			return;
		}
442

J
Joao Moreno 已提交
443 444 445 446 447
		if (!this.options.selectOnMouseDown) {
			const focus = this.list.getFocus();
			this.list.setSelection(focus);
			this.list.open(focus);
		}
J
Joao Moreno 已提交
448
	}
449

J
Joao Moreno 已提交
450
	private onDoubleClick(e: IListMouseEvent<T>): void {
J
Joao Moreno 已提交
451
		if (this.multipleSelectionSupport && isSelectionChangeEvent(e)) {
J
Joao Moreno 已提交
452 453 454 455 456 457 458 459
			return;
		}

		const focus = this.list.getFocus();
		this.list.setSelection(focus);
		this.list.pin(focus);
	}

J
Joao Moreno 已提交
460
	private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>, reference: number | undefined): void {
J
Joao Moreno 已提交
461
		const focus = e.index;
462

J
Joao Moreno 已提交
463 464 465
		if (isSelectionRangeChangeEvent(e) && reference !== undefined) {
			const min = Math.min(reference, focus);
			const max = Math.max(reference, focus);
J
Joao Moreno 已提交
466
			const rangeSelection = range(min, max + 1);
J
Joao Moreno 已提交
467 468
			const selection = this.list.getSelection();
			const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference);
469

J
Joao Moreno 已提交
470 471
			if (contiguousRange.length === 0) {
				return;
472 473
			}

J
Joao Moreno 已提交
474 475
			const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));
			this.list.setSelection(newSelection);
J
Joao Moreno 已提交
476

J
Joao Moreno 已提交
477
		} else if (isSelectionSingleChangeEvent(e)) {
J
Joao Moreno 已提交
478 479 480 481
			const selection = this.list.getSelection();
			const newSelection = selection.filter(i => i !== focus);

			if (selection.length === newSelection.length) {
482
				this.list.setSelection([...newSelection, focus]);
J
Joao Moreno 已提交
483 484 485 486
			} else {
				this.list.setSelection(newSelection);
			}
		}
487 488 489 490 491 492 493
	}

	dispose() {
		this.disposables = dispose(this.disposables);
	}
}

J
Joao Moreno 已提交
494
export interface IListOptions<T> extends IListViewOptions, IListStyles {
J
Joao Moreno 已提交
495
	identityProvider?: IIdentityProvider<T>;
J
João Moreno 已提交
496
	ariaLabel?: string;
497
	mouseSupport?: boolean;
J
Joao Moreno 已提交
498 499
	selectOnMouseDown?: boolean;
	focusOnMouseDown?: boolean;
500
	keyboardSupport?: boolean;
J
Joao Moreno 已提交
501
	multipleSelectionSupport?: boolean;
J
Joao Moreno 已提交
502 503
}

504 505
export interface IListStyles {
	listFocusBackground?: Color;
506
	listFocusForeground?: Color;
507 508 509 510 511
	listActiveSelectionBackground?: Color;
	listActiveSelectionForeground?: Color;
	listFocusAndSelectionBackground?: Color;
	listFocusAndSelectionForeground?: Color;
	listInactiveSelectionBackground?: Color;
512
	listInactiveSelectionForeground?: Color;
M
Martin Aeschlimann 已提交
513
	listInactiveFocusBackground?: Color;
514
	listHoverBackground?: Color;
515
	listHoverForeground?: Color;
516 517
	listDropBackground?: Color;
	listFocusOutline?: Color;
518 519 520
	listInactiveFocusOutline?: Color;
	listSelectionOutline?: Color;
	listHoverOutline?: Color;
521 522 523 524 525 526 527 528 529 530 531 532 533
}

const defaultStyles: IListStyles = {
	listFocusBackground: Color.fromHex('#073655'),
	listActiveSelectionBackground: Color.fromHex('#0E639C'),
	listActiveSelectionForeground: Color.fromHex('#FFFFFF'),
	listFocusAndSelectionBackground: Color.fromHex('#094771'),
	listFocusAndSelectionForeground: Color.fromHex('#FFFFFF'),
	listInactiveSelectionBackground: Color.fromHex('#3F3F46'),
	listHoverBackground: Color.fromHex('#2A2D2E'),
	listDropBackground: Color.fromHex('#383B3D')
};

J
Joao Moreno 已提交
534
const DefaultOptions: IListOptions<any> = {
535
	keyboardSupport: true,
I
isidor 已提交
536
	mouseSupport: true,
J
Joao Moreno 已提交
537
	multipleSelectionSupport: true
538
};
J
Joao Moreno 已提交
539

J
Joao Moreno 已提交
540 541
// TODO@Joao: move these utils into a SortedArray class

542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
function getContiguousRangeContaining(range: number[], value: number): number[] {
	const index = range.indexOf(value);

	if (index === -1) {
		return [];
	}

	const result = [];
	let i = index - 1;
	while (i >= 0 && range[i] === value - (index - i)) {
		result.push(range[i--]);
	}

	result.reverse();
	i = index;
	while (i < range.length && range[i] === value + (i - index)) {
		result.push(range[i++]);
	}

	return result;
}

/**
 * Given two sorted collections of numbers, returns the intersection
 * betweem them (OR).
 */
function disjunction(one: number[], other: number[]): number[] {
	const result = [];
	let i = 0, j = 0;

	while (i < one.length || j < other.length) {
		if (i >= one.length) {
			result.push(other[j++]);
		} else if (j >= other.length) {
			result.push(one[i++]);
		} else if (one[i] === other[j]) {
			result.push(one[i]);
			i++;
			j++;
			continue;
		} else if (one[i] < other[j]) {
			result.push(one[i++]);
		} else {
			result.push(other[j++]);
		}
	}

	return result;
}

/**
 * Given two sorted collections of numbers, returns the relative
 * complement between them (XOR).
 */
function relativeComplement(one: number[], other: number[]): number[] {
	const result = [];
	let i = 0, j = 0;

	while (i < one.length || j < other.length) {
		if (i >= one.length) {
			result.push(other[j++]);
		} else if (j >= other.length) {
			result.push(one[i++]);
		} else if (one[i] === other[j]) {
			i++;
			j++;
			continue;
		} else if (one[i] < other[j]) {
			result.push(one[i++]);
		} else {
			j++;
		}
	}

	return result;
}

619 620
const numericSort = (a: number, b: number) => a - b;

J
Joao Moreno 已提交
621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
class PipelineRenderer<T> implements IRenderer<T, any> {

	constructor(
		private _templateId: string,
		private renderers: IRenderer<T, any>[]
	) { }

	get templateId(): string {
		return this._templateId;
	}

	renderTemplate(container: HTMLElement): any[] {
		return this.renderers.map(r => r.renderTemplate(container));
	}

	renderElement(element: T, index: number, templateData: any[]): void {
J
Joao Moreno 已提交
637 638 639 640 641
		let i = 0;

		for (const renderer of this.renderers) {
			renderer.renderElement(element, index, templateData[i++]);
		}
J
Joao Moreno 已提交
642 643 644
	}

	disposeTemplate(templateData: any[]): void {
J
Joao Moreno 已提交
645 646 647 648 649
		let i = 0;

		for (const renderer of this.renderers) {
			renderer.disposeTemplate(templateData[i]);
		}
J
Joao Moreno 已提交
650 651 652
	}
}

653
export class List<T> implements ISpliceable<T>, IDisposable {
J
Joao Moreno 已提交
654

J
Joao Moreno 已提交
655
	private static InstanceCount = 0;
J
Johannes Rieken 已提交
656
	private idPrefix = `list_id_${++List.InstanceCount}`;
A
Alex Dima 已提交
657 658 659

	private focus: Trait<T>;
	private selection: Trait<T>;
J
Joao Moreno 已提交
660
	private eventBufferer = new EventBufferer();
J
Joao Moreno 已提交
661
	private view: ListView<T>;
662
	private spliceable: ISpliceable<T>;
J
Joao Moreno 已提交
663
	private disposables: IDisposable[];
664
	private styleElement: HTMLStyleElement;
J
Joao Moreno 已提交
665

J
Joao Moreno 已提交
666
	@memoize get onFocusChange(): Event<IListEvent<T>> {
J
Joao Moreno 已提交
667
		return mapEvent(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e));
J
Joao Moreno 已提交
668 669
	}

J
Joao Moreno 已提交
670
	@memoize get onSelectionChange(): Event<IListEvent<T>> {
J
Joao Moreno 已提交
671
		return mapEvent(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e));
J
Joao Moreno 已提交
672 673
	}

674
	readonly onContextMenu: Event<IListContextMenuEvent<T>> = Event.None;
J
Joao Moreno 已提交
675

J
Joao Moreno 已提交
676 677 678 679 680
	private _onOpen = new Emitter<number[]>();
	@memoize get onOpen(): Event<IListEvent<T>> {
		return mapEvent(this._onOpen.event, indexes => this.toListEvent({ indexes }));
	}

J
Joao Moreno 已提交
681 682 683 684 685
	private _onPin = new Emitter<number[]>();
	@memoize get onPin(): Event<IListEvent<T>> {
		return mapEvent(this._onPin.event, indexes => this.toListEvent({ indexes }));
	}

J
Joao Moreno 已提交
686 687 688 689 690 691 692 693 694 695
	get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }
	get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }
	get onMouseUp(): Event<IListMouseEvent<T>> { return this.view.onMouseUp; }
	get onMouseDown(): Event<IListMouseEvent<T>> { return this.view.onMouseDown; }
	get onMouseOver(): Event<IListMouseEvent<T>> { return this.view.onMouseOver; }
	get onMouseMove(): Event<IListMouseEvent<T>> { return this.view.onMouseMove; }
	get onMouseOut(): Event<IListMouseEvent<T>> { return this.view.onMouseOut; }
	get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }
	get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }

J
Joao Moreno 已提交
696 697 698 699
	get onKeyDown(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keydown'); }
	get onKeyUp(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keyup'); }
	get onKeyPress(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keypress'); }

700 701
	readonly onDidFocus: Event<void>;
	readonly onDidBlur: Event<void>;
702

703 704
	private _onDidDispose = new Emitter<void>();
	get onDidDispose(): Event<void> { return this._onDidDispose.event; }
705

J
Joao Moreno 已提交
706 707 708
	constructor(
		container: HTMLElement,
		delegate: IDelegate<T>,
J
Joao Moreno 已提交
709
		renderers: IRenderer<T, any>[],
J
Joao Moreno 已提交
710
		options: IListOptions<T> = DefaultOptions
J
Joao Moreno 已提交
711
	) {
712
		const aria = new Aria();
J
Joao Moreno 已提交
713
		this.focus = new FocusTrait(i => this.getElementDomId(i));
J
Joao Moreno 已提交
714
		this.selection = new Trait('selected');
715

716
		mixin(options, defaultStyles, false);
J
Joao Moreno 已提交
717

718
		renderers = renderers.map(r => new PipelineRenderer(r.templateId, [aria, this.focus.renderer, this.selection.renderer, r]));
J
Joao Moreno 已提交
719

J
Joao Moreno 已提交
720
		this.view = new ListView(container, delegate, renderers, options);
J
João Moreno 已提交
721
		this.view.domNode.setAttribute('role', 'tree');
722
		DOM.addClass(this.view.domNode, this.idPrefix);
J
Joao Moreno 已提交
723 724
		this.view.domNode.tabIndex = 0;

725 726
		this.styleElement = DOM.createStyleSheet(this.view.domNode);

727
		this.spliceable = new CombinedSpliceable([
728
			aria,
729 730 731 732 733
			new TraitSpliceable(this.focus, this.view, options.identityProvider),
			new TraitSpliceable(this.selection, this.view, options.identityProvider),
			this.view
		]);

734
		this.disposables = [this.focus, this.selection, this.view, this._onDidDispose];
735

736 737
		this.onDidFocus = mapEvent(domEvent(this.view.domNode, 'focus', true), () => null);
		this.onDidBlur = mapEvent(domEvent(this.view.domNode, 'blur', true), () => null);
738

739
		if (typeof options.keyboardSupport !== 'boolean' || options.keyboardSupport) {
J
Joao Moreno 已提交
740
			const controller = new KeyboardController(this, this.view, options);
741
			this.disposables.push(controller);
742 743 744
		}

		if (typeof options.mouseSupport !== 'boolean' || options.mouseSupport) {
745
			const controller = new MouseController(this, this.view, options);
746
			this.disposables.push(controller);
747
			this.onContextMenu = controller.onContextMenu;
748
		}
749

J
Joao Moreno 已提交
750
		this.onFocusChange(this._onFocusChange, this, this.disposables);
751
		this.onSelectionChange(this._onSelectionChange, this, this.disposables);
J
João Moreno 已提交
752 753 754 755

		if (options.ariaLabel) {
			this.view.domNode.setAttribute('aria-label', options.ariaLabel);
		}
756 757

		this.style(options);
J
Joao Moreno 已提交
758 759
	}

J
Joao Moreno 已提交
760
	splice(start: number, deleteCount: number, elements: T[] = []): void {
J
Joao Moreno 已提交
761 762 763 764
		if (deleteCount === 0 && elements.length === 0) {
			return;
		}

I
polish  
isidor 已提交
765
		this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));
J
Joao Moreno 已提交
766 767 768 769 770 771
	}

	get length(): number {
		return this.view.length;
	}

J
Joao Moreno 已提交
772
	get contentHeight(): number {
773
		return this.view.getContentHeight();
J
Joao Moreno 已提交
774 775
	}

J
Joao Moreno 已提交
776 777 778 779
	get scrollTop(): number {
		return this.view.getScrollTop();
	}

J
Joao Moreno 已提交
780 781 782 783
	set scrollTop(scrollTop: number) {
		this.view.setScrollTop(scrollTop);
	}

J
Joao Moreno 已提交
784 785 786 787
	domFocus(): void {
		this.view.domNode.focus();
	}

J
Joao Moreno 已提交
788 789 790 791
	layout(height?: number): void {
		this.view.layout(height);
	}

J
Joao Moreno 已提交
792
	setSelection(indexes: number[]): void {
793
		indexes = indexes.sort(numericSort);
J
Joao Moreno 已提交
794
		this.selection.set(indexes);
J
Joao Moreno 已提交
795 796
	}

J
Joao Moreno 已提交
797
	selectNext(n = 1, loop = false): void {
J
Joao Moreno 已提交
798
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
799 800
		const selection = this.selection.get();
		let index = selection.length > 0 ? selection[0] + n : 0;
J
Joao Moreno 已提交
801
		this.setSelection(loop ? [index % this.length] : [Math.min(index, this.length - 1)]);
J
Joao Moreno 已提交
802 803 804
	}

	selectPrevious(n = 1, loop = false): void {
J
Joao Moreno 已提交
805
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
806 807
		const selection = this.selection.get();
		let index = selection.length > 0 ? selection[0] - n : 0;
A
tslint  
Alex Dima 已提交
808 809 810
		if (loop && index < 0) {
			index = this.length + (index % this.length);
		}
J
Joao Moreno 已提交
811
		this.setSelection([Math.max(index, 0)]);
J
Joao Moreno 已提交
812 813
	}

J
Joao Moreno 已提交
814 815 816 817
	getSelection(): number[] {
		return this.selection.get();
	}

818 819 820 821
	getSelectedElements(): T[] {
		return this.getSelection().map(i => this.view.element(i));
	}

J
Joao Moreno 已提交
822
	setFocus(indexes: number[]): void {
823
		indexes = indexes.sort(numericSort);
J
Joao Moreno 已提交
824
		this.focus.set(indexes);
J
Joao Moreno 已提交
825 826
	}

J
Joao Moreno 已提交
827
	focusNext(n = 1, loop = false): void {
J
Joao Moreno 已提交
828
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
829 830
		const focus = this.focus.get();
		let index = focus.length > 0 ? focus[0] + n : 0;
J
Joao Moreno 已提交
831
		this.setFocus(loop ? [index % this.length] : [Math.min(index, this.length - 1)]);
J
Joao Moreno 已提交
832 833 834
	}

	focusPrevious(n = 1, loop = false): void {
J
Joao Moreno 已提交
835
		if (this.length === 0) { return; }
J
Joao Moreno 已提交
836 837
		const focus = this.focus.get();
		let index = focus.length > 0 ? focus[0] - n : 0;
J
Joao Moreno 已提交
838
		if (loop && index < 0) { index = (this.length + (index % this.length)) % this.length; }
J
Joao Moreno 已提交
839
		this.setFocus([Math.max(index, 0)]);
J
Joao Moreno 已提交
840 841
	}

J
Joao Moreno 已提交
842 843 844 845
	focusNextPage(): void {
		let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);
		lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
		const lastPageElement = this.view.element(lastPageIndex);
J
Joao Moreno 已提交
846
		const currentlyFocusedElement = this.getFocusedElements()[0];
J
Joao Moreno 已提交
847 848

		if (currentlyFocusedElement !== lastPageElement) {
J
Joao Moreno 已提交
849
			this.setFocus([lastPageIndex]);
J
Joao Moreno 已提交
850 851
		} else {
			const previousScrollTop = this.view.getScrollTop();
J
Joao Moreno 已提交
852
			this.view.setScrollTop(previousScrollTop + this.view.renderHeight - this.view.elementHeight(lastPageIndex));
J
Joao Moreno 已提交
853 854 855 856 857 858 859 860 861

			if (this.view.getScrollTop() !== previousScrollTop) {
				// Let the scroll event listener run
				setTimeout(() => this.focusNextPage(), 0);
			}
		}
	}

	focusPreviousPage(): void {
J
Johannes Rieken 已提交
862
		let firstPageIndex: number;
J
Joao Moreno 已提交
863 864 865 866 867 868 869 870 871
		const scrollTop = this.view.getScrollTop();

		if (scrollTop === 0) {
			firstPageIndex = this.view.indexAt(scrollTop);
		} else {
			firstPageIndex = this.view.indexAfter(scrollTop - 1);
		}

		const firstPageElement = this.view.element(firstPageIndex);
J
Joao Moreno 已提交
872
		const currentlyFocusedElement = this.getFocusedElements()[0];
J
Joao Moreno 已提交
873 874

		if (currentlyFocusedElement !== firstPageElement) {
J
Joao Moreno 已提交
875
			this.setFocus([firstPageIndex]);
J
Joao Moreno 已提交
876 877 878 879 880 881 882 883 884 885 886
		} else {
			const previousScrollTop = scrollTop;
			this.view.setScrollTop(scrollTop - this.view.renderHeight);

			if (this.view.getScrollTop() !== previousScrollTop) {
				// Let the scroll event listener run
				setTimeout(() => this.focusPreviousPage(), 0);
			}
		}
	}

887 888 889 890 891 892 893 894 895 896
	focusLast(): void {
		if (this.length === 0) { return; }
		this.setFocus([this.length - 1]);
	}

	focusFirst(): void {
		if (this.length === 0) { return; }
		this.setFocus([0]);
	}

J
Joao Moreno 已提交
897 898 899 900 901 902
	getFocus(): number[] {
		return this.focus.get();
	}

	getFocusedElements(): T[] {
		return this.getFocus().map(i => this.view.element(i));
J
Joao Moreno 已提交
903 904
	}

J
Joao Moreno 已提交
905 906 907 908 909 910 911 912 913 914
	reveal(index: number, relativeTop?: number): void {
		const scrollTop = this.view.getScrollTop();
		const elementTop = this.view.elementTop(index);
		const elementHeight = this.view.elementHeight(index);

		if (isNumber(relativeTop)) {
			relativeTop = relativeTop < 0 ? 0 : relativeTop;
			relativeTop = relativeTop > 1 ? 1 : relativeTop;

			// y = mx + b
J
Joao Moreno 已提交
915
			const m = elementHeight - this.view.renderHeight;
J
Joao Moreno 已提交
916 917
			this.view.setScrollTop(m * relativeTop + elementTop);
		} else {
J
Joao Moreno 已提交
918
			const viewItemBottom = elementTop + elementHeight;
J
Joao Moreno 已提交
919
			const wrapperBottom = scrollTop + this.view.renderHeight;
J
Joao Moreno 已提交
920 921 922 923

			if (elementTop < scrollTop) {
				this.view.setScrollTop(elementTop);
			} else if (viewItemBottom >= wrapperBottom) {
J
Joao Moreno 已提交
924
				this.view.setScrollTop(viewItemBottom - this.view.renderHeight);
J
Joao Moreno 已提交
925 926 927 928
			}
		}
	}

J
Joao Moreno 已提交
929
	private getElementDomId(index: number): string {
J
Johannes Rieken 已提交
930
		return `${this.idPrefix}_${index}`;
J
Joao Moreno 已提交
931 932
	}

933 934 935 936
	isDOMFocused(): boolean {
		return this.view.domNode === document.activeElement;
	}

937 938 939 940
	getHTMLElement(): HTMLElement {
		return this.view.domNode;
	}

J
Joao Moreno 已提交
941 942 943 944
	open(indexes: number[]): void {
		this._onOpen.fire(indexes);
	}

J
Joao Moreno 已提交
945 946 947 948
	pin(indexes: number[]): void {
		this._onPin.fire(indexes);
	}

949
	style(styles: IListStyles): void {
950
		const content: string[] = [];
951

952
		if (styles.listFocusBackground) {
953
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`);
954
		}
955

956 957 958 959
		if (styles.listFocusForeground) {
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`);
		}

960
		if (styles.listActiveSelectionBackground) {
961
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`);
962
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!
963
		}
964

965
		if (styles.listActiveSelectionForeground) {
966
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`);
967
		}
968

969
		if (styles.listFocusAndSelectionBackground) {
970
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }`);
971
		}
972

973
		if (styles.listFocusAndSelectionForeground) {
974
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }`);
975
		}
976

977
		if (styles.listInactiveFocusBackground) {
978 979
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row.focused { background-color:  ${styles.listInactiveFocusBackground}; }`);
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row.focused:hover { background-color:  ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!
980
		}
981

982
		if (styles.listInactiveSelectionBackground) {
983 984
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row.selected { background-color:  ${styles.listInactiveSelectionBackground}; }`);
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row.selected:hover { background-color:  ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!
985
		}
986

987 988 989 990
		if (styles.listInactiveSelectionForeground) {
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`);
		}

991
		if (styles.listHoverBackground) {
992
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row:hover { background-color:  ${styles.listHoverBackground}; }`);
993
		}
994

995 996 997 998
		if (styles.listHoverForeground) {
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row:hover { color:  ${styles.listHoverForeground}; }`);
		}

999
		if (styles.listSelectionOutline) {
1000
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);
1001
		}
1002

1003
		if (styles.listFocusOutline) {
1004
			content.push(`.monaco-list.${this.idPrefix}:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }`);
1005
		}
1006

1007
		if (styles.listInactiveFocusOutline) {
1008
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);
1009
		}
1010

1011
		if (styles.listHoverOutline) {
1012
			content.push(`.monaco-list.${this.idPrefix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);
1013
		}
1014 1015

		this.styleElement.innerHTML = content.join('\n');
1016 1017
	}

1018
	private toListEvent({ indexes }: ITraitChangeEvent) {
J
Joao Moreno 已提交
1019 1020 1021
		return { indexes, elements: indexes.map(i => this.view.element(i)) };
	}

J
Joao Moreno 已提交
1022
	private _onFocusChange(): void {
J
João Moreno 已提交
1023 1024 1025
		const focus = this.focus.get();

		if (focus.length > 0) {
J
Joao Moreno 已提交
1026
			this.view.domNode.setAttribute('aria-activedescendant', this.getElementDomId(focus[0]));
J
João Moreno 已提交
1027 1028 1029 1030 1031 1032
		} else {
			this.view.domNode.removeAttribute('aria-activedescendant');
		}

		this.view.domNode.setAttribute('role', 'tree');
		DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0);
J
Joao Moreno 已提交
1033 1034
	}

1035 1036 1037 1038 1039 1040 1041 1042
	private _onSelectionChange(): void {
		const selection = this.selection.get();

		DOM.toggleClass(this.view.domNode, 'selection-none', selection.length === 0);
		DOM.toggleClass(this.view.domNode, 'selection-single', selection.length === 1);
		DOM.toggleClass(this.view.domNode, 'selection-multiple', selection.length > 1);
	}

J
Joao Moreno 已提交
1043
	dispose(): void {
1044
		this._onDidDispose.fire();
J
Joao Moreno 已提交
1045
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
1046 1047
	}
}