breadcrumbsPicker.ts 18.7 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 * as dom from 'vs/base/browser/dom';
import { compareFileNames } from 'vs/base/common/comparers';
J
Johannes Rieken 已提交
8
import { onUnexpectedError } from 'vs/base/common/errors';
9
import { Emitter, Event } from 'vs/base/common/event';
10
import { createMatches, FuzzyScore } from 'vs/base/common/filters';
J
Johannes Rieken 已提交
11
import * as glob from 'vs/base/common/glob';
12
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
13
import { posix } from 'vs/base/common/path';
J
Johannes Rieken 已提交
14
import { basename, dirname, isEqual } from 'vs/base/common/resources';
15
import { URI } from 'vs/base/common/uri';
16 17
import 'vs/css!./media/breadcrumbscontrol';
import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
18
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
19
import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
20
import { IConstructorSignature1, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
21
import { WorkbenchDataTree, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
J
Johannes Rieken 已提交
22 23
import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
24
import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from 'vs/workbench/browser/labels';
25
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
J
Johannes Rieken 已提交
26
import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
J
Johannes Rieken 已提交
27
import { IFileIconTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
28
import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from 'vs/base/browser/ui/tree/tree';
J
renames  
Johannes Rieken 已提交
29
import { OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItemComparator, OutlineIdentityProvider, OutlineNavigationLabelProvider, OutlineDataSource, OutlineSortOrder, OutlineItem } from 'vs/editor/contrib/documentSymbols/outlineTree';
30
import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
31

32
export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker {
J
Johannes Rieken 已提交
33 34 35 36
	const ctor: IConstructorSignature1<HTMLElement, BreadcrumbsPicker> = element instanceof FileElement
		? BreadcrumbsFilePicker
		: BreadcrumbsOutlinePicker;

37 38 39
	return instantiationService.createInstance(ctor, parent);
}

40
interface ILayoutInfo {
J
Johannes Rieken 已提交
41
	maxHeight: number;
42 43 44 45 46 47
	width: number;
	arrowSize: number;
	arrowOffset: number;
	inputHeight: number;
}

J
Johannes Rieken 已提交
48
type Tree<I, E> = WorkbenchDataTree<I, E, FuzzyScore> | WorkbenchAsyncDataTree<I, E, FuzzyScore>;
49

50 51 52 53
export abstract class BreadcrumbsPicker {

	protected readonly _disposables = new Array<IDisposable>();
	protected readonly _domNode: HTMLDivElement;
J
Johannes Rieken 已提交
54 55 56 57 58
	protected _arrow: HTMLDivElement;
	protected _treeContainer: HTMLDivElement;
	protected _tree: Tree<any, any>;
	protected _fakeEvent = new UIEvent('fakeEvent');
	protected _layoutInfo: ILayoutInfo;
59

J
Johannes Rieken 已提交
60 61
	private readonly _onDidPickElement = new Emitter<{ target: any, payload: any }>();
	readonly onDidPickElement: Event<{ target: any, payload: any }> = this._onDidPickElement.event;
62

J
Johannes Rieken 已提交
63 64 65
	private readonly _onDidFocusElement = new Emitter<{ target: any, payload: any }>();
	readonly onDidFocusElement: Event<{ target: any, payload: any }> = this._onDidFocusElement.event;

66
	constructor(
67
		parent: HTMLElement,
68
		@IInstantiationService protected readonly _instantiationService: IInstantiationService,
J
Johannes Rieken 已提交
69
		@IWorkbenchThemeService protected readonly _themeService: IWorkbenchThemeService,
J
Johannes Rieken 已提交
70
		@IConfigurationService protected readonly _configurationService: IConfigurationService,
71 72
	) {
		this._domNode = document.createElement('div');
73
		this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons';
74
		parent.appendChild(this._domNode);
J
Johannes Rieken 已提交
75 76 77 78 79 80 81 82 83
	}

	dispose(): void {
		dispose(this._disposables);
		this._onDidPickElement.dispose();
		this._tree.dispose();
	}

	show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void {
84

85 86 87 88
		const theme = this._themeService.getTheme();
		const color = theme.getColor(breadcrumbsPickerBackground);

		this._arrow = document.createElement('div');
J
Johannes Rieken 已提交
89
		this._arrow.className = 'arrow';
90 91 92
		this._arrow.style.borderColor = `transparent transparent ${color.toString()}`;
		this._domNode.appendChild(this._arrow);

93 94 95
		this._treeContainer = document.createElement('div');
		this._treeContainer.style.background = color.toString();
		this._treeContainer.style.paddingTop = '2px';
J
Johannes Rieken 已提交
96
		this._treeContainer.style.boxShadow = `0px 5px 8px ${this._themeService.getTheme().getColor(widgetShadow)}`;
97
		this._domNode.appendChild(this._treeContainer);
98

99

100 101 102
		const filterConfig = BreadcrumbsConfig.FilterOnType.bindTo(this._configurationService);
		this._disposables.push(filterConfig);

103
		this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 };
104 105
		this._tree = this._createTree(this._treeContainer);

106
		this._disposables.push(this._tree.onDidChangeSelection(e => {
107 108
			if (e.browserEvent !== this._fakeEvent) {
				const target = this._getTargetFromEvent(e.elements[0], e.browserEvent);
J
Johannes Rieken 已提交
109 110
				if (target) {
					setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click
111
						this._onDidPickElement.fire({ target, payload: undefined });
J
Johannes Rieken 已提交
112
					}, 0);
J
Johannes Rieken 已提交
113
				}
J
Johannes Rieken 已提交
114 115 116
			}
		}));
		this._disposables.push(this._tree.onDidChangeFocus(e => {
117
			const target = this._getTargetFromEvent(e.elements[0], e.browserEvent);
J
Johannes Rieken 已提交
118
			if (target) {
119
				this._onDidFocusElement.fire({ target, payload: undefined });
120 121
			}
		}));
122
		this._disposables.push(this._tree.onDidChangeContentHeight(() => {
123
			this._layout();
124
		}));
125

126 127 128 129 130 131 132
		// filter on type: state
		const cfgFilterOnType = BreadcrumbsConfig.FilterOnType.bindTo(this._configurationService);
		this._tree.updateOptions({ filterOnType: cfgFilterOnType.getValue() });
		this._disposables.push(this._tree.onDidUpdateOptions(e => {
			this._configurationService.updateValue(cfgFilterOnType.name, e.filterOnType, ConfigurationTarget.MEMORY);
		}));

133
		this._domNode.focus();
134

135 136 137
		this._setInput(input).then(() => {
			this._layout();
		}).catch(onUnexpectedError);
J
Johannes Rieken 已提交
138 139
	}

140
	protected _layout(): void {
141

142 143
		const headerHeight = 2 * this._layoutInfo.arrowSize;
		const treeHeight = Math.min(this._layoutInfo.maxHeight - headerHeight, this._tree.contentHeight);
J
Johannes Rieken 已提交
144
		const totalHeight = treeHeight + headerHeight;
145 146

		this._domNode.style.height = `${totalHeight}px`;
147 148 149 150
		this._domNode.style.width = `${this._layoutInfo.width}px`;
		this._arrow.style.top = `-${2 * this._layoutInfo.arrowSize}px`;
		this._arrow.style.borderWidth = `${this._layoutInfo.arrowSize}px`;
		this._arrow.style.marginLeft = `${this._layoutInfo.arrowOffset}px`;
151
		this._treeContainer.style.height = `${treeHeight}px`;
152
		this._treeContainer.style.width = `${this._layoutInfo.width}px`;
153
		this._tree.layout();
154

155 156
	}

157
	protected abstract _setInput(element: BreadcrumbElement): Promise<void>;
J
Johannes Rieken 已提交
158
	protected abstract _createTree(container: HTMLElement): Tree<any, any>;
159
	protected abstract _getTargetFromEvent(element: any, payload: UIEvent): any | undefined;
160 161 162 163
}

