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.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 [ -
      {this.renderItems()}
    , -
    - ]; - } - - 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(); });