treeView.ts 37.9 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  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!./media/views';
import { Event, Emitter } from 'vs/base/common/event';
8
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
9
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
10
import { IAction, ActionRunner } from 'vs/base/common/actions';
11 12
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
13
import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
14
import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
15
import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
16
import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ITreeItemLabel, Extensions, IViewDescriptorService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views';
17
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
18 19
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService } from 'vs/platform/notification/common/notification';
20
import { IProgressService } from 'vs/platform/progress/common/progress';
21 22 23
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import * as DOM from 'vs/base/browser/dom';
B
Benjamin Pasero 已提交
24
import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels';
25
import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
26
import { URI } from 'vs/base/common/uri';
B
Benjamin Pasero 已提交
27
import { dirname, basename } from 'vs/base/common/resources';
28
import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService';
29
import { FileKind } from 'vs/platform/files/common/files';
J
João Moreno 已提交
30
import { WorkbenchAsyncDataTree, TreeResourceNavigator } from 'vs/platform/list/browser/listService';
S
SteVen Batten 已提交
31
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer';
S
Sandeep Somavarapu 已提交
32
import { localize } from 'vs/nls';
33
import { timeout } from 'vs/base/common/async';
J
Joao Moreno 已提交
34
import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry';
35
import { isString } from 'vs/base/common/types';
36
import { ILabelService } from 'vs/platform/label/common/label';
37
import { Registry } from 'vs/platform/registry/common/platform';
38
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
39
import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
40 41
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults';
J
Johannes Rieken 已提交
42
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
S
SteVen Batten 已提交
43
import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme';
44
import { IOpenerService } from 'vs/platform/opener/common/opener';
45
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
46

47
export class TreeViewPane extends ViewPane {
48

S
rename  
Sandeep Somavarapu 已提交
49
	private treeView: ITreeView;
50 51 52 53 54 55

	constructor(
		options: IViewletViewOptions,
		@IKeybindingService keybindingService: IKeybindingService,
		@IContextMenuService contextMenuService: IContextMenuService,
		@IConfigurationService configurationService: IConfigurationService,
S
Sandeep Somavarapu 已提交
56
		@IContextKeyService contextKeyService: IContextKeyService,
57
		@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
58
		@IInstantiationService instantiationService: IInstantiationService,
59 60
		@IOpenerService openerService: IOpenerService,
		@IThemeService themeService: IThemeService,
61
		@ITelemetryService telemetryService: ITelemetryService,
62
	) {
63
		super({ ...(options as IViewPaneOptions), titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
64
		const { treeView } = (<ITreeViewDescriptor>Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).getView(options.id));
S
rename  
Sandeep Somavarapu 已提交
65
		this.treeView = treeView;
M
Matt Bierner 已提交
66
		this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this));
67
		this._register(this.treeView.onDidChangeTitle((newTitle) => this.updateTitle(newTitle)));
M
Matt Bierner 已提交
68 69
		this._register(toDisposable(() => this.treeView.setVisibility(false)));
		this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility()));
70
		this._register(this.treeView.onDidChangeWelcomeState(() => this._onDidChangeViewWelcomeState.fire()));
71
		this.updateTreeVisibility();
72 73 74 75
	}

	focus(): void {
		super.focus();
S
rename  
Sandeep Somavarapu 已提交
76
		this.treeView.focus();
77 78 79
	}

	renderBody(container: HTMLElement): void {
J
Joao Moreno 已提交
80 81
		super.renderBody(container);

82
		if (this.treeView instanceof TreeView) {
S
Sandeep Somavarapu 已提交
83 84
			this.treeView.show(container);
		}
85 86
	}

87
	shouldShowWelcome(): boolean {
88
		return ((this.treeView.dataProvider === undefined) || !!this.treeView.dataProvider.isTreeEmpty) && (this.treeView.message === undefined);
89 90
	}

91
	layoutBody(height: number, width: number): void {
J
João Moreno 已提交
92
		super.layoutBody(height, width);
93
		this.treeView.layout(height, width);
94 95 96
	}

	getOptimalWidth(): number {
S
rename  
Sandeep Somavarapu 已提交
97
		return this.treeView.getOptimalWidth();
98 99 100
	}

	private updateTreeVisibility(): void {
101
		this.treeView.setVisibility(this.isBodyVisible());
102 103 104 105
	}
}

class Root implements ITreeItem {
S
Sandeep Somavarapu 已提交
106
	label = { label: 'root' };
107
	handle = '0';
S
Sandeep Somavarapu 已提交
108
	parentHandle: string | undefined = undefined;
109
	collapsibleState = TreeItemCollapsibleState.Expanded;
110
	children: ITreeItem[] | undefined = undefined;
111 112
}

