breadcrumbsPicker.ts 17.3 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 42 43 44
export abstract class BreadcrumbsPicker {

	protected readonly _disposables = new Array<IDisposable>();
	protected readonly _domNode: HTMLDivElement;
45
	protected readonly _arrow: HTMLDivElement;
46
	protected readonly _treeContainer: HTMLDivElement;
47
	protected readonly _tree: HighlightingWorkbenchTree;
48
	protected readonly _focus: dom.IFocusTracker;
49

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

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

56
	constructor(
57
		parent: HTMLElement,
58
		@IInstantiationService protected readonly _instantiationService: IInstantiationService,
J
Johannes Rieken 已提交
59
		@IWorkbenchThemeService protected readonly _themeService: IWorkbenchThemeService,
60
		@IConfigurationService private readonly _configurationService: IConfigurationService,
61 62
	) {
		this._domNode = document.createElement('div');
63
		this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons';
64
		parent.appendChild(this._domNode);
65 66

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

70 71 72 73 74 75 76 77 78 79
		const theme = this._themeService.getTheme();
		const color = theme.getColor(breadcrumbsPickerBackground);

		this._arrow = document.createElement('div');
		this._arrow.style.width = '0';
		this._arrow.style.borderStyle = 'solid';
		this._arrow.style.borderWidth = '8px';
		this._arrow.style.borderColor = `transparent transparent ${color.toString()}`;
		this._domNode.appendChild(this._arrow);

80 81 82
		this._treeContainer = document.createElement('div');
		this._treeContainer.style.background = color.toString();
		this._treeContainer.style.paddingTop = '2px';
J
Johannes Rieken 已提交
83
		this._treeContainer.style.boxShadow = `0px 5px 8px ${this._themeService.getTheme().getColor(widgetShadow)}`;
84
		this._domNode.appendChild(this._treeContainer);
85

86 87 88
		const filterConfig = BreadcrumbsConfig.FilterOnType.bindTo(this._configurationService);
		this._disposables.push(filterConfig);

89
		const treeConifg = this._completeTreeConfiguration({ dataSource: undefined, renderer: undefined, highlighter: undefined });
90 91
		this._tree = this._instantiationService.createInstance(
			HighlightingWorkbenchTree,
92
			this._treeContainer,
93
			treeConifg,
J
Johannes Rieken 已提交
94
			<IHighlightingTreeOptions>{ useShadows: false, filterOnType: filterConfig.getValue(), showTwistie: false, twistiePixels: 12 },
95 96
			{ placeholder: localize('placeholder', "Find") }
		);
97 98
		this._disposables.push(this._tree.onDidChangeSelection(e => {
			if (e.payload !== this._tree) {
J
Johannes Rieken 已提交
99 100 101 102 103
				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 已提交
104
				}
J
Johannes Rieken 已提交
105 106 107 108 109 110
			}
		}));
		this._disposables.push(this._tree.onDidChangeFocus(e => {
			const target = this._getTargetFromEvent(e.focus, e.payload);
			if (target) {
				this._onDidFocusElement.fire({ target, payload: e.payload });
111 112 113
			}
		}));

J
Johannes Rieken 已提交
114 115 116 117 118 119 120 121 122 123
		// 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());

124
		this._domNode.focus();
125 126 127 128 129 130 131 132 133
	}

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

J
Johannes Rieken 已提交
134 135 136 137 138
	setInput(input: any): void {
		let actualInput = this._getInput(input);
		this._tree.setInput(actualInput).then(() => {
			let selection = this._getInitialSelection(this._tree, input);
			if (selection) {
J
Johannes Rieken 已提交
139
				this._tree.reveal(selection, .5).then(() => {
J
Johannes Rieken 已提交
140 141
					this._tree.setSelection([selection], this._tree);
					this._tree.setFocus(selection);
142
					this._tree.domFocus();
J
Johannes Rieken 已提交
143
				});
144 145 146 147
			} else {
				this._tree.focusFirst();
				this._tree.setSelection([this._tree.getFocus()], this._tree);
				this._tree.domFocus();
J
Johannes Rieken 已提交
148 149 150 151
			}
		}, onUnexpectedError);
	}

152
	layout(height: number, width: number, arrowSize: number, arrowOffset: number) {
153 154 155 156 157 158 159 160 161 162

		let treeHeight = height - 2 * arrowSize;
		let elementHeight = 22;
		let elementCount = treeHeight / elementHeight;
		if (elementCount % 2 !== 1) {
			treeHeight = elementHeight * (elementCount + 1);
		}
		let totalHeight = treeHeight + 2 + arrowSize;

		this._domNode.style.height = `${totalHeight}px`;
163
		this._domNode.style.width = `${width}px`;
164 165
		this._arrow.style.borderWidth = `${arrowSize}px`;
		this._arrow.style.marginLeft = `${arrowOffset}px`;
166
		this._treeContainer.style.height = `${treeHeight}px`;
167 168
		this._treeContainer.style.width = `${width}px`;
		this._tree.layout();
169 170 171 172 173
	}

	protected abstract _getInput(input: BreadcrumbElement): any;
	protected abstract _getInitialSelection(tree: ITree, input: BreadcrumbElement): any;
	protected abstract _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration;
J
Johannes Rieken 已提交
174
	protected abstract _getTargetFromEvent(element: any, payload: any): any | undefined;
175 176 177 178 179 180
}

//#region - Files

export class FileDataSource implements IDataSource {

181
	private readonly _parents = new WeakMap<object, IWorkspaceFolder | IFileStat>();
182 183 184 185 186

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

187 188 189 190 191 192 193 194 195 196
	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();
		}
