breadcrumbsPicker.ts 18.0 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.
 *--------------------------------------------------------------------------------------------*/

'use strict';

J
Johannes Rieken 已提交
8
import { onDidChangeZoomLevel } from 'vs/base/browser/browser';
9 10
import * as dom from 'vs/base/browser/dom';
import { compareFileNames } from 'vs/base/common/comparers';
J
Johannes Rieken 已提交
11
import { onUnexpectedError } from 'vs/base/common/errors';
12
import { Emitter, Event } from 'vs/base/common/event';
J
Johannes Rieken 已提交
13 14
import { createMatches, FuzzyScore, fuzzyScore } from 'vs/base/common/filters';
import * as glob from 'vs/base/common/glob';
15
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
16
import { join } from 'vs/base/common/paths';
J
Johannes Rieken 已提交
17
import { basename, dirname, isEqual } from 'vs/base/common/resources';
18
import { URI } from 'vs/base/common/uri';
19
import { TPromise } from 'vs/base/common/winjs.base';
J
Johannes Rieken 已提交
20
import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree';
21 22 23 24
import 'vs/css!./media/breadcrumbscontrol';
import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { OutlineDataSource, OutlineItemComparator, OutlineRenderer } from 'vs/editor/contrib/documentSymbols/outlineTree';
import { localize } from 'vs/nls';
J
Johannes Rieken 已提交
25
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
26
import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
27
import { IConstructorSignature1, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
28
import { HighlightingWorkbenchTree, IHighlighter, IHighlightingTreeConfiguration, IHighlightingTreeOptions } from 'vs/platform/list/browser/listService';
J
Johannes Rieken 已提交
29 30
import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
31
import { FileLabel } from 'vs/workbench/browser/labels';
32
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
J
Johannes Rieken 已提交
33
import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
J
Johannes Rieken 已提交
34
import { IFileIconTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
35

36 37 38 39 40
export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker {
	let ctor: IConstructorSignature1<HTMLElement, BreadcrumbsPicker> = element instanceof FileElement ? BreadcrumbsFilePicker : BreadcrumbsOutlinePicker;
	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;
}

49 50 51 52
export abstract class BreadcrumbsPicker {

	protected readonly _disposables = new Array<IDisposable>();
	protected readonly _domNode: HTMLDivElement;
53
	protected readonly _arrow: HTMLDivElement;
54
	protected readonly _treeContainer: HTMLDivElement;
55
	protected readonly _tree: HighlightingWorkbenchTree;
56
	protected readonly _focus: dom.IFocusTracker;
57
	private _layoutInfo: ILayoutInfo;
58

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

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

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

		this._focus = dom.trackFocus(this._domNode);
J
Johannes Rieken 已提交
76
		this._focus.onDidBlur(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined }), undefined, this._disposables);
J
Johannes Rieken 已提交
77
		this._disposables.push(onDidChangeZoomLevel(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined })));
78

79 80 81 82
		const theme = this._themeService.getTheme();
		const color = theme.getColor(breadcrumbsPickerBackground);

		this._arrow = document.createElement('div');
J
Johannes Rieken 已提交
83
		this._arrow.className = 'arrow';
84 85 86
		this._arrow.style.borderColor = `transparent transparent ${color.toString()}`;
		this._domNode.appendChild(this._arrow);

87 88 89
		this._treeContainer = document.createElement('div');
		this._treeContainer.style.background = color.toString();
		this._treeContainer.style.paddingTop = '2px';
J
Johannes Rieken 已提交
90
		this._treeContainer.style.boxShadow = `0px 5px 8px ${this._themeService.getTheme().getColor(widgetShadow)}`;
91
		this._domNode.appendChild(this._treeContainer);
92

93 94 95
		const filterConfig = BreadcrumbsConfig.FilterOnType.bindTo(this._configurationService);
		this._disposables.push(filterConfig);

96
		const treeConifg = this._completeTreeConfiguration({ dataSource: undefined, renderer: undefined, highlighter: undefined });
