From 84f046ffe14ca8073e2c56f2e10c79b3c6bd03bf Mon Sep 17 00:00:00 2001 From: Kenneth Marut Date: Thu, 28 Jan 2021 16:44:07 -0600 Subject: [PATCH] Feature: Open editors widget in navigator Signed-off-by: Kenneth Marut --- packages/core/src/browser/view-container.ts | 1 + packages/navigator/compile.tsconfig.json | 6 + packages/navigator/package.json | 2 + .../src/browser/navigator-container.ts | 4 +- .../src/browser/navigator-contribution.ts | 119 +++++++++++- .../src/browser/navigator-frontend-module.ts | 16 ++ .../src/browser/navigator-widget-factory.ts | 16 +- ...avigator-open-editors-decorator-service.ts | 84 ++++++++ .../navigator-open-editors-file-decorator.ts | 112 +++++++++++ .../navigator-open-editors-menus.ts | 27 +++ .../navigator-open-editors-tree-model.ts | 112 +++++++++++ .../navigator-open-editors-widget.tsx | 179 ++++++++++++++++++ .../open-editors-widget/open-editors.css | 31 +++ .../browser/terminal-frontend-contribution.ts | 5 + scripts/check-publish.js | 2 +- 15 files changed, 707 insertions(+), 9 deletions(-) create mode 100644 packages/navigator/src/browser/open-editors-widget/navigator-open-editors-decorator-service.ts create mode 100644 packages/navigator/src/browser/open-editors-widget/navigator-open-editors-file-decorator.ts create mode 100644 packages/navigator/src/browser/open-editors-widget/navigator-open-editors-menus.ts create mode 100644 packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts create mode 100644 packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx create mode 100644 packages/navigator/src/browser/open-editors-widget/open-editors.css diff --git a/packages/core/src/browser/view-container.ts b/packages/core/src/browser/view-container.ts index 400d8fc385f6d..fd97cdd527ab1 100644 --- a/packages/core/src/browser/view-container.ts +++ b/packages/core/src/browser/view-container.ts @@ -418,6 +418,7 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica waitForRevealed(this).then(() => { this.containerLayout.setPartSizes(partStates.map(partState => partState.relativeSize)); }); + } /** diff --git a/packages/navigator/compile.tsconfig.json b/packages/navigator/compile.tsconfig.json index 7f099ca696f57..5b11c867ad27c 100644 --- a/packages/navigator/compile.tsconfig.json +++ b/packages/navigator/compile.tsconfig.json @@ -17,6 +17,12 @@ }, { "path": "../workspace/compile.tsconfig.json" + }, + { + "path": "../editor-preview/compile.tsconfig.json" + }, + { + "path": "../editor/compile.tsconfig.json" } ] } diff --git a/packages/navigator/package.json b/packages/navigator/package.json index d8236aa417d71..08ab625c775ff 100644 --- a/packages/navigator/package.json +++ b/packages/navigator/package.json @@ -4,6 +4,8 @@ "description": "Theia - Navigator Extension", "dependencies": { "@theia/core": "1.12.0", + "@theia/editor": "^1.12.0", + "@theia/editor-preview": "1.12.0", "@theia/filesystem": "1.12.0", "@theia/workspace": "1.12.0", "minimatch": "^3.0.4" diff --git a/packages/navigator/src/browser/navigator-container.ts b/packages/navigator/src/browser/navigator-container.ts index 42df4dcadcc39..a937a5d43c8df 100644 --- a/packages/navigator/src/browser/navigator-container.ts +++ b/packages/navigator/src/browser/navigator-container.ts @@ -17,12 +17,11 @@ import { Container, interfaces } from '@theia/core/shared/inversify'; import { Tree, TreeModel, TreeProps, defaultTreeProps, TreeDecoratorService } from '@theia/core/lib/browser'; import { createFileTreeContainer, FileTree, FileTreeModel, FileTreeWidget } from '@theia/filesystem/lib/browser'; -import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { FileNavigatorTree } from './navigator-tree'; import { FileNavigatorModel } from './navigator-model'; import { FileNavigatorWidget } from './navigator-widget'; import { NAVIGATOR_CONTEXT_MENU } from './navigator-contribution'; -import { NavigatorDecoratorService, NavigatorTreeDecorator } from './navigator-decorator-service'; +import { NavigatorDecoratorService } from './navigator-decorator-service'; export const FILE_NAVIGATOR_PROPS = { ...defaultTreeProps, @@ -50,7 +49,6 @@ export function createFileNavigatorContainer(parent: interfaces.Container): Cont child.bind(NavigatorDecoratorService).toSelf().inSingletonScope(); child.rebind(TreeDecoratorService).toService(NavigatorDecoratorService); - bindContributionProvider(child, NavigatorTreeDecorator); return child; } diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index e17d31912bc20..3862503d2e4ea 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -29,7 +29,7 @@ import { SelectableTreeNode, SHELL_TABBAR_CONTEXT_MENU, Widget, - Title + Title, } from '@theia/core/lib/browser'; import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution'; import { @@ -70,6 +70,10 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { SelectionService } from '@theia/core/lib/common/selection-service'; import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import URI from '@theia/core/lib/common/uri'; +import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; +import { OpenEditorsWidget } from './open-editors-widget/navigator-open-editors-widget'; +import { OpenEditorsContextMenu, OPEN_EDITORS_CONTEXT_MENU } from './open-editors-widget/navigator-open-editors-menus'; +import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser'; export namespace FileNavigatorCommands { export const REVEAL_IN_NAVIGATOR: Command = { @@ -106,7 +110,8 @@ export namespace FileNavigatorCommands { label: 'Focus on Files Explorer' }; export const COPY_RELATIVE_FILE_PATH: Command = { - id: 'navigator.copyRelativeFilePath' + id: 'navigator.copyRelativeFilePath', + label: 'Copy Relative Path' }; export const OPEN: Command = { id: 'navigator.open', @@ -158,6 +163,28 @@ export namespace NavigatorContextMenu { export const FILE_NAVIGATOR_TOGGLE_COMMAND_ID = 'fileNavigator:toggle'; +export namespace OpenEditorsCommands { + export const CLOSE_ALL_TABS_FROM_TOOLBAR: Command = { + id: 'navigator.close.all.editors', + category: 'File', + label: 'Close All Editors', + iconClass: 'codicon codicon-close-all' + }; + + export const SAVE_ALL_TABS: Command = { + id: 'navigator.save.all.editors', + category: 'File', + label: 'Save All Editor', + iconClass: 'codicon codicon-save-all' + }; + + export const SAVE_EDITOR: Command = { + id: 'navigator.save.editor', + category: 'File', + label: 'Save' + }; +} + @injectable() export class FileNavigatorContribution extends AbstractViewContribution implements FrontendApplicationContribution, TabBarToolbarContribution { @@ -185,6 +212,9 @@ export class FileNavigatorContribution extends AbstractViewContribution this.withOpenEditorsWidget(widget, async () => { + this.shell.widgets.forEach(w => { + if (w instanceof EditorWidget || w instanceof EditorPreviewWidget) { + w.close(); + } + }); + }), + isEnabled: widget => this.withOpenEditorsWidget(widget, () => !!this.editorManager.all.length), + isVisible: widget => this.withOpenEditorsWidget(widget, () => !!this.editorManager.all.length) + }); + registry.registerCommand(OpenEditorsCommands.SAVE_ALL_TABS, { + execute: widget => this.withOpenEditorsWidget(widget, () => this.editorManager.all.forEach(editor => editor.saveable.save())), + isEnabled: widget => this.withOpenEditorsWidget(widget, () => !!this.editorManager.all.length), + isVisible: widget => this.withOpenEditorsWidget(widget, () => !!this.editorManager.all.length) + }); } protected getSelectedFileNodes(): FileNode[] { @@ -371,6 +417,13 @@ export class FileNavigatorContribution extends AbstractViewContribution(widget: Widget, cb: (navigator: OpenEditorsWidget) => T): T | false { + if (widget instanceof OpenEditorsWidget && widget.id === OpenEditorsWidget.ID) { + return cb(widget); + } + return false; + } + registerMenus(registry: MenuModelRegistry): void { super.registerMenus(registry); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_MENU, { @@ -413,7 +466,7 @@ export class FileNavigatorContribution extends AbstractViewContribution { bindFileNavigatorPreferences(bind); @@ -56,6 +62,16 @@ export default new ContainerModule(bind => { id: FILE_NAVIGATOR_ID, createWidget: () => container.get(FileNavigatorWidget) })).inSingletonScope(); + bindContributionProvider(bind, NavigatorTreeDecorator); + bindContributionProvider(bind, OpenEditorsTreeDecorator); + bind(OpenEditorsTreeDecorator).to(OpenEditorsFileDecorator); + + bind(OpenEditorsWidget).toSelf().inSingletonScope(); + bind(WidgetFactory).toDynamicValue(({ container }) => ({ + id: OpenEditorsWidget.ID, + createWidget: () => OpenEditorsWidget.createWidget(container) + })).inSingletonScope(); + bind(NavigatorWidgetFactory).toSelf().inSingletonScope(); bind(WidgetFactory).toService(NavigatorWidgetFactory); bind(ApplicationShellLayoutMigration).to(NavigatorLayoutVersion3Migration).inSingletonScope(); diff --git a/packages/navigator/src/browser/navigator-widget-factory.ts b/packages/navigator/src/browser/navigator-widget-factory.ts index 4c7f2c4649338..236662bef8ac9 100644 --- a/packages/navigator/src/browser/navigator-widget-factory.ts +++ b/packages/navigator/src/browser/navigator-widget-factory.ts @@ -22,6 +22,7 @@ import { WidgetManager } from '@theia/core/lib/browser'; import { FILE_NAVIGATOR_ID } from './navigator-widget'; +import { OpenEditorsWidget } from './open-editors-widget/navigator-open-editors-widget'; export const EXPLORER_VIEW_CONTAINER_ID = 'explorer-view-container'; export const EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = { @@ -38,8 +39,17 @@ export class NavigatorWidgetFactory implements WidgetFactory { readonly id = NavigatorWidgetFactory.ID; protected fileNavigatorWidgetOptions: ViewContainer.Factory.WidgetOptions = { + order: 1, canHide: false, initiallyCollapsed: false, + weight: 80 + }; + + protected openEditorsWidgetOptions: ViewContainer.Factory.WidgetOptions = { + order: 0, + canHide: true, + initiallyCollapsed: true, + weight: 20 }; @inject(ViewContainer.Factory) @@ -52,8 +62,10 @@ export class NavigatorWidgetFactory implements WidgetFactory { progressLocationId: 'explorer' }); viewContainer.setTitleOptions(EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS); - const widget = await this.widgetManager.getOrCreateWidget(FILE_NAVIGATOR_ID); - viewContainer.addWidget(widget, this.fileNavigatorWidgetOptions); + const openEditorsWidget = await this.widgetManager.getOrCreateWidget(OpenEditorsWidget.ID); + const navigatorWidget = await this.widgetManager.getOrCreateWidget(FILE_NAVIGATOR_ID); + viewContainer.addWidget(openEditorsWidget, this.openEditorsWidgetOptions); + viewContainer.addWidget(navigatorWidget, this.fileNavigatorWidgetOptions); return viewContainer; } } diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-decorator-service.ts b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-decorator-service.ts new file mode 100644 index 0000000000000..10d81029d333f --- /dev/null +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-decorator-service.ts @@ -0,0 +1,84 @@ +/******************************************************************************** + * 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 { inject, injectable, named } from '@theia/core/shared/inversify'; +import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; +import { TreeDecorator, AbstractTreeDecoratorService, TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator'; +import { NavigatorTreeDecorator } from '../navigator-decorator-service'; +import { Tree } from '@theia/core/lib/browser'; + +/** + * Symbol for all decorators that would like to contribute into the navigator. + */ +export const OpenEditorsTreeDecorator = Symbol('OpenEditorsTreeDecorator'); + +/** + * Decorator service for the navigator. + */ +@injectable() +export class OpenEditorsTreeDecoratorService extends AbstractTreeDecoratorService { + + constructor(@inject(ContributionProvider) @named(OpenEditorsTreeDecorator) protected readonly contributions: ContributionProvider, + @inject(ContributionProvider) @named(NavigatorTreeDecorator) protected readonly navigatorContributions: ContributionProvider) { + super([...contributions.getContributions(), ...navigatorContributions.getContributions()]); + } + + async getDecorations(tree: Tree): Promise> { + const changes = new Map(); + const colorMap = new Map(); + for (const decorator of this.decorators) { + // if the ProblemDecorator has provided a color, retrieve that color and apply it to the suffix decoration + // for consistency + for (const [id, data] of (await decorator.decorations(tree)).entries()) { + const colorFromIncomingData = data.fontData?.color; + if (colorFromIncomingData) { + colorMap.set(id, colorFromIncomingData); + } + const color = colorMap.get(id) ?? data.fontData?.color; + + const existingDecorations = changes.get(id); + if (existingDecorations) { + existingDecorations.push(data); + // iterate through existing decorations and add color to suffixes if they exist + const existingDecorationsWithColor = existingDecorations?.map(existingDec => { + if (existingDec.captionSuffixes) { + const captionSuffixCopy = existingDec.captionSuffixes.map(suffix => { + const fontData = { ...suffix.fontData, color }; + return { ...suffix, fontData }; + }); + return { ...existingDec, captionSuffixes: captionSuffixCopy }; + } + return existingDec; + }); + changes.set(id, existingDecorationsWithColor); + + } else { + // if the decoration has suffixes, then colorize them + let dataCopy = data; + if (data.captionSuffixes) { + const captionSuffixCopy = data.captionSuffixes.map(suffix => { + const fontData = { ...suffix.fontData, color }; + return { ...suffix, fontData }; + }); + dataCopy = { ...data, captionSuffixes: captionSuffixCopy }; + } + changes.set(id, [dataCopy]); + } + } + } + return changes; + } +} diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-file-decorator.ts b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-file-decorator.ts new file mode 100644 index 0000000000000..eaad04d37f253 --- /dev/null +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-file-decorator.ts @@ -0,0 +1,112 @@ +/******************************************************************************** + * 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; +import { TreeDecorator, TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator'; +import { Emitter } from '@theia/core/lib/common/event'; +import { Tree } from '@theia/core/lib/browser/tree/tree'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { ApplicationShell, DepthFirstTreeIterator, LabelProvider, Saveable } from '@theia/core/lib/browser'; +import URI from '@theia/core/lib/common/uri'; +import { OpenEditorNode } from './navigator-open-editors-tree-model'; +import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser'; +import { Disposable } from '@theia/core/lib/common'; + +export interface OpenEditorTreeDecorationData extends TreeDecoration.Data { + dirty?: boolean; +} + +@injectable() +export class OpenEditorsFileDecorator implements TreeDecorator { + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + @inject(ApplicationShell) protected readonly shell: ApplicationShell; + + readonly id = 'theia-open-editors-file-decorator'; + protected decorationsMap = new Map(); + + protected readonly decorationsChangedEmitter = new Emitter(); + readonly onDidChangeDecorations = this.decorationsChangedEmitter.event; + protected readonly toDisposeOnDirtyChanged = new Map(); + + @postConstruct() + init(): void { + this.workspaceService.onWorkspaceChanged(event => { + this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)); + }); + this.workspaceService.onWorkspaceLocationChanged(event => { + this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)); + }); + + this.shell.onDidAddWidget(widget => { + const saveable = Saveable.get(widget); + if (saveable) { + this.toDisposeOnDirtyChanged.set(widget.id, saveable.onDirtyChanged(() => { + this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)); + })); + } + }); + this.shell.onDidRemoveWidget(widget => this.toDisposeOnDirtyChanged.get(widget.id)?.dispose()); + } + + protected fireDidChangeDecorations(event: (tree: Tree) => Promise>): void { + this.decorationsChangedEmitter.fire(event); + } + + async decorations(tree: Tree): Promise> { + return this.collectDecorators(tree); + } + + protected async collectDecorators(tree: Tree): Promise> { + // Add add workspace root as caption affix and italicize if PreviewWidget + const result = new Map(); + if (tree.root === undefined) { + return result; + } + for (const node of new DepthFirstTreeIterator(tree.root)) { + if (OpenEditorNode.is(node)) { + const isPreviewWidget = node.widget.parent instanceof EditorPreviewWidget; + const saveable = Saveable.get(node.widget); + const path = await this.resolvePathString(node.uri); + const decorations: OpenEditorTreeDecorationData = { + dirty: saveable?.dirty, + captionSuffixes: [ + { + data: path, + fontData: { style: isPreviewWidget ? 'italic' : undefined } + } + ], + fontData: { style: isPreviewWidget ? 'italic' : undefined } + }; + result.set(node.id, decorations); + } + } + return result; + } + + protected async resolvePathString(nodeURI: URI): Promise { + const workspaceRoots = await this.workspaceService.roots; + const parentWorkspace = workspaceRoots.find(({ resource }) => resource.isEqualOrParent(nodeURI)); + if (parentWorkspace) { + const relativePathURI = parentWorkspace.resource.relative(nodeURI)?.dir; + const workspacePrefixString = workspaceRoots.length > 1 ? parentWorkspace.name : ''; + const filePathString = relativePathURI?.hasDir ? relativePathURI.toString() : ''; + const separator = filePathString && workspacePrefixString ? ' \u2022 ' : ''; + return `${workspacePrefixString}${separator}${filePathString}`; + } + return ''; + } +} diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-menus.ts b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-menus.ts new file mode 100644 index 0000000000000..c3ebdd2f43472 --- /dev/null +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-menus.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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 { MenuPath } from '@theia/core/lib/common'; + +export const OPEN_EDITORS_CONTEXT_MENU: MenuPath = ['open-editors-context-menu']; +export namespace OpenEditorsContextMenu { + export const NAVIGATION = [...OPEN_EDITORS_CONTEXT_MENU, '1_navigation']; + export const CLIPBOARD = [...OPEN_EDITORS_CONTEXT_MENU, '2_clipboard']; + export const SAVE = [...OPEN_EDITORS_CONTEXT_MENU, '3_save']; + export const COMPARE = [...OPEN_EDITORS_CONTEXT_MENU, '4_compare']; + export const MODIFICATION = [...OPEN_EDITORS_CONTEXT_MENU, '5_modification']; +} + diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts new file mode 100644 index 0000000000000..42f0a0aa7d5f1 --- /dev/null +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts @@ -0,0 +1,112 @@ +/******************************************************************************** + * 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; +import { FileStatNode, FileTreeModel } from '@theia/filesystem/lib/browser'; +import { ApplicationShell, CompositeTreeNode, Navigatable, SelectableTreeNode, Widget } from '@theia/core/lib/browser'; +import { EditorWidget } from '@theia/editor/lib/browser'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { EditorPreviewManager, EditorPreviewWidget } from '@theia/editor-preview/lib/browser'; +import { DisposableCollection } from '@theia/core/lib/common'; +import debounce = require('@theia/core/shared/lodash.debounce'); + +export interface OpenEditorNode extends FileStatNode { + widget: Widget; +}; + +export namespace OpenEditorNode { + export function is(node: object | undefined): node is OpenEditorNode { + return FileStatNode.is(node) && 'widget' in node; + } +} + +@injectable() +export class OpenEditorsModel extends FileTreeModel { + @inject(ApplicationShell) protected readonly applicationShell: ApplicationShell; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(EditorPreviewManager) protected readonly editorPreviewManager: EditorPreviewManager; + + protected toDisposeOnPreviewWidgetReplaced = new DisposableCollection(); + + @postConstruct() + protected init(): void { + super.init(); + this.initializeRoot(); + } + + protected async initializeRoot(): Promise { + this.selectionService.onSelectionChanged(selection => { + const { widget } = (selection[0] as OpenEditorNode); + this.applicationShell.activateWidget(widget.id); + }); + this.toDispose.push(this.applicationShell.onDidChangeCurrentWidget(async ({ newValue }) => { + const nodeToSelect = this.tree.getNode(newValue?.id) as SelectableTreeNode; + if (nodeToSelect) { + this.selectNode(nodeToSelect); + } + })); + this.toDispose.push(this.workspaceService.onWorkspaceChanged(() => this.updateOpenWidgets())); + this.toDispose.push(this.workspaceService.onWorkspaceLocationChanged(() => this.updateOpenWidgets())); + this.toDispose.push(this.applicationShell.onDidAddWidget(() => this.updateOpenWidgets())); + this.toDispose.push(this.applicationShell.onDidRemoveWidget(() => this.updateOpenWidgets())); + this.toDispose.push(this.editorPreviewManager.onCreated(previewWidget => { + if (previewWidget instanceof EditorPreviewWidget) { + this.toDisposeOnPreviewWidgetReplaced.dispose(); + this.toDisposeOnPreviewWidgetReplaced.push(previewWidget.onPinned(() => this.updateOpenWidgets())); + this.toDisposeOnPreviewWidgetReplaced.push(previewWidget.onDidChangeTrackableWidgets(() => this.updateOpenWidgets())); + } + })); + await this.updateOpenWidgets(); + this.fireChanged(); + } + + protected updateOpenWidgets = debounce(this.doUpdateOpenWidgets, 250); + + protected async doUpdateOpenWidgets(): Promise { + const editorWidgets = this.applicationShell.widgets.filter(widget => widget instanceof EditorWidget); + this.root = await this.buildRootFromOpenedWidgets(editorWidgets); + } + + protected async buildRootFromOpenedWidgets(openWidgets: Widget[]): Promise { + const newRoot: CompositeTreeNode = { + id: 'open-editors:root', + parent: undefined, + visible: false, + children: [] + }; + for (const widget of openWidgets) { + if (Navigatable.is(widget)) { + const uri = widget.getResourceUri(); + if (uri) { + const fileStat = await this.fileService.resolve(uri); + const openEditorNode: OpenEditorNode = { + id: widget.id, + fileStat, + uri, + selected: false, + parent: undefined, + name: widget.title.label, + icon: widget.title.iconClass, + widget + }; + CompositeTreeNode.addChild(newRoot, openEditorNode); + } + } + + } + return newRoot; + } +} diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx new file mode 100644 index 0000000000000..a58725716dda9 --- /dev/null +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx @@ -0,0 +1,179 @@ +/******************************************************************************** + * 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 * as React from '@theia/core/shared/react'; +import { injectable, interfaces, Container, postConstruct, inject } from '@theia/core/shared/inversify'; +import { + ApplicationShell, + ContextMenuRenderer, + defaultTreeProps, + NodeProps, + TreeDecoratorService, + TreeModel, + TreeNode, + TreeProps, + TREE_NODE_CONTENT_CLASS, +} from '@theia/core/lib/browser'; +import { OpenEditorNode, OpenEditorsModel } from './navigator-open-editors-tree-model'; +import { createFileTreeContainer, FileTreeModel, FileTreeWidget } from '@theia/filesystem/lib/browser'; +import { OpenEditorsTreeDecoratorService } from './navigator-open-editors-decorator-service'; +import { OpenEditorTreeDecorationData } from './navigator-open-editors-file-decorator'; +import { notEmpty } from '@theia/core/lib/common'; +import { OPEN_EDITORS_CONTEXT_MENU } from './navigator-open-editors-menus'; +import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser'; + +const CLOSE_ICON_WIDTH = 16; + +export const OPEN_EDITORS_PROPS: TreeProps = { + ...defaultTreeProps, + virtualized: false, + contextMenuPath: OPEN_EDITORS_CONTEXT_MENU, + globalSelection: true, + // leftPadding: 40 +}; +@injectable() +export class OpenEditorsWidget extends FileTreeWidget { + static ID = 'theia-open-editors-widget'; + static LABEL = 'Open Editors'; + + static PREFIX_ICON_CLASS = 'open-editors-prefix-icon'; + + @inject(ApplicationShell) protected readonly applicationShell: ApplicationShell; + + static createContainer(parent: interfaces.Container): Container { + const child = createFileTreeContainer(parent); + + child.unbind(FileTreeModel); + child.bind(OpenEditorsModel).toSelf(); + child.rebind(TreeModel).toService(OpenEditorsModel); + + child.unbind(FileTreeWidget); + child.bind(OpenEditorsWidget).toSelf(); + + child.rebind(TreeProps).toConstantValue(OPEN_EDITORS_PROPS); + + child.bind(OpenEditorsTreeDecoratorService).toSelf().inSingletonScope(); + child.rebind(TreeDecoratorService).toService(OpenEditorsTreeDecoratorService); + return child; + } + + static createWidget(parent: interfaces.Container): OpenEditorsWidget { + return OpenEditorsWidget.createContainer(parent).get(OpenEditorsWidget); + } + + constructor( + @inject(TreeProps) readonly props: TreeProps, + @inject(OpenEditorsModel) readonly model: OpenEditorsModel, + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer + ) { + super(props, model, contextMenuRenderer); + } + + @postConstruct() + init(): void { + super.init(); + this.id = OpenEditorsWidget.ID; + this.title.label = OpenEditorsWidget.LABEL; + this.addClass(OpenEditorsWidget.ID); + this.update(); + } + protected activeTreeNodePrefixElement: string | undefined | null; + + protected renderNode(node: OpenEditorNode, props: NodeProps): React.ReactNode { + if (!TreeNode.isVisible(node)) { + return undefined; + } + const attributes = this.createNodeAttributes(node, props); + const content =
+ {this.renderPrefixIcon(node)} + {this.decorateIcon(node, this.renderIcon(node, props))} + {this.renderCaptionAffixes(node, props, 'captionPrefixes')} + {this.renderCaption(node, props)} + {this.renderCaptionAffixes(node, props, 'captionSuffixes')} + {this.renderTailDecorations(node, props)} +
; + return React.createElement('div', attributes, content); + } + + protected handleMouseEnter = (e: React.MouseEvent) => this.doHandleMouseEnter(e); + protected doHandleMouseEnter(e: React.MouseEvent): void { + if (e.currentTarget) { + this.activeTreeNodePrefixElement = e.currentTarget.querySelector(`.${OpenEditorsWidget.PREFIX_ICON_CLASS}`)?.getAttribute('data-id'); + this.update(); + } + } + + protected handleMouseLeave = (e: React.MouseEvent) => this.doHandleMouseLeave(e); + protected doHandleMouseLeave(e: React.MouseEvent): void { + if (e.currentTarget) { + this.activeTreeNodePrefixElement = undefined; + this.update(); + } + } + + protected renderPrefixIcon(node: OpenEditorNode): React.ReactNode { + return (
); + } + + protected getPrefixIconClass(node: OpenEditorNode): string { + const isDirty = (this.getDecorationData(node, 'dirty'))[0]; + const isHighlighedNode = this.activeTreeNodePrefixElement === node.id; + if (isHighlighedNode) { + return 'codicon-close'; + } else if (isDirty) { + return 'codicon-circle-filled'; + } + return ''; + } + + protected getDecorationData(node: TreeNode, key: K): OpenEditorTreeDecorationData[K][] { + return this.getDecorations(node).filter(data => data[key] !== undefined).map(data => data[key]).filter(notEmpty); + } + + protected getDecorations(node: TreeNode): OpenEditorTreeDecorationData[] { + // Return type is set to custom DecorationData. This method is to satisfy TS + return super.getDecorations(node); + } + + protected closeEditor = async (e: React.MouseEvent) => this.doCloseEditor(e); + protected async doCloseEditor(e: React.MouseEvent): Promise { + const widgetId = e.currentTarget.getAttribute('data-id'); + if (widgetId) { + await this.applicationShell.closeWidget(widgetId); + } + } + + protected renderFileIcon(node: TreeNode, props: NodeProps): React.ReactNode { + const icon = this.toNodeIcon(node); + return icon &&
; + } + + protected handleDblClickEvent(node: OpenEditorNode | undefined, event: React.MouseEvent): void { + super.handleDblClickEvent(node, event); + if (node?.widget.parent instanceof EditorPreviewWidget) { + node.widget.parent.pinEditorWidget(); + } + } + + protected getPaddingLeft(node: TreeNode, props: NodeProps): number { + return super.getPaddingLeft(node, props) + CLOSE_ICON_WIDTH; + } +} diff --git a/packages/navigator/src/browser/open-editors-widget/open-editors.css b/packages/navigator/src/browser/open-editors-widget/open-editors.css new file mode 100644 index 0000000000000..64dcf6c162efa --- /dev/null +++ b/packages/navigator/src/browser/open-editors-widget/open-editors.css @@ -0,0 +1,31 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +:root { + --theia-open-editors-icon-width: 16px; +} + +.theia-open-editors-widget .theia-caption-suffix { + margin-left: var(--theia-ui-padding); + font-size: var(--theia-ui-font-size0); +} + +.open-editors-prefix-icon { + width: var(--theia-open-editors-icon-width); + padding-right: var(--theia-ui-padding); + position: absolute; + left: var(--theia-open-editors-icon-width); +} diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index 0de9bcecfb331..7fa6d8ce19687 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -62,6 +62,7 @@ export namespace TerminalMenus { export const TERMINAL_TASKS_INFO = [...TERMINAL_TASKS, '3_terminal']; export const TERMINAL_TASKS_CONFIG = [...TERMINAL_TASKS, '4_terminal']; export const TERMINAL_NAVIGATOR_CONTEXT_MENU = ['navigator-context-menu', 'navigation']; + export const TERMINAL_OPEN_EDITORS_CONTEXT_MENU = ['open-editors-context-menu', 'navigation']; } export namespace TerminalCommands { @@ -408,6 +409,10 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon commandId: TerminalCommands.TERMINAL_CONTEXT.id, order: 'z' }); + menus.registerMenuAction(TerminalMenus.TERMINAL_OPEN_EDITORS_CONTEXT_MENU, { + commandId: TerminalCommands.TERMINAL_CONTEXT.id, + order: 'z' + }); } registerToolbarItems(toolbar: TabBarToolbarRegistry): void { diff --git a/scripts/check-publish.js b/scripts/check-publish.js index 7451d4f0e8b60..33e486ef7e2e6 100644 --- a/scripts/check-publish.js +++ b/scripts/check-publish.js @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ // @ts-check - +df const path = require('path'); const chalk = require('chalk').default; const cp = require('child_process');