diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs-popups.tsx b/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container-renderer.tsx
similarity index 51%
rename from packages/editor/src/browser/breadcrumbs/breadcrumbs-popups.tsx
rename to packages/core/src/browser/breadcrumbs/breadcrumb-popup-container-renderer.tsx
index d5d9a1839db90..279139e77ef68 100644
--- a/packages/editor/src/browser/breadcrumbs/breadcrumbs-popups.tsx
+++ b/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container-renderer.tsx
@@ -15,36 +15,68 @@
********************************************************************************/
import * as React from 'react';
-import { ReactRenderer } from '@theia/core/lib/browser';
+import { ReactRenderer } from '../widgets';
+import { injectable } from 'inversify';
import { Breadcrumbs } from './breadcrumbs';
import PerfectScrollbar from 'perfect-scrollbar';
+import { BreadcrumbPopup } from './breadcrumb-popup';
-export class BreadcrumbsListPopup extends ReactRenderer {
+export const BreadcrumbPopupContainerRenderer = Symbol('BreadcrumbPopupContainerRenderer');
+export interface BreadcrumbPopupContainerRenderer {
+ /**
+ * Renders the given breadcrumb and attaches it as child to the given parent element at the given anchor position.
+ */
+ render(breadcrumbId: string, anchor: { x: number, y: number }, content: React.ReactNode, parentElement: HTMLElement): BreadcrumbPopup;
+}
+
+@injectable()
+export class DefaultBreadcrumbPopupContainerRenderer implements BreadcrumbPopupContainerRenderer {
+ render(breadcrumbId: string, anchor: { x: number, y: number }, content: React.ReactNode, parentElement: HTMLElement): BreadcrumbPopup {
+ const renderer = new ReactBreadcrumbPopupContainerRenderer(breadcrumbId, anchor, content, parentElement);
+ renderer.render();
+ return renderer;
+ }
+}
+
+class ReactBreadcrumbPopupContainerRenderer extends ReactRenderer implements BreadcrumbPopup {
+
+ isOpen: boolean = false;
protected scrollbar: PerfectScrollbar | undefined;
constructor(
- protected readonly items: { label: string, title: string, iconClass: string, action: () => void }[],
- protected readonly anchor: { x: number, y: number },
- host: HTMLElement
- ) {
- super(host);
- }
+ readonly breadcrumbId: string,
+ readonly anchor: { x: number, y: number },
+ readonly content: React.ReactNode,
+ host: HTMLElement,
+ ) { super(host); }
protected doRender(): React.ReactNode {
return
this.dispose()}
+ onBlur={this.onBlur}
tabIndex={0}
>
-
- {this.items.map((item, index) => - item.action()}>
- {item.label}
-
)}
-
+ {this.content}
;
}
+ protected onBlur = (event: React.FocusEvent) => {
+ 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 breadcrumbId = event.relatedTarget.getAttribute('data-breadcrumb-id');
+ if (breadcrumbId && breadcrumbId === this.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;
+ }
+ }
+ this.dispose();
+ }
+
render(): void {
super.render();
if (!this.scrollbar) {
@@ -61,6 +93,7 @@ export class BreadcrumbsListPopup extends ReactRenderer {
}
this.focus();
document.addEventListener('keyup', this.escFunction);
+ this.isOpen = true;
}
focus(): boolean {
@@ -78,6 +111,7 @@ export class BreadcrumbsListPopup extends ReactRenderer {
this.scrollbar = undefined;
}
document.removeEventListener('keyup', this.escFunction);
+ this.isOpen = false;
}
protected escFunction = (event: KeyboardEvent) => {
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumb-popup.ts b/packages/core/src/browser/breadcrumbs/breadcrumb-popup.ts
new file mode 100644
index 0000000000000..1f0a3c5e14884
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumb-popup.ts
@@ -0,0 +1,24 @@
+/********************************************************************************
+ * 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 } from '../../common/disposable';
+
+export interface BreadcrumbPopup extends Disposable {
+
+ breadcrumbId: string
+
+ isOpen: boolean
+}
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..e12b5f5a03d50
--- /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 { BreadcrumbPopup } from './breadcrumb-popup';
+
+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;
+
+ /**
+ * Opens the breadcrumb popup for the given breadcrumb at the given position.
+ * Parent is used as host element.
+ */
+ openPopup(breadcrumb: Breadcrumb, position: { x: number, y: number }, 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..67a096d6e99f2
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
@@ -0,0 +1,193 @@
+/********************************************************************************
+ * 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 '../../../src/browser/breadcrumbs/breadcrumbs.css';
+
+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 { BreadcrumbPopup } from './breadcrumb-popup';
+import { DisposableCollection } from '../../common/disposable';
+
+export const BreadcrumbsURI = Symbol('BreadcrumbsURI');
+
+@injectable()
+export class BreadcrumbsRenderer extends ReactRenderer {
+
+ @inject(BreadcrumbsService)
+ protected readonly breadcrumbsService: BreadcrumbsService;
+
+ @inject(BreadcrumbRenderer)
+ protected readonly breadcrumbRenderer: BreadcrumbRenderer;
+
+ private breadcrumbs: Breadcrumb[] = [];
+
+ private popup: BreadcrumbPopup | 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(); } }));
+ }
+
+ 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 {
+ this.breadcrumbs = await this.breadcrumbsService.getBreadcrumbs(this.uri);
+ 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 parentElement = breadcrumbsHtmlElement.parentElement.lastElementChild;
+ if (!parentElement.classList.contains(Breadcrumbs.Styles.BREADCRUMB_POPUP_CONTAINER)) {
+ // this is unexpected
+ console.warn('Could not find breadcrumb popup container.');
+ } else {
+ const anchor: { x: number, y: number } = BreadcrumbsRenderer.determinePopupAnchor(event.nativeEvent) || event.nativeEvent;
+ this.breadcrumbsService.openPopup(breadcrumb, anchor, parentElement as HTMLElement).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..72cda701f074a
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts
@@ -0,0 +1,73 @@
+/********************************************************************************
+ * 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 } from 'inversify';
+import { ContributionProvider, Prioritizeable, Emitter, Event } from '../../common';
+import URI from '../../common/uri';
+import { Breadcrumb } from './breadcrumb';
+import { BreadcrumbPopup } from './breadcrumb-popup';
+import { BreadcrumbsContribution } from './breadcrumbs-contribution';
+
+@injectable()
+export class BreadcrumbsService {
+
+ @inject(ContributionProvider) @named(BreadcrumbsContribution)
+ protected readonly contributions: ContributionProvider;
+
+ protected readonly onBreadcrumbsChangeEmitter = new Emitter();
+
+ /**
+ * 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. `parent` is used as the host element for the newly created popup element.
+ */
+ async openPopup(breadcrumb: Breadcrumb, position: { x: number, y: number }, parent: HTMLElement): Promise {
+ const contribution = this.contributions.getContributions().find(c => c.type === breadcrumb.type);
+ if (contribution) { return contribution.openPopup(breadcrumb, position, parent); }
+ }
+
+}
diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs.css b/packages/core/src/browser/breadcrumbs/breadcrumbs.css
similarity index 69%
rename from packages/editor/src/browser/breadcrumbs/breadcrumbs.css
rename to packages/core/src/browser/breadcrumbs/breadcrumbs.css
index bb69218b870e2..094edba83602d 100644
--- a/packages/editor/src/browser/breadcrumbs/breadcrumbs.css
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs.css
@@ -1,3 +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-breadcrumbs {
position: relative;
user-select: none;
diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs.ts b/packages/core/src/browser/breadcrumbs/breadcrumbs.ts
similarity index 100%
rename from packages/editor/src/browser/breadcrumbs/breadcrumbs.ts
rename to packages/core/src/browser/breadcrumbs/breadcrumbs.ts
diff --git a/packages/core/src/browser/breadcrumbs/index.ts b/packages/core/src/browser/breadcrumbs/index.ts
new file mode 100644
index 0000000000000..4af0ff7e6bb42
--- /dev/null
+++ b/packages/core/src/browser/breadcrumbs/index.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 * from './breadcrumb';
+export * from './breadcrumb-popup';
+export * from './breadcrumb-popup-container-renderer';
+export * from './breadcrumb-renderer';
+
+export * from './breadcrumbs';
+export * from './breadcrumbs-contribution';
+export * from './breadcrumbs-renderer';
+export * from './breadcrumbs-service';
diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts
index 20c91dbc4747d..da36f0f4aef1d 100644
--- a/packages/core/src/browser/frontend-application-module.ts
+++ b/packages/core/src/browser/frontend-application-module.ts
@@ -83,6 +83,12 @@ 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,
+ BreadcrumbPopupContainerRenderer,
+ DefaultBreadcrumbPopupContainerRenderer
+} from './breadcrumbs';
export { bindResourceProvider, bindMessageService, bindPreferenceService };
@@ -286,4 +292,8 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
bind(ProgressService).toSelf().inSingletonScope();
bind(ContextMenuContext).toSelf().inSingletonScope();
+
+ bindContributionProvider(bind, BreadcrumbsContribution);
+ bind(BreadcrumbsService).toSelf().inSingletonScope();
+ bind(BreadcrumbPopupContainerRenderer).to(DefaultBreadcrumbPopupContainerRenderer).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/editor/package.json b/packages/editor/package.json
index cfcb2377ecd3b..324488adb817e 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -4,9 +4,6 @@
"description": "Theia - Editor Extension",
"dependencies": {
"@theia/core": "^0.11.0",
- "@theia/filesystem": "^0.11.0",
- "@theia/workspace": "^0.11.0",
- "@theia/outline-view": "^0.11.0",
"@theia/languages": "^0.11.0",
"@theia/variable-resolver": "^0.11.0",
"@types/base64-arraybuffer": "0.1.0",
diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs-items.tsx b/packages/editor/src/browser/breadcrumbs/breadcrumbs-items.tsx
deleted file mode 100644
index 298589e8fd1c1..0000000000000
--- a/packages/editor/src/browser/breadcrumbs/breadcrumbs-items.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-/********************************************************************************
- * 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 { Breadcrumbs } from './breadcrumbs';
-import URI from '@theia/core/lib/common/uri';
-import { FileSystem } from '@theia/filesystem/lib/common';
-import { LabelProvider, OpenerService } from '@theia/core/lib/browser';
-import { MessageService } from '@theia/core';
-import { BreadcrumbsListPopup } from './breadcrumbs-popups';
-import { findParentBreadcrumbsHtmlElement, determinePopupAnchor } from './breadcrumbs-utils';
-import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser';
-import { TextEditor, Range } from '../editor';
-import { OutlineViewService } from '@theia/outline-view/lib/browser/outline-view-service';
-
-export interface BreadcrumbItem {
- render(index: number): JSX.Element;
-}
-
-export class SimpleBreadcrumbItem implements BreadcrumbItem {
- constructor(readonly text: string) { }
- render(index: number): JSX.Element {
- return
- {this.text}
- ;
- }
-}
-
-abstract class BreadcrumbItemWithPopup implements BreadcrumbItem {
-
- protected popup: BreadcrumbsListPopup | undefined;
-
- abstract render(index: number): JSX.Element;
-
- protected showPopup = (event: React.MouseEvent) => {
- if (this.popup) {
- // Popup already shown. Hide popup instead.
- this.popup.dispose();
- this.popup = undefined;
- } else {
- if (event.nativeEvent.target && event.nativeEvent.target instanceof HTMLElement) {
- const breadcrumbsHtmlElement = findParentBreadcrumbsHtmlElement(event.nativeEvent.target as HTMLElement);
- if (breadcrumbsHtmlElement && breadcrumbsHtmlElement.parentElement && breadcrumbsHtmlElement.parentElement.lastElementChild) {
- const parentElement = breadcrumbsHtmlElement.parentElement.lastElementChild;
- if (!parentElement.classList.contains(Breadcrumbs.Styles.BREADCRUMB_POPUP_CONTAINER)) {
- // this is unexpected
- } else {
- const anchor: { x: number, y: number } = determinePopupAnchor(event.nativeEvent) || event.nativeEvent;
- this.createPopup(parentElement as HTMLElement, anchor).then(popup => { this.popup = popup; });
- event.stopPropagation();
- event.preventDefault();
- }
- }
- }
- }
- }
-
- protected abstract async createPopup(parent: HTMLElement, anchor: { x: number, y: number }): Promise;
-}
-
-export class FileBreadcrumbItem extends BreadcrumbItemWithPopup {
-
- constructor(
- readonly text: string,
- readonly title: string = text,
- readonly icon: string,
- readonly uri: URI,
- readonly itemCssClass: string,
- protected readonly fileSystem: FileSystem,
- protected readonly labelProvider: LabelProvider,
- protected readonly openerService: OpenerService,
- protected readonly messageService: MessageService,
- ) { super(); }
-
- render(index: number): JSX.Element {
- return
- {this.text}
- ;
- }
-
- protected async createPopup(parent: HTMLElement, anchor: { x: number, y: number }): Promise {
-
- const folderFileStat = await this.fileSystem.getFileStat(this.uri.parent.toString());
-
- if (folderFileStat && folderFileStat.children) {
- const items = await Promise.all(folderFileStat.children
- .filter(child => !child.isDirectory)
- .filter(child => child.uri !== this.uri.toString())
- .map(child => new URI(child.uri))
- .map(
- async u => ({
- label: this.labelProvider.getName(u),
- title: this.labelProvider.getLongName(u),
- iconClass: await this.labelProvider.getIcon(u) + ' file-icon',
- action: () => this.openFile(u)
- })
- ));
- if (items.length > 0) {
- const filelistPopup = new BreadcrumbsListPopup(items, anchor, parent);
- filelistPopup.render();
- return filelistPopup;
- }
- }
- }
-
- protected openFile = (uri: URI) => {
- this.openerService.getOpener(uri)
- .then(opener => opener.open(uri))
- .catch(error => this.messageService.error(error));
- }
-}
-
-export class OutlineBreadcrumbItem extends BreadcrumbItemWithPopup {
-
- constructor(
- protected readonly node: OutlineSymbolInformationNode,
- protected readonly editor: TextEditor,
- protected readonly outlineViewService: OutlineViewService
- ) { super(); }
-
- render(index: number): JSX.Element {
- return
- {this.node.name}
- ;
- }
-
- protected async createPopup(parent: HTMLElement, anchor: { x: number, y: number }): Promise {
- const items = this.siblings().map(node => ({
- label: node.name,
- title: node.name,
- iconClass: 'symbol-icon symbol-icon-center ' + this.node.iconClass,
- action: () => this.revealInEditor(node)
- }));
- if (items.length > 0) {
- const filelistPopup = new BreadcrumbsListPopup(items, anchor, parent);
- filelistPopup.render();
- return filelistPopup;
- }
- }
-
- private revealInEditor(node: OutlineSymbolInformationNode): void {
- if (OutlineNodeWithRange.is(node)) {
- this.editor.cursor = node.range.end;
- this.editor.selection = node.range;
- this.editor.revealRange(node.range);
- this.editor.focus();
- }
- }
-
- private hasPopup(): boolean {
- return this.siblings().length > 0;
- }
-
- private siblings(): OutlineSymbolInformationNode[] {
- if (!this.node.parent) { return []; }
- return this.node.parent.children.filter(n => n !== this.node).map(n => n as OutlineSymbolInformationNode);
- }
-}
-
-interface OutlineNodeWithRange extends OutlineSymbolInformationNode {
- range: Range;
-}
-namespace OutlineNodeWithRange {
- export function is(node: OutlineSymbolInformationNode): node is OutlineNodeWithRange {
- return 'range' in node;
- }
-}
diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs-renderer.tsx b/packages/editor/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
deleted file mode 100644
index 5e92a977ab15b..0000000000000
--- a/packages/editor/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-/********************************************************************************
- * 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 '../../../src/browser/breadcrumbs/breadcrumbs.css';
-
-import * as React from 'react';
-import { ReactRenderer, LabelProvider, OpenerService } from '@theia/core/lib/browser';
-import { TextEditor } from '../editor';
-import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
-import URI from '@theia/core/lib/common/uri';
-import { MessageService, DisposableCollection } from '@theia/core';
-import { WorkspaceService } from '@theia/workspace/lib/browser';
-import { Breadcrumbs } from './breadcrumbs';
-import { BreadcrumbItem, FileBreadcrumbItem, OutlineBreadcrumbItem } from './breadcrumbs-items';
-import { OutlineViewService } from '@theia/outline-view/lib/browser/outline-view-service';
-import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser';
-import { toOutlinePath, findSelectedNode } from './breadcrumbs-utils';
-import PerfectScrollbar from 'perfect-scrollbar';
-import { inject, injectable, postConstruct } from 'inversify';
-
-@injectable()
-export class BreadcrumbsRenderer extends ReactRenderer {
-
- @inject('TextEditor')
- protected readonly editor: TextEditor;
-
- @inject(FileSystem)
- protected readonly fileSystem: FileSystem;
-
- @inject(LabelProvider)
- protected readonly labelProvider: LabelProvider;
-
- @inject(OpenerService)
- protected readonly openerService: OpenerService;
-
- @inject(MessageService)
- protected readonly messageService: MessageService;
-
- @inject(WorkspaceService)
- protected readonly workspaceService: WorkspaceService;
-
- @inject(OutlineViewService)
- protected readonly outlineViewService: OutlineViewService;
-
- private readonly filePathItems = new Array();
- private outlineItems = new Array();
-
- private disposables = new DisposableCollection();
- protected scrollbar: PerfectScrollbar | undefined;
-
- @postConstruct()
- init(): void {
- this.createFilePathItems();
- }
-
- protected async createFilePathItems(): Promise {
- const resourceUri = this.editor.getResourceUri();
- const workspaceRootUri = this.workspaceService.getWorkspaceRootUri(resourceUri);
- if (resourceUri) {
- for (const uri of resourceUri.allLocations.reverse().filter(u => !u.path.isRoot && (!workspaceRootUri || !u.isEqualOrParent(workspaceRootUri)))) {
- const icon = await this.labelProvider.getIcon(uri);
- const label = this.labelProvider.getName(uri);
- const title = this.labelProvider.getLongName(uri);
- const itemCssClass = Breadcrumbs.Styles.BREADCRUMB_ITEM + (await this.hasSiblings(uri) ? ' ' + Breadcrumbs.Styles.BREADCRUMB_ITEM_HAS_POPUP : '');
- this.filePathItems.push(new FileBreadcrumbItem(
- label,
- title,
- icon,
- uri,
- itemCssClass,
- this.fileSystem,
- this.labelProvider,
- this.openerService,
- this.messageService
- ));
- }
- }
- this.refresh();
- }
-
- protected async updateOutlineItems(outlinePath: OutlineSymbolInformationNode[]): Promise {
- this.outlineItems = outlinePath.map(node => new OutlineBreadcrumbItem(node, this.editor, this.outlineViewService));
- this.refresh();
- }
-
- dispose(): void {
- super.dispose();
- this.disposables.dispose();
- if (this.scrollbar) {
- this.scrollbar.destroy();
- this.scrollbar = undefined;
- }
- }
-
- refresh(): void {
- 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();
- if (this.disposables.disposed) {
- this.createOutlineChangeListener();
- }
- }
-
- onAfterShow(): void {
- this.createOutlineChangeListener();
- }
-
- private createOutlineChangeListener(): void {
- this.disposables.push(this.outlineViewService.onDidChangeOutline(roots => {
- if (this.editor.isFocused()) {
- const outlinePath = toOutlinePath(findSelectedNode(roots));
- if (outlinePath) { this.updateOutlineItems(outlinePath); }
- }
- }));
-
- this.disposables.push(this.outlineViewService.onDidSelect(node => {
- // Check if this event is for this editor (by comparing URIs)
- if (OutlineNodeWithUri.is(node) && node.uri.toString() === this.editor.uri.toString()) {
- const outlinePath = toOutlinePath(node);
- if (outlinePath) { this.updateOutlineItems(outlinePath); }
- }
- }));
- }
-
- onAfterHide(): void {
- this.disposables.dispose();
- }
-
- private scrollToEnd(): void {
- if (this.host.firstChild) {
- const breadcrumbsHtmlElement = (this.host.firstChild as HTMLElement);
- breadcrumbsHtmlElement.scrollLeft = breadcrumbsHtmlElement.scrollWidth;
- }
- }
-
- private async hasSiblings(uri: URI): Promise {
- const fileStat = await this.fileSystem.getFileStat(uri.parent.toString());
-
- if (fileStat && fileStat.children) {
- const length = fileStat.children.filter(child => !child.isDirectory).filter(child => child.uri !== uri.toString()).length;
- return length > 0;
- }
- return false;
- }
-
- protected doRender(): React.ReactNode {
- return [
- ,
-
- ];
- }
-
- protected renderItems(): JSX.Element[] {
- return [...this.filePathItems, ...this.outlineItems].map((item, index) => item.render(index));
- }
-}
-
-export const BreadcrumbsRendererFactory = Symbol('BreadcrumbsRendererFactory');
-export interface BreadcrumbsRendererFactory {
- (editor: TextEditor): BreadcrumbsRenderer;
-}
-
-interface OutlineNodeWithUri extends OutlineSymbolInformationNode {
- uri: URI;
-}
-namespace OutlineNodeWithUri {
- export function is(node: OutlineSymbolInformationNode): node is OutlineNodeWithUri {
- return 'uri' in node;
- }
-}
diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs-utils.ts b/packages/editor/src/browser/breadcrumbs/breadcrumbs-utils.ts
deleted file mode 100644
index 5df8e97563a6a..0000000000000
--- a/packages/editor/src/browser/breadcrumbs/breadcrumbs-utils.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-/********************************************************************************
- * 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 { Breadcrumbs } from './breadcrumbs';
-import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser';
-
-/**
- * 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
- };
- }
-}
-
-/**
- * Find the node that is selected. Returns after the first match.
- */
-export function findSelectedNode(roots: OutlineSymbolInformationNode[]): OutlineSymbolInformationNode | undefined {
- const result = roots.find(node => node.selected);
- if (result) {
- return result;
- }
- for (const node of roots) {
- const result2 = findSelectedNode(node.children.map(child => child as OutlineSymbolInformationNode));
- if (result2) {
- return result2;
- }
- }
-}
-
-/**
- * Returns the path of the given outline node.
- */
-export function toOutlinePath(node: OutlineSymbolInformationNode | undefined, path: OutlineSymbolInformationNode[] = []): OutlineSymbolInformationNode[] | undefined {
- if (!node) { return undefined; }
- if (node.id === 'outline-view-root') { return path; }
- if (node.parent) {
- return toOutlinePath(node.parent as OutlineSymbolInformationNode, [node, ...path]);
- } else {
- return [node, ...path];
- }
-}
diff --git a/packages/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts
index aaae9e9349bf6..92ec657c2ba38 100644
--- a/packages/editor/src/browser/editor-frontend-module.ts
+++ b/packages/editor/src/browser/editor-frontend-module.ts
@@ -33,8 +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 { TextEditor } from './editor';
-import { BreadcrumbsRenderer, BreadcrumbsRendererFactory } from './breadcrumbs/breadcrumbs-renderer';
+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);
@@ -78,10 +84,11 @@ export default new ContainerModule(bind => {
bind(EditorAccess).to(ActiveEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.ACTIVE);
bind(BreadcrumbsRendererFactory).toFactory(ctx =>
- (editor: TextEditor) => {
+ (uri: URI) => {
const childContainer = ctx.container.createChild();
- childContainer.bind('TextEditor').toConstantValue(editor);
+ 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 5d7a32ea16566..f915d7191be81 100644
--- a/packages/editor/src/browser/editor-widget-factory.ts
+++ b/packages/editor/src/browser/editor-widget-factory.ts
@@ -20,7 +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 './breadcrumbs/breadcrumbs-renderer';
+import { BreadcrumbsRendererFactory } from '@theia/core/lib/browser/breadcrumbs';
@injectable()
export class EditorWidgetFactory implements WidgetFactory {
@@ -49,7 +49,7 @@ 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 breadcrumbsRenderer = this.breadcrumbsRendererFactory(textEditor);
+ const breadcrumbsRenderer = this.breadcrumbsRendererFactory(uri);
const newEditor = new EditorWidget(textEditor, breadcrumbsRenderer, this.selectionService);
newEditor.id = this.id + ':' + uri.toString();
newEditor.title.closable = true;
diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts
index c7fb25ab1290f..9629f8fa2a78f 100644
--- a/packages/editor/src/browser/editor-widget.ts
+++ b/packages/editor/src/browser/editor-widget.ts
@@ -18,7 +18,7 @@ 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 './breadcrumbs/breadcrumbs-renderer';
+import { BreadcrumbsRenderer } from '@theia/core/lib/browser/breadcrumbs';
export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget {
@@ -82,11 +82,6 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
this.breadcrumbsRenderer.refresh();
}
- protected onAfterHide(msg: Message): void {
- super.onAfterHide(msg);
- this.breadcrumbsRenderer.onAfterHide();
- }
-
protected onResize(msg: Widget.ResizeMessage): void {
if (msg.width < 0 || msg.height < 0) {
this.editor.resizeToFit();
diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json
index 9762c33692c8f..59e3fdd5e3b08 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-contribution.tsx b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.tsx
new file mode 100644
index 0000000000000..8de06fbe29e5e
--- /dev/null
+++ b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.tsx
@@ -0,0 +1,101 @@
+/********************************************************************************
+ * 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 { 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, OpenerService, BreadcrumbPopupContainerRenderer, BreadcrumbPopup } from '@theia/core/lib/browser';
+import { FileSystem } from '../../common';
+import URI from '@theia/core/lib/common/uri';
+
+export const FilepathBreadcrumbType = Symbol('FilepathBreadcrumb');
+
+@injectable()
+export class FilepathBreadcrumbsContribution implements BreadcrumbsContribution {
+
+ @inject(LabelProvider)
+ protected readonly labelProvider: LabelProvider;
+
+ @inject(FileSystem)
+ protected readonly fileSystem: FileSystem;
+
+ @inject(OpenerService)
+ protected readonly openerService: OpenerService;
+
+ @inject(BreadcrumbPopupContainerRenderer)
+ protected readonly breadcrumbPopupContainerRenderer: BreadcrumbPopupContainerRenderer;
+
+ 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 openPopup(breadcrumb: Breadcrumb, position: { x: number, y: number }, parent: HTMLElement): Promise {
+ if (!FilepathBreadcrumb.is(breadcrumb)) {
+ return undefined;
+ }
+ const folderFileStat = await this.fileSystem.getFileStat(breadcrumb.uri.parent.toString());
+
+ if (folderFileStat && folderFileStat.children) {
+ const items = await Promise.all(folderFileStat.children
+ .filter(child => !child.isDirectory)
+ .filter(child => child.uri !== breadcrumb.uri.toString())
+ .map(child => new URI(child.uri))
+ .map(
+ async u => ({
+ label: this.labelProvider.getName(u),
+ title: this.labelProvider.getLongName(u),
+ iconClass: await this.labelProvider.getIcon(u) + ' file-icon',
+ action: () => this.openFile(u)
+ })
+ ));
+ if (items.length > 0) {
+ return this.breadcrumbPopupContainerRenderer.render(breadcrumb.id, position, this.renderItems(items), parent);
+ }
+ }
+ return this.breadcrumbPopupContainerRenderer.render(breadcrumb.id, position, No siblings.
, parent);
+ }
+
+ protected renderItems(items: { label: string, title: string, iconClass: string, action: () => void }[]): React.ReactNode {
+ return
+ {items.map((item, index) => - item.action()}>
+ {item.label}
+
)}
+
;
+ }
+
+ protected openFile = (uri: URI) => {
+ this.openerService.getOpener(uri)
+ .then(opener => opener.open(uri));
+ }
+}
diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts
index 618fefa59c3ef..0b8a9f9ce9175 100644
--- a/packages/filesystem/src/browser/filesystem-frontend-module.ts
+++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts
@@ -30,6 +30,8 @@ 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';
export default new ContainerModule(bind => {
bindFileSystemPreferences(bind);
@@ -62,6 +64,9 @@ export default new ContainerModule(bind => {
bind(FileSystemFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(FileSystemFrontendContribution);
bind(FrontendApplicationContribution).toService(FileSystemFrontendContribution);
+
+ bind(FilepathBreadcrumbsContribution).toSelf().inSingletonScope();
+ bind(BreadcrumbsContribution).toService(FilepathBreadcrumbsContribution);
});
export function bindFileResource(bind: interfaces.Bind): void {
diff --git a/packages/outline-view/package.json b/packages/outline-view/package.json
index 6f3b0e924c4e4..1fa8a13105c5e 100644
--- a/packages/outline-view/package.json
+++ b/packages/outline-view/package.json
@@ -3,7 +3,8 @@
"version": "0.11.0",
"description": "Theia - Outline View Extension",
"dependencies": {
- "@theia/core": "^0.11.0"
+ "@theia/core": "^0.11.0",
+ "@theia/editor": "^0.11.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..b28e22c481727
--- /dev/null
+++ b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx
@@ -0,0 +1,180 @@
+/********************************************************************************
+ * 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 { 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, BreadcrumbPopupContainerRenderer, BreadcrumbPopup, 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';
+
+export const OutlineBreadcrumbType = Symbol('OutlineBreadcrumb');
+
+@injectable()
+export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
+
+ @inject(LabelProvider)
+ protected readonly labelProvider: LabelProvider;
+
+ @inject(BreadcrumbPopupContainerRenderer)
+ protected readonly breadcrumbPopupContainerRenderer: BreadcrumbPopupContainerRenderer;
+
+ @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[] = [];
+
+ @postConstruct()
+ init(): void {
+ this.outlineViewService.onDidChangeOutline(roots => {
+ if (roots.length > 0) {
+ const first = roots[0];
+ if ('uri' in first) {
+ this.updateOutlineItems(first['uri'] as URI, this.findSelectedNode(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)
+ );
+ } else {
+ this.currentBreadcrumbs = [];
+ }
+ this.breadcrumbsService.breadcrumbsChanges(uri);
+ }
+
+ async computeBreadcrumbs(uri: URI): Promise {
+ if (this.currentUri && uri.toString() === this.currentUri.toString()) {
+ return this.currentBreadcrumbs;
+ }
+ return [];
+ }
+
+ async openPopup(breadcrumb: Breadcrumb, position: { x: number, y: number }, parent: HTMLElement): Promise {
+ if (!OutlineBreadcrumb.is(breadcrumb)) {
+ return undefined;
+ }
+ const items = this.siblings(breadcrumb.node).map(node => ({
+ label: node.name,
+ title: node.name,
+ iconClass: 'symbol-icon symbol-icon-center ' + node.iconClass,
+ action: () => this.revealInEditor(node)
+ }));
+ if (items.length > 0) {
+ return this.breadcrumbPopupContainerRenderer.render(breadcrumb.id, position, this.renderItems(items), parent);
+ }
+ return this.breadcrumbPopupContainerRenderer.render(breadcrumb.id, position, No siblings.
, parent);
+ }
+
+ 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,
+ 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 0558e338ffabd..7c3f42ddb477e 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);
});
function createOutlineViewWidget(parent: interfaces.Container): OutlineViewWidget {
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..6774249ab5e50
--- /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 WorkspaceBreadcrumbsContriubtion 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..9131db6a573d9 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 { WorkspaceBreadcrumbsContriubtion } 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(WorkspaceBreadcrumbsContriubtion).inSingletonScope();
});