diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc28aab082361..18a5cb905b259 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Change Log
+## v1.18.0 - 9/30/2021
+
+- [core, outline-view, file-system, workspace] added breadcrumbs contribution points and renderers to `core` and contributions to other packages. [#9920](https://github.com/eclipse-theia/theia/pull/9920)
+
+[Breaking Changes:](#breaking_changes_1.18.0)
+
+- [core] added `BreadcrumbsRendererFactory` to constructor arguments of `DockPanelRenderer` and `ToolbarAwareTabBar`. [#9920](https://github.com/eclipse-theia/theia/pull/9920)
+
## v1.17.2 - 9/1/2021
[1.17.2 Milestone](https://github.com/eclipse-theia/theia/milestone/27)
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts b/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts
index f3e08fe86865a..c5036bb4f8f86 100644
--- a/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts
+++ b/packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts
@@ -14,84 +14,88 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
+import { inject, injectable, postConstruct } from '../../../shared/inversify';
+import { Emitter, Event } from '../../common';
import { Disposable, DisposableCollection } from '../../common/disposable';
-import { Breadcrumbs } from './breadcrumbs';
+import { Coordinate } from '../context-menu-renderer';
+import { RendererHost } from '../widgets/react-renderer';
+import { Styles } from './breadcrumbs-constants';
+
+export interface BreadcrumbPopupContainerFactory {
+ (parent: HTMLElement, breadcrumbId: string, position: Coordinate): BreadcrumbPopupContainer;
+}
+export const BreadcrumbPopupContainerFactory = Symbol('BreadcrumbPopupContainerFactory');
+
+export type BreadcrumbID = string;
+export const BreadcrumbID = Symbol('BreadcrumbID');
/**
* This class creates a popup container at the given position
* so that contributions can attach their HTML elements
- * as childs of `BreadcrumbPopupContainer#container`.
+ * as children of `BreadcrumbPopupContainer#container`.
*
* - `dispose()` is called on blur or on hit on escape
*/
+@injectable()
export class BreadcrumbPopupContainer implements Disposable {
+ @inject(RendererHost) protected readonly parent: RendererHost;
+ @inject(BreadcrumbID) public readonly breadcrumbId: BreadcrumbID;
+ @inject(Coordinate) protected readonly position: Coordinate;
- protected toDispose: DisposableCollection = new DisposableCollection();
+ protected onDidDisposeEmitter = new Emitter();
+ protected toDispose: DisposableCollection = new DisposableCollection(this.onDidDisposeEmitter);
+ get onDidDispose(): Event {
+ return this.onDidDisposeEmitter.event;
+ }
+
+ protected _container: HTMLElement;
+ get container(): HTMLElement {
+ return this._container;
+ }
- readonly container: HTMLElement;
- public isOpen: boolean;
+ protected _isOpen: boolean;
+ get isOpen(): boolean {
+ return this._isOpen;
+ }
- constructor(
- protected readonly parent: HTMLElement,
- public readonly breadcrumbId: string,
- position: { x: number, y: number }
- ) {
- this.container = this.createPopupDiv(position);
+ @postConstruct()
+ protected init(): void {
+ this._container = this.createPopupDiv(this.position);
document.addEventListener('keyup', this.escFunction);
- this.container.focus();
- this.isOpen = true;
+ this._container.focus();
+ this._isOpen = true;
}
- protected createPopupDiv(position: { x: number, y: number }): HTMLDivElement {
+ protected createPopupDiv(position: Coordinate): HTMLDivElement {
const result = window.document.createElement('div');
- result.className = Breadcrumbs.Styles.BREADCRUMB_POPUP;
+ result.className = Styles.BREADCRUMB_POPUP;
result.style.left = `${position.x}px`;
result.style.top = `${position.y}px`;
result.tabIndex = 0;
- result.onblur = event => this.onBlur(event, this.breadcrumbId);
+ result.addEventListener('focusout', this.onFocusOut);
this.parent.appendChild(result);
return result;
}
- protected onBlur = (event: FocusEvent, breadcrumbId: string) => {
- if (event.relatedTarget && event.relatedTarget instanceof HTMLElement) {
- // event.relatedTarget is the element that has the focus after this popup looses the focus.
- // If a breadcrumb was clicked the following holds the breadcrumb ID of the clicked breadcrumb.
- const clickedBreadcrumbId = event.relatedTarget.getAttribute('data-breadcrumb-id');
- if (clickedBreadcrumbId && clickedBreadcrumbId === breadcrumbId) {
- // This is a click on the breadcrumb that has openend this popup.
- // We do not close this popup here but let the click event of the breadcrumb handle this instead
- // because it needs to know that this popup is open to decide if it just closes this popup or
- // also open a new popup.
- return;
- }
- if (this.container.contains(event.relatedTarget)) {
- // A child element gets focus. Set the focus to the container again.
- // Otherwise the popup would not be closed when elements outside the popup get the focus.
- // A popup content should not relay on getting a focus.
- this.container.focus();
- return;
- }
+ protected onFocusOut = (event: FocusEvent) => {
+ if (!(event.relatedTarget instanceof Element) || !this._container.contains(event.relatedTarget)) {
+ this.dispose();
}
- this.dispose();
- }
+ };
protected escFunction = (event: KeyboardEvent) => {
if (event.key === 'Escape' || event.key === 'Esc') {
this.dispose();
}
- }
+ };
dispose(): void {
- this.toDispose.dispose();
- if (this.parent.contains(this.container)) {
- this.parent.removeChild(this.container);
+ if (!this.toDispose.disposed) {
+ this.onDidDisposeEmitter.fire();
+ this.toDispose.dispose();
+ this._container.remove();
+ this._isOpen = false;
+ document.removeEventListener('keyup', this.escFunction);
}
- this.isOpen = false;
- document.removeEventListener('keyup', this.escFunction);
- }
-
- addDisposable(disposable: Disposable | undefined): void {
- if (disposable) { this.toDispose.push(disposable); }
}
}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx b/packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
index 6385e8c8f6244..aa863343ebc24 100644
--- a/packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
+++ b/packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
@@ -16,23 +16,22 @@
import * as React from 'react';
import { injectable } from 'inversify';
-import { Breadcrumb } from './breadcrumb';
-import { Breadcrumbs } from './breadcrumbs';
+import { Breadcrumb, Styles } from './breadcrumbs-constants';
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;
+ render(breadcrumb: Breadcrumb, onMouseDown?: (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 {
+ render(breadcrumb: Breadcrumb, onMouseDown?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode {
return onClick && onClick(breadcrumb, event)}
+ className={Styles.BREADCRUMB_ITEM + (!onMouseDown ? '' : ' ' + Styles.BREADCRUMB_ITEM_HAS_POPUP)}
+ onMouseDown={event => onMouseDown && onMouseDown(breadcrumb, event)}
tabIndex={0}
data-breadcrumb-id={breadcrumb.id}
>
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumb.ts b/packages/core/src/browser/breadcrumbs/breadcrumb.ts
deleted file mode 100644
index 5d549b251b519..0000000000000
--- a/packages/core/src/browser/breadcrumbs/breadcrumb.ts
+++ /dev/null
@@ -1,34 +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
- ********************************************************************************/
-
-/** A single breadcrumb in the breadcrumbs bar. */
-export interface Breadcrumb {
-
- /** An ID of this breadcrumb that should be unique in the breadcrumbs bar. */
- readonly id: string
-
- /** The breadcrumb type. Should be the same as the contribution type `BreadcrumbsContribution#type`. */
- readonly type: symbol
-
- /** The text that will be rendered as label. */
- readonly label: string
-
- /** A longer text that will be used as tooltip text. */
- readonly longLabel: string
-
- /** A CSS class for the icon. */
- readonly iconClass?: string
-}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs-contribution.ts b/packages/core/src/browser/breadcrumbs/breadcrumbs-constants.ts
similarity index 52%
rename from packages/core/src/browser/breadcrumbs/breadcrumbs-contribution.ts
rename to packages/core/src/browser/breadcrumbs/breadcrumbs-constants.ts
index 959fa63ce77f5..e77495723fbc1 100644
--- a/packages/core/src/browser/breadcrumbs/breadcrumbs-contribution.ts
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-constants.ts
@@ -14,9 +14,39 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
+import { MaybePromise, Event } from '../../common';
+import { Disposable } from '../../../shared/vscode-languageserver-protocol';
import URI from '../../common/uri';
-import { Breadcrumb } from './breadcrumb';
-import { Disposable } from '../../common';
+
+export namespace Styles {
+ export const BREADCRUMBS = 'theia-breadcrumbs';
+ export const BREADCRUMB_ITEM = 'theia-breadcrumb-item';
+ export const BREADCRUMB_POPUP_OVERLAY_CONTAINER = 'theia-breadcrumbs-popups-overlay';
+ export const BREADCRUMB_POPUP = 'theia-breadcrumbs-popup';
+ export const BREADCRUMB_ITEM_HAS_POPUP = 'theia-breadcrumb-item-haspopup';
+}
+
+/** A single breadcrumb in the breadcrumbs bar. */
+export interface Breadcrumb {
+
+ /** An ID of this breadcrumb that should be unique in the breadcrumbs bar. */
+ readonly id: string;
+
+ /** The breadcrumb type. Should be the same as the contribution type `BreadcrumbsContribution#type`. */
+ readonly type: symbol;
+
+ /** The text that will be rendered as label. */
+ readonly label: string;
+
+ /** A longer text that will be used as tooltip text. */
+ readonly longLabel: string;
+
+ /** A CSS class for the icon. */
+ readonly iconClass?: string;
+
+ /** CSS classes for the container. */
+ readonly containerClass?: string;
+}
export const BreadcrumbsContribution = Symbol('BreadcrumbsContribution');
export interface BreadcrumbsContribution {
@@ -27,14 +57,19 @@ export interface BreadcrumbsContribution {
readonly type: symbol;
/**
- * The priority of this breadcrumbs contribution. Contributions with lower priority are rendered first.
+ * The priority of this breadcrumbs contribution. Contributions are rendered left to right in order of ascending priority.
*/
readonly priority: number;
+ /**
+ * An event emitter that should fire when breadcrumbs change for a given URI.
+ */
+ readonly onDidChangeBreadcrumbs: Event;
+
/**
* Computes breadcrumbs for a given URI.
*/
- computeBreadcrumbs(uri: URI): Promise;
+ computeBreadcrumbs(uri: URI): MaybePromise;
/**
* Attaches the breadcrumb popup content for the given breadcrumb as child to the given parent.
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx b/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
index 198fb59f8f47d..30129fbc95e13 100644
--- a/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-renderer.tsx
@@ -17,17 +17,20 @@
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 { Emitter, Event } from '../../common';
import { BreadcrumbPopupContainer } from './breadcrumb-popup-container';
import { DisposableCollection } from '../../common/disposable';
import { CorePreferences } from '../core-preferences';
+import { Breadcrumb, Styles } from './breadcrumbs-constants';
+import { LabelProvider } from '../label-provider';
-export const BreadcrumbsURI = Symbol('BreadcrumbsURI');
+interface Cancelable {
+ canceled: boolean;
+}
@injectable()
export class BreadcrumbsRenderer extends ReactRenderer {
@@ -41,22 +44,44 @@ export class BreadcrumbsRenderer extends ReactRenderer {
@inject(CorePreferences)
protected readonly corePreferences: CorePreferences;
- private breadcrumbs: Breadcrumb[] = [];
+ @inject(LabelProvider)
+ protected readonly labelProvider: LabelProvider;
- private popup: BreadcrumbPopupContainer | undefined;
+ protected readonly onDidChangeActiveStateEmitter = new Emitter();
+ get onDidChangeActiveState(): Event {
+ return this.onDidChangeActiveStateEmitter.event;
+ }
- private scrollbar: PerfectScrollbar | undefined;
+ protected uri: URI | undefined;
+ protected breadcrumbs: Breadcrumb[] = [];
+ protected popup: BreadcrumbPopupContainer | undefined;
+ protected scrollbar: PerfectScrollbar | undefined;
+ protected toDispose: DisposableCollection = new DisposableCollection();
- private toDispose: DisposableCollection = new DisposableCollection();
+ get active(): boolean {
+ return !!this.breadcrumbs.length;
+ }
+
+ protected get breadCrumbsContainer(): Element | undefined {
+ return this.host.firstElementChild ?? undefined;
+ }
- constructor(
- @inject(BreadcrumbsURI) readonly uri: URI
- ) { super(); }
+ protected refreshCancellationMarker: Cancelable = { canceled: true };
@postConstruct()
- init(): void {
- this.toDispose.push(this.breadcrumbsService.onDidChangeBreadcrumbs(uri => { if (this.uri.toString() === uri.toString()) { this.refresh(); } }));
- this.toDispose.push(this.corePreferences.onPreferenceChanged(_ => this.refresh()));
+ protected init(): void {
+ this.toDispose.push(this.onDidChangeActiveStateEmitter);
+ this.toDispose.push(this.breadcrumbsService.onDidChangeBreadcrumbs(uri => {
+ if (this.uri?.isEqual(uri)) {
+ this.refresh(this.uri);
+ }
+ }));
+ this.toDispose.push(this.corePreferences.onPreferenceChanged(change => {
+ if (change.preferenceName === 'breadcrumbs.enabled') {
+ this.refresh(this.uri);
+ }
+ }));
+ this.toDispose.push(this.labelProvider.onDidChange(() => this.refresh(this.uri)));
}
dispose(): void {
@@ -69,38 +94,63 @@ export class BreadcrumbsRenderer extends ReactRenderer {
}
}
- async refresh(): Promise {
- if (this.corePreferences['breadcrumbs.enabled']) {
- this.breadcrumbs = await this.breadcrumbsService.getBreadcrumbs(this.uri);
+ async refresh(uri?: URI): Promise {
+ this.uri = uri;
+ this.refreshCancellationMarker.canceled = true;
+ const currentCallCanceled = { canceled: false };
+ this.refreshCancellationMarker = currentCallCanceled;
+ let breadcrumbs: Breadcrumb[];
+ if (uri && this.corePreferences['breadcrumbs.enabled']) {
+ breadcrumbs = await this.breadcrumbsService.getBreadcrumbs(uri);
} else {
- this.breadcrumbs = [];
+ breadcrumbs = [];
+ }
+ if (currentCallCanceled.canceled) {
+ return;
+ }
+
+ const wasActive = this.active;
+ this.breadcrumbs = breadcrumbs;
+ const isActive = this.active;
+ if (wasActive !== isActive) {
+ this.onDidChangeActiveStateEmitter.fire(isActive);
}
+
+ this.update();
+ }
+
+ protected update(): 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
- });
- }
+ this.createScrollbar();
} 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 createScrollbar(): void {
+ const { breadCrumbsContainer } = this;
+ if (breadCrumbsContainer) {
+ this.scrollbar = new PerfectScrollbar(breadCrumbsContainer, {
+ handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
+ useBothWheelAxes: true,
+ scrollXMarginOffset: 4,
+ suppressScrollY: true
+ });
+ }
+ }
+
+ protected scrollToEnd(): void {
+ const { breadCrumbsContainer } = this;
+ if (breadCrumbsContainer) {
+ breadCrumbsContainer.scrollLeft = breadCrumbsContainer.scrollWidth;
}
}
protected doRender(): React.ReactNode {
- return {this.renderBreadcrumbs()}
;
+ return {this.renderBreadcrumbs()}
;
}
protected renderBreadcrumbs(): React.ReactNode {
@@ -111,81 +161,27 @@ export class BreadcrumbsRenderer extends ReactRenderer {
event.stopPropagation();
event.preventDefault();
let openPopup = true;
- if (this.popup) {
- if (this.popup.isOpen) {
- this.popup.dispose();
+ 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);
- }
+ // There is a popup open. If the popup is the popup that belongs to the currently clicked breadcrumb
+ // just close the popup. If another breadcrumb was clicked, open the new popup immediately.
+ openPopup = this.popup.breadcrumbId !== breadcrumb.id;
+ } else {
this.popup = undefined;
}
if (openPopup) {
- if (event.nativeEvent.target && event.nativeEvent.target instanceof HTMLElement) {
- const breadcrumbsHtmlElement = BreadcrumbsRenderer.findParentBreadcrumbsHtmlElement(event.nativeEvent.target as HTMLElement);
- if (breadcrumbsHtmlElement && breadcrumbsHtmlElement.parentElement && breadcrumbsHtmlElement.parentElement.lastElementChild) {
- const position: { x: number, y: number } = BreadcrumbsRenderer.determinePopupAnchor(event.nativeEvent) || event.nativeEvent;
- this.breadcrumbsService.openPopup(breadcrumb, position).then(popup => { this.popup = popup; });
- }
+ const { currentTarget } = event;
+ const breadcrumbElement = currentTarget.closest(`.${Styles.BREADCRUMB_ITEM}`);
+ if (breadcrumbElement) {
+ const { left: x, bottom: y } = breadcrumbElement.getBoundingClientRect();
+ this.breadcrumbsService.openPopup(breadcrumb, { x, y }).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;
+ (): BreadcrumbsRenderer;
}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts b/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts
index 63cd18ee39483..d4b527a28437c 100644
--- a/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts
+++ b/packages/core/src/browser/breadcrumbs/breadcrumbs-service.ts
@@ -17,10 +17,9 @@
import { inject, injectable, named, postConstruct } from 'inversify';
import { ContributionProvider, Prioritizeable, Emitter, Event } from '../../common';
import URI from '../../common/uri';
-import { Breadcrumb } from './breadcrumb';
-import { BreadcrumbPopupContainer } from './breadcrumb-popup-container';
-import { BreadcrumbsContribution } from './breadcrumbs-contribution';
-import { Breadcrumbs } from './breadcrumbs';
+import { Coordinate } from '../context-menu-renderer';
+import { BreadcrumbPopupContainer, BreadcrumbPopupContainerFactory } from './breadcrumb-popup-container';
+import { BreadcrumbsContribution, Styles, Breadcrumb } from './breadcrumbs-constants';
@injectable()
export class BreadcrumbsService {
@@ -28,6 +27,10 @@ export class BreadcrumbsService {
@inject(ContributionProvider) @named(BreadcrumbsContribution)
protected readonly contributions: ContributionProvider;
+ @inject(BreadcrumbPopupContainerFactory) protected readonly breadcrumbPopupContainerFactory: BreadcrumbPopupContainerFactory;
+
+ protected hasSubscribed = false;
+
protected popupsOverlayContainer: HTMLDivElement;
protected readonly onDidChangeBreadcrumbsEmitter = new Emitter();
@@ -39,26 +42,34 @@ export class BreadcrumbsService {
protected createOverlayContainer(): void {
this.popupsOverlayContainer = window.document.createElement('div');
- this.popupsOverlayContainer.id = Breadcrumbs.Styles.BREADCRUMB_POPUP_OVERLAY_CONTAINER;
+ this.popupsOverlayContainer.id = Styles.BREADCRUMB_POPUP_OVERLAY_CONTAINER;
if (window.document.body) {
window.document.body.appendChild(this.popupsOverlayContainer);
}
}
/**
- * Subscribe to this event emitter to be notifed when the breadcrumbs have changed.
+ * Subscribe to this event emitter to be notified when the breadcrumbs have changed.
* The URI is the URI of the editor the breadcrumbs have changed for.
*/
get onDidChangeBreadcrumbs(): Event {
+ // This lazy subscription is to address problems in inversify's instantiation routine
+ // related to use of the IconThemeService by different components instantiated by the
+ // ContributionProvider.
+ if (!this.hasSubscribed) {
+ this.subscribeToContributions();
+ }
return this.onDidChangeBreadcrumbsEmitter.event;
}
/**
- * Notifies that the breadcrumbs for the given URI have changed and should be re-rendered.
- * This fires an `onBreadcrumsChange` event.
+ * Subscribes to the onDidChangeBreadcrumbs events for all contributions.
*/
- breadcrumbsChanges(uri: URI): void {
- this.onDidChangeBreadcrumbsEmitter.fire(uri);
+ protected subscribeToContributions(): void {
+ this.hasSubscribed = true;
+ for (const contribution of this.contributions.getContributions()) {
+ contribution.onDidChangeBreadcrumbs(uri => this.onDidChangeBreadcrumbsEmitter.fire(uri));
+ }
}
/**
@@ -81,11 +92,16 @@ export class BreadcrumbsService {
/**
* Opens a popup for the given breadcrumb at the given position.
*/
- async openPopup(breadcrumb: Breadcrumb, position: { x: number, y: number }): Promise {
+ async openPopup(breadcrumb: Breadcrumb, position: Coordinate): Promise {
const contribution = this.contributions.getContributions().find(c => c.type === breadcrumb.type);
if (contribution) {
- const popup = new BreadcrumbPopupContainer(this.popupsOverlayContainer, breadcrumb.id, position);
- popup.addDisposable(await contribution.attachPopupContent(breadcrumb, popup.container));
+ const popup = this.breadcrumbPopupContainerFactory(this.popupsOverlayContainer, breadcrumb.id, position);
+ const popupContent = await contribution.attachPopupContent(breadcrumb, popup.container);
+ if (popupContent && popup.isOpen) {
+ popup.onDidDispose(() => popupContent.dispose());
+ } else {
+ popupContent?.dispose();
+ }
return popup;
}
}
diff --git a/packages/core/src/browser/breadcrumbs/breadcrumbs.ts b/packages/core/src/browser/breadcrumbs/breadcrumbs.ts
deleted file mode 100644
index afc1c1e84a0b3..0000000000000
--- a/packages/core/src/browser/breadcrumbs/breadcrumbs.ts
+++ /dev/null
@@ -1,25 +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
- ********************************************************************************/
-
-export namespace Breadcrumbs {
- export namespace Styles {
- export const BREADCRUMBS = 'theia-breadcrumbs';
- export const BREADCRUMB_ITEM = 'theia-breadcrumb-item';
- export const BREADCRUMB_POPUP_OVERLAY_CONTAINER = 'theia-breadcrumbs-popups-overlay';
- export const BREADCRUMB_POPUP = 'theia-breadcrumbs-popup';
- export const BREADCRUMB_ITEM_HAS_POPUP = 'theia-breadcrumb-item-haspopup';
- }
-}
diff --git a/packages/core/src/browser/breadcrumbs/index.ts b/packages/core/src/browser/breadcrumbs/index.ts
index 88a71e8df3d44..876c2a406e7a6 100644
--- a/packages/core/src/browser/breadcrumbs/index.ts
+++ b/packages/core/src/browser/breadcrumbs/index.ts
@@ -16,8 +16,6 @@
export * from './breadcrumb-popup-container';
export * from './breadcrumb-renderer';
-export * from './breadcrumb';
-export * from './breadcrumbs-contribution';
export * from './breadcrumbs-renderer';
export * from './breadcrumbs-service';
-export * from './breadcrumbs';
+export * from './breadcrumbs-constants';
diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts
index 0afbb5e5f7d2c..54743fe588b7d 100644
--- a/packages/core/src/browser/common-frontend-contribution.ts
+++ b/packages/core/src/browser/common-frontend-contribution.ts
@@ -1921,6 +1921,51 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
light: '#c5c5c5',
hc: '#c5c5c5'
}, description: 'Editor gutter decoration color for commenting ranges.'
+ },
+ {
+ id: 'breadcrumb.foreground',
+ defaults: {
+ dark: Color.transparent('foreground', 0.8),
+ light: Color.transparent('foreground', 0.8),
+ hc: Color.transparent('foreground', 0.8),
+ },
+ description: 'Color of breadcrumb item text'
+ },
+ {
+ id: 'breadcrumb.background',
+ defaults: {
+ dark: 'editor.background',
+ light: 'editor.background',
+ hc: 'editor.background',
+ },
+ description: 'Color of breadcrumb item background'
+ },
+ {
+ id: 'breadcrumb.focusForeground',
+ defaults: {
+ dark: Color.lighten('foreground', 0.1),
+ light: Color.darken('foreground', 0.2),
+ hc: Color.lighten('foreground', 0.1),
+ },
+ description: 'Color of breadcrumb item text when focused'
+ },
+ {
+ id: 'breadcrumb.activeSelectionForeground',
+ defaults: {
+ dark: Color.lighten('foreground', 0.1),
+ light: Color.darken('foreground', 0.2),
+ hc: Color.lighten('foreground', 0.1),
+ },
+ description: 'Color of selected breadcrumb item'
+ },
+ {
+ id: 'breadcrumbPicker.background',
+ defaults: {
+ dark: 'editorWidget.background',
+ light: 'editorWidget.background',
+ hc: 'editorWidget.background',
+ },
+ description: 'Background color of breadcrumb item picker'
}
);
}
diff --git a/packages/core/src/browser/context-menu-renderer.ts b/packages/core/src/browser/context-menu-renderer.ts
index b3f1f265683e3..59c0a43365fc7 100644
--- a/packages/core/src/browser/context-menu-renderer.ts
+++ b/packages/core/src/browser/context-menu-renderer.ts
@@ -20,12 +20,20 @@ import { injectable } from 'inversify';
import { MenuPath } from '../common/menu';
import { Disposable, DisposableCollection } from '../common/disposable';
-export type Anchor = MouseEvent | { x: number, y: number };
+export interface Coordinate { x: number; y: number; }
+export const Coordinate = Symbol('Coordinate');
-export function toAnchor(anchor: HTMLElement | { x: number, y: number }): Anchor {
+export type Anchor = MouseEvent | Coordinate;
+
+export function toAnchor(anchor: HTMLElement | Coordinate): Anchor {
return anchor instanceof HTMLElement ? { x: anchor.offsetLeft, y: anchor.offsetTop } : anchor;
}
+export function coordinateFromAnchor(anchor: Anchor): Coordinate {
+ const { x, y } = anchor instanceof MouseEvent ? { x: anchor.clientX, y: anchor.clientY } : anchor;
+ return { x, y };
+}
+
export abstract class ContextMenuAccess implements Disposable {
protected readonly toDispose = new DisposableCollection();
diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts
index 3b667c1c7698d..e4d3ab0381215 100644
--- a/packages/core/src/browser/frontend-application-module.ts
+++ b/packages/core/src/browser/frontend-application-module.ts
@@ -50,7 +50,7 @@ import { StatusBar, StatusBarImpl } from './status-bar/status-bar';
import { LabelParser } from './label-parser';
import { LabelProvider, LabelProviderContribution, DefaultUriLabelProviderContribution } from './label-provider';
import { PreferenceService } from './preferences';
-import { ContextMenuRenderer } from './context-menu-renderer';
+import { ContextMenuRenderer, Coordinate } from './context-menu-renderer';
import { ThemeService } from './theming';
import { ConnectionStatusService, FrontendConnectionStatusService, ApplicationConnectionStatusContribution, PingService } from './connection-status-service';
import { DiffUriLabelProviderContribution } from './diff-uris';
@@ -96,17 +96,28 @@ import { keytarServicePath, KeytarService } from '../common/keytar-protocol';
import { CredentialsService, CredentialsServiceImpl } from './credentials-service';
import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter';
import { QuickCommandFrontendContribution } from './quick-input/quick-command-frontend-contribution';
-import { QuickHelpService } from './quick-input/quick-help-service';
import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service';
import {
QuickPickServiceImpl,
- QuickInputFrontendContribution
+ QuickInputFrontendContribution,
+ QuickAccessContribution,
+ QuickCommandService,
+ QuickHelpService
} from './quick-input';
-import { QuickAccessContribution } from './quick-input/quick-access';
-import { QuickCommandService } from './quick-input/quick-command-service';
import { SidebarBottomMenuWidget } from './shell/sidebar-bottom-menu-widget';
import { WindowContribution } from './window-contribution';
-import { BreadcrumbsContribution, BreadcrumbsService } from './breadcrumbs';
+import {
+ BreadcrumbID,
+ BreadcrumbPopupContainer,
+ BreadcrumbPopupContainerFactory,
+ BreadcrumbRenderer,
+ BreadcrumbsContribution,
+ BreadcrumbsRenderer,
+ BreadcrumbsRendererFactory,
+ BreadcrumbsService,
+ DefaultBreadcrumbRenderer,
+} from './breadcrumbs';
+import { RendererHost } from './widgets';
export { bindResourceProvider, bindMessageService, bindPreferenceService };
@@ -151,13 +162,7 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
return container.get(TabBarToolbar);
});
- bind(DockPanelRendererFactory).toFactory(context => () => {
- const { container } = context;
- const tabBarToolbarRegistry = container.get(TabBarToolbarRegistry);
- const tabBarRendererFactory: () => TabBarRenderer = container.get(TabBarRendererFactory);
- const tabBarToolbarFactory: () => TabBarToolbar = container.get(TabBarToolbarFactory);
- return new DockPanelRenderer(tabBarRendererFactory, tabBarToolbarRegistry, tabBarToolbarFactory);
- });
+ bind(DockPanelRendererFactory).toFactory(context => () => context.container.get(DockPanelRenderer));
bind(DockPanelRenderer).toSelf();
bind(TabBarRendererFactory).toFactory(context => () => {
const contextMenuRenderer = context.container.get(ContextMenuRenderer);
@@ -364,4 +369,20 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
}
bindContributionProvider(bind, BreadcrumbsContribution);
bind(BreadcrumbsService).toSelf().inSingletonScope();
+ bind(BreadcrumbsRenderer).toSelf();
+ bind(BreadcrumbsRendererFactory).toFactory(ctx =>
+ () => {
+ const childContainer = ctx.container.createChild();
+ childContainer.bind(BreadcrumbRenderer).to(DefaultBreadcrumbRenderer).inSingletonScope();
+ return childContainer.get(BreadcrumbsRenderer);
+ }
+ );
+ bind(BreadcrumbPopupContainer).toSelf();
+ bind(BreadcrumbPopupContainerFactory).toFactory(({ container }) => (parent: HTMLElement, breadcrumbId: string, position: Coordinate): BreadcrumbPopupContainer => {
+ const child = container.createChild();
+ child.bind(RendererHost).toConstantValue(parent);
+ child.bind(BreadcrumbID).toConstantValue(breadcrumbId);
+ child.bind(Coordinate).toConstantValue(position);
+ return child.get(BreadcrumbPopupContainer);
+ });
});
diff --git a/packages/core/src/browser/menu/browser-context-menu-renderer.ts b/packages/core/src/browser/menu/browser-context-menu-renderer.ts
index e0d6a38b79149..2b664b75f0238 100644
--- a/packages/core/src/browser/menu/browser-context-menu-renderer.ts
+++ b/packages/core/src/browser/menu/browser-context-menu-renderer.ts
@@ -18,7 +18,7 @@
import { inject, injectable } from 'inversify';
import { Menu } from '../widgets';
-import { ContextMenuAccess, ContextMenuRenderer, RenderContextMenuOptions } from '../context-menu-renderer';
+import { ContextMenuAccess, ContextMenuRenderer, coordinateFromAnchor, RenderContextMenuOptions } from '../context-menu-renderer';
import { BrowserMainMenuFactory } from './browser-menu-plugin';
export class BrowserContextMenuAccess extends ContextMenuAccess {
@@ -38,7 +38,7 @@ export class BrowserContextMenuRenderer extends ContextMenuRenderer {
protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): BrowserContextMenuAccess {
const contextMenu = this.menuFactory.createContextMenu(menuPath, args);
- const { x, y } = anchor instanceof MouseEvent ? { x: anchor.clientX, y: anchor.clientY } : anchor!;
+ const { x, y } = coordinateFromAnchor(anchor);
if (onHide) {
contextMenu.aboutToClose.connect(() => onHide!());
}
diff --git a/packages/core/src/browser/navigatable-types.ts b/packages/core/src/browser/navigatable-types.ts
new file mode 100644
index 0000000000000..ae0a381627b21
--- /dev/null
+++ b/packages/core/src/browser/navigatable-types.ts
@@ -0,0 +1,78 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson 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 { MaybeArray } from '../common/types';
+import { Widget, BaseWidget } from './widgets';
+
+/**
+ * `Navigatable` provides an access to an URI of an underlying instance of `Resource`.
+ */
+export interface Navigatable {
+ /**
+ * Return an underlying resource URI.
+ */
+ getResourceUri(): URI | undefined;
+ /**
+ * Creates a new URI to which this navigatable should moved based on the given target resource URI.
+ */
+ createMoveToUri(resourceUri: URI): URI | undefined;
+}
+
+export namespace Navigatable {
+ export function is(arg: Object | undefined): arg is Navigatable {
+ return !!arg && 'getResourceUri' in arg && 'createMoveToUri' in arg;
+ }
+}
+
+export type NavigatableWidget = BaseWidget & Navigatable;
+export namespace NavigatableWidget {
+ export function is(arg: Object | undefined): arg is NavigatableWidget {
+ return arg instanceof BaseWidget && Navigatable.is(arg);
+ }
+ export function* getAffected(
+ widgets: Iterable,
+ context: MaybeArray
+ ): IterableIterator<[URI, T & NavigatableWidget]> {
+ const uris = Array.isArray(context) ? context : [context];
+ return get(widgets, resourceUri => uris.some(uri => uri.isEqualOrParent(resourceUri)));
+ }
+ export function* get(
+ widgets: Iterable,
+ filter: (resourceUri: URI) => boolean = () => true
+ ): IterableIterator<[URI, T & NavigatableWidget]> {
+ for (const widget of widgets) {
+ if (NavigatableWidget.is(widget)) {
+ const resourceUri = widget.getResourceUri();
+ if (resourceUri && filter(resourceUri)) {
+ yield [resourceUri, widget];
+ }
+ }
+ }
+ }
+}
+
+export interface NavigatableWidgetOptions {
+ kind: 'navigatable',
+ uri: string,
+ counter?: number,
+}
+export namespace NavigatableWidgetOptions {
+ export function is(arg: Object | undefined): arg is NavigatableWidgetOptions {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return !!arg && 'kind' in arg && (arg as any).kind === 'navigatable';
+ }
+}
diff --git a/packages/core/src/browser/navigatable.ts b/packages/core/src/browser/navigatable.ts
index 9429dc76ee007..9206549f2b7d3 100644
--- a/packages/core/src/browser/navigatable.ts
+++ b/packages/core/src/browser/navigatable.ts
@@ -15,68 +15,9 @@
********************************************************************************/
import URI from '../common/uri';
-import { MaybeArray } from '../common/types';
-import { Widget, BaseWidget } from './widgets';
import { WidgetOpenHandler, WidgetOpenerOptions } from './widget-open-handler';
-
-/**
- * `Navigatable` provides an access to an URI of an underlying instance of `Resource`.
- */
-export interface Navigatable {
- /**
- * Return an underlying resource URI.
- */
- getResourceUri(): URI | undefined;
- /**
- * Creates a new URI to which this navigatable should moved based on the given target resource URI.
- */
- createMoveToUri(resourceUri: URI): URI | undefined;
-}
-
-export namespace Navigatable {
- export function is(arg: Object | undefined): arg is Navigatable {
- return !!arg && 'getResourceUri' in arg && 'createMoveToUri' in arg;
- }
-}
-
-export type NavigatableWidget = BaseWidget & Navigatable;
-export namespace NavigatableWidget {
- export function is(arg: Object | undefined): arg is NavigatableWidget {
- return arg instanceof BaseWidget && Navigatable.is(arg);
- }
- export function* getAffected(
- widgets: Iterable,
- context: MaybeArray
- ): IterableIterator<[URI, T & NavigatableWidget]> {
- const uris = Array.isArray(context) ? context : [context];
- return get(widgets, resourceUri => uris.some(uri => uri.isEqualOrParent(resourceUri)));
- }
- export function* get(
- widgets: Iterable,
- filter: (resourceUri: URI) => boolean = () => true
- ): IterableIterator<[URI, T & NavigatableWidget]> {
- for (const widget of widgets) {
- if (NavigatableWidget.is(widget)) {
- const resourceUri = widget.getResourceUri();
- if (resourceUri && filter(resourceUri)) {
- yield [resourceUri, widget];
- }
- }
- }
- }
-}
-
-export interface NavigatableWidgetOptions {
- kind: 'navigatable',
- uri: string,
- counter?: number,
-}
-export namespace NavigatableWidgetOptions {
- export function is(arg: Object | undefined): arg is NavigatableWidgetOptions {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return !!arg && 'kind' in arg && (arg as any).kind === 'navigatable';
- }
-}
+import { NavigatableWidget, NavigatableWidgetOptions } from './navigatable-types';
+export * from './navigatable-types';
export abstract class NavigatableWidgetOpenHandler extends WidgetOpenHandler {
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index 0732c39415eba..ff0026676d375 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -38,6 +38,7 @@ import { Emitter } from '../../common/event';
import { waitForRevealed, waitForClosed } from '../widgets';
import { CorePreferences } from '../core-preferences';
import { environment } from '../../common';
+import { BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer';
/** The class name added to ApplicationShell instances. */
const APPLICATION_SHELL_CLASS = 'theia-ApplicationShell';
@@ -80,19 +81,24 @@ export class DockPanelRenderer implements DockLayout.IRenderer {
constructor(
@inject(TabBarRendererFactory) protected readonly tabBarRendererFactory: () => TabBarRenderer,
@inject(TabBarToolbarRegistry) protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
- @inject(TabBarToolbarFactory) protected readonly tabBarToolbarFactory: () => TabBarToolbar
+ @inject(TabBarToolbarFactory) protected readonly tabBarToolbarFactory: () => TabBarToolbar,
+ @inject(BreadcrumbsRendererFactory) protected readonly breadcrumbsRendererFactory: BreadcrumbsRendererFactory,
) { }
createTabBar(): TabBar {
const renderer = this.tabBarRendererFactory();
- const tabBar = new ToolbarAwareTabBar(this.tabBarToolbarRegistry, this.tabBarToolbarFactory, {
- renderer,
- // Scroll bar options
- handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
- useBothWheelAxes: true,
- scrollXMarginOffset: 4,
- suppressScrollY: true
- });
+ const tabBar = new ToolbarAwareTabBar(
+ this.tabBarToolbarRegistry,
+ this.tabBarToolbarFactory,
+ this.breadcrumbsRendererFactory,
+ {
+ renderer,
+ // Scroll bar options
+ handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
+ useBothWheelAxes: true,
+ scrollXMarginOffset: 4,
+ suppressScrollY: true
+ });
this.tabBarClasses.forEach(c => tabBar.addClass(c));
renderer.tabBar = tabBar;
tabBar.disposed.connect(() => renderer.dispose());
diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts
index d7ee97c36c8c0..65768bbad632a 100644
--- a/packages/core/src/browser/shell/tab-bars.ts
+++ b/packages/core/src/browser/shell/tab-bars.ts
@@ -20,7 +20,7 @@ import { VirtualElement, h, VirtualDOM, ElementInlineStyle } from '@phosphor/vir
import { Disposable, DisposableCollection, MenuPath, notEmpty } from '../../common';
import { ContextMenuRenderer } from '../context-menu-renderer';
import { Signal, Slot } from '@phosphor/signaling';
-import { Message } from '@phosphor/messaging';
+import { Message, MessageLoop } from '@phosphor/messaging';
import { ArrayExt } from '@phosphor/algorithm';
import { ElementExt } from '@phosphor/domutils';
import { TabBarToolbarRegistry, TabBarToolbar } from './tab-bar-toolbar';
@@ -28,6 +28,8 @@ import { TheiaDockPanel, MAIN_AREA_ID, BOTTOM_AREA_ID } from './theia-dock-panel
import { WidgetDecoration } from '../widget-decoration';
import { TabBarDecoratorService } from './tab-bar-decorator';
import { IconThemeService } from '../icon-theme-service';
+import { BreadcrumbsRenderer, BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer';
+import { NavigatableWidget } from '../navigatable-types';
/** The class name added to hidden content nodes, which are required to render vertical side bars. */
const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content';
@@ -576,22 +578,38 @@ export class ScrollableTabBar extends TabBar {
*
* +-------------------------+-----------------+
* |[TAB_0][TAB_1][TAB_2][TAB| Toolbar |
- * +-------------Scrollable--+-None-Scrollable-+
+ * +-------------Scrollable--+-Non-Scrollable-+
*
*/
export class ToolbarAwareTabBar extends ScrollableTabBar {
- protected contentContainer: HTMLElement | undefined;
+ protected contentContainer: HTMLElement;
protected toolbar: TabBarToolbar | undefined;
+ protected breadcrumbsContainer: HTMLElement;
+ protected readonly breadcrumbsRenderer: BreadcrumbsRenderer;
+ protected topRow: HTMLElement;
constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
protected readonly tabBarToolbarFactory: () => TabBarToolbar,
- protected readonly options?: TabBar.IOptions & PerfectScrollbar.Options) {
-
+ protected readonly breadcrumbsRendererFactory: BreadcrumbsRendererFactory,
+ protected readonly options?: TabBar.IOptions & PerfectScrollbar.Options,
+ ) {
super(options);
+ this.breadcrumbsRenderer = this.breadcrumbsRendererFactory();
this.rewireDOM();
this.toDispose.push(this.tabBarToolbarRegistry.onDidChange(() => this.update()));
+ this.toDispose.push(this.breadcrumbsRenderer);
+ this.toDispose.push(this.breadcrumbsRenderer.onDidChangeActiveState(active => {
+ this.node.classList.toggle('theia-tabBar-multirow', active);
+ if (this.parent) {
+ MessageLoop.sendMessage(this.parent, new Message('fit-request'));
+ }
+ }));
+ this.node.classList.toggle('theia-tabBar-multirow', this.breadcrumbsRenderer.active);
+ const handler = () => this.updateBreadcrumbs();
+ this.currentChanged.connect(handler);
+ this.toDispose.push(Disposable.create(() => this.currentChanged.disconnect(handler)));
}
/**
@@ -612,12 +630,22 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
return this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER)[0] as HTMLElement;
}
+ protected async updateBreadcrumbs(): Promise {
+ const current = this.currentTitle?.owner;
+ const uri = NavigatableWidget.is(current) ? current.getResourceUri() : undefined;
+ await this.breadcrumbsRenderer.refresh(uri);
+ }
+
protected onAfterAttach(msg: Message): void {
if (this.toolbar) {
if (this.toolbar.isAttached) {
Widget.detach(this.toolbar);
}
- Widget.attach(this.toolbar, this.node);
+ Widget.attach(this.toolbar, this.topRow);
+ if (this.breadcrumbsContainer) {
+ this.node.appendChild(this.breadcrumbsContainer);
+ }
+ this.breadcrumbsRenderer?.refresh();
}
super.onAfterAttach(msg);
}
@@ -660,16 +688,22 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
protected rewireDOM(): void {
const contentNode = this.node.getElementsByClassName(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT)[0];
if (!contentNode) {
- throw new Error("'this.node' does not have the content as a direct children with class name 'p-TabBar-content'.");
+ throw new Error("'this.node' does not have the content as a direct child with class name 'p-TabBar-content'.");
}
this.node.removeChild(contentNode);
+ this.topRow = document.createElement('div');
+ this.topRow.classList.add('theia-tabBar-tab-row');
this.contentContainer = document.createElement('div');
this.contentContainer.classList.add(ToolbarAwareTabBar.Styles.TAB_BAR_CONTENT_CONTAINER);
this.contentContainer.appendChild(contentNode);
- this.node.appendChild(this.contentContainer);
+ this.topRow.appendChild(this.contentContainer);
+ this.node.appendChild(this.topRow);
this.toolbar = this.tabBarToolbarFactory();
+ this.breadcrumbsContainer = document.createElement('div');
+ this.breadcrumbsContainer.classList.add('theia-tabBar-breadcrumb-row');
+ this.breadcrumbsContainer.appendChild(this.breadcrumbsRenderer.host);
+ this.node.appendChild(this.breadcrumbsContainer);
}
-
}
export namespace ToolbarAwareTabBar {
diff --git a/packages/core/src/browser/style/breadcrumbs.css b/packages/core/src/browser/style/breadcrumbs.css
index 04c13d176e12b..f7c858c6a1141 100644
--- a/packages/core/src/browser/style/breadcrumbs.css
+++ b/packages/core/src/browser/style/breadcrumbs.css
@@ -14,21 +14,30 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
+:root {
+ --theia-breadcrumbs-height: 22px;
+}
+
.theia-breadcrumbs {
+ height: var(--theia-breadcrumbs-height);
position: relative;
user-select: none;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
+ align-items: center;
outline-style: none;
margin: .5rem;
list-style-type: none;
overflow: hidden;
+ padding: 0;
+ margin: 0;
+ background-color: var(--theia-breadcrumb-background);
}
.theia-breadcrumbs .ps__thumb-x {
- /* Same scrollbar height than in tab bar. */
+ /* Same scrollbar height as in tab bar. */
height: var(--theia-private-horizontal-tab-scrollbar-height) !important;
}
@@ -39,18 +48,36 @@
white-space: nowrap;
align-self: center;
height: 100%;
+ color: var(--theia-breadcrumb-foreground);
outline: none;
- padding: .25rem .3rem .25rem .25rem;
+ padding: 0 .3rem 0 .25rem;
}
-.theia-breadcrumbs .theia-breadcrumb-item::before {
- font-family: FontAwesome;
- font-size: calc(var(--theia-content-font-size) * 0.8);
- content: "\F0DA";
+.theia-breadcrumbs .theia-breadcrumb-item:hover {
+ color: var(--theia-breadcrumb-focusForeground);
+}
+
+.theia-breadcrumbs .theia-breadcrumb-item:not(:last-of-type)::after {
+ font-family: codicon;
+ font-size: var(--theia-ui-font-size2);
+ content: "\eab6";
display: flex;
align-items: center;
width: .8em;
text-align: right;
+ padding-left: 4px;
+}
+
+.theia-breadcrumbs .theia-breadcrumb-item::before {
+ width: 16px;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.theia-breadcrumbs .theia-breadcrumb-item:first-of-type::before {
+ content: " ";
}
.theia-breadcrumb-item-haspopup:hover {
@@ -68,7 +95,7 @@
max-height: 200px;
z-index: 10000;
padding: 0px;
- background: var(--theia-menu-color1);
+ background: var(--theia-breadcrumbPicker-background);
font-size: var(--theia-ui-font-size1);
color: var(--theia-ui-font-color1);
box-shadow: 0px 1px 6px rgba(0, 0, 0, 0.2);
diff --git a/packages/core/src/browser/style/scrollbars.css b/packages/core/src/browser/style/scrollbars.css
index cf731c6c3f2d7..94c79dbef8703 100644
--- a/packages/core/src/browser/style/scrollbars.css
+++ b/packages/core/src/browser/style/scrollbars.css
@@ -48,12 +48,14 @@
|----------------------------------------------------------------------------*/
#theia-app-shell .ps__rail-x,
-#theia-dialog-shell .ps__rail-x {
+#theia-dialog-shell .ps__rail-x,
+#theia-breadcrumbs-popups-overlay .ps__rail-x {
height: var(--theia-scrollbar-rail-width);
}
#theia-app-shell .ps__rail-x > .ps__thumb-x,
-#theia-dialog-shell .ps__rail-x > .ps__thumb-x {
+#theia-dialog-shell .ps__rail-x > .ps__thumb-x,
+#theia-breadcrumbs-popups-overlay .ps__thumb-x {
height: var(--theia-scrollbar-width);
bottom: calc((var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2);
background: var(--theia-scrollbarSlider-background);
@@ -63,7 +65,9 @@
#theia-app-shell .ps__rail-x:hover,
#theia-app-shell .ps__rail-x:focus,
#theia-dialog-shell .ps__rail-x:hover,
-#theia-dialog-shell .ps__rail-x:focus {
+#theia-dialog-shell .ps__rail-x:focus,
+#theia-breadcrumbs-popups-overlay .ps__rail-x:hover,
+#theia-breadcrumbs-popups-overlay .ps__rail-x:focus {
height: var(--theia-scrollbar-rail-width);
}
@@ -72,17 +76,22 @@
#theia-app-shell .ps__rail-x.ps--clicking .ps__thumb-x,
#theia-dialog-shell .ps__rail-x:hover > .ps__thumb-x,
#theia-dialog-shell .ps__rail-x:focus > .ps__thumb-x,
-#theia-dialog-shell .ps__rail-x.ps--clicking .ps__thumb-x {
+#theia-dialog-shell .ps__rail-x.ps--clicking .ps__thumb-x,
+#theia-breadcrumbs-popups-overlay .ps__rail-x:hover > .ps__thumb-x,
+#theia-breadcrumbs-popups-overlay .ps__rail-x:focus > .ps__thumb-x,
+#theia-breadcrumbs-popups-overlay .ps__rail-x.ps--clicking .ps__thumb-x {
height: var(--theia-scrollbar-width);
}
#theia-app-shell .ps__rail-y,
-#theia-dialog-shell .ps__rail-y {
+#theia-dialog-shell .ps__rail-y,
+#theia-breadcrumbs-popups-overlay .ps__rail-y {
width: var(--theia-scrollbar-rail-width);
}
#theia-app-shell .ps__rail-y > .ps__thumb-y,
-#theia-dialog-shell .ps__rail-y > .ps__thumb-y {
+#theia-dialog-shell .ps__rail-y > .ps__thumb-y,
+#theia-breadcrumbs-popups-overlay .ps__rail-y > .ps__thumb-y {
width: var(--theia-scrollbar-width);
right: calc((var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2);
background: var(--theia-scrollbarSlider-background);
@@ -92,7 +101,9 @@
#theia-app-shell .ps__rail-y:hover,
#theia-app-shell .ps__rail-y:focus,
#theia-dialog-shell .ps__rail-y:hover,
-#theia-dialog-shell .ps__rail-y:focus {
+#theia-dialog-shell .ps__rail-y:focus,
+#theia-breadcrumbs-popups-overlay .ps__rail-y:hover,
+#theia-breadcrumbs-popups-overlay .ps__rail-y:focus {
width: var(--theia-scrollbar-rail-width);
}
@@ -101,27 +112,32 @@
#theia-app-shell .ps__rail-y.ps--clicking .ps__thumb-y,
#theia-dialog-shell .ps__rail-y:hover > .ps__thumb-y,
#theia-dialog-shell .ps__rail-y:focus > .ps__thumb-y,
-#theia-dialog-shell .ps__rail-y.ps--clicking .ps__thumb-y {
+#theia-dialog-shell .ps__rail-y.ps--clicking .ps__thumb-y,
+#theia-breadcrumbs-popups-overlay .ps__rail-y:hover > .ps__thumb-y,
+#theia-breadcrumbs-popups-overlay .ps__rail-y:focus > .ps__thumb-y,
+#theia-breadcrumbs-popups-overlay .ps__rail-y.ps--clicking .ps__thumb-y {
right: calc((var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2);
width: var(--theia-scrollbar-width);
}
#theia-app-shell .ps [class^='ps__rail'].ps--clicking > [class^='ps__thumb'],
-#theia-dialog-shell .ps [class^='ps__rail'].ps--clicking > [class^='ps__thumb']{
+#theia-dialog-shell .ps [class^='ps__rail'].ps--clicking > [class^='ps__thumb'],
+#theia-breadcrumbs-popups-overlay .ps [class^='ps__rail'].ps--clicking > [class^='ps__thumb'] {
background-color: var(--theia-scrollbarSlider-activeBackground);
}
#theia-app-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:hover,
#theia-app-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:focus,
#theia-dialog-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:hover,
-#theia-dialog-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:focus
-{
+#theia-dialog-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:focus,
+#theia-breadcrumbs-popups-overlay .ps [class^='ps__rail'] > [class^='ps__thumb']:hover,
+#theia-breadcrumbs-popups-overlay .ps [class^='ps__rail'] > [class^='ps__thumb']:focus {
background: var(--theia-scrollbarSlider-hoverBackground);
}
#theia-app-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:active,
-#theia-dialog-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:active
-{
+#theia-dialog-shell .ps [class^='ps__rail'] > [class^='ps__thumb']:active,
+#theia-breadcrumbs-popups-overlay .ps [class^='ps__rail'] > [class^='ps__thumb']:active {
background: var(--theia-scrollbarSlider-activeBackground);
}
@@ -130,7 +146,10 @@
#theia-app-shell .ps--scrolling-y > [class^='ps__rail'],
#theia-dialog-shell .ps:hover > [class^='ps__rail'],
#theia-dialog-shell .ps--focus > [class^='ps__rail'],
-#theia-dialog-shell .ps--scrolling-y > [class^='ps__rail'] {
+#theia-dialog-shell .ps--scrolling-y > [class^='ps__rail'],
+#theia-breadcrumbs-popups-overlay .ps:hover > [class^='ps__rail'],
+#theia-breadcrumbs-popups-overlay .ps--focus > [class^='ps__rail'],
+#theia-breadcrumbs-popups-overlay .ps--scrolling-y > [class^='ps__rail'] {
opacity: 1;
background: transparent;
}
diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css
index 112fa4c685405..e369c48720421 100644
--- a/packages/core/src/browser/style/tabs.css
+++ b/packages/core/src/browser/style/tabs.css
@@ -9,6 +9,7 @@
--theia-private-horizontal-tab-scrollbar-height: 5px;
--theia-tabbar-toolbar-z-index: 1001;
--theia-toolbar-active-transform-scale: 1.272019649;
+ --theia-horizontal-toolbar-height: calc(var(--theia-private-horizontal-tab-height) + var(--theia-private-horizontal-tab-scrollbar-rail-height) / 2);
}
/*-----------------------------------------------------------------------------
@@ -22,7 +23,7 @@
.p-TabBar[data-orientation='horizontal'] {
overflow-x: hidden;
overflow-y: hidden;
- min-height: calc(var(--theia-private-horizontal-tab-height) + var(--theia-private-horizontal-tab-scrollbar-rail-height) / 2);
+ min-height: var(--theia-horizontal-toolbar-height);
}
.p-TabBar .p-TabBar-content {
@@ -31,7 +32,7 @@
.p-TabBar[data-orientation='horizontal'] .p-TabBar-tab {
flex: none;
- height: calc(var(--theia-private-horizontal-tab-height) + var(--theia-private-horizontal-tab-scrollbar-rail-height) / 2);
+ height: var(--theia-horizontal-toolbar-height);
min-width: 35px;
line-height: var(--theia-private-horizontal-tab-height);
padding: 0px 8px;
@@ -279,23 +280,23 @@ body.theia-editor-highlightModifiedTabs
| Perfect scrollbar
|----------------------------------------------------------------------------*/
-.p-TabBar[data-orientation='horizontal'] > .p-TabBar-content-container > .ps__rail-x {
+.p-TabBar[data-orientation='horizontal'] .p-TabBar-content-container > .ps__rail-x {
height: var(--theia-private-horizontal-tab-scrollbar-rail-height);
z-index: 1000;
}
-.p-TabBar[data-orientation='horizontal'] > .p-TabBar-content-container > .ps__rail-x > .ps__thumb-x {
+.p-TabBar[data-orientation='horizontal'] .p-TabBar-content-container > .ps__rail-x > .ps__thumb-x {
height: var(--theia-private-horizontal-tab-scrollbar-height) !important;
bottom: calc((var(--theia-private-horizontal-tab-scrollbar-rail-height) - var(--theia-private-horizontal-tab-scrollbar-height)) / 2);
}
-.p-TabBar[data-orientation='horizontal'] > .p-TabBar-content-container > .ps__rail-x:hover,
-.p-TabBar[data-orientation='horizontal'] > .p-TabBar-content-container > .ps__rail-x:focus {
+.p-TabBar[data-orientation='horizontal'] .p-TabBar-content-container > .ps__rail-x:hover,
+.p-TabBar[data-orientation='horizontal'] .p-TabBar-content-container > .ps__rail-x:focus {
height: var(--theia-private-horizontal-tab-scrollbar-rail-height) !important;
}
-.p-TabBar[data-orientation='horizontal'] > .p-TabBar-content-container > .ps__rail-x:hover > .ps__thumb-x,
-.p-TabBar[data-orientation='horizontal'] > .p-TabBar-content-container > .ps__rail-x:focus > .ps__thumb-x {
+.p-TabBar[data-orientation='horizontal'] .p-TabBar-content-container > .ps__rail-x:hover > .ps__thumb-x,
+.p-TabBar[data-orientation='horizontal'] .p-TabBar-content-container > .ps__rail-x:focus > .ps__thumb-x {
height: calc(var(--theia-private-horizontal-tab-scrollbar-height) / 2) !important;
bottom: calc((var(--theia-private-horizontal-tab-scrollbar-rail-height) - var(--theia-private-horizontal-tab-scrollbar-height)) / 2);
}
@@ -387,3 +388,18 @@ body.theia-editor-highlightModifiedTabs
.p-TabBar-toolbar .item .cancel {
background: var(--theia-icon-close) no-repeat;
}
+
+.theia-tabBar-breadcrumb-row {
+ min-width: 100%;
+}
+
+.p-TabBar.theia-tabBar-multirow[data-orientation='horizontal'] {
+ min-height: calc(var(--theia-breadcrumbs-height) + var(--theia-horizontal-toolbar-height));
+ flex-direction: column;
+}
+
+.theia-tabBar-tab-row {
+ display: flex;
+ flex-flow: row nowrap;
+ min-width: 100%;
+}
diff --git a/packages/core/src/browser/widgets/react-renderer.tsx b/packages/core/src/browser/widgets/react-renderer.tsx
index f4d829268d0a6..0db1c2595e9b6 100644
--- a/packages/core/src/browser/widgets/react-renderer.tsx
+++ b/packages/core/src/browser/widgets/react-renderer.tsx
@@ -14,17 +14,19 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import { injectable } from 'inversify';
+import { inject, injectable, optional } from 'inversify';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Disposable } from '../../common';
-import { injectable } from 'inversify';
+
+export type RendererHost = HTMLElement;
+export const RendererHost = Symbol('RendererHost');
@injectable()
export class ReactRenderer implements Disposable {
readonly host: HTMLElement;
constructor(
- host?: HTMLElement
+ @inject(RendererHost) @optional() host?: RendererHost
) {
this.host = host || document.createElement('div');
}
diff --git a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
index 537958969084c..c73b72d4565d2 100644
--- a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
+++ b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
@@ -18,7 +18,7 @@
import * as electron from '../../../shared/electron';
import { inject, injectable } from 'inversify';
-import { ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands } from '../../browser';
+import { ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor } from '../../browser';
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { ContextMenuContext } from '../../browser/menu/context-menu-context';
import { MenuPath, MenuContribution, MenuModelRegistry } from '../../common';
@@ -84,7 +84,7 @@ export class ElectronContextMenuRenderer extends ContextMenuRenderer {
protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): ElectronContextMenuAccess {
const menu = this.menuFactory.createContextMenu(menuPath, args);
- const { x, y } = anchor instanceof MouseEvent ? { x: anchor.clientX, y: anchor.clientY } : anchor!;
+ const { x, y } = coordinateFromAnchor(anchor);
const zoom = electron.webFrame.getZoomFactor();
// x and y values must be Ints or else there is a conversion error
menu.popup({ x: Math.round(x * zoom), y: Math.round(y * zoom) });
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 7e8a07c085dcf..e11061b38b2f4 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -6,8 +6,7 @@
"@theia/core": "1.17.0",
"@theia/variable-resolver": "1.17.0",
"@types/base64-arraybuffer": "0.1.0",
- "base64-arraybuffer": "^0.1.5",
- "perfect-scrollbar": "^1.3.0"
+ "base64-arraybuffer": "^0.1.5"
},
"publishConfig": {
"access": "public"
diff --git a/packages/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts
index e16130d59c37b..330b5f8bdb67b 100644
--- a/packages/editor/src/browser/editor-frontend-module.ts
+++ b/packages/editor/src/browser/editor-frontend-module.ts
@@ -35,14 +35,6 @@ import { NavigationLocationSimilarity } from './navigation/navigation-location-s
import { EditorVariableContribution } from './editor-variable-contribution';
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
import { QuickEditorService } from './quick-editor-service';
-import {
- BreadcrumbsRendererFactory,
- BreadcrumbsRenderer,
- BreadcrumbsURI,
- BreadcrumbRenderer,
- DefaultBreadcrumbRenderer
-} from '@theia/core/lib/browser/breadcrumbs';
-import URI from '@theia/core/lib/common/uri';
export default new ContainerModule(bind => {
bindEditorPreferences(bind);
@@ -87,14 +79,4 @@ export default new ContainerModule(bind => {
bind(ActiveEditorAccess).toSelf().inSingletonScope();
bind(EditorAccess).to(CurrentEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.CURRENT);
bind(EditorAccess).to(ActiveEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.ACTIVE);
-
- bind(BreadcrumbsRendererFactory).toFactory(ctx =>
- (uri: URI) => {
- const childContainer = ctx.container.createChild();
- childContainer.bind(BreadcrumbsURI).toConstantValue(uri);
- childContainer.bind(BreadcrumbsRenderer).toSelf();
- childContainer.bind(BreadcrumbRenderer).to(DefaultBreadcrumbRenderer).inSingletonScope();
- return childContainer.get(BreadcrumbsRenderer);
- }
- );
});
diff --git a/packages/editor/src/browser/editor-widget-factory.ts b/packages/editor/src/browser/editor-widget-factory.ts
index 985eae6e39b5f..f9e007059ea31 100644
--- a/packages/editor/src/browser/editor-widget-factory.ts
+++ b/packages/editor/src/browser/editor-widget-factory.ts
@@ -20,7 +20,6 @@ import { SelectionService } from '@theia/core/lib/common';
import { NavigatableWidgetOptions, WidgetFactory, LabelProvider } from '@theia/core/lib/browser';
import { EditorWidget } from './editor-widget';
import { TextEditorProvider } from './editor';
-import { BreadcrumbsRendererFactory } from '@theia/core/lib/browser/breadcrumbs';
@injectable()
export class EditorWidgetFactory implements WidgetFactory {
@@ -44,9 +43,6 @@ 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, options);
@@ -71,8 +67,7 @@ export class EditorWidgetFactory implements WidgetFactory {
protected async constructEditor(uri: URI): Promise {
const textEditor = await this.editorProvider(uri);
- const breadcrumbsRenderer = this.breadcrumbsRendererFactory(uri);
- return new EditorWidget(textEditor, breadcrumbsRenderer, this.selectionService);
+ return new EditorWidget(textEditor, this.selectionService);
}
private setLabels(editor: EditorWidget, uri: URI): void {
diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts
index 04ef3af4dd1ad..f875433999f91 100644
--- a/packages/editor/src/browser/editor-widget.ts
+++ b/packages/editor/src/browser/editor-widget.ts
@@ -18,19 +18,14 @@ import { Disposable, SelectionService, Event } from '@theia/core/lib/common';
import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { TextEditor } from './editor';
-import { BreadcrumbsRenderer } from '@theia/core/lib/browser/breadcrumbs';
export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget {
constructor(
readonly editor: TextEditor,
- readonly breadcrumbsRenderer: BreadcrumbsRenderer,
protected readonly selectionService: SelectionService
) {
- super(EditorWidget.createParentNode(editor, breadcrumbsRenderer));
-
- this.toDispose.push(this.breadcrumbsRenderer);
-
+ super(editor);
this.addClass('theia-editor');
this.toDispose.push(this.editor);
this.toDispose.push(this.editor.onSelectionChanged(() => this.setSelection()));
@@ -48,13 +43,6 @@ 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;
}
@@ -76,14 +64,12 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
super.onAfterAttach(msg);
if (this.isVisible) {
this.editor.refresh();
- this.breadcrumbsRenderer.refresh();
}
}
protected onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.editor.refresh();
- this.breadcrumbsRenderer.refresh();
}
protected onResize(msg: Widget.ResizeMessage): void {
diff --git a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumb.ts b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumb.ts
index ab76042f7fdcc..4ab4303fdc4c0 100644
--- a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumb.ts
+++ b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumb.ts
@@ -14,16 +14,17 @@
* 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';
+import { Breadcrumb } from '@theia/core/lib/browser/breadcrumbs/breadcrumbs-constants';
+import { FilepathBreadcrumbType } from './filepath-breadcrumbs-contribution';
export class FilepathBreadcrumb implements Breadcrumb {
constructor(
readonly uri: URI,
readonly label: string,
readonly longLabel: string,
- readonly iconClass: string
+ readonly iconClass: string,
+ readonly containerClass: string,
) { }
get id(): string {
diff --git a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts
index 843caed6cadd1..3c47fd0064cc5 100644
--- a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts
+++ b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-container.ts
@@ -14,8 +14,8 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import { Container, interfaces, injectable, inject } from 'inversify';
-import { TreeProps, ContextMenuRenderer, TreeNode, OpenerService, NodeProps } from '@theia/core/lib/browser';
+import { Container, interfaces, injectable, inject } from '@theia/core/shared/inversify';
+import { TreeProps, ContextMenuRenderer, TreeNode, OpenerService, open, NodeProps, defaultTreeProps } from '@theia/core/lib/browser';
import { createFileTreeContainer, FileTreeWidget } from '../';
import { FileTreeModel, FileStatNode } from '../file-tree';
@@ -24,6 +24,7 @@ const BREADCRUMBS_FILETREE_CLASS = 'theia-FilepathBreadcrumbFileTree';
export function createFileTreeBreadcrumbsContainer(parent: interfaces.Container): Container {
const child = createFileTreeContainer(parent);
child.unbind(FileTreeWidget);
+ child.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, virtualized: false });
child.bind(BreadcrumbsFileTreeWidget).toSelf();
return child;
}
@@ -57,8 +58,7 @@ export class BreadcrumbsFileTreeWidget extends FileTreeWidget {
protected handleClickEvent(node: TreeNode | undefined, event: React.MouseEvent): void {
if (FileStatNode.is(node) && !node.fileStat.isDirectory) {
- this.openerService.getOpener(node.uri)
- .then(opener => opener.open(node.uri));
+ open(this.openerService, node.uri, { preview: true });
} else {
super.handleClickEvent(node, event);
}
diff --git a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.ts b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.ts
index aab9dbe35ff0d..7663cd01ddb35 100644
--- a/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.ts
+++ b/packages/filesystem/src/browser/breadcrumbs/filepath-breadcrumbs-contribution.ts
@@ -14,31 +14,39 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import { BreadcrumbsContribution } from '@theia/core/lib/browser/breadcrumbs/breadcrumbs-contribution';
-import { Breadcrumb } from '@theia/core/lib/browser/breadcrumbs/breadcrumb';
-import { FilepathBreadcrumb } from './filepath-breadcrumb';
-import { injectable, inject } from 'inversify';
-import { LabelProvider, Widget } from '@theia/core/lib/browser';
-import { FileSystem, FileStat } from '../../common';
+import { Disposable, Emitter, Event } from '@theia/core';
+import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
+import { Breadcrumb, BreadcrumbsContribution, CompositeTreeNode, LabelProvider, SelectableTreeNode, Widget } from '@theia/core/lib/browser';
+import { FilepathBreadcrumb } from './filepath-breadcrumb';
import { BreadcrumbsFileTreeWidget } from './filepath-breadcrumbs-container';
import { DirNode } from '../file-tree';
-import { Disposable } from '@theia/core';
+import { FileService } from '../file-service';
+import { FileStat } from '../../common/files';
export const FilepathBreadcrumbType = Symbol('FilepathBreadcrumb');
+export interface FilepathBreadcrumbClassNameFactory {
+ (location: URI, index: number): string;
+}
+
@injectable()
export class FilepathBreadcrumbsContribution implements BreadcrumbsContribution {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
- @inject(FileSystem)
- protected readonly fileSystem: FileSystem;
+ @inject(FileService)
+ protected readonly fileSystem: FileService;
@inject(BreadcrumbsFileTreeWidget)
protected readonly breadcrumbsFileTreeWidget: BreadcrumbsFileTreeWidget;
+ protected readonly onDidChangeBreadcrumbsEmitter = new Emitter();
+ get onDidChangeBreadcrumbs(): Event {
+ return this.onDidChangeBreadcrumbsEmitter.event;
+ }
+
readonly type = FilepathBreadcrumbType;
readonly priority: number = 100;
@@ -46,13 +54,30 @@ export class FilepathBreadcrumbsContribution implements BreadcrumbsContribution
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));
+ const getContainerClass = this.getContainerClassCreator(uri);
+ const getIconClass = this.getIconClassCreator(uri);
+ return uri.allLocations
+ .map((location, index) => {
+ const icon = getIconClass(location, index);
+ const containerClass = getContainerClass(location, index);
+ return new FilepathBreadcrumb(
+ location,
+ this.labelProvider.getName(location),
+ this.labelProvider.getLongName(location),
+ icon,
+ containerClass,
+ );
+ })
+ .filter(b => this.filterBreadcrumbs(uri, b))
+ .reverse();
+ }
+
+ protected getContainerClassCreator(fileURI: URI): FilepathBreadcrumbClassNameFactory {
+ return (location, index) => location.isEqual(fileURI) ? 'file' : 'folder';
+ }
+
+ protected getIconClassCreator(fileURI: URI): FilepathBreadcrumbClassNameFactory {
+ return (location, index) => location.isEqual(fileURI) ? this.labelProvider.getIcon(location) + ' file-icon' : '';
}
protected filterBreadcrumbs(_: URI, breadcrumb: FilepathBreadcrumb): boolean {
@@ -63,30 +88,42 @@ export class FilepathBreadcrumbsContribution implements BreadcrumbsContribution
if (!FilepathBreadcrumb.is(breadcrumb)) {
return undefined;
}
- const folderFileStat = await this.fileSystem.getFileStat(breadcrumb.uri.parent.toString());
+ const folderFileStat = await this.fileSystem.resolve(breadcrumb.uri.parent);
if (folderFileStat) {
const rootNode = await this.createRootNode(folderFileStat);
- await this.breadcrumbsFileTreeWidget.model.navigateTo(rootNode);
- Widget.attach(this.breadcrumbsFileTreeWidget, parent);
- return {
- dispose: () => {
- // Clear model otherwise the next time a popup is opened the old model is rendered first
- // and is shown for a short time period.
- this.breadcrumbsFileTreeWidget.model.root = undefined;
- Widget.detach(this.breadcrumbsFileTreeWidget);
- }
- };
+ if (rootNode) {
+ const { model } = this.breadcrumbsFileTreeWidget;
+ await model.navigateTo({ ...rootNode, visible: false });
+ Widget.attach(this.breadcrumbsFileTreeWidget, parent);
+ const toDisposeOnTreePopulated = model.onChanged(() => {
+ if (CompositeTreeNode.is(model.root) && model.root.children.length > 0) {
+ toDisposeOnTreePopulated.dispose();
+ const targetNode = model.getNode(breadcrumb.uri.path.toString());
+ if (targetNode && SelectableTreeNode.is(targetNode)) {
+ model.selectNode(targetNode);
+ }
+ this.breadcrumbsFileTreeWidget.activate();
+ }
+ });
+ return {
+ dispose: () => {
+ // Clear model otherwise the next time a popup is opened the old model is rendered first
+ // and is shown for a short time period.
+ toDisposeOnTreePopulated.dispose();
+ this.breadcrumbsFileTreeWidget.model.root = undefined;
+ Widget.detach(this.breadcrumbsFileTreeWidget);
+ }
+ };
+ }
}
}
protected async createRootNode(folderToOpen: FileStat): Promise {
- const folderUri = new URI(folderToOpen.uri);
+ const folderUri = folderToOpen.resource;
const rootUri = folderToOpen.isDirectory ? folderUri : folderUri.parent;
- const name = this.labelProvider.getName(rootUri);
- const rootStat = await this.fileSystem.getFileStat(rootUri.toString());
+ const rootStat = await this.fileSystem.resolve(rootUri);
if (rootStat) {
- const label = await this.labelProvider.getIcon(rootStat);
- return DirNode.createRoot(rootStat, name, label);
+ return DirNode.createRoot(rootStat);
}
}
}
diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts
index f50947daab644..3e7d70bcb2d4d 100644
--- a/packages/filesystem/src/browser/filesystem-frontend-module.ts
+++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts
@@ -18,7 +18,7 @@ import '../../src/browser/style/index.css';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { ResourceResolver, CommandContribution } from '@theia/core/lib/common';
-import { WebSocketConnectionProvider, FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser';
+import { WebSocketConnectionProvider, FrontendApplicationContribution, LabelProviderContribution, BreadcrumbsContribution } from '@theia/core/lib/browser';
import { FileResourceResolver } from './file-resource';
import { bindFileSystemPreferences } from './filesystem-preferences';
import { FileSystemWatcher } from './filesystem-watcher';
@@ -37,7 +37,6 @@ import { RemoteFileServiceContribution } from './remote-file-service-contributio
import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler';
import { UTF8 } from '@theia/core/lib/common/encodings';
import { FilepathBreadcrumbsContribution } from './breadcrumbs/filepath-breadcrumbs-contribution';
-import { BreadcrumbsContribution } from '@theia/core/lib/browser/breadcrumbs/breadcrumbs-contribution';
import { BreadcrumbsFileTreeWidget, createFileTreeBreadcrumbsWidget } from './breadcrumbs/filepath-breadcrumbs-container';
export default new ContainerModule(bind => {
diff --git a/packages/filesystem/src/browser/style/filepath-breadcrumbs.css b/packages/filesystem/src/browser/style/filepath-breadcrumbs.css
index bfa2f6c8052d4..6c15e0ff49ee3 100644
--- a/packages/filesystem/src/browser/style/filepath-breadcrumbs.css
+++ b/packages/filesystem/src/browser/style/filepath-breadcrumbs.css
@@ -15,5 +15,6 @@
********************************************************************************/
.theia-FilepathBreadcrumbFileTree {
- height: 200px;
+ height: auto;
+ max-height: 200px;
}
diff --git a/packages/monaco/src/browser/monaco-outline-contribution.ts b/packages/monaco/src/browser/monaco-outline-contribution.ts
index f6d6fb97f20c8..c8cdbe3b10943 100644
--- a/packages/monaco/src/browser/monaco-outline-contribution.ts
+++ b/packages/monaco/src/browser/monaco-outline-contribution.ts
@@ -43,7 +43,7 @@ export class MonacoOutlineContribution implements FrontendApplicationContributio
onStart(app: FrontendApplication): void {
// updateOutline and handleCurrentEditorChanged need to be called even when the outline view widget is closed
- // in order to udpate breadcrumbs.
+ // in order to update breadcrumbs.
DocumentSymbolProviderRegistry.onDidChange(
debounce(() => this.updateOutline())
);
diff --git a/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx
index 219d91776a463..f25ab1fdaedb4 100644
--- a/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx
+++ b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx
@@ -14,24 +14,41 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import * as React from 'react';
-import * as ReactDOM from 'react-dom';
-import { BreadcrumbsContribution } from '@theia/core/lib/browser/breadcrumbs/breadcrumbs-contribution';
-import { Breadcrumb } from '@theia/core/lib/browser/breadcrumbs/breadcrumb';
-import { injectable, inject, postConstruct } from 'inversify';
-import { LabelProvider, BreadcrumbsService } from '@theia/core/lib/browser';
+import * as React from '@theia/core/shared/react';
+import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
+import { LabelProvider, BreadcrumbsService, Widget, TreeNode, OpenerService, open, SelectableTreeNode, BreadcrumbsContribution, Breadcrumb } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { OutlineViewService } from './outline-view-service';
-import { OutlineSymbolInformationNode } from './outline-view-widget';
-import { EditorManager } from '@theia/editor/lib/browser';
-import { Disposable } from '@theia/core/lib/common';
-import PerfectScrollbar from 'perfect-scrollbar';
+import { OutlineSymbolInformationNode, OutlineViewWidget } from './outline-view-widget';
+import { Disposable, DisposableCollection, Emitter, Event } from '@theia/core/lib/common';
+import { UriSelection } from '@theia/core/lib/common';
export const OutlineBreadcrumbType = Symbol('OutlineBreadcrumb');
+export const BreadcrumbPopupOutlineViewFactory = Symbol('BreadcrumbPopupOutlineViewFactory');
+export const OUTLINE_BREADCRUMB_CONTAINER_CLASS = 'outline-element';
+export interface BreadcrumbPopupOutlineViewFactory {
+ (): BreadcrumbPopupOutlineView;
+}
+export class BreadcrumbPopupOutlineView extends OutlineViewWidget {
+ @inject(OpenerService) protected readonly openerService: OpenerService;
+
+ protected handleClickEvent(node: TreeNode | undefined, event: React.MouseEvent): void {
+ if (UriSelection.is(node) && OutlineSymbolInformationNode.hasRange(node)) {
+ open(this.openerService, node.uri, { selection: node.range });
+ } else {
+ super.handleClickEvent(node, event);
+ }
+ }
+
+ cloneState(roots: OutlineSymbolInformationNode[]): void {
+ const nodes = this.reconcileTreeState(roots);
+ const root = this.getRoot(nodes);
+ this.model.root = this.inflateFromStorage(this.deflateForStorage(root));
+ }
+}
@injectable()
export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
-
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@@ -41,24 +58,34 @@ export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
@inject(BreadcrumbsService)
protected readonly breadcrumbsService: BreadcrumbsService;
- @inject(EditorManager)
- protected readonly editorManager: EditorManager;
+ @inject(BreadcrumbPopupOutlineViewFactory)
+ protected readonly outlineFactory: BreadcrumbPopupOutlineViewFactory;
+
+ protected outlineView: BreadcrumbPopupOutlineView;
readonly type = OutlineBreadcrumbType;
readonly priority: number = 200;
- private currentUri: URI | undefined = undefined;
- private currentBreadcrumbs: OutlineBreadcrumb[] = [];
- private roots: OutlineSymbolInformationNode[] = [];
+ protected currentUri: URI | undefined = undefined;
+ protected currentBreadcrumbs: OutlineBreadcrumb[] = [];
+ protected roots: OutlineSymbolInformationNode[] = [];
+
+ protected readonly onDidChangeBreadcrumbsEmitter = new Emitter();
+ get onDidChangeBreadcrumbs(): Event {
+ return this.onDidChangeBreadcrumbsEmitter.event;
+ }
@postConstruct()
init(): void {
+ this.outlineView = this.outlineFactory();
+ this.outlineView.node.style.height = 'auto';
+ this.outlineView.node.style.maxHeight = '200px';
this.outlineViewService.onDidChangeOutline(roots => {
if (roots.length > 0) {
this.roots = roots;
const first = roots[0];
- if ('uri' in first) {
- this.updateOutlineItems(first['uri'] as URI, this.findSelectedNode(roots));
+ if (UriSelection.is(first)) {
+ this.updateOutlineItems(first.uri, this.findSelectedNode(roots));
}
} else {
this.currentBreadcrumbs = [];
@@ -66,8 +93,8 @@ export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
}
});
this.outlineViewService.onDidSelect(node => {
- if ('uri' in node) {
- this.updateOutlineItems(node['uri'] as URI, node);
+ if (UriSelection.is(node)) {
+ this.updateOutlineItems(node.uri, node);
}
});
}
@@ -77,19 +104,39 @@ export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
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)
+ new OutlineBreadcrumb(
+ node,
+ uri,
+ index.toString(),
+ this.labelProvider.getName(node),
+ 'symbol-icon symbol-icon-center ' + node.iconClass,
+ OUTLINE_BREADCRUMB_CONTAINER_CLASS,
+ )
);
if (selectedNode.children && selectedNode.children.length > 0) {
- this.currentBreadcrumbs.push(new OutlineBreadcrumb(selectedNode.children as OutlineSymbolInformationNode[],
- uri, this.currentBreadcrumbs.length.toString(), '…', ''));
+ this.currentBreadcrumbs.push(new OutlineBreadcrumb(
+ selectedNode.children as OutlineSymbolInformationNode[],
+ uri,
+ this.currentBreadcrumbs.length.toString(),
+ '…',
+ '',
+ OUTLINE_BREADCRUMB_CONTAINER_CLASS,
+ ));
}
} else {
this.currentBreadcrumbs = [];
if (this.roots) {
- this.currentBreadcrumbs.push(new OutlineBreadcrumb(this.roots, uri, this.currentBreadcrumbs.length.toString(), '…', ''));
+ this.currentBreadcrumbs.push(new OutlineBreadcrumb(
+ this.roots,
+ uri,
+ this.currentBreadcrumbs.length.toString(),
+ '…',
+ '',
+ OUTLINE_BREADCRUMB_CONTAINER_CLASS
+ ));
}
}
- this.breadcrumbsService.breadcrumbsChanges(uri);
+ this.onDidChangeBreadcrumbsEmitter.fire(uri);
}
async computeBreadcrumbs(uri: URI): Promise {
@@ -103,59 +150,32 @@ export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
if (!OutlineBreadcrumb.is(breadcrumb)) {
return undefined;
}
- const nodes = Array.isArray(breadcrumb.node) ? breadcrumb.node : this.siblings(breadcrumb.node);
- const items = nodes.map(node => ({
- label: node.name,
- title: node.name,
- iconClass: 'symbol-icon symbol-icon-center ' + node.iconClass,
- action: () => this.revealInEditor(node)
- }));
- if (items.length > 0) {
- ReactDOM.render({this.renderItems(items)}, parent);
- const scrollbar = new PerfectScrollbar(parent, {
- handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
- useBothWheelAxes: true,
- scrollYMarginOffset: 8,
- suppressScrollX: true
- });
- return {
- dispose: () => {
- scrollbar.destroy();
- ReactDOM.unmountComponentAtNode(parent);
-
- }
- };
- }
- const noContent = document.createElement('div');
- noContent.style.margin = '.5rem';
- noContent.style.fontStyle = 'italic';
- noContent.innerText = '(no content)';
- parent.appendChild(noContent);
- }
-
- private revealInEditor(node: OutlineSymbolInformationNode): void {
- if ('range' in node && this.currentUri) {
- this.editorManager.open(this.currentUri, { selection: node['range'] });
+ const node = Array.isArray(breadcrumb.node) ? breadcrumb.node[0] : breadcrumb.node;
+ if (!node.parent) {
+ return undefined;
}
- }
-
- 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);
+ const siblings = node.parent.children.filter((child): child is OutlineSymbolInformationNode => OutlineSymbolInformationNode.is(child));
+
+ const toDisposeOnHide = new DisposableCollection();
+ this.outlineView.cloneState(siblings);
+ this.outlineView.model.selectNode(node);
+ this.outlineView.model.collapseAll();
+ Widget.attach(this.outlineView, parent);
+ this.outlineView.activate();
+ toDisposeOnHide.pushAll([
+ this.outlineView.model.onExpansionChanged(expandedNode => SelectableTreeNode.is(expandedNode) && this.outlineView.model.selectNode(expandedNode)),
+ Disposable.create(() => {
+ this.outlineView.model.root = undefined;
+ Widget.detach(this.outlineView);
+ }),
+ ]);
+ return toDisposeOnHide;
}
/**
* Returns the path of the given outline node.
*/
- private toOutlinePath(node: OutlineSymbolInformationNode | undefined, path: OutlineSymbolInformationNode[] = []): OutlineSymbolInformationNode[] | undefined {
+ protected toOutlinePath(node: OutlineSymbolInformationNode | undefined, path: OutlineSymbolInformationNode[] = []): OutlineSymbolInformationNode[] | undefined {
if (!node) { return undefined; }
if (node.id === 'outline-view-root') { return path; }
if (node.parent) {
@@ -168,7 +188,7 @@ export class OutlineBreadcrumbsContribution implements BreadcrumbsContribution {
/**
* Find the node that is selected. Returns after the first match.
*/
- private findSelectedNode(roots: OutlineSymbolInformationNode[]): OutlineSymbolInformationNode | undefined {
+ protected findSelectedNode(roots: OutlineSymbolInformationNode[]): OutlineSymbolInformationNode | undefined {
const result = roots.find(node => node.selected);
if (result) {
return result;
@@ -188,7 +208,8 @@ export class OutlineBreadcrumb implements Breadcrumb {
readonly uri: URI,
readonly index: string,
readonly label: string,
- readonly iconClass: string
+ readonly iconClass: string,
+ readonly containerClass: string,
) { }
get id(): string {
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 b1a7e47b919de..a3534056196dd 100644
--- a/packages/outline-view/src/browser/outline-view-frontend-module.ts
+++ b/packages/outline-view/src/browser/outline-view-frontend-module.ts
@@ -36,13 +36,20 @@ 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-model';
-import { OutlineBreadcrumbsContribution } from './outline-breadcrumbs-contribution';
+import { BreadcrumbPopupOutlineView, BreadcrumbPopupOutlineViewFactory, OutlineBreadcrumbsContribution } from './outline-breadcrumbs-contribution';
export default new ContainerModule(bind => {
bind(OutlineViewWidgetFactory).toFactory(ctx =>
() => createOutlineViewWidget(ctx.container)
);
+ bind(BreadcrumbPopupOutlineViewFactory).toFactory(({ container }) => () => {
+ const child = createOutlineViewWidgetContainer(container);
+ child.rebind(OutlineViewWidget).to(BreadcrumbPopupOutlineView);
+ child.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, expandOnlyOnExpansionToggleClick: true, search: false, virtualized: false });
+ return child.get(OutlineViewWidget);
+ });
+
bind(OutlineViewService).toSelf().inSingletonScope();
bind(WidgetFactory).toService(OutlineViewService);
@@ -54,16 +61,7 @@ export default new ContainerModule(bind => {
bind(BreadcrumbsContribution).toService(OutlineBreadcrumbsContribution);
});
-/**
- * Create an `OutlineViewWidget`.
- * - The creation of the `OutlineViewWidget` includes:
- * - The creation of the tree widget itself with it's own customized props.
- * - The binding of necessary components into the container.
- * @param parent the Inversify container.
- *
- * @returns the `OutlineViewWidget`.
- */
-function createOutlineViewWidget(parent: interfaces.Container): OutlineViewWidget {
+function createOutlineViewWidgetContainer(parent: interfaces.Container): interfaces.Container {
const child = createTreeContainer(parent);
child.rebind(TreeProps).toConstantValue({ ...defaultTreeProps, expandOnlyOnExpansionToggleClick: true, search: true });
@@ -78,6 +76,20 @@ function createOutlineViewWidget(parent: interfaces.Container): OutlineViewWidge
child.bind(OutlineDecoratorService).toSelf().inSingletonScope();
child.rebind(TreeDecoratorService).toDynamicValue(ctx => ctx.container.get(OutlineDecoratorService)).inSingletonScope();
bindContributionProvider(child, OutlineTreeDecorator);
+ return child;
+}
+
+/**
+ * Create an `OutlineViewWidget`.
+ * - The creation of the `OutlineViewWidget` includes:
+ * - The creation of the tree widget itself with it's own customized props.
+ * - The binding of necessary components into the container.
+ * @param parent the Inversify container.
+ *
+ * @returns the `OutlineViewWidget`.
+ */
+function createOutlineViewWidget(parent: interfaces.Container): OutlineViewWidget {
+ const child = createOutlineViewWidgetContainer(parent);
return child.get(OutlineViewWidget);
}
diff --git a/packages/outline-view/src/browser/outline-view-widget.tsx b/packages/outline-view/src/browser/outline-view-widget.tsx
index b2a897b564759..40d4791c02248 100644
--- a/packages/outline-view/src/browser/outline-view-widget.tsx
+++ b/packages/outline-view/src/browser/outline-view-widget.tsx
@@ -27,9 +27,11 @@ import {
} from '@theia/core/lib/browser';
import { OutlineViewTreeModel } from './outline-view-tree-model';
import { Message } from '@theia/core/shared/@phosphor/messaging';
-import { Emitter } from '@theia/core';
+import { Emitter, Mutable, UriSelection } from '@theia/core';
import { CompositeTreeNode } from '@theia/core/lib/browser';
import * as React from '@theia/core/shared/react';
+import { Range } from '@theia/core/shared/vscode-languageserver-types';
+import URI from '@theia/core/lib/common/uri';
/**
* Representation of an outline symbol information node.
@@ -58,6 +60,10 @@ export namespace OutlineSymbolInformationNode {
export function is(node: TreeNode): node is OutlineSymbolInformationNode {
return !!node && SelectableTreeNode.is(node) && 'iconClass' in node;
}
+
+ export function hasRange(node: unknown): node is { range: Range } {
+ return typeof node === 'object' && !!node && 'range' in node && Range.is((node as { range: Range }).range);
+ }
}
export type OutlineViewWidgetFactory = () => OutlineViewWidget;
@@ -91,13 +97,17 @@ export class OutlineViewWidget extends TreeWidget {
// Gather the list of available nodes.
const nodes = this.reconcileTreeState(roots);
// Update the model root node, appending the outline symbol information nodes as children.
- this.model.root = {
+ this.model.root = this.getRoot(nodes);
+ }
+
+ protected getRoot(children: TreeNode[]): CompositeTreeNode {
+ return {
id: 'outline-view-root',
name: 'Outline Root',
visible: false,
- children: nodes,
+ children,
parent: undefined
- } as CompositeTreeNode;
+ };
}
/**
@@ -171,4 +181,19 @@ export class OutlineViewWidget extends TreeWidget {
return super.renderTree(model);
}
+ protected deflateForStorage(node: TreeNode): object {
+ const deflated = super.deflateForStorage(node) as { uri: string };
+ if (UriSelection.is(node)) {
+ deflated.uri = node.uri.toString();
+ }
+ return deflated;
+ }
+
+ protected inflateFromStorage(node: any, parent?: TreeNode): TreeNode { /* eslint-disable-line @typescript-eslint/no-explicit-any */
+ const inflated = super.inflateFromStorage(node, parent) as Mutable;
+ if (node && 'uri' in node && typeof node.uri === 'string') {
+ inflated.uri = new URI(node.uri);
+ }
+ return inflated;
+ }
}
diff --git a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts
index 1cae03025f0a8..8740ae7e074eb 100644
--- a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts
+++ b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts
@@ -36,6 +36,7 @@ import { WorkspaceRootNode } from '@theia/navigator/lib/browser/navigator-tree';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat, FileChangeType } from '@theia/filesystem/lib/common/files';
+import { WorkspaceService } from '@theia/workspace/lib/browser';
export interface PluginIconDefinition {
iconPath: string;
@@ -107,6 +108,9 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh
@inject(PluginIconThemeDefinition)
protected readonly definition: PluginIconThemeDefinition;
+ @inject(WorkspaceService)
+ protected readonly workspaceService: WorkspaceService;
+
protected readonly onDidChangeEmitter = new Emitter();
readonly onDidChange = this.onDidChangeEmitter.event;
@@ -508,9 +512,15 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh
const name = this.labelProvider.getName(element);
const classNames = this.fileNameIcon(name);
if (uri) {
- const language = monaco.services.StaticServices.modeService.get().createByFilepathOrFirstLine(monaco.Uri.parse(uri));
+ const parsedURI = new URI(uri);
+ const isRoot = this.workspaceService.getWorkspaceRootUri(new URI(uri))?.isEqual(parsedURI);
+ if (isRoot) {
+ classNames.unshift(this.rootFolderIcon);
+ } else {
+ classNames.unshift(this.fileIcon);
+ }
+ const language = monaco.services.StaticServices.modeService.get().createByFilepathOrFirstLine(parsedURI['codeUri']);
classNames.push(this.languageIcon(language.languageIdentifier.language));
- classNames.unshift(this.fileIcon);
}
return classNames;
}
diff --git a/packages/preferences/src/browser/util/preference-tree-generator.ts b/packages/preferences/src/browser/util/preference-tree-generator.ts
index c92b454990908..8c67a0569df28 100644
--- a/packages/preferences/src/browser/util/preference-tree-generator.ts
+++ b/packages/preferences/src/browser/util/preference-tree-generator.ts
@@ -48,6 +48,7 @@ export class PreferenceTreeGenerator {
['extensions', 'Extensions']
]);
protected readonly sectionAssignments = new Map([
+ ['breadcrumbs', 'workbench'],
['comments', 'features'],
['debug', 'features'],
['diffEditor', 'editor'],
diff --git a/packages/workspace/src/browser/workspace-breadcrumbs-contribution.ts b/packages/workspace/src/browser/workspace-breadcrumbs-contribution.ts
index ef9e7acd688db..5903c2f4beb7c 100644
--- a/packages/workspace/src/browser/workspace-breadcrumbs-contribution.ts
+++ b/packages/workspace/src/browser/workspace-breadcrumbs-contribution.ts
@@ -15,8 +15,8 @@
********************************************************************************/
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 { FilepathBreadcrumbClassNameFactory, FilepathBreadcrumbsContribution } from '@theia/filesystem/lib/browser/breadcrumbs/filepath-breadcrumbs-contribution';
+import { inject, injectable } from '@theia/core/shared/inversify';
import { WorkspaceService } from './workspace-service';
import URI from '@theia/core/lib/common/uri';
@@ -26,8 +26,31 @@ export class WorkspaceBreadcrumbsContribution extends FilepathBreadcrumbsContrib
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
+ getContainerClassCreator(fileURI: URI): FilepathBreadcrumbClassNameFactory {
+ const workspaceRoot = this.workspaceService.getWorkspaceRootUri(fileURI);
+ return (location, index) => {
+ if (location.isEqual(fileURI)) {
+ return 'file';
+ } else if (workspaceRoot?.isEqual(location)) {
+ return 'root_folder';
+ }
+ return 'folder';
+ };
+ }
+
+ getIconClassCreator(fileURI: URI): FilepathBreadcrumbClassNameFactory {
+ const workspaceRoot = this.workspaceService.getWorkspaceRootUri(fileURI);
+ return (location, index) => {
+ if (location.isEqual(fileURI) || workspaceRoot?.isEqual(location)) {
+ return this.labelProvider.getIcon(location) + ' file-icon';
+ }
+ return '';
+ };
+ }
+
protected filterBreadcrumbs(uri: URI, breadcrumb: FilepathBreadcrumb): boolean {
const workspaceRootUri = this.workspaceService.getWorkspaceRootUri(uri);
- return super.filterBreadcrumbs(uri, breadcrumb) && (!workspaceRootUri || !breadcrumb.uri.isEqualOrParent(workspaceRootUri));
+ const firstCrumbToHide = this.workspaceService.isMultiRootWorkspaceOpened ? workspaceRootUri?.parent : workspaceRootUri;
+ return super.filterBreadcrumbs(uri, breadcrumb) && (!firstCrumbToHide || !breadcrumb.uri.isEqualOrParent(firstCrumbToHide));
}
}
diff --git a/packages/workspace/src/browser/workspace-uri-contribution.spec.ts b/packages/workspace/src/browser/workspace-uri-contribution.spec.ts
index 46661f03372af..6c53a5ed1a6a1 100644
--- a/packages/workspace/src/browser/workspace-uri-contribution.spec.ts
+++ b/packages/workspace/src/browser/workspace-uri-contribution.spec.ts
@@ -100,13 +100,13 @@ describe('WorkspaceUriLabelProviderContribution class', () => {
expect(labelProvider.getIcon(FileStat.file('file:///home/test'))).eq(labelProvider.defaultFileIcon);
});
- it('should return folder icon from a folder URI', async () => {
+ it('should return folder icon from a folder FileStat', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stubs.push(sinon.stub(DefaultUriLabelProviderContribution.prototype, 'getFileIcon').returns(undefined));
expect(labelProvider.getIcon(FileStat.dir('file:///home/test'))).eq(labelProvider.defaultFolderIcon);
});
- it('should return file icon from a file URI', async () => {
+ it('should return file icon from a file FileStat', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stubs.push(sinon.stub(DefaultUriLabelProviderContribution.prototype, 'getFileIcon').returns(undefined));
expect(labelProvider.getIcon(FileStat.file('file:///home/test'))).eq(labelProvider.defaultFileIcon);
@@ -119,6 +119,11 @@ describe('WorkspaceUriLabelProviderContribution class', () => {
expect(labelProvider.getIcon(new URI('file:///home/test'))).eq(ret);
expect(labelProvider.getIcon(FileStat.file('file:///home/test'))).eq(ret);
});
+
+ it('should return rootfolder-icon for a URI or file stat that corresponds to a workspace root', () => {
+ expect(labelProvider.getIcon(new URI('file:///workspace'))).eq('rootfolder-icon');
+ expect(labelProvider.getIcon(FileStat.dir('file:///workspace'))).eq('rootfolder-icon');
+ });
});
describe('getName()', () => {
diff --git a/packages/workspace/src/browser/workspace-uri-contribution.ts b/packages/workspace/src/browser/workspace-uri-contribution.ts
index afd07c37ecb3e..4db41501821f1 100644
--- a/packages/workspace/src/browser/workspace-uri-contribution.ts
+++ b/packages/workspace/src/browser/workspace-uri-contribution.ts
@@ -39,6 +39,10 @@ export class WorkspaceUriLabelProviderContribution extends DefaultUriLabelProvid
}
getIcon(element: URI | URIIconReference | FileStat): string {
+ const uri = this.getUri(element);
+ if (uri && this.workspaceVariable.getWorkspaceRootUri(uri)?.isEqual(uri)) {
+ return 'rootfolder-icon';
+ }
return super.getIcon(this.asURIIconReference(element));
}