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 b515c84b0480f..cfcb2377ecd3b 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -4,10 +4,14 @@
"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",
- "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/breadcrumbs/breadcrumbs-items.tsx b/packages/editor/src/browser/breadcrumbs/breadcrumbs-items.tsx
new file mode 100644
index 0000000000000..298589e8fd1c1
--- /dev/null
+++ b/packages/editor/src/browser/breadcrumbs/breadcrumbs-items.tsx
@@ -0,0 +1,186 @@
+/********************************************************************************
+ * 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-popups.tsx b/packages/editor/src/browser/breadcrumbs/breadcrumbs-popups.tsx
new file mode 100644
index 0000000000000..d5d9a1839db90
--- /dev/null
+++ b/packages/editor/src/browser/breadcrumbs/breadcrumbs-popups.tsx
@@ -0,0 +1,88 @@
+/********************************************************************************
+ * 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 { ReactRenderer } from '@theia/core/lib/browser';
+import { Breadcrumbs } from './breadcrumbs';
+import PerfectScrollbar from 'perfect-scrollbar';
+
+export class BreadcrumbsListPopup extends ReactRenderer {
+
+ 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);
+ }
+
+ protected doRender(): React.ReactNode {
+ return this.dispose()}
+ tabIndex={0}
+ >
+
+ {this.items.map((item, index) => - item.action()}>
+ {item.label}
+
)}
+
+
;
+ }
+
+ render(): void {
+ super.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,
+ scrollYMarginOffset: 8,
+ suppressScrollX: true
+ });
+ }
+ } else {
+ this.scrollbar.update();
+ }
+ this.focus();
+ document.addEventListener('keyup', this.escFunction);
+ }
+
+ focus(): boolean {
+ if (this.host && this.host.firstChild) {
+ (this.host.firstChild as HTMLElement).focus();
+ return true;
+ }
+ return false;
+ }
+
+ dispose(): void {
+ super.dispose();
+ if (this.scrollbar) {
+ this.scrollbar.destroy();
+ this.scrollbar = undefined;
+ }
+ document.removeEventListener('keyup', this.escFunction);
+ }
+
+ protected escFunction = (event: KeyboardEvent) => {
+ if (event.key === 'Escape' || event.key === 'Esc') {
+ this.dispose();
+ }
+ }
+}
diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs-renderer.tsx b/packages/editor/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
new file mode 100644
index 0000000000000..9e6ccc6ffc075
--- /dev/null
+++ b/packages/editor/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
@@ -0,0 +1,196 @@
+/********************************************************************************
+ * 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) {
+ // slice(1) because the first one is '/'
+ for (const uri of resourceUri.allLocations.reverse().slice(1).filter(u => !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
new file mode 100644
index 0000000000000..5df8e97563a6a
--- /dev/null
+++ b/packages/editor/src/browser/breadcrumbs/breadcrumbs-utils.ts
@@ -0,0 +1,96 @@
+/********************************************************************************
+ * 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/breadcrumbs/breadcrumbs.css b/packages/editor/src/browser/breadcrumbs/breadcrumbs.css
new file mode 100644
index 0000000000000..bb69218b870e2
--- /dev/null
+++ b/packages/editor/src/browser/breadcrumbs/breadcrumbs.css
@@ -0,0 +1,84 @@
+.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-popup {
+ position: fixed;
+ width: 8cm;
+ max-height: 5cm;
+ 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);
+}
\ No newline at end of file
diff --git a/packages/editor/src/browser/breadcrumbs/breadcrumbs.ts b/packages/editor/src/browser/breadcrumbs/breadcrumbs.ts
new file mode 100644
index 0000000000000..1027c2a5ed523
--- /dev/null
+++ b/packages/editor/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_CONTAINER = 'theia-breadcrumbs-popup-container';
+ export const BREADCRUMB_POPUP = 'theia-breadcrumbs-popup';
+ export const BREADCRUMB_ITEM_HAS_POPUP = 'theia-breadcrumb-item-haspopup';
+ }
+}
diff --git a/packages/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts
index 418f2ddb663b3..aaae9e9349bf6 100644
--- a/packages/editor/src/browser/editor-frontend-module.ts
+++ b/packages/editor/src/browser/editor-frontend-module.ts
@@ -33,6 +33,8 @@ 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';
export default new ContainerModule(bind => {
bindEditorPreferences(bind);
@@ -74,4 +76,13 @@ 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 =>
+ (editor: TextEditor) => {
+ const childContainer = ctx.container.createChild();
+ childContainer.bind('TextEditor').toConstantValue(editor);
+ childContainer.bind(BreadcrumbsRenderer).toSelf();
+ 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..5d7a32ea16566 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 './breadcrumbs/breadcrumbs-renderer';
@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(textEditor);
+ 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..c7fb25ab1290f 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 './breadcrumbs/breadcrumbs-renderer';
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,19 @@ 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 onAfterHide(msg: Message): void {
+ super.onAfterHide(msg);
+ this.breadcrumbsRenderer.onAfterHide();
}
protected onResize(msg: Widget.ResizeMessage): void {
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/src/browser/outline-view-service.ts b/packages/outline-view/src/browser/outline-view-service.ts
index 0252677b17b54..d520862febc23 100644
--- a/packages/outline-view/src/browser/outline-view-service.ts
+++ b/packages/outline-view/src/browser/outline-view-service.ts
@@ -56,8 +56,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 {