97 98
		this._tree = this._instantiationService.createInstance(
			HighlightingWorkbenchTree,
99
			this._treeContainer,
100
			treeConifg,
J
Johannes Rieken 已提交
101
			<IHighlightingTreeOptions>{ useShadows: false, filterOnType: filterConfig.getValue(), showTwistie: false, twistiePixels: 12 },
102 103
			{ placeholder: localize('placeholder', "Find") }
		);
104 105
		this._disposables.push(this._tree.onDidChangeSelection(e => {
			if (e.payload !== this._tree) {
J
Johannes Rieken 已提交
106 107 108 109 110
				const target = this._getTargetFromEvent(e.selection[0], e.payload);
				if (target) {
					setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click
						this._onDidPickElement.fire({ target, payload: e.payload });
					}, 0);
J
Johannes Rieken 已提交
111
				}
J
Johannes Rieken 已提交
112 113 114 115 116 117
			}
		}));
		this._disposables.push(this._tree.onDidChangeFocus(e => {
			const target = this._getTargetFromEvent(e.focus, e.payload);
			if (target) {
				this._onDidFocusElement.fire({ target, payload: e.payload });
118 119
			}
		}));
120
		this._disposables.push(this._tree.onDidStartFiltering(() => {
121 122 123 124 125 126 127 128
			this._layoutInfo.inputHeight = 36;
			this._layout();
		}));
		this._disposables.push(this._tree.onDidExpandItem(() => {
			this._layout();
		}));
		this._disposables.push(this._tree.onDidCollapseItem(() => {
			this._layout();
129
		}));
130

J
Johannes Rieken 已提交
131 132 133 134 135 136 137 138 139 140
		// 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(_themeService.onDidFileIconThemeChange(onFileIconThemeChange));
		onFileIconThemeChange(_themeService.getFileIconTheme());

141
		this._domNode.focus();
142 143 144 145 146 147 148 149 150
	}

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

J
Johannes Rieken 已提交
151
	setInput(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void {
J
Johannes Rieken 已提交
152 153
		let actualInput = this._getInput(input);
		this._tree.setInput(actualInput).then(() => {
154

J
Johannes Rieken 已提交
155
			this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 };
156
			this._layout();
157 158

			// use proper selection, reveal
J
Johannes Rieken 已提交
159 160
			let selection = this._getInitialSelection(this._tree, input);
			if (selection) {
161
				return this._tree.reveal(selection, .5).then(() => {
J
Johannes Rieken 已提交
162 163
					this._tree.setSelection([selection], this._tree);
					this._tree.setFocus(selection);
164
					this._tree.domFocus();
J
Johannes Rieken 已提交
165
				});
166 167 168 169
			} else {
				this._tree.focusFirst();
				this._tree.setSelection([this._tree.getFocus()], this._tree);
				this._tree.domFocus();
170
				return Promise.resolve(null);
J
Johannes Rieken 已提交
171 172 173 174
			}
		}, onUnexpectedError);
	}

175 176 177 178 179
	private _layout(info: ILayoutInfo = this._layoutInfo): void {

		let count = 0;
		let nav = this._tree.getNavigator(undefined, false);
		while (nav.next() && count < 13) { count += 1; }
180

J
Johannes Rieken 已提交
181 182 183
		let headerHeight = 2 * info.arrowSize;
		let treeHeight = Math.min(info.maxHeight - headerHeight, count * 22);
		let totalHeight = treeHeight + headerHeight;
184 185

		this._domNode.style.height = `${totalHeight}px`;
186
		this._domNode.style.width = `${info.width}px`;
J
Johannes Rieken 已提交
187
		this._arrow.style.top = `-${2 * info.arrowSize}px`;
188 189
		this._arrow.style.borderWidth = `${info.arrowSize}px`;
		this._arrow.style.marginLeft = `${info.arrowOffset}px`;
190
		this._treeContainer.style.height = `${treeHeight}px`;
191
		this._treeContainer.style.width = `${info.width}px`;
192
		this._tree.layout();
193 194
		this._layoutInfo = info;

195 196 197 198 199
	}

	protected abstract _getInput(input: BreadcrumbElement): any;
	protected abstract _getInitialSelection(tree: ITree, input: BreadcrumbElement): any;
	protected abstract _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration;