113 114
const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data.");

115
class Tree extends WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> { }
M
Matt Bierner 已提交
116

117
export class TreeView extends Disposable implements ITreeView {
118 119 120 121

	private isVisible: boolean = false;
	private _hasIconForParentNode = false;
	private _hasIconForLeafNode = false;
122 123 124 125 126

	private readonly collapseAllContextKey: RawContextKey<boolean>;
	private readonly collapseAllContext: IContextKey<boolean>;
	private readonly refreshContextKey: RawContextKey<boolean>;
	private readonly refreshContext: IContextKey<boolean>;
127

S
Sandeep Somavarapu 已提交
128
	private focused: boolean = false;
129 130
	private domNode!: HTMLElement;
	private treeContainer!: HTMLElement;
131
	private _messageValue: string | undefined;
132
	private _canSelectMany: boolean = false;
133
	private messageElement!: HTMLDivElement;
134
	private tree: Tree | undefined;
135
	private treeLabels: ResourceLabels | undefined;
136

137 138 139
	private root: ITreeItem;
	private elementsToRefresh: ITreeItem[] = [];

M
Matt Bierner 已提交
140
	private readonly _onDidExpandItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());
141 142
	readonly onDidExpandItem: Event<ITreeItem> = this._onDidExpandItem.event;

M
Matt Bierner 已提交
143
	private readonly _onDidCollapseItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());
144 145 146 147 148
	readonly onDidCollapseItem: Event<ITreeItem> = this._onDidCollapseItem.event;

	private _onDidChangeSelection: Emitter<ITreeItem[]> = this._register(new Emitter<ITreeItem[]>());
	readonly onDidChangeSelection: Event<ITreeItem[]> = this._onDidChangeSelection.event;

M
Matt Bierner 已提交
149
	private readonly _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());
150 151
	readonly onDidChangeVisibility: Event<boolean> = this._onDidChangeVisibility.event;

M
Matt Bierner 已提交
152
	private readonly _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());
153 154
	readonly onDidChangeActions: Event<void> = this._onDidChangeActions.event;

155 156 157
	private readonly _onDidChangeWelcomeState: Emitter<void> = this._register(new Emitter<void>());
	readonly onDidChangeWelcomeState: Event<void> = this._onDidChangeWelcomeState.event;

158 159 160
	private readonly _onDidChangeTitle: Emitter<string> = this._register(new Emitter<string>());
	readonly onDidChangeTitle: Event<string> = this._onDidChangeTitle.event;

161 162
	private readonly _onDidCompleteRefresh: Emitter<void> = this._register(new Emitter<void>());

163
	constructor(
S
#93960  
Sandeep Somavarapu 已提交
164
		readonly id: string,
165
		private _title: string,
166
		@IThemeService private readonly themeService: IThemeService,
167 168 169
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@ICommandService private readonly commandService: ICommandService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
170
		@IProgressService protected readonly progressService: IProgressService,
171
		@IContextMenuService private readonly contextMenuService: IContextMenuService,
172
		@IKeybindingService private readonly keybindingService: IKeybindingService,
173
		@INotificationService private readonly notificationService: INotificationService,
174 175
		@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
		@IContextKeyService contextKeyService: IContextKeyService
176 177 178
	) {
		super();
		this.root = new Root();
179 180 181 182 183
		this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, false);
		this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService);
		this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, false);
		this.refreshContext = this.refreshContextKey.bindTo(contextKeyService);

184
		this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
M
Martin Aeschlimann 已提交
185
		this._register(this.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
S
Sandeep Somavarapu 已提交
186 187 188 189 190
		this._register(this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer.decorations')) {
				this.doRefresh([this.root]); /** soft refresh **/
			}
		}));
191 192 193
		this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => {
			if (views.some(v => v.id === this.id)) {
				this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } });
194 195
			}
		}));
196
		this.registerActions();
S
SteVen Batten 已提交
197

S
Sandeep Somavarapu 已提交
198
		this.create();
199 200
	}

201
	get viewContainer(): ViewContainer {
202
		return this.viewDescriptorService.getViewContainerByViewId(this.id)!;
203 204 205
	}

	get viewLocation(): ViewContainerLocation {
206
		return this.viewDescriptorService.getViewLocationById(this.id)!;
207 208
	}

209 210
	private _dataProvider: ITreeViewDataProvider | undefined;
	get dataProvider(): ITreeViewDataProvider | undefined {
211 212 213
		return this._dataProvider;
	}