//#region - Files

164 165 166 167 168 169 170 171
class FileVirtualDelegate implements IListVirtualDelegate<IFileStat | IWorkspaceFolder> {
	getHeight(_element: IFileStat | IWorkspaceFolder) {
		return 22;
	}
	getTemplateId(_element: IFileStat | IWorkspaceFolder): string {
		return 'FileStat';
	}
}
172

173 174
class FileIdentityProvider implements IIdentityProvider<IWorkspace | IWorkspaceFolder | IFileStat | URI> {
	getId(element: IWorkspace | IWorkspaceFolder | IFileStat | URI): { toString(): string; } {
175 176 177 178 179 180 181 182 183
		if (URI.isUri(element)) {
			return element.toString();
		} else if (IWorkspace.isIWorkspace(element)) {
			return element.id;
		} else if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
			return element.uri.toString();
		} else {
			return element.resource.toString();
		}
184
	}
185 186 187 188
}


class FileDataSource implements IAsyncDataSource<IWorkspace | URI, IWorkspaceFolder | IFileStat> {
189

190 191 192 193 194 195 196 197 198 199 200
	private readonly _parents = new WeakMap<object, IWorkspaceFolder | IFileStat>();

	constructor(
		@IFileService private readonly _fileService: IFileService,
	) { }

	hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean {
		return URI.isUri(element)
			|| IWorkspace.isIWorkspace(element)
			|| IWorkspaceFolder.isIWorkspaceFolder(element)
			|| element.isDirectory;
201 202
	}

203 204
	getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> {

205
		if (IWorkspace.isIWorkspace(element)) {
206
			return Promise.resolve(element.folders).then(folders => {
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
				for (let child of folders) {
					this._parents.set(element, child);
				}
				return folders;
			});
		}
		let uri: URI;
		if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
			uri = element.uri;
		} else if (URI.isUri(element)) {
			uri = element;
		} else {
			uri = element.resource;
		}
		return this._fileService.resolveFile(uri).then(stat => {
			for (let child of stat.children) {
				this._parents.set(stat, child);
224 225 226 227
			}
			return stat.children;
		});
	}
228 229 230 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 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
}

class FileRenderer implements ITreeRenderer<IFileStat | IWorkspaceFolder, FuzzyScore, IResourceLabel> {

	readonly templateId: string = 'FileStat';

	constructor(
		private readonly _labels: ResourceLabels,
		@IConfigurationService private readonly _configService: IConfigurationService,
	) { }


	renderTemplate(container: HTMLElement): IResourceLabel {
		return this._labels.create(container, { supportHighlights: true });
	}

	renderElement(node: ITreeNode<IWorkspaceFolder | IFileStat, [number, number, number]>, index: number, templateData: IResourceLabel): void {
		const fileDecorations = this._configService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations');
		const { element } = node;
		let resource: URI;
		let fileKind: FileKind;
		if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
			resource = element.uri;
			fileKind = FileKind.ROOT_FOLDER;
		} else {
			resource = element.resource;
			fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE;
		}
		templateData.setFile(resource, {
			fileKind,
			hidePath: true,
			fileDecorations: fileDecorations,
			matches: createMatches(node.filterData),
			extraClasses: ['picker-item']
		});
	}

	disposeTemplate(templateData: IResourceLabel): void {
		templateData.dispose();
	}
}

class FileNavigationLabelProvider implements IKeyboardNavigationLabelProvider<IWorkspaceFolder | IFileStat> {
271

272 273
	getKeyboardNavigationLabel(element: IWorkspaceFolder | IFileStat): { toString(): string; } {
		return element.name;
274 275 276
	}
}

277
class FileFilter implements ITreeFilter<IWorkspaceFolder | IFileStat> {
278 279 280 281 282 283 284 285 286 287 288 289

	private readonly _cachedExpressions = new Map<string, glob.ParsedExpression>();
	private readonly _disposables: IDisposable[] = [];

