Path: blob/main/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { compareFileNames } from '../../../../base/common/comparers.js';6import { onUnexpectedError } from '../../../../base/common/errors.js';7import { Emitter, Event } from '../../../../base/common/event.js';8import { createMatches, FuzzyScore } from '../../../../base/common/filters.js';9import * as glob from '../../../../base/common/glob.js';10import { IDisposable, DisposableStore, MutableDisposable, Disposable } from '../../../../base/common/lifecycle.js';11import { posix, relative } from '../../../../base/common/path.js';12import { basename, dirname, isEqual } from '../../../../base/common/resources.js';13import { URI } from '../../../../base/common/uri.js';14import './media/breadcrumbscontrol.css';15import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';16import { FileKind, IFileService, IFileStat } from '../../../../platform/files/common/files.js';17import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';18import { WorkbenchDataTree, WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';19import { breadcrumbsPickerBackground, widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js';20import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';21import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from '../../labels.js';22import { BreadcrumbsConfig } from './breadcrumbs.js';23import { OutlineElement2, FileElement } from './breadcrumbsModel.js';24import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from '../../../../base/browser/ui/tree/tree.js';25import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from '../../../../base/browser/ui/list/list.js';26import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js';27import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';28import { localize } from '../../../../nls.js';29import { IOutline, IOutlineComparator } from '../../../services/outline/browser/outline.js';30import { IEditorOptions } from '../../../../platform/editor/common/editor.js';31import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';32import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';3334interface ILayoutInfo {35maxHeight: number;36width: number;37arrowSize: number;38arrowOffset: number;39inputHeight: number;40}4142type Tree<I, E> = WorkbenchDataTree<I, E, FuzzyScore> | WorkbenchAsyncDataTree<I, E, FuzzyScore>;4344export interface SelectEvent {45target: any;46browserEvent: UIEvent;47}4849export abstract class BreadcrumbsPicker {5051protected readonly _disposables = new DisposableStore();52protected readonly _domNode: HTMLDivElement;53protected _arrow!: HTMLDivElement;54protected _treeContainer!: HTMLDivElement;55protected _tree!: Tree<any, any>;56protected _fakeEvent = new UIEvent('fakeEvent');57protected _layoutInfo!: ILayoutInfo;5859protected readonly _onWillPickElement = new Emitter<void>();60readonly onWillPickElement: Event<void> = this._onWillPickElement.event;6162private readonly _previewDispoables = new MutableDisposable();6364constructor(65parent: HTMLElement,66protected resource: URI,67@IInstantiationService protected readonly _instantiationService: IInstantiationService,68@IThemeService protected readonly _themeService: IThemeService,69@IConfigurationService protected readonly _configurationService: IConfigurationService,70) {71this._domNode = document.createElement('div');72this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons';73parent.appendChild(this._domNode);74}7576dispose(): void {77this._disposables.dispose();78this._previewDispoables.dispose();79this._onWillPickElement.dispose();80this._domNode.remove();81setTimeout(() => this._tree.dispose(), 0); // tree cannot be disposed while being opened...82}8384async show(input: FileElement | OutlineElement2, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): Promise<void> {8586const theme = this._themeService.getColorTheme();87const color = theme.getColor(breadcrumbsPickerBackground);8889this._arrow = document.createElement('div');90this._arrow.className = 'arrow';91this._arrow.style.borderColor = `transparent transparent ${color ? color.toString() : ''}`;92this._domNode.appendChild(this._arrow);9394this._treeContainer = document.createElement('div');95this._treeContainer.style.background = color ? color.toString() : '';96this._treeContainer.style.paddingTop = '2px';97this._treeContainer.style.borderRadius = '3px';98this._treeContainer.style.boxShadow = `0 0 8px 2px ${this._themeService.getColorTheme().getColor(widgetShadow)}`;99this._treeContainer.style.border = `1px solid ${this._themeService.getColorTheme().getColor(widgetBorder)}`;100this._domNode.appendChild(this._treeContainer);101102this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 };103this._tree = this._createTree(this._treeContainer, input);104105this._disposables.add(this._tree.onDidOpen(async e => {106const { element, editorOptions, sideBySide } = e;107const didReveal = await this._revealElement(element, { ...editorOptions, preserveFocus: false }, sideBySide);108if (!didReveal) {109return;110}111}));112this._disposables.add(this._tree.onDidChangeFocus(e => {113this._previewDispoables.value = this._previewElement(e.elements[0]);114}));115this._disposables.add(this._tree.onDidChangeContentHeight(() => {116this._layout();117}));118119this._domNode.focus();120try {121await this._setInput(input);122this._layout();123} catch (err) {124onUnexpectedError(err);125}126}127128protected _layout(): void {129130const headerHeight = 2 * this._layoutInfo.arrowSize;131const treeHeight = Math.min(this._layoutInfo.maxHeight - headerHeight, this._tree.contentHeight);132const totalHeight = treeHeight + headerHeight;133134this._domNode.style.height = `${totalHeight}px`;135this._domNode.style.width = `${this._layoutInfo.width}px`;136this._arrow.style.top = `-${2 * this._layoutInfo.arrowSize}px`;137this._arrow.style.borderWidth = `${this._layoutInfo.arrowSize}px`;138this._arrow.style.marginLeft = `${this._layoutInfo.arrowOffset}px`;139this._treeContainer.style.height = `${treeHeight}px`;140this._treeContainer.style.width = `${this._layoutInfo.width}px`;141this._tree.layout(treeHeight, this._layoutInfo.width);142}143144restoreViewState(): void { }145146protected abstract _setInput(element: FileElement | OutlineElement2): Promise<void>;147protected abstract _createTree(container: HTMLElement, input: any): Tree<any, any>;148protected abstract _previewElement(element: any): IDisposable;149protected abstract _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise<boolean>;150151}152153//#region - Files154155class FileVirtualDelegate implements IListVirtualDelegate<IFileStat | IWorkspaceFolder> {156getHeight(_element: IFileStat | IWorkspaceFolder) {157return 22;158}159getTemplateId(_element: IFileStat | IWorkspaceFolder): string {160return 'FileStat';161}162}163164class FileIdentityProvider implements IIdentityProvider<IWorkspace | IWorkspaceFolder | IFileStat | URI> {165getId(element: IWorkspace | IWorkspaceFolder | IFileStat | URI): { toString(): string } {166if (URI.isUri(element)) {167return element.toString();168} else if (isWorkspace(element)) {169return element.id;170} else if (isWorkspaceFolder(element)) {171return element.uri.toString();172} else {173return element.resource.toString();174}175}176}177178179class FileDataSource implements IAsyncDataSource<IWorkspace | URI, IWorkspaceFolder | IFileStat> {180181constructor(182@IFileService private readonly _fileService: IFileService,183) { }184185hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean {186return URI.isUri(element)187|| isWorkspace(element)188|| isWorkspaceFolder(element)189|| element.isDirectory;190}191192async getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> {193if (isWorkspace(element)) {194return element.folders;195}196let uri: URI;197if (isWorkspaceFolder(element)) {198uri = element.uri;199} else if (URI.isUri(element)) {200uri = element;201} else {202uri = element.resource;203}204const stat = await this._fileService.resolve(uri);205return stat.children ?? [];206}207}208209class FileRenderer implements ITreeRenderer<IFileStat | IWorkspaceFolder, FuzzyScore, IResourceLabel> {210211readonly templateId: string = 'FileStat';212213constructor(214private readonly _labels: ResourceLabels,215@IConfigurationService private readonly _configService: IConfigurationService,216) { }217218219renderTemplate(container: HTMLElement): IResourceLabel {220return this._labels.create(container, { supportHighlights: true });221}222223renderElement(node: ITreeNode<IWorkspaceFolder | IFileStat, [number, number, number]>, index: number, templateData: IResourceLabel): void {224const fileDecorations = this._configService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');225const { element } = node;226let resource: URI;227let fileKind: FileKind;228if (isWorkspaceFolder(element)) {229resource = element.uri;230fileKind = FileKind.ROOT_FOLDER;231} else {232resource = element.resource;233fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE;234}235templateData.setFile(resource, {236fileKind,237hidePath: true,238fileDecorations: fileDecorations,239matches: createMatches(node.filterData),240extraClasses: ['picker-item']241});242}243244disposeTemplate(templateData: IResourceLabel): void {245templateData.dispose();246}247}248249class FileNavigationLabelProvider implements IKeyboardNavigationLabelProvider<IWorkspaceFolder | IFileStat> {250251getKeyboardNavigationLabel(element: IWorkspaceFolder | IFileStat): { toString(): string } {252return element.name;253}254}255256class FileAccessibilityProvider implements IListAccessibilityProvider<IWorkspaceFolder | IFileStat> {257258getWidgetAriaLabel(): string {259return localize('breadcrumbs', "Breadcrumbs");260}261262getAriaLabel(element: IWorkspaceFolder | IFileStat): string | null {263return element.name;264}265}266267class FileFilter implements ITreeFilter<IWorkspaceFolder | IFileStat> {268269private readonly _cachedExpressions = new Map<string, glob.ParsedExpression>();270private readonly _disposables = new DisposableStore();271272constructor(273@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,274@IConfigurationService configService: IConfigurationService,275) {276const config = BreadcrumbsConfig.FileExcludes.bindTo(configService);277const update = () => {278_workspaceService.getWorkspace().folders.forEach(folder => {279const excludesConfig = config.getValue({ resource: folder.uri });280if (!excludesConfig) {281return;282}283// adjust patterns to be absolute in case they aren't284// free floating (**/)285const adjustedConfig: glob.IExpression = {};286for (const pattern in excludesConfig) {287if (typeof excludesConfig[pattern] !== 'boolean') {288continue;289}290const patternAbs = pattern.indexOf('**/') !== 0291? posix.join(folder.uri.path, pattern)292: pattern;293294adjustedConfig[patternAbs] = excludesConfig[pattern];295}296this._cachedExpressions.set(folder.uri.toString(), glob.parse(adjustedConfig));297});298};299update();300this._disposables.add(config);301this._disposables.add(config.onDidChange(update));302this._disposables.add(_workspaceService.onDidChangeWorkspaceFolders(update));303}304305dispose(): void {306this._disposables.dispose();307}308309filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean {310if (isWorkspaceFolder(element)) {311// not a file312return true;313}314const folder = this._workspaceService.getWorkspaceFolder(element.resource);315if (!folder || !this._cachedExpressions.has(folder.uri.toString())) {316// no folder or no filer317return true;318}319320const expression = this._cachedExpressions.get(folder.uri.toString())!;321return !expression(relative(folder.uri.path, element.resource.path), basename(element.resource));322}323}324325326export class FileSorter implements ITreeSorter<IFileStat | IWorkspaceFolder> {327compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {328if (isWorkspaceFolder(a) && isWorkspaceFolder(b)) {329return a.index - b.index;330}331if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) {332// same type -> compare on names333return compareFileNames(a.name, b.name);334} else if ((a as IFileStat).isDirectory) {335return -1;336} else {337return 1;338}339}340}341342export class BreadcrumbsFilePicker extends BreadcrumbsPicker {343344constructor(345parent: HTMLElement,346resource: URI,347@IInstantiationService instantiationService: IInstantiationService,348@IThemeService themeService: IThemeService,349@IConfigurationService configService: IConfigurationService,350@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,351@IEditorService private readonly _editorService: IEditorService,352) {353super(parent, resource, instantiationService, themeService, configService);354}355356protected _createTree(container: HTMLElement) {357358// tree icon theme specials359this._treeContainer.classList.add('file-icon-themable-tree');360this._treeContainer.classList.add('show-file-icons');361const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => {362this._treeContainer.classList.toggle('align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons);363this._treeContainer.classList.toggle('hide-arrows', fileIconTheme.hidesExplorerArrows === true);364};365this._disposables.add(this._themeService.onDidFileIconThemeChange(onFileIconThemeChange));366onFileIconThemeChange(this._themeService.getFileIconTheme());367368const labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER /* TODO@Jo visibility propagation */);369this._disposables.add(labels);370371return this._instantiationService.createInstance(372WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>,373'BreadcrumbsFilePicker',374container,375new FileVirtualDelegate(),376[this._instantiationService.createInstance(FileRenderer, labels)],377this._instantiationService.createInstance(FileDataSource),378{379multipleSelectionSupport: false,380sorter: new FileSorter(),381filter: this._instantiationService.createInstance(FileFilter),382identityProvider: new FileIdentityProvider(),383keyboardNavigationLabelProvider: new FileNavigationLabelProvider(),384accessibilityProvider: this._instantiationService.createInstance(FileAccessibilityProvider),385showNotFoundMessage: false,386overrideStyles: {387listBackground: breadcrumbsPickerBackground388},389});390}391392protected async _setInput(element: FileElement | OutlineElement2): Promise<void> {393const { uri, kind } = (element as FileElement);394let input: IWorkspace | URI;395if (kind === FileKind.ROOT_FOLDER) {396input = this._workspaceService.getWorkspace();397} else {398input = dirname(uri);399}400401const tree = this._tree as WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>;402await tree.setInput(input);403let focusElement: IWorkspaceFolder | IFileStat | undefined;404for (const { element } of tree.getNode().children) {405if (isWorkspaceFolder(element) && isEqual(element.uri, uri)) {406focusElement = element;407break;408} else if (isEqual((element as IFileStat).resource, uri)) {409focusElement = element as IFileStat;410break;411}412}413if (focusElement) {414tree.reveal(focusElement, 0.5);415tree.setFocus([focusElement], this._fakeEvent);416}417tree.domFocus();418}419420protected _previewElement(_element: any): IDisposable {421return Disposable.None;422}423424protected async _revealElement(element: IFileStat | IWorkspaceFolder, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {425if (!isWorkspaceFolder(element) && element.isFile) {426this._onWillPickElement.fire();427await this._editorService.openEditor({ resource: element.resource, options }, sideBySide ? SIDE_GROUP : undefined);428return true;429}430return false;431}432}433//#endregion434435//#region - Outline436437class OutlineTreeSorter<E> implements ITreeSorter<E> {438439private _order: 'name' | 'type' | 'position';440441constructor(442private comparator: IOutlineComparator<E>,443uri: URI | undefined,444@ITextResourceConfigurationService configService: ITextResourceConfigurationService,445) {446this._order = configService.getValue(uri, 'breadcrumbs.symbolSortOrder');447}448449compare(a: E, b: E): number {450if (this._order === 'name') {451return this.comparator.compareByName(a, b);452} else if (this._order === 'type') {453return this.comparator.compareByType(a, b);454} else {455return this.comparator.compareByPosition(a, b);456}457}458}459460export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {461462protected _createTree(container: HTMLElement, input: OutlineElement2) {463464const { config } = input.outline;465466return this._instantiationService.createInstance(467WorkbenchDataTree<IOutline<any>, any, FuzzyScore>,468'BreadcrumbsOutlinePicker',469container,470config.delegate,471config.renderers,472config.treeDataSource,473{474...config.options,475sorter: this._instantiationService.createInstance(OutlineTreeSorter, config.comparator, undefined),476collapseByDefault: true,477expandOnlyOnTwistieClick: true,478multipleSelectionSupport: false,479showNotFoundMessage: false480}481);482}483484protected _setInput(input: OutlineElement2): Promise<void> {485486const viewState = input.outline.captureViewState();487this.restoreViewState = () => { viewState.dispose(); };488489const tree = this._tree as WorkbenchDataTree<IOutline<any>, any, FuzzyScore>;490491tree.setInput(input.outline);492if (input.element !== input.outline) {493tree.reveal(input.element, 0.5);494tree.setFocus([input.element], this._fakeEvent);495}496tree.domFocus();497498return Promise.resolve();499}500501protected _previewElement(element: any): IDisposable {502const outline: IOutline<any> = this._tree.getInput();503return outline.preview(element);504}505506protected async _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {507this._onWillPickElement.fire();508const outline: IOutline<any> = this._tree.getInput();509await outline.reveal(element, options, sideBySide, false);510return true;511}512}513514//#endregion515516517