214
	set dataProvider(dataProvider: ITreeViewDataProvider | undefined) {
215 216 217 218
		if (this.tree === undefined) {
			this.createTree();
		}

219 220
		if (dataProvider) {
			this._dataProvider = new class implements ITreeViewDataProvider {
221 222 223 224 225 226 227 228
				private _isEmpty: boolean = true;
				private _onDidChangeEmpty: Emitter<void> = new Emitter();
				public onDidChangeEmpty: Event<void> = this._onDidChangeEmpty.event;

				get isTreeEmpty(): boolean {
					return this._isEmpty;
				}

229
				async getChildren(node: ITreeItem): Promise<ITreeItem[]> {
230
					let children: ITreeItem[];
231
					if (node && node.children) {
232 233 234 235 236 237 238 239 240 241 242
						children = node.children;
					} else {
						children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node));
						node.children = children;
					}
					if (node instanceof Root) {
						const oldEmpty = this._isEmpty;
						this._isEmpty = children.length === 0;
						if (oldEmpty !== this._isEmpty) {
							this._onDidChangeEmpty.fire();
						}
243
					}
244
					return children;
245 246
				}
			};
247 248 249
			if (this._dataProvider.onDidChangeEmpty) {
				this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire()));
			}
250
			this.updateMessage();
S
Sandeep Somavarapu 已提交
251
			this.refresh();
252
		} else {
253
			this._dataProvider = undefined;
254
			this.updateMessage();
255
		}
256 257

		this._onDidChangeWelcomeState.fire();
258 259
	}

260 261
	private _message: string | undefined;
	get message(): string | undefined {
262 263 264
		return this._message;
	}

265
	set message(message: string | undefined) {
266 267
		this._message = message;
		this.updateMessage();
268
		this._onDidChangeWelcomeState.fire();
269 270
	}

271 272 273 274 275 276 277 278 279
	get title(): string {
		return this._title;
	}

	set title(name: string) {
		this._title = name;
		this._onDidChangeTitle.fire(this._title);
	}

280 281 282 283 284 285 286 287
	get canSelectMany(): boolean {
		return this._canSelectMany;
	}

	set canSelectMany(canSelectMany: boolean) {
		this._canSelectMany = canSelectMany;
	}

288 289 290 291 292 293 294 295
	get hasIconForParentNode(): boolean {
		return this._hasIconForParentNode;
	}

	get hasIconForLeafNode(): boolean {
		return this._hasIconForLeafNode;
	}

S
Sandeep Somavarapu 已提交
296 297 298 299
	get visible(): boolean {
		return this.isVisible;
	}

300
	get showCollapseAllAction(): boolean {
301
		return !!this.collapseAllContext.get();
302 303 304
	}

	set showCollapseAllAction(showCollapseAllAction: boolean) {
305
		this.collapseAllContext.set(showCollapseAllAction);
306 307
	}

308
	get showRefreshAction(): boolean {
309
		return !!this.refreshContext.get();
310 311 312
	}

	set showRefreshAction(showRefreshAction: boolean) {
313 314 315 316 317
		this.refreshContext.set(showRefreshAction);
	}

	private registerActions() {
		const that = this;
S
Sandeep Somavarapu 已提交
318
		this._register(registerAction2(class extends Action2 {
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
			constructor() {
				super({
					id: `workbench.actions.treeView.${that.id}.refresh`,
					title: localize('refresh', "Refresh"),
					menu: {
						id: MenuId.ViewTitle,
						when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey),
						group: 'navigation',
						order: Number.MAX_SAFE_INTEGER - 1,
					},
					icon: { id: 'codicon/refresh' }
				});
			}
			async run(): Promise<void> {
				return that.refresh();
			}
S
Sandeep Somavarapu 已提交
335 336
		}));
		this._register(registerAction2(class extends Action2 {
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
			constructor() {
				super({
					id: `workbench.actions.treeView.${that.id}.collapseAll`,
					title: localize('collapseAll', "Collapse All"),
					menu: {
						id: MenuId.ViewTitle,
						when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey),
						group: 'navigation',
						order: Number.MAX_SAFE_INTEGER,
					},
					icon: { id: 'codicon/collapse-all' }
				});
			}
			async run(): Promise<void> {
				if (that.tree) {
					return new CollapseAllAction<ITreeItem, ITreeItem, FuzzyScore>(that.tree, true).run();
				}
			}
S
Sandeep Somavarapu 已提交
355
		}));
356 357
	}