	constructor(
		@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
		@IConfigurationService configService: IConfigurationService,
	) {
		const config = BreadcrumbsConfig.FileExcludes.bindTo(configService);
		const update = () => {
			_workspaceService.getWorkspace().folders.forEach(folder => {
				const excludesConfig = config.getValue({ resource: folder.uri });
290 291
				if (!excludesConfig) {
					return;
292
				}
293 294 295 296 297 298 299 300
				// adjust patterns to be absolute in case they aren't
				// free floating (**/)
				const adjustedConfig: glob.IExpression = {};
				for (const pattern in excludesConfig) {
					if (typeof excludesConfig[pattern] !== 'boolean') {
						continue;
					}
					let patternAbs = pattern.indexOf('**/') !== 0
301
						? posix.join(folder.uri.path, pattern)
302 303 304 305 306
						: pattern;

					adjustedConfig[patternAbs] = excludesConfig[pattern];
				}
				this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig));
307 308 309 310 311 312 313 314 315 316 317 318 319 320
			});
		};
		update();
		this._disposables.push(
			config,
			config.onDidChange(update),
			_workspaceService.onDidChangeWorkspaceFolders(update)
		);
	}

	dispose(): void {
		dispose(this._disposables);
	}

321
	filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean {
322 323 324 325 326 327 328 329 330 331 332
		if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
			// not a file
			return true;
		}
		const folder = this._workspaceService.getWorkspaceFolder(element.resource);
		if (!folder || !this._cachedExpressions.has(folder.uri.toString())) {
			// no folder or no filer
			return true;
		}

		const expression = this._cachedExpressions.get(folder.uri.toString());
333
		return !expression(element.resource.path, basename(element.resource));
334 335 336
	}
}

337

338 339
export class FileSorter implements ITreeSorter<IFileStat | IWorkspaceFolder> {
	compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {
340 341
		if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) {
			return a.index - b.index;
342 343 344 345 346 347
		}
		if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) {
			// same type -> compare on names
			return compareFileNames(a.name, b.name);
		} else if ((a as IFileStat).isDirectory) {
			return -1;
348
		} else {
349
			return 1;
350 351 352 353 354 355
		}
	}
}

export class BreadcrumbsFilePicker extends BreadcrumbsPicker {

356 357 358
	constructor(
		parent: HTMLElement,
		@IInstantiationService instantiationService: IInstantiationService,
J
Johannes Rieken 已提交
359
		@IWorkbenchThemeService themeService: IWorkbenchThemeService,
360
		@IConfigurationService configService: IConfigurationService,
361 362
		@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
	) {
363
		super(parent, instantiationService, themeService, configService);
364 365
	}

366 367
	_createTree(container: HTMLElement) {

J
Johannes Rieken 已提交
368 369 370 371 372 373 374 375 376 377
		// tree icon theme specials
		dom.addClass(this._treeContainer, 'file-icon-themable-tree');
		dom.addClass(this._treeContainer, 'show-file-icons');
		const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => {
			dom.toggleClass(this._treeContainer, 'align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons);
			dom.toggleClass(this._treeContainer, 'hide-arrows', fileIconTheme.hidesExplorerArrows === true);
		};
		this._disposables.push(this._themeService.onDidFileIconThemeChange(onFileIconThemeChange));
		onFileIconThemeChange(this._themeService.getFileIconTheme());

378 379
		const labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER /* TODO@Jo visibility propagation */);
		this._disposables.push(labels);
380

381 382 383 384 385 386 387 388
		return this._instantiationService.createInstance(
			WorkbenchAsyncDataTree,
			container,
			new FileVirtualDelegate(),
			[this._instantiationService.createInstance(FileRenderer, labels)],
			this._instantiationService.createInstance(FileDataSource),
			{
				filterOnType: true,
J
Johannes Rieken 已提交
389
				multipleSelectionSupport: false,
390 391 392 393
				sorter: new FileSorter(),
				filter: this._instantiationService.createInstance(FileFilter),
				identityProvider: new FileIdentityProvider(),
				keyboardNavigationLabelProvider: new FileNavigationLabelProvider()
394
			}
395
		) as WorkbenchAsyncDataTree<BreadcrumbElement, any, FuzzyScore>;
396 397
	}

398 399 400 401 402 403 404 405
	_setInput(element: BreadcrumbElement): Promise<void> {
		const { uri, kind } = (element as FileElement);
		let input: IWorkspace | URI;
		if (kind === FileKind.ROOT_FOLDER) {
			input = this._workspaceService.getWorkspace();
		} else {
			input = dirname(uri);
		}
406

407 408
		const tree = this._tree as WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>;
		return tree.setInput(input).then(() => {
409
			let focusElement: IWorkspaceFolder | IFileStat | undefined;
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
			for (const { element } of tree.getNode().children) {
				if (IWorkspaceFolder.isIWorkspaceFolder(element) && isEqual(element.uri, uri)) {
					focusElement = element;
					break;
				} else if (isEqual((element as IFileStat).resource, uri)) {
					focusElement = element as IFileStat;
					break;
				}
			}
			if (focusElement) {
				tree.reveal(focusElement, 0.5);
				tree.setFocus([focusElement], this._fakeEvent);
			}
			tree.domFocus();
		});
425 426
	}

J
Johannes Rieken 已提交
427
	protected _getTargetFromEvent(element: any, _payload: any): any | undefined {
428
		// todo@joh
J
Johannes Rieken 已提交
429 430
		if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) {
			return new FileElement((element as IFileStat).resource, FileKind.FILE);
431 432 433 434 435 436 437 438 439
		}
	}
}
//#endregion

