breadcrumbsPicker.ts 18.6 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 { DisposableStore } 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 } 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, IDataSource } 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
import { IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree';
32

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

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

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

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

51 52 53 54 55
export interface SelectEvent {
	target: any;
	browserEvent: UIEvent;
}

56 57
export abstract class BreadcrumbsPicker {

58
	protected readonly _disposables = new DisposableStore();
59
	protected readonly _domNode: HTMLDivElement;
J
Johannes Rieken 已提交
60 61 62 63 64
	protected _arrow: HTMLDivElement;
	protected _treeContainer: HTMLDivElement;
	protected _tree: Tree<any, any>;
	protected _fakeEvent = new UIEvent('fakeEvent');
	protected _layoutInfo: ILayoutInfo;
65

66 67
	private readonly _onDidPickElement = new Emitter<SelectEvent>();
	readonly onDidPickElement: Event<SelectEvent> = this._onDidPickElement.event;
68

69 70
	private readonly _onDidFocusElement = new Emitter<SelectEvent>();
	readonly onDidFocusElement: Event<SelectEvent> = this._onDidFocusElement.event;
J
Johannes Rieken 已提交
71

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

	dispose(): void {
84
		this._disposables.dispose();
J
Johannes Rieken 已提交
85 86 87 88 89
		this._onDidPickElement.dispose();
		this._tree.dispose();
	}

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

91 92 93 94
		const theme = this._themeService.getTheme();
		const color = theme.getColor(breadcrumbsPickerBackground);

		this._arrow = document.createElement('div');
J
Johannes Rieken 已提交
95
		this._arrow.className = 'arrow';
M
Matt Bierner 已提交
96
		this._arrow.style.borderColor = `transparent transparent ${color ? color.toString() : ''}`;
97 98
		this._domNode.appendChild(this._arrow);

99
		this._treeContainer = document.createElement('div');
M
Matt Bierner 已提交
100
		this._treeContainer.style.background = color ? color.toString() : '';
101
		this._treeContainer.style.paddingTop = '2px';
J
Johannes Rieken 已提交
102
		this._treeContainer.style.boxShadow = `0px 5px 8px ${this._themeService.getTheme().getColor(widgetShadow)}`;
103
		this._domNode.appendChild(this._treeContainer);
104

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

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

128
		this._domNode.focus();
129

130 131 132
		this._setInput(input).then(() => {
			this._layout();
		}).catch(onUnexpectedError);
J
Johannes Rieken 已提交
133 134
	}

135
	protected _layout(): void {
136

137 138
		const headerHeight = 2 * this._layoutInfo.arrowSize;
		const treeHeight = Math.min(this._layoutInfo.maxHeight - headerHeight, this._tree.contentHeight);
J
Johannes Rieken 已提交
139
		const totalHeight = treeHeight + headerHeight;
140 141

		this._domNode.style.height = `${totalHeight}px`;
142 143 144 145
		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`;
146
		this._treeContainer.style.height = `${treeHeight}px`;
147
		this._treeContainer.style.width = `${this._layoutInfo.width}px`;
J
Johannes Rieken 已提交
148
		this._tree.layout(treeHeight, this._layoutInfo.width);
149

150 151
	}

152 153 154 155
	get useAltAsMultipleSelectionModifier() {
		return this._tree.useAltAsMultipleSelectionModifier;
	}

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

//#region - Files

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

172 173
class FileIdentityProvider implements IIdentityProvider<IWorkspace | IWorkspaceFolder | IFileStat | URI> {
	getId(element: IWorkspace | IWorkspaceFolder | IFileStat | URI): { toString(): string; } {
174 175 176 177 178 179 180 181 182
		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();
		}
183
	}
184 185 186 187
}


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

189 190 191 192 193 194 195 196 197 198 199
	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;
200 201
	}

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

204
		if (IWorkspace.isIWorkspace(element)) {
205
			return Promise.resolve(element.folders).then(folders => {
206 207 208 209 210 211 212 213 214 215 216 217 218 219
				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;
		}
B
Benjamin Pasero 已提交
220
		return this._fileService.resolve(uri).then(stat => {
M
Matt Bierner 已提交
221
			for (const child of stat.children || []) {
222
				this._parents.set(stat, child);
223
			}
M
Matt Bierner 已提交
224
			return stat.children || [];
225 226
		});
	}
227 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
}

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> {
270

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

276
class FileFilter implements ITreeFilter<IWorkspaceFolder | IFileStat> {
277 278

	private readonly _cachedExpressions = new Map<string, glob.ParsedExpression>();
279
	private readonly _disposables = new DisposableStore();
280 281 282 283 284 285 286 287 288

	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 });
289 290
				if (!excludesConfig) {
					return;
291
				}
292 293 294 295 296 297 298 299
				// 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
300
						? posix.join(folder.uri.path, pattern)
301 302 303 304 305
						: pattern;

					adjustedConfig[patternAbs] = excludesConfig[pattern];
				}
				this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig));
306 307 308
			});
		};
		update();
309 310 311
		this._disposables.add(config);
		this._disposables.add(config.onDidChange(update));
		this._disposables.add(_workspaceService.onDidChangeWorkspaceFolders(update));
312 313 314
	}

	dispose(): void {
315
		this._disposables.dispose();
316 317
	}

318
	filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean {
319 320 321 322 323 324 325 326 327 328
		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;
		}

329
		const expression = this._cachedExpressions.get(folder.uri.toString())!;
330
		return !expression(element.resource.path, basename(element.resource));
331 332 333
	}
}

334

335 336
export class FileSorter implements ITreeSorter<IFileStat | IWorkspaceFolder> {
	compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {
337 338
		if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) {
			return a.index - b.index;
339 340 341 342 343 344
		}
		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;
345
		} else {
346
			return 1;
347 348 349 350 351 352
		}
	}
}

export class BreadcrumbsFilePicker extends BreadcrumbsPicker {

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

363 364
	_createTree(container: HTMLElement) {

J
Johannes Rieken 已提交
365 366 367 368 369 370 371
		// 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);
		};
372
		this._disposables.add(this._themeService.onDidFileIconThemeChange(onFileIconThemeChange));
J
Johannes Rieken 已提交
373 374
		onFileIconThemeChange(this._themeService.getFileIconTheme());

375
		const labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER /* TODO@Jo visibility propagation */);
376
		this._disposables.add(labels);
377

M
Matt Bierner 已提交
378 379 380 381 382 383 384
		return this._instantiationService.createInstance(WorkbenchAsyncDataTree, container, new FileVirtualDelegate(), [this._instantiationService.createInstance(FileRenderer, labels)], this._instantiationService.createInstance(FileDataSource), {
			multipleSelectionSupport: false,
			sorter: new FileSorter(),
			filter: this._instantiationService.createInstance(FileFilter),
			identityProvider: new FileIdentityProvider(),
			keyboardNavigationLabelProvider: new FileNavigationLabelProvider()
		}) as WorkbenchAsyncDataTree<BreadcrumbElement | IFileStat, any, FuzzyScore>;
385 386
	}

387 388 389 390 391 392 393 394
	_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);
		}
395

396 397
		const tree = this._tree as WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>;
		return tree.setInput(input).then(() => {
398
			let focusElement: IWorkspaceFolder | IFileStat | undefined;
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
			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();
		});
414 415
	}

416
	protected _getTargetFromEvent(element: any): any | undefined {
417
		// todo@joh
J
Johannes Rieken 已提交
418 419
		if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) {
			return new FileElement((element as IFileStat).resource, FileKind.FILE);
420 421 422 423 424 425 426 427 428
		}
	}
}
//#endregion

//#region - Symbols

export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {

J
Johannes Rieken 已提交
429 430 431 432 433 434 435 436 437 438 439 440
	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);
	}

441
	protected _createTree(container: HTMLElement) {
442 443 444 445 446 447 448 449
		return this._instantiationService.createInstance<
			HTMLElement,
			IListVirtualDelegate<OutlineItem>,
			ITreeRenderer<any, FuzzyScore, any>[],
			IDataSource<OutlineModel, OutlineItem>,
			IDataTreeOptions<OutlineItem, FuzzyScore>,
			WorkbenchDataTree<OutlineModel, OutlineItem, FuzzyScore>
		>(
450 451
			WorkbenchDataTree,
			container,
J
Johannes Rieken 已提交
452 453 454
			new OutlineVirtualDelegate(),
			[new OutlineGroupRenderer(), this._instantiationService.createInstance(OutlineElementRenderer)],
			new OutlineDataSource(),
455
			{
456
				expandOnlyOnTwistieClick: true,
J
Johannes Rieken 已提交
457
				multipleSelectionSupport: false,
J
Johannes Rieken 已提交
458 459
				sorter: new OutlineItemComparator(this._getOutlineItemCompareType()),
				identityProvider: new OutlineIdentityProvider(),
J
Joao Moreno 已提交
460
				keyboardNavigationLabelProvider: new OutlineNavigationLabelProvider()
461
			}
J
Johannes Rieken 已提交
462 463 464 465 466 467
		) as WorkbenchDataTree<OutlineModel, OutlineItem, FuzzyScore>;
	}

	dispose(): void {
		this._symbolSortOrder.dispose();
		super.dispose();
468 469
	}

470 471
	protected _setInput(input: BreadcrumbElement): Promise<void> {
		const element = input as TreeElement;
472
		const model = OutlineModel.get(element)!;
473 474 475 476 477 478 479 480 481 482 483 484
		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();
485

486
		return Promise.resolve();
487 488
	}

J
Johannes Rieken 已提交
489
	protected _getTargetFromEvent(element: any): any | undefined {
J
Johannes Rieken 已提交
490 491
		if (element instanceof OutlineElement) {
			return element;
492 493
		}
	}
494

J
Johannes Rieken 已提交
495
	private _getOutlineItemCompareType(): OutlineSortOrder {
496
		switch (this._symbolSortOrder.getValue()) {
497
			case 'name':
J
Johannes Rieken 已提交
498
				return OutlineSortOrder.ByName;
499
			case 'type':
J
Johannes Rieken 已提交
500
				return OutlineSortOrder.ByKind;
501 502
			case 'position':
			default:
J
Johannes Rieken 已提交
503
				return OutlineSortOrder.ByPosition;
504 505
		}
	}
506 507 508
}

//#endregion