358
	setVisibility(isVisible: boolean): void {
S
Sandeep Somavarapu 已提交
359
		isVisible = !!isVisible;
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
		if (this.isVisible === isVisible) {
			return;
		}

		this.isVisible = isVisible;

		if (this.tree) {
			if (this.isVisible) {
				DOM.show(this.tree.getHTMLElement());
			} else {
				DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it
			}

			if (this.isVisible && this.elementsToRefresh.length) {
				this.doRefresh(this.elementsToRefresh);
				this.elementsToRefresh = [];
			}
		}
378 379

		this._onDidChangeVisibility.fire(this.isVisible);
380 381
	}

382
	focus(reveal: boolean = true): void {
S
Sandeep Somavarapu 已提交
383
		if (this.tree && this.root.children && this.root.children.length > 0) {
384 385
			// Make sure the current selected element is revealed
			const selectedElement = this.tree.getSelection()[0];
386
			if (selectedElement && reveal) {
387
				this.tree.reveal(selectedElement, 0.5);
388 389 390 391
			}

			// Pass Focus to Viewer
			this.tree.domFocus();
392 393
		} else if (this.tree) {
			this.tree.domFocus();
S
Sandeep Somavarapu 已提交
394 395
		} else {
			this.domNode.focus();
396 397 398 399
		}
	}

	show(container: HTMLElement): void {
S
Sandeep Somavarapu 已提交
400 401 402 403
		DOM.append(container, this.domNode);
	}

	private create() {
404 405
		this.domNode = DOM.$('.tree-explorer-viewlet-tree-view');
		this.messageElement = DOM.append(this.domNode, DOM.$('.message'));
S
Sandeep Somavarapu 已提交
406
		this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree'));
407 408
		DOM.addClass(this.treeContainer, 'file-icon-themable-tree');
		DOM.addClass(this.treeContainer, 'show-file-icons');
S
Sandeep Somavarapu 已提交
409 410 411
		const focusTracker = this._register(DOM.trackFocus(this.domNode));
		this._register(focusTracker.onDidFocus(() => this.focused = true));
		this._register(focusTracker.onDidBlur(() => this.focused = false));
412 413 414
	}

	private createTree() {
415
		const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined;
416
		const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id));
417
		this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
418
		const dataSource = this.instantiationService.createInstance(TreeDataSource, this, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));
419 420
		const aligner = new Aligner(this.themeService);
		const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner);
421
		const widgetAriaLabel = this._title;
422

423
		this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer],
424
			dataSource, {
425
			identityProvider: new TreeViewIdentityProvider(),
M
Matt Bierner 已提交
426 427
			accessibilityProvider: {
				getAriaLabel(element: ITreeItem): string {
428 429 430 431
					if (element.accessibilityInformation) {
						return element.accessibilityInformation.label;
					}

M
Matt Bierner 已提交
432
					return element.tooltip ? element.tooltip : element.label ? element.label.label : '';
433
				},
434 435 436
				getRole(element: ITreeItem): string | undefined {
					return element.accessibilityInformation?.role;
				},
437 438
				getWidgetAriaLabel(): string {
					return widgetAriaLabel;
M
Matt Bierner 已提交
439 440 441 442 443 444 445 446 447 448 449 450
				}
			},
			keyboardNavigationLabelProvider: {
				getKeyboardNavigationLabel: (item: ITreeItem) => {
					return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined);
				}
			},
			expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command,
			collapseByDefault: (e: ITreeItem): boolean => {
				return e.collapsibleState !== TreeItemCollapsibleState.Expanded;
			},
			multipleSelectionSupport: this.canSelectMany,
J
Joao Moreno 已提交
451
			overrideStyles: {
452
				listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND
J
Joao Moreno 已提交
453
			}
M
Matt Bierner 已提交
454
		}) as WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>);
455
		aligner.tree = this.tree;
456
		const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection());
457
		renderer.actionRunner = actionRunner;
458

459
		this.tree.contextKeyService.createKey<boolean>(this.id, true);
460
		this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));
461 462
		this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements)));
		this._register(this.tree.onDidChangeCollapseState(e => {
463 464 465 466
			if (!e.node.element) {
				return;
			}

467 468 469 470 471 472 473
			const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element;
			if (e.node.collapsed) {
				this._onDidCollapseItem.fire(element);
			} else {
				this._onDidExpandItem.fire(element);
			}
		}));
S
Sandeep Somavarapu 已提交
474
		this.tree.setInput(this.root).then(() => this.updateContentAreas());
475

J
João Moreno 已提交
476
		const treeNavigator = new TreeResourceNavigator(this.tree, { openOnFocus: false, openOnSelection: false });
477 478
		this._register(treeNavigator);
		this._register(treeNavigator.onDidOpenResource(e => {
479 480 481
			if (!e.browserEvent) {
				return;
			}
482
			const selection = this.tree!.getSelection();
483 484 485 486
			if ((selection.length === 1) && selection[0].command) {
				this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || []));
			}
		}));