//#region - Symbols

export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {

J
Johannes Rieken 已提交
440 441 442 443 444 445 446 447 448 449 450 451
	protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>;

	constructor(
		parent: HTMLElement,
		@IInstantiationService instantiationService: IInstantiationService,
		@IWorkbenchThemeService themeService: IWorkbenchThemeService,
		@IConfigurationService configurationService: IConfigurationService,
	) {
		super(parent, instantiationService, themeService, configurationService);
		this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService);
	}

452 453 454 455
	protected _createTree(container: HTMLElement) {
		return this._instantiationService.createInstance(
			WorkbenchDataTree,
			container,
J
Johannes Rieken 已提交
456 457 458
			new OutlineVirtualDelegate(),
			[new OutlineGroupRenderer(), this._instantiationService.createInstance(OutlineElementRenderer)],
			new OutlineDataSource(),
459 460
			{
				filterOnType: true,
461
				expandOnlyOnTwistieClick: true,
J
Johannes Rieken 已提交
462
				multipleSelectionSupport: false,
J
Johannes Rieken 已提交
463 464 465
				sorter: new OutlineItemComparator(this._getOutlineItemCompareType()),
				identityProvider: new OutlineIdentityProvider(),
				keyboardNavigationLabelProvider: this._instantiationService.createInstance(OutlineNavigationLabelProvider)
466
			}
J
Johannes Rieken 已提交
467 468 469 470 471 472
		) as WorkbenchDataTree<OutlineModel, OutlineItem, FuzzyScore>;
	}

	dispose(): void {
		this._symbolSortOrder.dispose();
		super.dispose();
473 474
	}

475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
	protected _setInput(input: BreadcrumbElement): Promise<void> {
		const element = input as TreeElement;
		const model = OutlineModel.get(element);
		const tree = this._tree as WorkbenchDataTree<OutlineModel, any, FuzzyScore>;
		tree.setInput(model);

		let focusElement: TreeElement;
		if (element === model) {
			focusElement = tree.navigate().first();
		} else {
			focusElement = element;
		}
		tree.reveal(focusElement, 0.5);
		tree.setFocus([focusElement], this._fakeEvent);
		tree.domFocus();
490

491
		return Promise.resolve();
492 493
	}

J
Johannes Rieken 已提交
494
	protected _getTargetFromEvent(element: any): any | undefined {
J
Johannes Rieken 已提交
495 496
		if (element instanceof OutlineElement) {
			return element;
497 498
		}
	}
499

J
Johannes Rieken 已提交
500
	private _getOutlineItemCompareType(): OutlineSortOrder {
501
		switch (this._symbolSortOrder.getValue()) {
502
			case 'name':
J
Johannes Rieken 已提交
503
				return OutlineSortOrder.ByName;
504
			case 'type':
J
Johannes Rieken 已提交
505
				return OutlineSortOrder.ByKind;
506 507
			case 'position':
			default:
J
Johannes Rieken 已提交
508
				return OutlineSortOrder.ByPosition;
509 510
		}
	}
511 512 513
}

//#endregion