197 198
	}

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

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
	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);
223 224 225 226 227
			}
			return stat.children;
		});
	}

228 229
	getParent(tree: ITree, element: IWorkspace | URI | IWorkspaceFolder | IFileStat): TPromise<IWorkspaceFolder | IFileStat> {
		return TPromise.as(this._parents.get(element));
230 231 232
	}
}

233 234 235 236 237 238 239 240 241 242 243 244 245
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 });
246 247
				if (!excludesConfig) {
					return;
248
				}
249 250 251 252 253 254 255 256 257 258 259 260 261 262
				// 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));
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
			});
		};
		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());
289
		return !expression(element.resource.path, basename(element.resource));
290 291 292
	}
}

293 294 295 296 297 298 299 300
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);
	}
}
301

302
export class FileRenderer implements IRenderer {
303 304

	constructor(
305 306
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
		@IConfigurationService private readonly _configService: IConfigurationService,
307 308 309 310 311 312 313 314 315 316 317 318 319 320
	) { }

	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 });
	}

321
	renderElement(tree: ITree, element: IFileStat | IWorkspaceFolder, templateId: string, templateData: FileLabel): void {
322
		let fileDecorations = this._configService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations');
J
Johannes Rieken 已提交
323 324
		let resource: URI;
		let fileKind: FileKind;
325
		if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
J
Johannes Rieken 已提交
326 327
			resource = element.uri;
			fileKind = FileKind.ROOT_FOLDER;
328
		} else {
J
Johannes Rieken 已提交
329 330
			resource = element.resource;
			fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE;
331
		}
J
Johannes Rieken 已提交
332 333 334 335
		templateData.setFile(resource, {
			fileKind,
			hidePath: true,
			fileDecorations: fileDecorations,
336
			matches: createMatches((tree as HighlightingWorkbenchTree).getHighlighterScore(element)),
J
Johannes Rieken 已提交
337
			extraClasses: ['picker-item']
J
Johannes Rieken 已提交
338
		});
339 340 341 342 343 344 345 346
	}

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

export class FileSorter implements ISorter {
347 348 349
	compare(tree: ITree, a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {
		if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) {
			return a.index - b.index;
350
		} else {
351 352 353 354 355 356 357 358
			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;
			}
359 360 361 362 363 364
		}
	}
}

export class BreadcrumbsFilePicker extends BreadcrumbsPicker {

365 366 367
	constructor(
		parent: HTMLElement,
		@IInstantiationService instantiationService: IInstantiationService,
J
Johannes Rieken 已提交
368
		@IWorkbenchThemeService themeService: IWorkbenchThemeService,
369
		@IConfigurationService configService: IConfigurationService,
370 371
		@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
	) {
372
		super(parent, instantiationService, themeService, configService);
373 374
	}

375
	protected _getInput(input: BreadcrumbElement): any {
376 377 378 379 380 381
		let { uri, kind } = (input as FileElement);
		if (kind === FileKind.ROOT_FOLDER) {
			return this._workspaceService.getWorkspace();
		} else {
			return dirname(uri);
		}
382 383 384 385 386 387
	}

	protected _getInitialSelection(tree: ITree, input: BreadcrumbElement): any {
		let { uri } = (input as FileElement);
		let nav = tree.getNavigator();
		while (nav.next()) {
388 389 390 391
			let cur = nav.current();
			let candidate = IWorkspaceFolder.isIWorkspaceFolder(cur) ? cur.uri : (cur as IFileStat).resource;
			if (isEqual(uri, candidate)) {
				return cur;
392 393 394 395 396 397 398
			}
		}
		return undefined;
	}

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

402 403 404
		config.dataSource = this._instantiationService.createInstance(FileDataSource);
		config.renderer = this._instantiationService.createInstance(FileRenderer);
		config.sorter = new FileSorter();
405
		config.highlighter = new FileHighlighter();
406
		config.filter = filter;
407 408 409
		return config;
	}

J
Johannes Rieken 已提交
410 411 412
	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);
413 414 415 416 417 418 419
		}
	}
}
//#endregion

//#region - Symbols

420 421 422 423
class OutlineHighlighter implements IHighlighter {
	getHighlights(tree: ITree, element: OutlineElement, pattern: string): FuzzyScore {
		OutlineModel.get(element).updateMatches(pattern);
		return element.score;
424 425 426 427 428 429 430
	}
}

export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {

	protected _getInput(input: BreadcrumbElement): any {
		let element = input as TreeElement;
431 432 433
		let model = OutlineModel.get(element);
		model.updateMatches('');
		return model;
434 435 436
	}

	protected _getInitialSelection(_tree: ITree, input: BreadcrumbElement): any {
437
		return input instanceof OutlineModel ? undefined : input;
438 439 440 441
	}

	protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration {
		config.dataSource = this._instantiationService.createInstance(OutlineDataSource);
442
		config.renderer = this._instantiationService.createInstance(OutlineRenderer);
443
		config.sorter = new OutlineItemComparator();
444
		config.highlighter = new OutlineHighlighter();
445 446 447
		return config;
	}

J
Johannes Rieken 已提交
448 449
	protected _getTargetFromEvent(element: any, payload: any): any | undefined {
		if (payload && payload.didClickOnTwistie) {
450 451
			return;
		}
J
Johannes Rieken 已提交
452 453
		if (element instanceof OutlineElement) {
			return element;
454 455 456 457 458
		}
	}
}

//#endregion