487 488
	}

489
	private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {
490 491 492 493 494 495 496 497 498
		const node: ITreeItem | null = treeEvent.element;
		if (node === null) {
			return;
		}
		const event: UIEvent = treeEvent.browserEvent;

		event.preventDefault();
		event.stopPropagation();

499
		this.tree!.setFocus([node]);
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
		const actions = treeMenus.getResourceContextActions(node);
		if (!actions.length) {
			return;
		}
		this.contextMenuService.showContextMenu({
			getAnchor: () => treeEvent.anchor,

			getActions: () => actions,

			getActionViewItem: (action) => {
				const keybinding = this.keybindingService.lookupKeybinding(action.id);
				if (keybinding) {
					return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });
				}
				return undefined;
			},

			onHide: (wasCancelled?: boolean) => {
				if (wasCancelled) {
519
					this.tree!.domFocus();
520 521 522 523 524
				}
			},

			getActionsContext: () => (<TreeViewItemHandleArg>{ $treeViewId: this.id, $treeItemHandle: node.handle }),

525
			actionRunner
526 527 528
		});
	}

529
	protected updateMessage(): void {
530 531 532 533 534 535 536
		if (this._message) {
			this.showMessage(this._message);
		} else if (!this.dataProvider) {
			this.showMessage(noDataProviderMessage);
		} else {
			this.hideMessage();
		}
S
Sandeep Somavarapu 已提交
537
		this.updateContentAreas();
538 539
	}

540
	private showMessage(message: string): void {
541
		DOM.removeClass(this.messageElement, 'hide');
542 543 544 545
		this.resetMessageElement();
		this._messageValue = message;
		if (!isFalsyOrWhitespace(this._message)) {
			this.messageElement.textContent = this._messageValue;
546
		}
547
		this.layout(this._height, this._width);
548 549
	}

550
	private hideMessage(): void {
551 552
		this.resetMessageElement();
		DOM.addClass(this.messageElement, 'hide');
553
		this.layout(this._height, this._width);
554 555
	}

556 557 558 559
	private resetMessageElement(): void {
		DOM.clearNode(this.messageElement);
	}

560 561
	private _height: number = 0;
	private _width: number = 0;
562 563 564 565 566 567
	layout(height: number, width: number) {
		if (height && width) {
			this._height = height;
			this._width = width;
			const treeHeight = height - DOM.getTotalHeight(this.messageElement);
			this.treeContainer.style.height = treeHeight + 'px';
568
			if (this.tree) {
569
				this.tree.layout(treeHeight, width);
570
			}
571 572 573 574 575 576
		}
	}

	getOptimalWidth(): number {
		if (this.tree) {
			const parentNode = this.tree.getHTMLElement();
577
			const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a'));
578 579 580 581 582
			return DOM.getLargestChildWidth(parentNode, childNodes);
		}
		return 0;
	}

583
	async refresh(elements?: ITreeItem[]): Promise<void> {
S
Sandeep Somavarapu 已提交
584
		if (this.dataProvider && this.tree) {
585 586 587
			if (this.refreshing) {
				await Event.toPromise(this._onDidCompleteRefresh.event);
			}
S
Sandeep Somavarapu 已提交
588 589 590 591 592
			if (!elements) {
				elements = [this.root];
				// remove all waiting elements to refresh if root is asked to refresh
				this.elementsToRefresh = [];
			}
593
			for (const element of elements) {
M
Matt Bierner 已提交
594
				element.children = undefined; // reset children
595 596 597 598
			}
			if (this.isVisible) {
				return this.doRefresh(elements);
			} else {
S
Sandeep Somavarapu 已提交
599 600 601 602 603 604 605 606 607 608 609
				if (this.elementsToRefresh.length) {
					const seen: Set<string> = new Set<string>();
					this.elementsToRefresh.forEach(element => seen.add(element.handle));
					for (const element of elements) {
						if (!seen.has(element.handle)) {
							this.elementsToRefresh.push(element);
						}
					}
				} else {
					this.elementsToRefresh.push(...elements);
				}
610 611
			}
		}
612
		return undefined;
613 614
	}