J
Johannes Rieken 已提交
200
	protected abstract _getTargetFromEvent(element: any, payload: any): any | undefined;
201 202 203 204 205 206
}

//#region - Files

export class FileDataSource implements IDataSource {

207
	private readonly _parents = new WeakMap<object, IWorkspaceFolder | IFileStat>();
208 209 210 211 212

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

213 214 215 216 217 218 219 220 221 222
	getId(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): string {
		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();
		}
223 224
	}

225 226
	hasChildren(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): boolean {
		return URI.isUri(element) || IWorkspace.isIWorkspace(element) || IWorkspaceFolder.isIWorkspaceFolder(element) || element.isDirectory;
227 228
	}

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
	getChildren(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): TPromise<IWorkspaceFolder[] | IFileStat[]> {
		if (IWorkspace.isIWorkspace(element)) {
			return TPromise.as(element.folders).then(folders => {
				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);
249 250 251 252 253
			}
			return stat.children;
		});
	}

254 255
	getParent(tree: ITree, element: IWorkspace | URI | IWorkspaceFolder | IFileStat): TPromise<IWorkspaceFolder | IFileStat> {
		return TPromise.as(this._parents.get(element));
256 257 258
	}
}

259 260 261 262 263 264 265 266 267 268 269 270 271
export class FileFilter implements IFilter {

	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 });
272 273
				if (!excludesConfig) {
					return;
274
				}
275 276 277 278 279 280 281 282 283 284 285 286 287 288
				// 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
						? join(folder.uri.path, pattern)
						: pattern;

					adjustedConfig[patternAbs] = excludesConfig[pattern];
				}
				this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig));
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
			});
		};
		update();
		this._disposables.push(
			config,
			config.onDidChange(update),
			_workspaceService.onDidChangeWorkspaceFolders(update)
		);
	}

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

	isVisible(tree: ITree, element: IWorkspaceFolder | IFileStat): boolean {
		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());
315
		return !expression(element.resource.path, basename(element.resource));
316 317 318
	}
}

319 320 321 322 323 324 325 326
export class FileHighlighter implements IHighlighter {
	getHighlightsStorageKey(element: IFileStat | IWorkspaceFolder): string {
		return IWorkspaceFolder.isIWorkspaceFolder(element) ? element.uri.toString() : element.resource.toString();
	}
	getHighlights(tree: ITree, element: IFileStat | IWorkspaceFolder, pattern: string): FuzzyScore {
		return fuzzyScore(pattern, element.name, undefined, true);
	}
}
327

328
export class FileRenderer implements IRenderer {
329 330

	constructor(
331 332
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
		@IConfigurationService private readonly _configService: IConfigurationService,
333 334 335 336 337 338 339 340 341 342 343 344 345 346
	) { }

	getHeight(tree: ITree, element: any): number {
		return 22;
	}

	getTemplateId(tree: ITree, element: any): string {
		return 'FileStat';
	}

	renderTemplate(tree: ITree, templateId: string, container: HTMLElement) {
		return this._instantiationService.createInstance(FileLabel, container, { supportHighlights: true });
	}

347
	renderElement(tree: ITree, element: IFileStat | IWorkspaceFolder, templateId: string, templateData: FileLabel): void {
348
		let fileDecorations = this._configService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations');
J
Johannes Rieken 已提交
349 350
		let resource: URI;
		let fileKind: FileKind;
351
		if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
J
Johannes Rieken 已提交
352 353
			resource = element.uri;
			fileKind = FileKind.ROOT_FOLDER;
354
		} else {
J
Johannes Rieken 已提交
355 356
			resource = element.resource;
			fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE;
357
		}
J
Johannes Rieken 已提交
358 359 360 361
		templateData.setFile(resource, {
			fileKind,
			hidePath: true,
			fileDecorations: fileDecorations,
362
			matches: createMatches((tree as HighlightingWorkbenchTree).getHighlighterScore(element)),
J
Johannes Rieken 已提交
363
			extraClasses: ['picker-item']
J
Johannes Rieken 已提交
364
		});
365 366 367 368 369 370 371 372
	}

	disposeTemplate(tree: ITree, templateId: string, templateData: FileLabel): void {
		templateData.dispose();
	}
}

