diff --git a/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts b/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts
new file mode 100644
index 0000000000000..f3e08fe86865a
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts
@@ -0,0 +1,97 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { Disposable, DisposableCollection } from '../../common/disposable';
+import { Breadcrumbs } from './breadcrumbs';
+
+/**
+ * This class creates a popup container at the given position
+ * so that contributions can attach their HTML elements
+ * as childs of `BreadcrumbPopupContainer#container`.
+ *
+ * - `dispose()` is called on blur or on hit on escape
+ */
+export class BreadcrumbPopupContainer implements Disposable {
+
+ protected toDispose: DisposableCollection = new DisposableCollection();
+
+ readonly container: HTMLElement;
+ public isOpen: boolean;
+
+ constructor(
+ protected readonly parent: HTMLElement,
+ public readonly breadcrumbId: string,
+ position: { x: number, y: number }
+ ) {
+ this.container = this.createPopupDiv(position);
+ document.addEventListener('keyup', this.escFunction);
+ this.container.focus();
+ this.isOpen = true;
+ }
+
+ protected createPopupDiv(position: { x: number, y: number }): HTMLDivElement {
+ const result = window.document.createElement('div');
+ result.className = Breadcrumbs.Styles.BREADCRUMB_POPUP;
+ result.style.left = `${position.x}px`;
+ result.style.top = `${position.y}px`;
+ result.tabIndex = 0;
+ result.onblur = event => this.onBlur(event, this.breadcrumbId);
+ this.parent.appendChild(result);
+ return result;
+ }
+
+ protected onBlur = (event: FocusEvent, breadcrumbId: string) => {
+ if (event.relatedTarget && event.relatedTarget instanceof HTMLElement) {
+ // event.relatedTarget is the element that has the focus after this popup looses the focus.
+ // If a breadcrumb was clicked the following holds the breadcrumb ID of the clicked breadcrumb.
+ const clickedBreadcrumbId = event.relatedTarget.getAttribute('data-breadcrumb-id');
+ if (clickedBreadcrumbId && clickedBreadcrumbId === breadcrumbId) {
+ // This is a click on the breadcrumb that has openend this popup.
+ // We do not close this popup here but let the click event of the breadcrumb handle this instead
+ // because it needs to know that this popup is open to decide if it just closes this popup or
+ // also open a new popup.
+ return;
+ }
+ if (this.container.contains(event.relatedTarget)) {
+ // A child element gets focus. Set the focus to the container again.
+ // Otherwise the popup would not be closed when elements outside the popup get the focus.
+ // A popup content should not relay on getting a focus.
+ this.container.focus();
+ return;
+ }
+ }
+ this.dispose();
+ }
+
+ protected escFunction = (event: KeyboardEvent) => {
+ if (event.key === 'Escape' || event.key === 'Esc') {
+ this.dispose();
+ }
+ }
+
+ dispose(): void {
+ this.toDispose.dispose();
+ if (this.parent.contains(this.container)) {
+ this.parent.removeChild(this.container);
+ }
+ this.isOpen = false;
+ document.removeEventListener('keyup', this.escFunction);
+ }
+
+ addDisposable(disposable: Disposable | undefined): void {
+ if (disposable) { this.toDispose.push(disposable); }
+ }
+}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx b/packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
new file mode 100644
index 0000000000000..6385e8c8f6244
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as React from 'react';
+import { injectable } from 'inversify';
+import { Breadcrumb } from './breadcrumb';
+import { Breadcrumbs } from './breadcrumbs';
+
+export const BreadcrumbRenderer = Symbol('BreadcrumbRenderer');
+export interface BreadcrumbRenderer {
+ /**
+ * Renders the given breadcrumb. If `onClick` is given, it is called on breadcrumb click.
+ */
+ render(breadcrumb: Breadcrumb, onClick?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode;
+}
+
+@injectable()
+export class DefaultBreadcrumbRenderer implements BreadcrumbRenderer {
+ render(breadcrumb: Breadcrumb, onClick?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode {
+ return
onClick && onClick(breadcrumb, event)}
+ tabIndex={0}
+ data-breadcrumb-id={breadcrumb.id}
+ >
+ {breadcrumb.iconClass && } {breadcrumb.label}
+ ;
+ }
+}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumb.ts b/packages/core/src/browser/breadcrumbs/breadcrumb.ts
new file mode 100644
index 0000000000000..74a01f80614e1
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumb.ts
@@ -0,0 +1,34 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+/** A single breadcrumb in the breadcrumbs bar. */
+export interface Breadcrumb {
+
+ /** An ID of this breadcrumb that should be unique in the breadcrumbs bar. */
+ id: string
+
+ /** The breadcrumb type. Should be the same as the contribution type `BreadcrumbsContribution#type`. */
+ type: symbol
+
+ /** The text that will be rendered as label. */
+ label: string
+
+ /** A longer text that will be used as tooltip text. */
+ longLabel: string
+
+ /** A CSS class for the icon. */
+ iconClass?: string
+}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs-contribution.ts b/packages/core/src/browser/breadcrumbs/breadcrumbs-contribution.ts
new file mode 100644
index 0000000000000..d95abfd8d75f5
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-contribution.ts
@@ -0,0 +1,44 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import URI from '../../common/uri';
+import { Breadcrumb } from './breadcrumb';
+import { Disposable } from '../../common';
+
+export const BreadcrumbsContribution = Symbol('BreadcrumbsContribution');
+export interface BreadcrumbsContribution {
+
+ /**
+ * The breadcrumb type. Breadcrumbs returned by `#computeBreadcrumbs(uri)` should have this as `Breadcrumb#type`.
+ */
+ type: symbol;
+
+ /**
+ * The priority of this breadcrumbs contribution. Contributions with lower priority are rendered first.
+ */
+ priority: number;
+
+ /**
+ * Computes breadcrumbs for a given URI.
+ */
+ computeBreadcrumbs(uri: URI): Promise;
+
+ /**
+ * Attaches the breadcrumb popup content for the given breadcrumb as child to the given parent.
+ * If it returns a Disposable, it is called when the popup closes.
+ */
+ attachPopupContent(breadcrumb: Breadcrumb, parent: HTMLElement): Promise;
+}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx b/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
new file mode 100644
index 0000000000000..9196ee671f31d
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
@@ -0,0 +1,191 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as React from 'react';
+import { injectable, inject, postConstruct } from 'inversify';
+import { ReactRenderer } from '../widgets';
+import { Breadcrumb } from './breadcrumb';
+import { Breadcrumbs } from './breadcrumbs';
+import { BreadcrumbsService } from './breadcrumbs-service';
+import { BreadcrumbRenderer } from './breadcrumb-renderer';
+import PerfectScrollbar from 'perfect-scrollbar';
+import URI from '../../common/uri';
+import { BreadcrumbPopupContainer } from './breadcrumb-popup-container';
+import { DisposableCollection } from '../../common/disposable';
+import { CorePreferences } from '../core-preferences';
+
+export const BreadcrumbsURI = Symbol('BreadcrumbsURI');
+
+@injectable()
+export class BreadcrumbsRenderer extends ReactRenderer {
+
+ @inject(BreadcrumbsService)
+ protected readonly breadcrumbsService: BreadcrumbsService;
+
+ @inject(BreadcrumbRenderer)
+ protected readonly breadcrumbRenderer: BreadcrumbRenderer;
+
+ @inject(CorePreferences)
+ protected readonly corePreferences: CorePreferences;
+
+ private breadcrumbs: Breadcrumb[] = [];
+
+ private popup: BreadcrumbPopupContainer | undefined;
+
+ private scrollbar: PerfectScrollbar | undefined;
+
+ private toDispose: DisposableCollection = new DisposableCollection();
+
+ constructor(
+ @inject(BreadcrumbsURI) readonly uri: URI
+ ) { super(); }
+
+ @postConstruct()
+ init(): void {
+ this.toDispose.push(this.breadcrumbsService.onBreadcrumbsChange(uri => { if (this.uri.toString() === uri.toString()) { this.refresh(); } }));
+ this.toDispose.push(this.corePreferences.onPreferenceChanged(_ => this.refresh()));
+ }
+
+ dispose(): void {
+ super.dispose();
+ this.toDispose.dispose();
+ if (this.popup) { this.popup.dispose(); }
+ if (this.scrollbar) {
+ this.scrollbar.destroy();
+ this.scrollbar = undefined;
+ }
+ }
+
+ async refresh(): Promise {
+ if (this.corePreferences['breadcrumbs.enabled']) {
+ this.breadcrumbs = await this.breadcrumbsService.getBreadcrumbs(this.uri);
+ } else {
+ this.breadcrumbs = [];
+ }
+ this.render();
+
+ if (!this.scrollbar) {
+ if (this.host.firstChild) {
+ this.scrollbar = new PerfectScrollbar(this.host.firstChild as HTMLElement, {
+ handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
+ useBothWheelAxes: true,
+ scrollXMarginOffset: 4,
+ suppressScrollY: true
+ });
+ }
+ } else {
+ this.scrollbar.update();
+ }
+ this.scrollToEnd();
+ }
+
+ private scrollToEnd(): void {
+ if (this.host.firstChild) {
+ const breadcrumbsHtmlElement = (this.host.firstChild as HTMLElement);
+ breadcrumbsHtmlElement.scrollLeft = breadcrumbsHtmlElement.scrollWidth;
+ }
+ }
+
+ protected doRender(): React.ReactNode {
+ return {this.renderBreadcrumbs()}
;
+ }
+
+ protected renderBreadcrumbs(): React.ReactNode {
+ return this.breadcrumbs.map(breadcrumb => this.breadcrumbRenderer.render(breadcrumb, this.togglePopup));
+ }
+
+ protected togglePopup = (breadcrumb: Breadcrumb, event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ let openPopup = true;
+ if (this.popup) {
+ if (this.popup.isOpen) {
+ this.popup.dispose();
+
+ // There is a popup open. If the popup is the popup that belongs to the currently clicked breadcrumb
+ // just close the popup. When another breadcrumb was clicked open the new popup immediately.
+ openPopup = !(this.popup.breadcrumbId === breadcrumb.id);
+ }
+ this.popup = undefined;
+ }
+ if (openPopup) {
+ if (event.nativeEvent.target && event.nativeEvent.target instanceof HTMLElement) {
+ const breadcrumbsHtmlElement = BreadcrumbsRenderer.findParentBreadcrumbsHtmlElement(event.nativeEvent.target as HTMLElement);
+ if (breadcrumbsHtmlElement && breadcrumbsHtmlElement.parentElement && breadcrumbsHtmlElement.parentElement.lastElementChild) {
+ const position: { x: number, y: number } = BreadcrumbsRenderer.determinePopupAnchor(event.nativeEvent) || event.nativeEvent;
+ this.breadcrumbsService.openPopup(breadcrumb, position).then(popup => { this.popup = popup; });
+ }
+ }
+ }
+ }
+}
+
+export namespace BreadcrumbsRenderer {
+
+ /**
+ * Traverse upstream (starting with the HTML element `child`) to find a parent HTML element
+ * that has the CSS class `Breadcrumbs.Styles.BREADCRUMB_ITEM`.
+ */
+ export function findParentItemHtmlElement(child: HTMLElement): HTMLElement | undefined {
+ return findParentHtmlElement(child, Breadcrumbs.Styles.BREADCRUMB_ITEM);
+ }
+
+ /**
+ * Traverse upstream (starting with the HTML element `child`) to find a parent HTML element
+ * that has the CSS class `Breadcrumbs.Styles.BREADCRUMBS`.
+ */
+ export function findParentBreadcrumbsHtmlElement(child: HTMLElement): HTMLElement | undefined {
+ return findParentHtmlElement(child, Breadcrumbs.Styles.BREADCRUMBS);
+ }
+
+ /**
+ * Traverse upstream (starting with the HTML element `child`) to find a parent HTML element
+ * that has the given CSS class.
+ */
+ export function findParentHtmlElement(child: HTMLElement, cssClass: string): HTMLElement | undefined {
+ if (child.classList.contains(cssClass)) {
+ return child;
+ } else {
+ if (child.parentElement !== null) {
+ return findParentHtmlElement(child.parentElement, cssClass);
+ }
+ }
+ }
+
+ /**
+ * Determines the popup anchor for the given mouse event.
+ *
+ * It finds the parent HTML element with CSS class `Breadcrumbs.Styles.BREADCRUMB_ITEM` of event's target element
+ * and return the bottom left corner of this element.
+ */
+ export function determinePopupAnchor(event: MouseEvent): { x: number, y: number } | undefined {
+ if (event.target === null || !(event.target instanceof HTMLElement)) {
+ return undefined;
+ }
+ const itemHtmlElement = findParentItemHtmlElement(event.target);
+ if (itemHtmlElement) {
+ return {
+ x: itemHtmlElement.getBoundingClientRect().left,
+ y: itemHtmlElement.getBoundingClientRect().bottom
+ };
+ }
+ }
+}
+
+export const BreadcrumbsRendererFactory = Symbol('BreadcrumbsRendererFactory');
+export interface BreadcrumbsRendererFactory {
+ (uri: URI): BreadcrumbsRenderer;
+}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts b/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts
new file mode 100644
index 0000000000000..378245fe1ba7f
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts
@@ -0,0 +1,92 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { inject, injectable, named, postConstruct } from 'inversify';
+import { ContributionProvider, Prioritizeable, Emitter, Event } from '../../common';
+import URI from '../../common/uri';
+import { Breadcrumb } from './breadcrumb';
+import { BreadcrumbPopupContainer } from './breadcrumb-popup-container';
+import { BreadcrumbsContribution } from './breadcrumbs-contribution';
+import { Breadcrumbs } from './breadcrumbs';
+
+@injectable()
+export class BreadcrumbsService {
+
+ @inject(ContributionProvider) @named(BreadcrumbsContribution)
+ protected readonly contributions: ContributionProvider;
+
+ protected popupsOverlayContainer: HTMLDivElement;
+
+ protected readonly onBreadcrumbsChangeEmitter = new Emitter();
+
+ @postConstruct()
+ init(): void {
+ this.createOverlayContainer();
+ }
+
+ protected createOverlayContainer(): void {
+ this.popupsOverlayContainer = window.document.createElement('div');
+ this.popupsOverlayContainer.id = Breadcrumbs.Styles.BREADCRUMB_POPUP_OVERLAY_CONTAINER;
+ if (window.document.body) {
+ window.document.body.appendChild(this.popupsOverlayContainer);
+ }
+ }
+
+ /**
+ * Subscribe to this event emitter to be notifed when the breadcrumbs have changed.
+ * The URI is the URI of the editor the breadcrumbs have changed for.
+ */
+ get onBreadcrumbsChange(): Event {
+ return this.onBreadcrumbsChangeEmitter.event;
+ }
+
+ /**
+ * Notifies that the breadcrumbs for the given URI have changed and should be re-rendered.
+ * This fires an `onBreadcrumsChange` event.
+ */
+ breadcrumbsChanges(uri: URI): void {
+ this.onBreadcrumbsChangeEmitter.fire(uri);
+ }
+
+ /**
+ * Returns the breadcrumbs for a given URI, possibly an empty list.
+ */
+ async getBreadcrumbs(uri: URI): Promise {
+ const result: Breadcrumb[] = [];
+ for (const contribution of await this.prioritizedContributions()) {
+ result.push(...await contribution.computeBreadcrumbs(uri));
+ }
+ return result;
+ }
+
+ protected async prioritizedContributions(): Promise {
+ const prioritized = await Prioritizeable.prioritizeAll(
+ this.contributions.getContributions(), contribution => contribution.priority);
+ return prioritized.map(p => p.value).reverse();
+ }
+
+ /**
+ * Opens a popup for the given breadcrumb at the given position.
+ */
+ async openPopup(breadcrumb: Breadcrumb, position: { x: number, y: number }): Promise {
+ const contribution = this.contributions.getContributions().find(c => c.type === breadcrumb.type);
+ if (contribution) {
+ const popup = new BreadcrumbPopupContainer(this.popupsOverlayContainer, breadcrumb.id, position);
+ popup.addDisposable(await contribution.attachPopupContent(breadcrumb, popup.container));
+ return popup;
+ }
+ }
+}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs.ts b/packages/core/src/browser/breadcrumbs/breadcrumbs.ts
new file mode 100644
index 0000000000000..afc1c1e84a0b3
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs.ts
@@ -0,0 +1,25 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+export namespace Breadcrumbs {
+ export namespace Styles {
+ export const BREADCRUMBS = 'theia-breadcrumbs';
+ export const BREADCRUMB_ITEM = 'theia-breadcrumb-item';
+ export const BREADCRUMB_POPUP_OVERLAY_CONTAINER = 'theia-breadcrumbs-popups-overlay';
+ export const BREADCRUMB_POPUP = 'theia-breadcrumbs-popup';
+ export const BREADCRUMB_ITEM_HAS_POPUP = 'theia-breadcrumb-item-haspopup';
+ }
+}
diff --git a/packages/core/src/browser/breadcrumbs/index.ts b/packages/core/src/browser/breadcrumbs/index.ts
new file mode 100644
index 0000000000000..88a71e8df3d44
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/index.ts
@@ -0,0 +1,23 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+export * from './breadcrumb-popup-container';
+export * from './breadcrumb-renderer';
+export * from './breadcrumb';
+export * from './breadcrumbs-contribution';
+export * from './breadcrumbs-renderer';
+export * from './breadcrumbs-service';
+export * from './breadcrumbs';
diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts
index 2721a58d77c9b..804d92e67f1ef 100644
--- a/packages/core/src/browser/core-preferences.ts
+++ b/packages/core/src/browser/core-preferences.ts
@@ -44,6 +44,11 @@ export const corePreferenceSchema: PreferenceSchema = {
default: 50,
minimum: 0,
description: 'Controls the number of recently used commands to keep in history for the command palette. Set to 0 to disable command history.'
+ },
+ 'breadcrumbs.enabled': {
+ 'type': 'boolean',
+ 'default': true,
+ 'description': 'Enable/disable navigation breadcrumbs.'
}
}
};
@@ -52,6 +57,7 @@ export interface CoreConfiguration {
'application.confirmExit': 'never' | 'ifRequired' | 'always';
'workbench.list.openMode': 'singleClick' | 'doubleClick';
'workbench.commandPalette.history': number;
+ 'breadcrumbs.enabled': boolean;
}
export const CorePreferences = Symbol('CorePreferences');
diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts
index 20c91dbc4747d..0deb14419eb56 100644
--- a/packages/core/src/browser/frontend-application-module.ts
+++ b/packages/core/src/browser/frontend-application-module.ts
@@ -83,6 +83,7 @@ import { ProgressStatusBarItem } from './progress-status-bar-item';
import { TabBarDecoratorService, TabBarDecorator } from './shell/tab-bar-decorator';
import { ContextMenuContext } from './menu/context-menu-context';
import { bindResourceProvider, bindMessageService, bindPreferenceService } from './frontend-application-bindings';
+import { BreadcrumbsContribution, BreadcrumbsService } from './breadcrumbs';
export { bindResourceProvider, bindMessageService, bindPreferenceService };
@@ -286,4 +287,7 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
bind(ProgressService).toSelf().inSingletonScope();
bind(ContextMenuContext).toSelf().inSingletonScope();
+
+ bindContributionProvider(bind, BreadcrumbsContribution);
+ bind(BreadcrumbsService).toSelf().inSingletonScope();
});
diff --git a/packages/core/src/browser/index.ts b/packages/core/src/browser/index.ts
index 584bb4cde2511..2b82fcc6deca5 100644
--- a/packages/core/src/browser/index.ts
+++ b/packages/core/src/browser/index.ts
@@ -39,3 +39,4 @@ export * from './navigatable';
export * from './diff-uris';
export * from './core-preferences';
export * from './view-container';
+export * from './breadcrumbs';
diff --git a/packages/core/src/browser/style/breadcrumbs.css b/packages/core/src/browser/style/breadcrumbs.css
new file mode 100644
index 0000000000000..04c13d176e12b
--- /dev/null
+++ b/packages/core/src/browser/style/breadcrumbs.css
@@ -0,0 +1,104 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+.theia-breadcrumbs {
+ position: relative;
+ user-select: none;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ outline-style: none;
+ margin: .5rem;
+ list-style-type: none;
+ overflow: hidden;
+}
+
+.theia-breadcrumbs .ps__thumb-x {
+ /* Same scrollbar height than in tab bar. */
+ height: var(--theia-private-horizontal-tab-scrollbar-height) !important;
+}
+
+.theia-breadcrumbs .theia-breadcrumb-item {
+ display: flex;
+ align-items: center;
+ flex: 0 1 auto;
+ white-space: nowrap;
+ align-self: center;
+ height: 100%;
+ outline: none;
+ padding: .25rem .3rem .25rem .25rem;
+}
+
+.theia-breadcrumbs .theia-breadcrumb-item::before {
+ font-family: FontAwesome;
+ font-size: calc(var(--theia-content-font-size) * 0.8);
+ content: "\F0DA";
+ display: flex;
+ align-items: center;
+ width: .8em;
+ text-align: right;
+}
+
+.theia-breadcrumb-item-haspopup:hover {
+ background: var(--theia-accent-color3);
+ cursor: pointer;
+}
+
+#theia-breadcrumbs-popups-overlay {
+ height: 0px;
+}
+
+.theia-breadcrumbs-popup {
+ position: fixed;
+ width: 300px;
+ max-height: 200px;
+ z-index: 10000;
+ padding: 0px;
+ background: var(--theia-menu-color1);
+ font-size: var(--theia-ui-font-size1);
+ color: var(--theia-ui-font-color1);
+ box-shadow: 0px 1px 6px rgba(0, 0, 0, 0.2);
+ overflow: hidden;
+}
+
+.theia-breadcrumbs-popup:focus {
+ outline-width: 0;
+ outline-style: none;
+}
+
+.theia-breadcrumbs-popup ul {
+ display: flex;
+ flex-direction: column;
+ outline-style: none;
+ list-style-type: none;
+ padding-inline-start: 0px;
+ margin: 0 0 0 4px;
+}
+
+.theia-breadcrumbs-popup ul li {
+ display: flex;
+ align-items: center;
+ flex: 0 1 auto;
+ white-space: nowrap;
+ cursor: pointer;
+ outline: none;
+ padding: .25rem .25rem .25rem .25rem;
+}
+
+.theia-breadcrumbs-popup ul li:hover {
+ background: var(--theia-accent-color3);
+}
diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css
index ce0551792645c..f7e51bf879236 100644
--- a/packages/core/src/browser/style/index.css
+++ b/packages/core/src/browser/style/index.css
@@ -198,3 +198,4 @@ textarea {
@import './widget.css';
@import './quick-title-bar.css';
@import './progress-bar.css';
+@import './breadcrumbs.css';
diff --git a/packages/core/src/browser/widgets/react-renderer.tsx b/packages/core/src/browser/widgets/react-renderer.tsx
index d75f02e0aaa60..1fc7e17425d8e 100644
--- a/packages/core/src/browser/widgets/react-renderer.tsx
+++ b/packages/core/src/browser/widgets/react-renderer.tsx
@@ -17,7 +17,9 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Disposable } from '../../common';
+import { injectable } from 'inversify';
+@injectable()
export class ReactRenderer implements Disposable {
readonly host: HTMLElement;
constructor(
diff --git a/packages/editor/package.json b/packages/editor/package.json
index e422bf2741df2..f4ce0386d595b 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -7,7 +7,8 @@
"@theia/languages": "^0.12.0",
"@theia/variable-resolver": "^0.12.0",
"@types/base64-arraybuffer": "0.1.0",
- "base64-arraybuffer": "^0.1.5"
+ "base64-arraybuffer": "^0.1.5",
+ "perfect-scrollbar": "^1.3.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts
index 418f2ddb663b3..92ec657c2ba38 100644
--- a/packages/editor/src/browser/editor-frontend-module.ts
+++ b/packages/editor/src/browser/editor-frontend-module.ts
@@ -33,6 +33,14 @@ import { NavigationLocationSimilarity } from './navigation/navigation-location-s
import { EditorVariableContribution } from './editor-variable-contribution';
import { SemanticHighlightingService } from './semantic-highlight/semantic-highlighting-service';
import { EditorQuickOpenService } from './editor-quick-open-service';
+import URI from '@theia/core/lib/common/uri';
+import {
+ BreadcrumbsRendererFactory,
+ BreadcrumbsRenderer,
+ BreadcrumbsURI,
+ BreadcrumbRenderer,
+ DefaultBreadcrumbRenderer
+} from '@theia/core/lib/browser/breadcrumbs';
export default new ContainerModule(bind => {
bindEditorPreferences(bind);
@@ -74,4 +82,14 @@ export default new ContainerModule(bind => {
bind(ActiveEditorAccess).toSelf().inSingletonScope();
bind(EditorAccess).to(CurrentEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.CURRENT);
bind(EditorAccess).to(ActiveEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.ACTIVE);
+
+ bind(BreadcrumbsRendererFactory).toFactory(ctx =>
+ (uri: URI) => {
+ const childContainer = ctx.container.createChild();
+ childContainer.bind(BreadcrumbsURI).toConstantValue(uri);
+ childContainer.bind(BreadcrumbsRenderer).toSelf();
+ childContainer.bind(BreadcrumbRenderer).to(DefaultBreadcrumbRenderer).inSingletonScope();
+ return childContainer.get(BreadcrumbsRenderer);
+ }
+ );
});
diff --git a/packages/editor/src/browser/editor-widget-factory.ts b/packages/editor/src/browser/editor-widget-factory.ts
index 4e07734147435..f915d7191be81 100644
--- a/packages/editor/src/browser/editor-widget-factory.ts
+++ b/packages/editor/src/browser/editor-widget-factory.ts
@@ -20,6 +20,7 @@ import { SelectionService } from '@theia/core/lib/common';
import { NavigatableWidgetOptions, WidgetFactory, LabelProvider } from '@theia/core/lib/browser';
import { EditorWidget } from './editor-widget';
import { TextEditorProvider } from './editor';
+import { BreadcrumbsRendererFactory } from '@theia/core/lib/browser/breadcrumbs';
@injectable()
export class EditorWidgetFactory implements WidgetFactory {
@@ -37,6 +38,9 @@ export class EditorWidgetFactory implements WidgetFactory {
@inject(SelectionService)
protected readonly selectionService: SelectionService;
+ @inject(BreadcrumbsRendererFactory)
+ protected readonly breadcrumbsRendererFactory: BreadcrumbsRendererFactory;
+
createWidget(options: NavigatableWidgetOptions): Promise {
const uri = new URI(options.uri);
return this.createEditor(uri);
@@ -45,7 +49,8 @@ export class EditorWidgetFactory implements WidgetFactory {
protected async createEditor(uri: URI): Promise {
const icon = await this.labelProvider.getIcon(uri);
return this.editorProvider(uri).then(textEditor => {
- const newEditor = new EditorWidget(textEditor, this.selectionService);
+ const breadcrumbsRenderer = this.breadcrumbsRendererFactory(uri);
+ const newEditor = new EditorWidget(textEditor, breadcrumbsRenderer, this.selectionService);
newEditor.id = this.id + ':' + uri.toString();
newEditor.title.closable = true;
newEditor.title.label = this.labelProvider.getName(uri);
diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts
index a126339bfda9f..9629f8fa2a78f 100644
--- a/packages/editor/src/browser/editor-widget.ts
+++ b/packages/editor/src/browser/editor-widget.ts
@@ -18,14 +18,19 @@ import { Disposable, SelectionService } from '@theia/core/lib/common';
import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { TextEditor } from './editor';
+import { BreadcrumbsRenderer } from '@theia/core/lib/browser/breadcrumbs';
export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget {
constructor(
readonly editor: TextEditor,
+ readonly breadcrumbsRenderer: BreadcrumbsRenderer,
protected readonly selectionService: SelectionService
) {
- super(editor);
+ super(EditorWidget.createParentNode(editor, breadcrumbsRenderer));
+
+ this.toDispose.push(this.breadcrumbsRenderer);
+
this.toDispose.push(this.editor);
this.toDispose.push(this.editor.onSelectionChanged(() => {
if (this.editor.isFocused()) {
@@ -39,6 +44,13 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
}));
}
+ static createParentNode(editor: TextEditor, breadcrumbsWidget: BreadcrumbsRenderer): Widget.IOptions {
+ const div = document.createElement('div');
+ div.appendChild(breadcrumbsWidget.host);
+ div.appendChild(editor.node);
+ return { node: div };
+ }
+
get saveable(): Saveable {
return this.editor.document;
}
@@ -60,12 +72,14 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
super.onAfterAttach(msg);
if (this.isVisible) {
this.editor.refresh();
+ this.breadcrumbsRenderer.refresh();
}
}
protected onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.editor.refresh();
+ this.breadcrumbsRenderer.refresh();
}
protected onResize(msg: Widget.ResizeMessage): void {
diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json
index 05f530632abb7..4ce57f0b911bd 100644
--- a/packages/filesystem/package.json
+++ b/packages/filesystem/package.json
@@ -17,6 +17,7 @@
"jschardet": "1.6.0",
"minimatch": "^3.0.4",
"mv": "^2.1.1",
+ "perfect-scrollbar": "^1.3.0",
"rimraf": "^2.6.2",
"tar-fs": "^1.16.2",
"touch": "^3.1.0",
diff --git a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumb.ts b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumb.ts
new file mode 100644
index 0000000000000..ab76042f7fdcc
--- /dev/null
+++ b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumb.ts
@@ -0,0 +1,42 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { Breadcrumb } from '@theia/core/lib/browser/breadcrumbs/breadcrumb';
+import { FilepathBreadcrumbType } from './filepath-breadcrumbs-contribution';
+import URI from '@theia/core/lib/common/uri';
+
+export class FilepathBreadcrumb implements Breadcrumb {
+ constructor(
+ readonly uri: URI,
+ readonly label: string,
+ readonly longLabel: string,
+ readonly iconClass: string
+ ) { }
+
+ get id(): string {
+ return this.type.toString() + '_' + this.uri.toString();
+ }
+
+ get type(): symbol {
+ return FilepathBreadcrumbType;
+ }
+}
+
+export namespace FilepathBreadcrumb {
+ export function is(breadcrumb: Breadcrumb): breadcrumb is FilepathBreadcrumb {
+ return 'uri' in breadcrumb;
+ }
+}
diff --git a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts
new file mode 100644
index 0000000000000..843caed6cadd1
--- /dev/null
+++ b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts
@@ -0,0 +1,66 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { Container, interfaces, injectable, inject } from 'inversify';
+import { TreeProps, ContextMenuRenderer, TreeNode, OpenerService, NodeProps } from '@theia/core/lib/browser';
+import { createFileTreeContainer, FileTreeWidget } from '../';
+import { FileTreeModel, FileStatNode } from '../file-tree';
+
+const BREADCRUMBS_FILETREE_CLASS = 'theia-FilepathBreadcrumbFileTree';
+
+export function createFileTreeBreadcrumbsContainer(parent: interfaces.Container): Container {
+ const child = createFileTreeContainer(parent);
+ child.unbind(FileTreeWidget);
+ child.bind(BreadcrumbsFileTreeWidget).toSelf();
+ return child;
+}
+
+export function createFileTreeBreadcrumbsWidget(parent: interfaces.Container): BreadcrumbsFileTreeWidget {
+ return createFileTreeBreadcrumbsContainer(parent).get(BreadcrumbsFileTreeWidget);
+}
+
+@injectable()
+export class BreadcrumbsFileTreeWidget extends FileTreeWidget {
+
+ @inject(OpenerService)
+ protected readonly openerService: OpenerService;
+
+ constructor(
+ @inject(TreeProps) readonly props: TreeProps,
+ @inject(FileTreeModel) readonly model: FileTreeModel,
+ @inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer
+ ) {
+ super(props, model, contextMenuRenderer);
+ this.addClass(BREADCRUMBS_FILETREE_CLASS);
+ }
+
+ protected createNodeAttributes(node: TreeNode, props: NodeProps): React.Attributes & React.HTMLAttributes {
+ const elementAttrs = super.createNodeAttributes(node, props);
+ return {
+ ...elementAttrs,
+ draggable: false
+ };
+ }
+
+ protected handleClickEvent(node: TreeNode | undefined, event: React.MouseEvent): void {
+ if (FileStatNode.is(node) && !node.fileStat.isDirectory) {
+ this.openerService.getOpener(node.uri)
+ .then(opener => opener.open(node.uri));
+ } else {
+ super.handleClickEvent(node, event);
+ }
+ }
+}
diff --git a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.ts b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.ts
new file mode 100644
index 0000000000000..aab9dbe35ff0d
--- /dev/null
+++ b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.ts
@@ -0,0 +1,92 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { BreadcrumbsContribution } from '@theia/core/lib/browser/breadcrumbs/breadcrumbs-contribution';
+import { Breadcrumb } from '@theia/core/lib/browser/breadcrumbs/breadcrumb';
+import { FilepathBreadcrumb } from './filepath-breadcrumb';
+import { injectable, inject } from 'inversify';
+import { LabelProvider, Widget } from '@theia/core/lib/browser';
+import { FileSystem, FileStat } from '../../common';
+import URI from '@theia/core/lib/common/uri';
+import { BreadcrumbsFileTreeWidget } from './filepath-breadcrumbs-container';
+import { DirNode } from '../file-tree';
+import { Disposable } from '@theia/core';
+
+export const FilepathBreadcrumbType = Symbol('FilepathBreadcrumb');
+
+@injectable()
+export class FilepathBreadcrumbsContribution implements BreadcrumbsContribution {
+
+ @inject(LabelProvider)
+ protected readonly labelProvider: LabelProvider;
+
+ @inject(FileSystem)
+ protected readonly fileSystem: FileSystem;
+
+ @inject(BreadcrumbsFileTreeWidget)
+ protected readonly breadcrumbsFileTreeWidget: BreadcrumbsFileTreeWidget;
+
+ readonly type = FilepathBreadcrumbType;
+ readonly priority: number = 100;
+
+ async computeBreadcrumbs(uri: URI): Promise {
+ if (uri.scheme !== 'file') {
+ return [];
+ }
+ return (await Promise.all(uri.allLocations.reverse()
+ .map(async u => new FilepathBreadcrumb(
+ u,
+ this.labelProvider.getName(u),
+ this.labelProvider.getLongName(u),
+ await this.labelProvider.getIcon(u) + ' file-icon'
+ )))).filter(b => this.filterBreadcrumbs(uri, b));
+ }
+
+ protected filterBreadcrumbs(_: URI, breadcrumb: FilepathBreadcrumb): boolean {
+ return !breadcrumb.uri.path.isRoot;
+ }
+
+ async attachPopupContent(breadcrumb: Breadcrumb, parent: HTMLElement): Promise {
+ if (!FilepathBreadcrumb.is(breadcrumb)) {
+ return undefined;
+ }
+ const folderFileStat = await this.fileSystem.getFileStat(breadcrumb.uri.parent.toString());
+ if (folderFileStat) {
+ const rootNode = await this.createRootNode(folderFileStat);
+ await this.breadcrumbsFileTreeWidget.model.navigateTo(rootNode);
+ Widget.attach(this.breadcrumbsFileTreeWidget, parent);
+ return {
+ dispose: () => {
+ // Clear model otherwise the next time a popup is opened the old model is rendered first
+ // and is shown for a short time period.
+ this.breadcrumbsFileTreeWidget.model.root = undefined;
+ Widget.detach(this.breadcrumbsFileTreeWidget);
+ }
+ };
+ }
+ }
+
+ protected async createRootNode(folderToOpen: FileStat): Promise {
+ const folderUri = new URI(folderToOpen.uri);
+ const rootUri = folderToOpen.isDirectory ? folderUri : folderUri.parent;
+ const name = this.labelProvider.getName(rootUri);
+ const rootStat = await this.fileSystem.getFileStat(rootUri.toString());
+ if (rootStat) {
+ const label = await this.labelProvider.getIcon(rootStat);
+ return DirNode.createRoot(rootStat, name, label);
+ }
+ }
+}
diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts
index 618fefa59c3ef..a9513635f87e3 100644
--- a/packages/filesystem/src/browser/filesystem-frontend-module.ts
+++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts
@@ -30,6 +30,9 @@ import { FileSystemWatcher } from './filesystem-watcher';
import { FileSystemFrontendContribution } from './filesystem-frontend-contribution';
import { FileSystemProxyFactory } from './filesystem-proxy-factory';
import { FileUploadService } from './file-upload-service';
+import { FilepathBreadcrumbsContribution } from './breadcrumbs/filepath-breadcrumbs-contribution';
+import { BreadcrumbsContribution } from '@theia/core/lib/browser/breadcrumbs/breadcrumbs-contribution';
+import { BreadcrumbsFileTreeWidget, createFileTreeBreadcrumbsWidget } from './breadcrumbs/filepath-breadcrumbs-container';
export default new ContainerModule(bind => {
bindFileSystemPreferences(bind);
@@ -62,6 +65,12 @@ export default new ContainerModule(bind => {
bind(FileSystemFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(FileSystemFrontendContribution);
bind(FrontendApplicationContribution).toService(FileSystemFrontendContribution);
+
+ bind(BreadcrumbsFileTreeWidget).toDynamicValue(ctx =>
+ createFileTreeBreadcrumbsWidget(ctx.container)
+ );
+ bind(FilepathBreadcrumbsContribution).toSelf().inSingletonScope();
+ bind(BreadcrumbsContribution).toService(FilepathBreadcrumbsContribution);
});
export function bindFileResource(bind: interfaces.Bind): void {
diff --git a/packages/filesystem/src/browser/style/filepath-breadcrumbs.css b/packages/filesystem/src/browser/style/filepath-breadcrumbs.css
new file mode 100644
index 0000000000000..bfa2f6c8052d4
--- /dev/null
+++ b/packages/filesystem/src/browser/style/filepath-breadcrumbs.css
@@ -0,0 +1,19 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+.theia-FilepathBreadcrumbFileTree {
+ height: 200px;
+}
diff --git a/packages/filesystem/src/browser/style/index.css b/packages/filesystem/src/browser/style/index.css
index f56e53a75ca14..59f94e5c37fd1 100644
--- a/packages/filesystem/src/browser/style/index.css
+++ b/packages/filesystem/src/browser/style/index.css
@@ -16,6 +16,7 @@
@import './file-dialog.css';
@import './file-icons.css';
+@import './filepath-breadcrumbs.css';
.theia-file-tree-drag-image {
position: absolute;
diff --git a/packages/monaco/src/browser/monaco-outline-contribution.ts b/packages/monaco/src/browser/monaco-outline-contribution.ts
index 14fec858971c8..9586672d9ca71 100644
--- a/packages/monaco/src/browser/monaco-outline-contribution.ts
+++ b/packages/monaco/src/browser/monaco-outline-contribution.ts
@@ -33,7 +33,6 @@ import debounce = require('lodash.debounce');
@injectable()
export class MonacoOutlineContribution implements FrontendApplicationContribution {
- protected readonly toDisposeOnClose = new DisposableCollection();
protected readonly toDisposeOnEditor = new DisposableCollection();
protected roots: MonacoOutlineSymbolInformationNode[] | undefined;
protected canUpdateOutline: boolean = true;
@@ -42,20 +41,17 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio
@inject(EditorManager) protected readonly editorManager: EditorManager;
onStart(app: FrontendApplication): void {
- this.outlineViewService.onDidChangeOpenState(async open => {
- if (open) {
- this.toDisposeOnClose.push(this.toDisposeOnEditor);
- this.toDisposeOnClose.push(DocumentSymbolProviderRegistry.onDidChange(
- debounce(() => this.updateOutline())
- ));
- this.toDisposeOnClose.push(this.editorManager.onCurrentEditorChanged(
- debounce(() => this.handleCurrentEditorChanged(), 50)
- ));
- this.handleCurrentEditorChanged();
- } else {
- this.toDisposeOnClose.dispose();
- }
- });
+
+ // updateOutline and handleCurrentEditorChanged need to be called even when the outline view widget is closed
+ // in order to udpate breadcrumbs.
+ DocumentSymbolProviderRegistry.onDidChange(
+ debounce(() => this.updateOutline())
+ );
+ this.editorManager.onCurrentEditorChanged(
+ debounce(() => this.handleCurrentEditorChanged(), 50)
+ );
+ this.handleCurrentEditorChanged();
+
this.outlineViewService.onDidSelect(async node => {
if (MonacoOutlineSymbolInformationNode.is(node) && node.parent) {
const options: EditorOpenerOptions = {
@@ -89,10 +85,6 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio
protected handleCurrentEditorChanged(): void {
this.toDisposeOnEditor.dispose();
- if (this.toDisposeOnClose.disposed) {
- return;
- }
- this.toDisposeOnClose.push(this.toDisposeOnEditor);
this.toDisposeOnEditor.push(Disposable.create(() => this.roots = undefined));
const editor = this.editorManager.currentEditor;
if (editor) {
diff --git a/packages/outline-view/package.json b/packages/outline-view/package.json
index a3413debf2cfa..cb616b52ccc22 100644
--- a/packages/outline-view/package.json
+++ b/packages/outline-view/package.json
@@ -3,7 +3,9 @@
"version": "0.12.0",
"description": "Theia - Outline View Extension",
"dependencies": {
- "@theia/core": "^0.12.0"
+ "@theia/core": "^0.12.0",
+ "@theia/editor": "^0.12.0",
+ "perfect-scrollbar": "^1.3.0"
},
"publishConfig": {
"access": "public"
diff --git a/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx
new file mode 100644
index 0000000000000..219d91776a463
--- /dev/null
+++ b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx
@@ -0,0 +1,210 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { BreadcrumbsContribution } from '@theia/core/lib/browser/breadcrumbs/breadcrumbs-contribution';
+import { Breadcrumb } from '@theia/core/lib/browser/breadcrumbs/breadcrumb';
+import { injectable, inject, postConstruct } from 'inversify';
+import { LabelProvider, BreadcrumbsService } from '@theia/core/lib/browser';
+import URI from '@theia/core/lib/common/uri';
+import { OutlineViewService } from './outline-view-service';
+import { OutlineSymbolInformationNode } from './outline-view-widget';
+import { EditorManager } from '@theia/editor/lib/browser';
+import { Disposable } from '@theia/core/lib/common';
+import PerfectScrollbar from 'perfect-scrollbar';
+
+export const OutlineBreadcrumbType = Symbol('OutlineBreadcrumb');
+
+@injectable()
+export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
+
+ @inject(LabelProvider)
+ protected readonly labelProvider: LabelProvider;
+
+ @inject(OutlineViewService)
+ protected readonly outlineViewService: OutlineViewService;
+
+ @inject(BreadcrumbsService)
+ protected readonly breadcrumbsService: BreadcrumbsService;
+
+ @inject(EditorManager)
+ protected readonly editorManager: EditorManager;
+
+ readonly type = OutlineBreadcrumbType;
+ readonly priority: number = 200;
+
+ private currentUri: URI | undefined = undefined;
+ private currentBreadcrumbs: OutlineBreadcrumb[] = [];
+ private roots: OutlineSymbolInformationNode[] = [];
+
+ @postConstruct()
+ init(): void {
+ this.outlineViewService.onDidChangeOutline(roots => {
+ if (roots.length > 0) {
+ this.roots = roots;
+ const first = roots[0];
+ if ('uri' in first) {
+ this.updateOutlineItems(first['uri'] as URI, this.findSelectedNode(roots));
+ }
+ } else {
+ this.currentBreadcrumbs = [];
+ this.roots = [];
+ }
+ });
+ this.outlineViewService.onDidSelect(node => {
+ if ('uri' in node) {
+ this.updateOutlineItems(node['uri'] as URI, node);
+ }
+ });
+ }
+
+ protected async updateOutlineItems(uri: URI, selectedNode: OutlineSymbolInformationNode | undefined): Promise {
+ this.currentUri = uri;
+ const outlinePath = this.toOutlinePath(selectedNode);
+ if (outlinePath && selectedNode) {
+ this.currentBreadcrumbs = outlinePath.map((node, index) =>
+ new OutlineBreadcrumb(node, uri, index.toString(), node.name, 'symbol-icon symbol-icon-center ' + node.iconClass)
+ );
+ if (selectedNode.children && selectedNode.children.length > 0) {
+ this.currentBreadcrumbs.push(new OutlineBreadcrumb(selectedNode.children as OutlineSymbolInformationNode[],
+ uri, this.currentBreadcrumbs.length.toString(), '…', ''));
+ }
+ } else {
+ this.currentBreadcrumbs = [];
+ if (this.roots) {
+ this.currentBreadcrumbs.push(new OutlineBreadcrumb(this.roots, uri, this.currentBreadcrumbs.length.toString(), '…', ''));
+ }
+ }
+ this.breadcrumbsService.breadcrumbsChanges(uri);
+ }
+
+ async computeBreadcrumbs(uri: URI): Promise {
+ if (this.currentUri && uri.toString() === this.currentUri.toString()) {
+ return this.currentBreadcrumbs;
+ }
+ return [];
+ }
+
+ async attachPopupContent(breadcrumb: Breadcrumb, parent: HTMLElement): Promise {
+ if (!OutlineBreadcrumb.is(breadcrumb)) {
+ return undefined;
+ }
+ const nodes = Array.isArray(breadcrumb.node) ? breadcrumb.node : this.siblings(breadcrumb.node);
+ const items = nodes.map(node => ({
+ label: node.name,
+ title: node.name,
+ iconClass: 'symbol-icon symbol-icon-center ' + node.iconClass,
+ action: () => this.revealInEditor(node)
+ }));
+ if (items.length > 0) {
+ ReactDOM.render({this.renderItems(items)}, parent);
+ const scrollbar = new PerfectScrollbar(parent, {
+ handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
+ useBothWheelAxes: true,
+ scrollYMarginOffset: 8,
+ suppressScrollX: true
+ });
+ return {
+ dispose: () => {
+ scrollbar.destroy();
+ ReactDOM.unmountComponentAtNode(parent);
+
+ }
+ };
+ }
+ const noContent = document.createElement('div');
+ noContent.style.margin = '.5rem';
+ noContent.style.fontStyle = 'italic';
+ noContent.innerText = '(no content)';
+ parent.appendChild(noContent);
+ }
+
+ private revealInEditor(node: OutlineSymbolInformationNode): void {
+ if ('range' in node && this.currentUri) {
+ this.editorManager.open(this.currentUri, { selection: node['range'] });
+ }
+ }
+
+ protected renderItems(items: { label: string, title: string, iconClass: string, action: () => void }[]): React.ReactNode {
+ return
+ {items.map((item, index) => - item.action()}>
+ {item.label}
+
)}
+
;
+ }
+
+ private siblings(node: OutlineSymbolInformationNode): OutlineSymbolInformationNode[] {
+ if (!node.parent) { return []; }
+ return node.parent.children.filter(n => n !== node).map(n => n as OutlineSymbolInformationNode);
+ }
+
+ /**
+ * Returns the path of the given outline node.
+ */
+ private toOutlinePath(node: OutlineSymbolInformationNode | undefined, path: OutlineSymbolInformationNode[] = []): OutlineSymbolInformationNode[] | undefined {
+ if (!node) { return undefined; }
+ if (node.id === 'outline-view-root') { return path; }
+ if (node.parent) {
+ return this.toOutlinePath(node.parent as OutlineSymbolInformationNode, [node, ...path]);
+ } else {
+ return [node, ...path];
+ }
+ }
+
+ /**
+ * Find the node that is selected. Returns after the first match.
+ */
+ private findSelectedNode(roots: OutlineSymbolInformationNode[]): OutlineSymbolInformationNode | undefined {
+ const result = roots.find(node => node.selected);
+ if (result) {
+ return result;
+ }
+ for (const node of roots) {
+ const result2 = this.findSelectedNode(node.children.map(child => child as OutlineSymbolInformationNode));
+ if (result2) {
+ return result2;
+ }
+ }
+ }
+}
+
+export class OutlineBreadcrumb implements Breadcrumb {
+ constructor(
+ readonly node: OutlineSymbolInformationNode | OutlineSymbolInformationNode[],
+ readonly uri: URI,
+ readonly index: string,
+ readonly label: string,
+ readonly iconClass: string
+ ) { }
+
+ get id(): string {
+ return this.type.toString() + '_' + this.uri.toString() + '_' + this.index;
+ }
+
+ get type(): symbol {
+ return OutlineBreadcrumbType;
+ }
+
+ get longLabel(): string {
+ return this.label;
+ }
+}
+export namespace OutlineBreadcrumb {
+ export function is(breadcrumb: Breadcrumb): breadcrumb is OutlineBreadcrumb {
+ return 'node' in breadcrumb && 'uri' in breadcrumb;
+ }
+}
diff --git a/packages/outline-view/src/browser/outline-view-frontend-module.ts b/packages/outline-view/src/browser/outline-view-frontend-module.ts
index 8c54193dd75c1..6255a5e87f2dc 100644
--- a/packages/outline-view/src/browser/outline-view-frontend-module.ts
+++ b/packages/outline-view/src/browser/outline-view-frontend-module.ts
@@ -27,7 +27,8 @@ import {
defaultTreeProps,
TreeDecoratorService,
TreeModel,
- TreeModelImpl
+ TreeModelImpl,
+ BreadcrumbsContribution
} from '@theia/core/lib/browser';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { OutlineViewWidgetFactory, OutlineViewWidget } from './outline-view-widget';
@@ -35,6 +36,7 @@ import '../../src/browser/styles/index.css';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { OutlineDecoratorService, OutlineTreeDecorator } from './outline-decorator-service';
import { OutlineViewTreeModel } from './outline-view-tree';
+import { OutlineBreadcrumbsContribution } from './outline-breadcrumbs-contribution';
export default new ContainerModule(bind => {
bind(OutlineViewWidgetFactory).toFactory(ctx =>
@@ -47,6 +49,9 @@ export default new ContainerModule(bind => {
bindViewContribution(bind, OutlineViewContribution);
bind(FrontendApplicationContribution).toService(OutlineViewContribution);
bind(TabBarToolbarContribution).toService(OutlineViewContribution);
+
+ bind(OutlineBreadcrumbsContribution).toSelf().inSingletonScope();
+ bind(BreadcrumbsContribution).toService(OutlineBreadcrumbsContribution);
});
/**
diff --git a/packages/outline-view/src/browser/outline-view-service.ts b/packages/outline-view/src/browser/outline-view-service.ts
index c2c21999a2ea2..5b85354ab3093 100644
--- a/packages/outline-view/src/browser/outline-view-service.ts
+++ b/packages/outline-view/src/browser/outline-view-service.ts
@@ -61,8 +61,10 @@ export class OutlineViewService implements WidgetFactory {
publish(roots: OutlineSymbolInformationNode[]): void {
if (this.widget) {
this.widget.setOutlineTree(roots);
- this.onDidChangeOutlineEmitter.fire(roots);
}
+ // onDidChangeOutline needs to be fired even when the outline view widget is closed
+ // in order to udpate breadcrumbs.
+ this.onDidChangeOutlineEmitter.fire(roots);
}
createWidget(): Promise {
diff --git a/packages/workspace/src/browser/workspace-breadcrumbs-contribution.ts b/packages/workspace/src/browser/workspace-breadcrumbs-contribution.ts
new file mode 100644
index 0000000000000..ef9e7acd688db
--- /dev/null
+++ b/packages/workspace/src/browser/workspace-breadcrumbs-contribution.ts
@@ -0,0 +1,33 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { FilepathBreadcrumb } from '@theia/filesystem/lib/browser/breadcrumbs/filepath-breadcrumb';
+import { FilepathBreadcrumbsContribution } from '@theia/filesystem/lib/browser/breadcrumbs/filepath-breadcrumbs-contribution';
+import { inject, injectable } from 'inversify';
+import { WorkspaceService } from './workspace-service';
+import URI from '@theia/core/lib/common/uri';
+
+@injectable()
+export class WorkspaceBreadcrumbsContribution extends FilepathBreadcrumbsContribution {
+
+ @inject(WorkspaceService)
+ protected readonly workspaceService: WorkspaceService;
+
+ protected filterBreadcrumbs(uri: URI, breadcrumb: FilepathBreadcrumb): boolean {
+ const workspaceRootUri = this.workspaceService.getWorkspaceRootUri(uri);
+ return super.filterBreadcrumbs(uri, breadcrumb) && (!workspaceRootUri || !breadcrumb.uri.isEqualOrParent(workspaceRootUri));
+ }
+}
diff --git a/packages/workspace/src/browser/workspace-frontend-module.ts b/packages/workspace/src/browser/workspace-frontend-module.ts
index 2300d21aa2ef0..bbc388f8fd8a6 100644
--- a/packages/workspace/src/browser/workspace-frontend-module.ts
+++ b/packages/workspace/src/browser/workspace-frontend-module.ts
@@ -44,6 +44,8 @@ import { WorkspaceDuplicateHandler } from './workspace-duplicate-handler';
import { WorkspaceUtils } from './workspace-utils';
import { WorkspaceCompareHandler } from './workspace-compare-handler';
import { DiffService } from './diff-service';
+import { WorkspaceBreadcrumbsContribution } from './workspace-breadcrumbs-contribution';
+import { FilepathBreadcrumbsContribution } from '@theia/filesystem/lib/browser/breadcrumbs/filepath-breadcrumbs-contribution';
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
bindWorkspacePreferences(bind);
@@ -89,4 +91,6 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
bind(QuickOpenWorkspace).toSelf().inSingletonScope();
bind(WorkspaceUtils).toSelf().inSingletonScope();
+
+ rebind(FilepathBreadcrumbsContribution).to(WorkspaceBreadcrumbsContribution).inSingletonScope();
});