615
	async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> {
616 617
		const tree = this.tree;
		if (tree) {
S
Sandeep Somavarapu 已提交
618
			itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
619
			await Promise.all(itemOrItems.map(element => {
620
				return tree.expand(element, false);
621
			}));
S
Sandeep Somavarapu 已提交
622
		}
623 624 625
	}

	setSelection(items: ITreeItem[]): void {
S
Sandeep Somavarapu 已提交
626
		if (this.tree) {
627
			this.tree.setSelection(items);
S
Sandeep Somavarapu 已提交
628
		}
629 630 631
	}

	setFocus(item: ITreeItem): void {
S
Sandeep Somavarapu 已提交
632 633
		if (this.tree) {
			this.focus();
634
			this.tree.setFocus([item]);
S
Sandeep Somavarapu 已提交
635
		}
636 637
	}

638
	async reveal(item: ITreeItem): Promise<void> {
S
Sandeep Somavarapu 已提交
639
		if (this.tree) {
640
			return this.tree.reveal(item);
S
Sandeep Somavarapu 已提交
641
		}
642 643
	}

644
	private refreshing: boolean = false;
645
	private async doRefresh(elements: ITreeItem[]): Promise<void> {
646
		const tree = this.tree;
647
		if (tree && this.visible) {
648
			this.refreshing = true;
J
Joao Moreno 已提交
649
			await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));
650
			this.refreshing = false;
651
			this._onDidCompleteRefresh.fire();
652 653
			this.updateContentAreas();
			if (this.focused) {
654
				this.focus(false);
655
			}
656 657 658
		}
	}

S
Sandeep Somavarapu 已提交
659
	private updateContentAreas(): void {
S
Sandeep Somavarapu 已提交
660
		const isTreeEmpty = !this.root.children || this.root.children.length === 0;
661 662
		// Hide tree container only when there is a message and tree is empty and not refreshing
		if (this._messageValue && isTreeEmpty && !this.refreshing) {
S
Sandeep Somavarapu 已提交
663
			DOM.addClass(this.treeContainer, 'hide');
S
Sandeep Somavarapu 已提交
664
			this.domNode.setAttribute('tabindex', '0');
S
Sandeep Somavarapu 已提交
665 666 667
		} else {
			DOM.removeClass(this.treeContainer, 'hide');
			this.domNode.removeAttribute('tabindex');
S
Sandeep Somavarapu 已提交
668 669
		}
	}
670 671
}

672
class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> {
673 674 675 676 677
	getId(element: ITreeItem): { toString(): string; } {
		return element.handle;
	}
}

678
class TreeViewDelegate implements IListVirtualDelegate<ITreeItem> {
679 680 681 682 683 684 685 686 687 688 689

	getHeight(element: ITreeItem): number {
		return TreeRenderer.ITEM_HEIGHT;
	}

	getTemplateId(element: ITreeItem): string {
		return TreeRenderer.TREE_TEMPLATE_ID;
	}
}

class TreeDataSource implements IAsyncDataSource<ITreeItem, ITreeItem> {
690 691

	constructor(
S
rename  
Sandeep Somavarapu 已提交
692
		private treeView: ITreeView,
693
		private withProgress: <T>(task: Promise<T>) => Promise<T>
694 695 696
	) {
	}

697 698
	hasChildren(element: ITreeItem): boolean {
		return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None);
699 700
	}

701
	async getChildren(element: ITreeItem): Promise<ITreeItem[]> {
702
		if (this.treeView.dataProvider) {
703
			return this.withProgress(this.treeView.dataProvider.getChildren(element));
704
		}
705
		return [];
706 707 708
	}
}

709 710 711
// todo@joh,sandy make this proper and contributable from extensions
registerThemingParticipant((theme, collector) => {

J
Joao Moreno 已提交
712 713 714 715
	const matchBackgroundColor = theme.getColor(listFilterMatchHighlight);
	if (matchBackgroundColor) {
		collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`);
		collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`);
716
	}
J
Joao Moreno 已提交
717 718 719 720
	const matchBorderColor = theme.getColor(listFilterMatchHighlightBorder);
	if (matchBorderColor) {
		collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`);
		collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`);
721
	}
S
Sandeep Somavarapu 已提交
722 723 724 725
	const link = theme.getColor(textLinkForeground);
	if (link) {
		collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`);
	}
726 727 728
	const focusBorderColor = theme.getColor(focusBorder);
	if (focusBorderColor) {
		collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focusBorderColor}; outline-offset: -1px; }`);
S
Sandeep Somavarapu 已提交
729
	}