export class FileSorter implements ISorter {
373 374 375
	compare(tree: ITree, a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {
		if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) {
			return a.index - b.index;
376
		} else {
377 378 379 380 381 382 383 384
			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;
			} else {
				return 1;
			}
385 386 387 388 389 390
		}
	}
}

export class BreadcrumbsFilePicker extends BreadcrumbsPicker {

391 392 393
	constructor(
		parent: HTMLElement,
		@IInstantiationService instantiationService: IInstantiationService,
J
Johannes Rieken 已提交
394
		@IWorkbenchThemeService themeService: IWorkbenchThemeService,
395
		@IConfigurationService configService: IConfigurationService,
396 397
		@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
	) {
398
		super(parent, instantiationService, themeService, configService);
399 400
	}

401
	protected _getInput(input: BreadcrumbElement): any {
402 403 404 405 406 407
		let { uri, kind } = (input as FileElement);
		if (kind === FileKind.ROOT_FOLDER) {
			return this._workspaceService.getWorkspace();
		} else {
			return dirname(uri);
		}
408 409 410 411 412 413
	}

	protected _getInitialSelection(tree: ITree, input: BreadcrumbElement): any {
		let { uri } = (input as FileElement);
		let nav = tree.getNavigator();
		while (nav.next()) {
414 415 416 417
			let cur = nav.current();
			let candidate = IWorkspaceFolder.isIWorkspaceFolder(cur) ? cur.uri : (cur as IFileStat).resource;
			if (isEqual(uri, candidate)) {
				return cur;
418 419 420 421 422 423 424
			}
		}
		return undefined;
	}

	protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration {
		// todo@joh reuse explorer implementations?
425 426 427
		const filter = this._instantiationService.createInstance(FileFilter);
		this._disposables.push(filter);

428 429 430
		config.dataSource = this._instantiationService.createInstance(FileDataSource);
		config.renderer = this._instantiationService.createInstance(FileRenderer);
		config.sorter = new FileSorter();
431
		config.highlighter = new FileHighlighter();
432
		config.filter = filter;
433 434 435
		return config;
	}

J
Johannes Rieken 已提交
436 437 438
	protected _getTargetFromEvent(element: any, _payload: any): any | undefined {
		if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) {
			return new FileElement((element as IFileStat).resource, FileKind.FILE);
439 440 441 442 443 444 445
		}
	}
}
//#endregion

//#region - Symbols

446 447 448 449
class OutlineHighlighter implements IHighlighter {
	getHighlights(tree: ITree, element: OutlineElement, pattern: string): FuzzyScore {
		OutlineModel.get(element).updateMatches(pattern);
		return element.score;
450 451 452 453 454 455 456
	}
}

export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {

	protected _getInput(input: BreadcrumbElement): any {
		let element = input as TreeElement;
457 458 459
		let model = OutlineModel.get(element);
		model.updateMatches('');
		return model;
460 461 462
	}

	protected _getInitialSelection(_tree: ITree, input: BreadcrumbElement): any {
463
		return input instanceof OutlineModel ? undefined : input;
464 465 466 467
	}

	protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration {
		config.dataSource = this._instantiationService.createInstance(OutlineDataSource);
468
		config.renderer = this._instantiationService.createInstance(OutlineRenderer);
469
		config.sorter = new OutlineItemComparator();
470
		config.highlighter = new OutlineHighlighter();
471 472 473
		return config;
	}

J
Johannes Rieken 已提交
474 475
	protected _getTargetFromEvent(element: any, payload: any): any | undefined {
		if (payload && payload.didClickOnTwistie) {
476 477
			return;
		}
J
Johannes Rieken 已提交
478 479
		if (element instanceof OutlineElement) {
			return element;
480 481 482 483 484
		}
	}
}

//#endregion