S
Sandeep Somavarapu 已提交
730 731 732 733
	const codeBackground = theme.getColor(textCodeBlockBackground);
	if (codeBackground) {
		collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`);
	}
734 735
});

736 737 738 739 740 741 742
interface ITreeExplorerTemplateData {
	elementDisposable: IDisposable;
	container: HTMLElement;
	resourceLabel: IResourceLabel;
	icon: HTMLElement;
	actionBar: ActionBar;
}
743

744 745 746
class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyScore, ITreeExplorerTemplateData> {
	static readonly ITEM_HEIGHT = 22;
	static readonly TREE_TEMPLATE_ID = 'treeExplorer';
747

748 749
	private _actionRunner: MultipleSelectionActionRunner | undefined;

750 751 752
	constructor(
		private treeViewId: string,
		private menus: TreeMenus,
B
Benjamin Pasero 已提交
753
		private labels: ResourceLabels,
754
		private actionViewItemProvider: IActionViewItemProvider,
755
		private aligner: Aligner,
756
		@IThemeService private readonly themeService: IThemeService,
757 758
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@ILabelService private readonly labelService: ILabelService
759
	) {
760
		super();
761 762
	}

763
	get templateId(): string {
764 765 766
		return TreeRenderer.TREE_TEMPLATE_ID;
	}

767 768 769 770
	set actionRunner(actionRunner: MultipleSelectionActionRunner) {
		this._actionRunner = actionRunner;
	}

771
	renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {
772 773 774
		DOM.addClass(container, 'custom-view-tree-node-item');

		const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon'));
775

776
		const resourceLabel = this.labels.create(container, { supportHighlights: true });
777
		const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions'));
778
		const actionBar = new ActionBar(actionsContainer, {
779
			actionViewItemProvider: this.actionViewItemProvider
780 781
		});

782
		return { resourceLabel, icon, actionBar, container, elementDisposable: Disposable.None };
783 784
	}

785 786 787
	renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
		templateData.elementDisposable.dispose();
		const node = element.element;
788
		const resource = node.resourceUri ? URI.revive(node.resourceUri) : null;
M
Matt Bierner 已提交
789
		const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : resource ? { label: basename(resource) } : undefined;
R
Rob Lourens 已提交
790 791
		const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined;
		const label = treeItemLabel ? treeItemLabel.label : undefined;
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808
		const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => {
			if ((Math.abs(start) > label.length) || (Math.abs(end) >= label.length)) {
				return ({ start: 0, end: 0 });
			}
			if (start < 0) {
				start = label.length + start;
			}
			if (end < 0) {
				end = label.length + end;
			}
			if (start > end) {
				const swap = start;
				start = end;
				end = swap;
			}
			return ({ start, end });
		}) : undefined;
M
Martin Aeschlimann 已提交
809
		const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark;
810
		const iconUrl = icon ? URI.revive(icon) : null;
R
Rob Lourens 已提交
811
		const title = node.tooltip ? node.tooltip : resource ? undefined : label;
812 813 814 815

		// reset
		templateData.actionBar.clear();

J
Johannes Rieken 已提交
816
		if (resource || this.isFileKindThemeIcon(node.themeIcon)) {
S
Sandeep Somavarapu 已提交
817
			const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations');
A
Alex Ross 已提交
818
			templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) });
819
		} else {
A
Alex Ross 已提交
820
			templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) });
821 822
		}

823
		templateData.icon.title = title ? title : '';
824

J
Johannes Rieken 已提交
825 826 827
		if (iconUrl) {
			templateData.icon.className = 'custom-view-tree-node-item-icon';
			templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl);
828

J
Johannes Rieken 已提交
829 830 831 832
		} else {
			let iconClass: string | undefined;
			if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) {
				iconClass = ThemeIcon.asClassName(node.themeIcon);
833
			}
J
Johannes Rieken 已提交
834
			templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : '';
J
Johannes Rieken 已提交
835
			templateData.icon.style.backgroundImage = '';
836 837
		}

838
		templateData.actionBar.context = <TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle };
839
		templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false });
840 841 842
		if (this._actionRunner) {
			templateData.actionBar.actionRunner = this._actionRunner;
		}
843 844 845
		this.setAlignment(templateData.container, node);
		templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node)));
	}
846

847 848
	private setAlignment(container: HTMLElement, treeItem: ITreeItem) {
		DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem));
849 850
	}

J
Johannes Rieken 已提交
851 852 853 854 855 856 857 858
	private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean {
		if (icon) {
			return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id;
		} else {
			return false;
		}
	}

859 860 861 862 863 864 865 866 867 868 869 870
	private getFileKind(node: ITreeItem): FileKind {
		if (node.themeIcon) {
			switch (node.themeIcon.id) {
				case FileThemeIcon.id:
					return FileKind.FILE;
				case FolderThemeIcon.id:
					return FileKind.FOLDER;
			}
		}
		return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE;
	}

871 872 873 874 875
	disposeElement(resource: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
		templateData.elementDisposable.dispose();
	}

	disposeTemplate(templateData: ITreeExplorerTemplateData): void {
876 877
		templateData.resourceLabel.dispose();
		templateData.actionBar.dispose();
878
		templateData.elementDisposable.dispose();
879 880 881 882
	}
}

class Aligner extends Disposable {
883
	private _tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> | undefined;
884

885
	constructor(private themeService: IThemeService) {
886 887 888
		super();
	}

889 890
	set tree(tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>) {
		this._tree = tree;
891 892
	}

893 894
	public alignIconWithTwisty(treeItem: ITreeItem): boolean {
		if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) {
895 896
			return false;
		}
897
		if (!this.hasIcon(treeItem)) {
898 899
			return false;
		}
900

901 902 903 904 905 906 907
		if (this._tree) {
			const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput();
			if (this.hasIcon(parent)) {
				return false;
			}
			return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c));
		} else {
908 909 910 911 912
			return false;
		}
	}

	private hasIcon(node: ITreeItem): boolean {
M
Martin Aeschlimann 已提交
913
		const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark;
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930
		if (icon) {
			return true;
		}
		if (node.resourceUri || node.themeIcon) {
			const fileIconTheme = this.themeService.getFileIconTheme();
			const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None;
			if (isFolder) {
				return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons;
			}
			return fileIconTheme.hasFileIcons;
		}
		return false;
	}
}

class MultipleSelectionActionRunner extends ActionRunner {

931
	constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) {
932
		super();
933 934 935 936 937
		this._register(this.onDidRun(e => {
			if (e.error) {
				notificationService.error(localize('command-error', 'Error running command {1}: {0}. This is likely caused by the extension that contributes {1}.', e.error.message, e.action.id));
			}
		}));
938 939
	}

940
	runAction(action: IAction, context: TreeViewItemHandleArg): Promise<void> {
941 942
		const selection = this.getSelectedResources();
		let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined;
943
		let actionInSelected: boolean = false;
944
		if (selection.length > 1) {
945 946 947 948 949 950 951 952 953 954
			selectionHandleArgs = selection.map(selected => {
				if (selected.handle === context.$treeItemHandle) {
					actionInSelected = true;
				}
				return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle };
			});
		}

		if (!actionInSelected) {
			selectionHandleArgs = undefined;
955 956
		}

957
		return action.run(...[context, selectionHandleArgs]);
958 959 960 961 962 963 964
	}
}

class TreeMenus extends Disposable implements IDisposable {

	constructor(
		private id: string,
965 966 967
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
		@IMenuService private readonly menuService: IMenuService,
		@IContextMenuService private readonly contextMenuService: IContextMenuService
968 969 970 971 972 973 974 975 976 977 978 979
	) {
		super();
	}

	getResourceActions(element: ITreeItem): IAction[] {
		return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).primary;
	}

	getResourceContextActions(element: ITreeItem): IAction[] {
		return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary;
	}

M
Matt Bierner 已提交
980
	private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } {
981 982 983 984 985 986 987 988
		const contextKeyService = this.contextKeyService.createScoped();
		contextKeyService.createKey('view', this.id);
		contextKeyService.createKey(context.key, context.value);

		const menu = this.menuService.createMenu(menuId, contextKeyService);
		const primary: IAction[] = [];
		const secondary: IAction[] = [];
		const result = { primary, secondary };
989
		createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g));
990 991 992 993 994 995 996

		menu.dispose();
		contextKeyService.dispose();

		return result;
	}
}
997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013

export class CustomTreeView extends TreeView {

	private activated: boolean = false;

	constructor(
		id: string,
		title: string,
		@IThemeService themeService: IThemeService,
		@IInstantiationService instantiationService: IInstantiationService,
		@ICommandService commandService: ICommandService,
		@IConfigurationService configurationService: IConfigurationService,
		@IProgressService progressService: IProgressService,
		@IContextMenuService contextMenuService: IContextMenuService,
		@IKeybindingService keybindingService: IKeybindingService,
		@INotificationService notificationService: INotificationService,
		@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
1014
		@IContextKeyService contextKeyService: IContextKeyService,
1015 1016
		@IExtensionService private readonly extensionService: IExtensionService,
	) {
1017
		super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, contextKeyService);
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028
	}

	setVisibility(isVisible: boolean): void {
		super.setVisibility(isVisible);
		if (this.visible) {
			this.activate();
		}
	}

	private activate() {
		if (!this.activated) {
1029
			this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
1030 1031 1032 1033 1034 1035 1036 1037
				.then(() => timeout(2000))
				.then(() => {
					this.updateMessage();
				});
			this.activated = true;
		}
	}
}