From 038072f08d2d6fc39396bf2f38542ed98960e9d7 Mon Sep 17 00:00:00 2001 From: eeakrnm Date: Tue, 18 Jul 2023 11:05:57 -0500 Subject: [PATCH 1/6] initial commit --- examples/browser/package.json | 1 + examples/browser/tsconfig.json | 3 + examples/electron/package.json | 1 + examples/electron/tsconfig.json | 3 + packages/terminal-manager/.eslintrc.js | 10 + packages/terminal-manager/package.json | 40 ++ .../browser/terminal-manager-alert-dialog.tsx | 115 ++++ .../terminal-manager-frontend-module.ts | 62 ++ ...inal-manager-frontend-view-contribution.ts | 329 +++++++++ .../browser/terminal-manager-preferences.ts | 41 ++ .../browser/terminal-manager-tree-model.ts | 349 ++++++++++ .../browser/terminal-manager-tree-widget.tsx | 305 +++++++++ .../src/browser/terminal-manager-types.ts | 173 +++++ .../src/browser/terminal-manager-widget.ts | 629 ++++++++++++++++++ .../src/browser/terminal-manager.css | 133 ++++ packages/terminal-manager/tsconfig.json | 12 + tsconfig.json | 3 + yarn.lock | 106 +++ 18 files changed, 2315 insertions(+) create mode 100644 packages/terminal-manager/.eslintrc.js create mode 100644 packages/terminal-manager/package.json create mode 100644 packages/terminal-manager/src/browser/terminal-manager-alert-dialog.tsx create mode 100644 packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts create mode 100644 packages/terminal-manager/src/browser/terminal-manager-frontend-view-contribution.ts create mode 100644 packages/terminal-manager/src/browser/terminal-manager-preferences.ts create mode 100644 packages/terminal-manager/src/browser/terminal-manager-tree-model.ts create mode 100644 packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx create mode 100644 packages/terminal-manager/src/browser/terminal-manager-types.ts create mode 100644 packages/terminal-manager/src/browser/terminal-manager-widget.ts create mode 100644 packages/terminal-manager/src/browser/terminal-manager.css create mode 100644 packages/terminal-manager/tsconfig.json diff --git a/examples/browser/package.json b/examples/browser/package.json index 37e1245fd8de4..e0dc7771a0754 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -50,6 +50,7 @@ "@theia/secondary-window": "1.39.0", "@theia/task": "1.39.0", "@theia/terminal": "1.39.0", + "@theia/terminal-manager": "1.39.0", "@theia/timeline": "1.39.0", "@theia/toolbar": "1.39.0", "@theia/typehierarchy": "1.39.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index d4bcfc14426b9..7922d7db37287 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -113,6 +113,9 @@ { "path": "../../packages/terminal" }, + { + "path": "../../packages/terminal-manager" + }, { "path": "../../packages/timeline" }, diff --git a/examples/electron/package.json b/examples/electron/package.json index 84bc7ac61dfb1..529777ff5386a 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -52,6 +52,7 @@ "@theia/task": "1.39.0", "@theia/terminal": "1.39.0", "@theia/timeline": "1.39.0", + "@theia/terminal-manager": "1.39.0", "@theia/toolbar": "1.39.0", "@theia/typehierarchy": "1.39.0", "@theia/userstorage": "1.39.0", diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json index ccd4b9fbd4d34..8dec8b742a367 100644 --- a/examples/electron/tsconfig.json +++ b/examples/electron/tsconfig.json @@ -116,6 +116,9 @@ { "path": "../../packages/terminal" }, + { + "path": "../../packages/terminal-manager" + }, { "path": "../../packages/timeline" }, diff --git a/packages/terminal-manager/.eslintrc.js b/packages/terminal-manager/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/terminal-manager/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + } +}; diff --git a/packages/terminal-manager/package.json b/packages/terminal-manager/package.json new file mode 100644 index 0000000000000..ba743b0e98be1 --- /dev/null +++ b/packages/terminal-manager/package.json @@ -0,0 +1,40 @@ +{ + "name": "@theia/terminal-manager", + "version": "1.39.0", + "description": "Theia - Terminal Manager Extension", + "keywords": [ + "theia-extension" + ], + "homepage": "https://github.com/eclipse-theia/theia", + "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "author": "Ericsson", + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "lint": "theiaext lint", + "test": "theiaext test", + "watch": "theiaext watch" + }, + "dependencies": { + "@theia/core": "latest", + "@theia/preferences": "latest", + "@theia/terminal": "latest" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/terminal-manager-frontend-module" + } + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/terminal-manager/src/browser/terminal-manager-alert-dialog.tsx b/packages/terminal-manager/src/browser/terminal-manager-alert-dialog.tsx new file mode 100644 index 0000000000000..7b6dc4d09a7c0 --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-alert-dialog.tsx @@ -0,0 +1,115 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as React from '@theia/core/shared/react'; +import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog'; +import { injectable, inject, interfaces } from '@theia/core/shared/inversify'; +import { codicon, DialogProps } from '@theia/core/lib/browser'; + +export const AlertDialogSettings = Symbol('AlertDialogSettings'); +export interface AlertDialogSettings { + message: string; + title: string; + className: string; + type: 'error' | 'warning' | 'info' + primaryButtons: string[], + secondaryButton: string, +} + +export const AlertDialogFactory = Symbol('AlertDialogFactory'); +export interface AlertDialogFactory { + (settings: AlertDialogSettings): AlertDialog; +} + +@injectable() +export class AlertDialog extends ReactDialog { + protected selectedValue: string | 'close' = ''; + + constructor( + @inject(DialogProps) protected override readonly props: DialogProps, + @inject(AlertDialogSettings) protected readonly dialogSettings: AlertDialogSettings, + ) { + super(props); + this.addClass('theia-alert-dialog'); + this.addClass(dialogSettings.type); + this.addClass(dialogSettings.className); + } + + protected render(): React.ReactNode { + const { className, type, message, primaryButtons, secondaryButton } = this.dialogSettings; + return ( +
+
+
+
+
{message}
+
+
+
+ { + primaryButtons.map(button => ( + + )) + } + +
+
+ ); + } + + protected handleButton = (e: React.MouseEvent): void => this.doHandleButton(e); + protected doHandleButton(e: React.MouseEvent): void { + const buttonText = e.currentTarget.getAttribute('data-id'); + if (buttonText) { + this.selectedValue = buttonText; + } + this.accept(); + } + + get value(): string | 'close' { + return this.selectedValue; + } +} + +export const bindGenericErrorDialogFactory = (bind: interfaces.Bind): void => { + bind(AlertDialogFactory) + .toFactory(({ container }) => (settings: AlertDialogSettings): AlertDialog => { + const child = container.createChild(); + child.bind(DialogProps).toConstantValue({ + title: settings.title, + wordWrap: 'break-word', + }); + child.bind(AlertDialogSettings).toConstantValue(settings); + child.bind(AlertDialog).toSelf().inSingletonScope(); + return child.get(AlertDialog); + }); +}; diff --git a/packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts b/packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts new file mode 100644 index 0000000000000..9d42eee762271 --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts @@ -0,0 +1,62 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; +import { + bindViewContribution, + PreferenceContribution, + WidgetFactory, + WidgetManager, +} from '@theia/core/lib/browser'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { PreferenceProxyFactory } from '@theia/core/lib/browser/preferences/injectable-preference-proxy'; +import { TerminalManagerWidget } from './terminal-manager-widget'; +import { TerminalManagerFrontendViewContribution } from './terminal-manager-frontend-view-contribution'; +import { TerminalManagerPreferenceContribution, TerminalManagerPreferences, TerminalManagerPreferenceSchema } from './terminal-manager-preferences'; +import { TerminalManagerTreeWidget } from './terminal-manager-tree-widget'; +import { bindGenericErrorDialogFactory } from './terminal-manager-alert-dialog'; +import '../../src/browser/terminal-manager.css'; + +export default new ContainerModule((bind: interfaces.Bind) => { + bindViewContribution(bind, TerminalManagerFrontendViewContribution); + bind(TabBarToolbarContribution).toService(TerminalManagerFrontendViewContribution); + + bind(WidgetFactory).toDynamicValue(({ container }) => ({ + id: TerminalManagerTreeWidget.ID, + createWidget: () => TerminalManagerTreeWidget.createWidget(container), + })).inSingletonScope(); + + bind(WidgetFactory).toDynamicValue(({ container }) => ({ + id: TerminalManagerWidget.ID, + createWidget: async () => { + const child = container.createChild(); + const terminalManagerTreeWidget = await container.get(WidgetManager) + .getOrCreateWidget(TerminalManagerTreeWidget.ID); + child.bind(TerminalManagerTreeWidget).toConstantValue(terminalManagerTreeWidget); + return TerminalManagerWidget.createWidget(child); + }, + })); + + bindGenericErrorDialogFactory(bind); + + bind(TerminalManagerPreferences).toDynamicValue(ctx => { + const factory = ctx.container.get(PreferenceProxyFactory); + return factory(TerminalManagerPreferenceSchema); + }).inSingletonScope(); + bind(TerminalManagerPreferenceContribution).toConstantValue({ schema: TerminalManagerPreferenceSchema }); + bind(PreferenceContribution).toService(TerminalManagerPreferenceContribution); +}); + diff --git a/packages/terminal-manager/src/browser/terminal-manager-frontend-view-contribution.ts b/packages/terminal-manager/src/browser/terminal-manager-frontend-view-contribution.ts new file mode 100644 index 0000000000000..3558163f62886 --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-frontend-view-contribution.ts @@ -0,0 +1,329 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + AbstractViewContribution, + codicon, + KeybindingContribution, + KeybindingRegistry, + Widget, +} from '@theia/core/lib/browser'; +import { CommandRegistry, Emitter, MenuModelRegistry } from '@theia/core'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { BOTTOM_AREA_ID, MAXIMIZED_CLASS } from '@theia/core/lib/browser/shell/theia-dock-panel'; +import { TerminalManagerCommands, TerminalManagerTreeTypes, TERMINAL_MANAGER_TREE_CONTEXT_MENU } from './terminal-manager-types'; +import { TerminalManagerWidget } from './terminal-manager-widget'; +import { TerminalManagerTreeWidget } from './terminal-manager-tree-widget'; +import { AlertDialogFactory } from './terminal-manager-alert-dialog'; + +/* eslint-disable max-lines-per-function */ +@injectable() +export class TerminalManagerFrontendViewContribution extends AbstractViewContribution + implements TabBarToolbarContribution, KeybindingContribution { + protected onBottomPanelMaximizeDidChangeEmitter = new Emitter(); + protected onBottomPanelMaximizeDidChange = this.onBottomPanelMaximizeDidChangeEmitter.event; + + @inject(AlertDialogFactory) protected readonly alertDialogFactory: AlertDialogFactory; + + constructor() { + super({ + widgetId: TerminalManagerWidget.ID, + widgetName: 'Terminal Manager', + defaultWidgetOptions: { + area: 'bottom', + }, + }); + } + + override registerCommands(commands: CommandRegistry): void { + super.registerCommands(commands); + commands.registerCommand(TerminalManagerCommands.MANAGER_NEW_TERMINAL_GROUP, { + execute: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + ) => { + const nodeId = args[1]; + if (TerminalManagerTreeTypes.isPageId(nodeId)) { + this.createNewTerminalGroup(nodeId); + } + }, + isVisible: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + ) => args[0] instanceof TerminalManagerTreeWidget && TerminalManagerTreeTypes.isPageId(args[1]), + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_SHOW_TREE_TOOLBAR, { + execute: () => this.handleToggleTree(), + isVisible: widget => widget instanceof TerminalManagerWidget, + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_NEW_PAGE_BOTTOM_TOOLBAR, { + execute: () => this.createNewTerminalPage(), + isVisible: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + // eslint-disable-next-line max-len + ) => args[0] instanceof TerminalManagerWidget, + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_DELETE_TERMINAL, { + execute: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + ) => TerminalManagerTreeTypes.isTerminalKey(args[1]) && this.deleteTerminalFromManager(args[1]), + isVisible: (...args: TerminalManagerTreeTypes.ContextMenuArgs) => { + const treeWidget = args[0]; + const nodeId = args[1]; + if (treeWidget instanceof TerminalManagerTreeWidget && TerminalManagerTreeTypes.isTerminalKey(nodeId)) { + const { model } = treeWidget; + const terminalNode = model.getNode(nodeId); + return TerminalManagerTreeTypes.isTerminalNode(terminalNode); + } + return false; + }, + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_DELETE_PAGE, { + execute: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + ) => TerminalManagerTreeTypes.isPageId(args[1]) && this.deletePageFromManager(args[1]), + isVisible: (...args: TerminalManagerTreeTypes.ContextMenuArgs) => { + const widget = args[0]; + return widget instanceof TerminalManagerTreeWidget && TerminalManagerTreeTypes.isPageId(args[1]) && widget.model.pages.size >= 1; + }, + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_RENAME_TERMINAL, { + execute: (...args: TerminalManagerTreeTypes.ContextMenuArgs) => this.toggleRenameTerminalFromManager(args[1]), + isVisible: (...args: TerminalManagerTreeTypes.ContextMenuArgs) => args[0] instanceof TerminalManagerTreeWidget, + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_ADD_TERMINAL_TO_GROUP, { + execute: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + ) => { + const nodeId = args[1]; + if (TerminalManagerTreeTypes.isGroupId(nodeId)) { + this.addTerminalToGroup(nodeId); + } + }, + isVisible: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + ) => args[0] instanceof TerminalManagerTreeWidget && TerminalManagerTreeTypes.isGroupId(args[1]), + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_DELETE_GROUP, { + execute: ( + ...args: TerminalManagerTreeTypes.ContextMenuArgs + ) => TerminalManagerTreeTypes.isGroupId(args[1]) && this.deleteGroupFromManager(args[1]), + isVisible: (...args: TerminalManagerTreeTypes.ContextMenuArgs) => { + const treeWidget = args[0]; + const groupId = args[1]; + if (treeWidget instanceof TerminalManagerTreeWidget && TerminalManagerTreeTypes.isGroupId(groupId)) { + const { model } = treeWidget; + const groupNode = model.getNode(groupId); + return TerminalManagerTreeTypes.isGroupNode(groupNode); + } + return false; + }, + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_MAXIMIZE_BOTTOM_PANEL_TOOLBAR, { + execute: () => this.maximizeBottomPanel(), + isVisible: widget => widget instanceof Widget + && widget.parent?.id === BOTTOM_AREA_ID + && !this.shell.bottomPanel.hasClass(MAXIMIZED_CLASS), + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR, { + execute: () => this.maximizeBottomPanel(), + isVisible: widget => widget instanceof Widget + && widget.parent?.id === BOTTOM_AREA_ID + && this.shell.bottomPanel.hasClass(MAXIMIZED_CLASS), + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_CLEAR_ALL, { + isVisible: widget => widget instanceof TerminalManagerWidget, + execute: async widget => { + if (widget instanceof TerminalManagerWidget) { + const PRIMARY_BUTTON = 'Reset Layout'; + const dialogResponse = await this.confirmUserAction({ + title: 'Do you want to reset the terminal manager layout?', + message: 'Once the layout is reset, it cannot be restored. Are you sure you would like to clear the layout?', + primaryButtonText: PRIMARY_BUTTON, + }); + if (dialogResponse === PRIMARY_BUTTON) { + for (const id of widget.pagePanels.keys()) { + widget.deletePage(id); + } + } + } + }, + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_OPEN_VIEW, { + execute: () => this.openView({ activate: true }), + }); + commands.registerCommand(TerminalManagerCommands.MANAGER_CLOSE_VIEW, { + isVisible: () => Boolean(this.tryGetWidget()), + isEnabled: () => Boolean(this.tryGetWidget()), + execute: () => this.closeView(), + }); + } + + protected async confirmUserAction(options: { title: string, message: string, primaryButtonText: string }): Promise { + const dialog = this.alertDialogFactory({ + title: options.title, + message: options.message, + type: 'info', + className: 'terminal-manager-close-alert', + primaryButtons: [options.primaryButtonText], + secondaryButton: 'Cancel', + }); + const dialogResponse = await dialog.open(); + dialog.dispose(); + return dialogResponse; + } + + override async closeView(): Promise { + const CLOSE = 'Close'; + const userResponse = await this.confirmUserAction({ + title: 'Do you want to close the terminal manager?', + message: 'Once the Terminal Manager is closed, its layout cannot be restored. Are you sure you want to close the Terminal Manager?', + primaryButtonText: CLOSE, + }); + if (userResponse === CLOSE) { + return super.closeView(); + } + return undefined; + } + + protected maximizeBottomPanel(): void { + this.shell.bottomPanel.toggleMaximized(); + this.onBottomPanelMaximizeDidChangeEmitter.fire(); + } + + protected async createNewTerminalPage(): Promise { + const terminalManagerWidget = await this.widget; + const terminalWidget = await terminalManagerWidget.createTerminalWidget(); + terminalManagerWidget.addTerminalPage(terminalWidget); + } + + protected async createNewTerminalGroup(pageId: TerminalManagerTreeTypes.PageId): Promise { + const terminalManagerWidget = await this.widget; + const terminalWidget = await terminalManagerWidget.createTerminalWidget(); + terminalManagerWidget.addTerminalGroupToPage(terminalWidget, pageId); + } + + protected async addTerminalToGroup(groupId: TerminalManagerTreeTypes.GroupId): Promise { + const terminalManagerWidget = await this.widget; + const terminalWidget = await terminalManagerWidget.createTerminalWidget(); + terminalManagerWidget.addWidgetToTerminalGroup(terminalWidget, groupId); + } + + protected async handleToggleTree(): Promise { + const terminalManagerWidget = await this.widget; + terminalManagerWidget.toggleTreeVisibility(); + } + + protected async deleteTerminalFromManager(terminalId: TerminalManagerTreeTypes.TerminalKey): Promise { + const terminalManagerWidget = await this.widget; + terminalManagerWidget?.deleteTerminal(terminalId); + } + + protected async deleteGroupFromManager(groupId: TerminalManagerTreeTypes.GroupId): Promise { + const widget = await this.widget; + widget.deleteGroup(groupId); + } + + protected async deletePageFromManager(pageId: TerminalManagerTreeTypes.PageId): Promise { + const widget = await this.widget; + widget.deletePage(pageId); + } + + protected async toggleRenameTerminalFromManager(entityId: TerminalManagerTreeTypes.TerminalManagerValidId): Promise { + const widget = await this.widget; + widget.toggleRenameTerminal(entityId); + } + + override registerMenus(menus: MenuModelRegistry): void { + super.registerMenus(menus); + menus.registerMenuAction(TERMINAL_MANAGER_TREE_CONTEXT_MENU, { + commandId: TerminalManagerCommands.MANAGER_ADD_TERMINAL_TO_GROUP.id, + order: 'a', + }); + menus.registerMenuAction(TERMINAL_MANAGER_TREE_CONTEXT_MENU, { + commandId: TerminalManagerCommands.MANAGER_RENAME_TERMINAL.id, + order: 'b', + }); + menus.registerMenuAction(TERMINAL_MANAGER_TREE_CONTEXT_MENU, { + commandId: TerminalManagerCommands.MANAGER_DELETE_TERMINAL.id, + order: 'c', + }); + menus.registerMenuAction(TERMINAL_MANAGER_TREE_CONTEXT_MENU, { + commandId: TerminalManagerCommands.MANAGER_DELETE_PAGE.id, + order: 'c', + }); + menus.registerMenuAction(TERMINAL_MANAGER_TREE_CONTEXT_MENU, { + commandId: TerminalManagerCommands.MANAGER_DELETE_GROUP.id, + order: 'c', + }); + + menus.registerMenuAction(TerminalManagerTreeTypes.PAGE_NODE_MENU, { + commandId: TerminalManagerCommands.MANAGER_NEW_TERMINAL_GROUP.id, + order: 'a', + }); + menus.registerMenuAction(TerminalManagerTreeTypes.PAGE_NODE_MENU, { + commandId: TerminalManagerCommands.MANAGER_DELETE_PAGE.id, + order: 'b', + }); + + menus.registerMenuAction(TerminalManagerTreeTypes.TERMINAL_NODE_MENU, { + commandId: TerminalManagerCommands.MANAGER_DELETE_TERMINAL.id, + order: 'c', + }); + + menus.registerMenuAction(TerminalManagerTreeTypes.GROUP_NODE_MENU, { + commandId: TerminalManagerCommands.MANAGER_ADD_TERMINAL_TO_GROUP.id, + order: 'a', + }); + menus.registerMenuAction(TerminalManagerTreeTypes.GROUP_NODE_MENU, { + commandId: TerminalManagerCommands.MANAGER_DELETE_GROUP.id, + order: 'c', + }); + } + + registerToolbarItems(toolbar: TabBarToolbarRegistry): void { + toolbar.registerItem({ + id: TerminalManagerCommands.MANAGER_NEW_PAGE_BOTTOM_TOOLBAR.id, + command: TerminalManagerCommands.MANAGER_NEW_PAGE_BOTTOM_TOOLBAR.id, + }); + toolbar.registerItem({ + id: TerminalManagerCommands.MANAGER_SHOW_TREE_TOOLBAR.id, + command: TerminalManagerCommands.MANAGER_SHOW_TREE_TOOLBAR.id, + }); + toolbar.registerItem({ + id: TerminalManagerCommands.MANAGER_CLEAR_ALL.id, + command: TerminalManagerCommands.MANAGER_CLEAR_ALL.id, + }); + toolbar.registerItem({ + id: TerminalManagerCommands.MANAGER_MAXIMIZE_BOTTOM_PANEL_TOOLBAR.id, + command: TerminalManagerCommands.MANAGER_MAXIMIZE_BOTTOM_PANEL_TOOLBAR.id, + icon: codicon('chevron-up'), + onDidChange: this.onBottomPanelMaximizeDidChange, + }); + toolbar.registerItem({ + id: TerminalManagerCommands.MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR.id, + command: TerminalManagerCommands.MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR.id, + icon: codicon('chevron-down'), + onDidChange: this.onBottomPanelMaximizeDidChange, + }); + } + + override registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ + command: TerminalManagerCommands.MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR.id, + keybinding: 'alt+q', + }); + } +} diff --git a/packages/terminal-manager/src/browser/terminal-manager-preferences.ts b/packages/terminal-manager/src/browser/terminal-manager-preferences.ts new file mode 100644 index 0000000000000..05aae557aefa2 --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-preferences.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { nls } from '@theia/core'; +import { PreferenceProxy, PreferenceSchema } from '@theia/core/lib/browser'; + +export const TerminalManagerPreferenceSchema: PreferenceSchema = { + type: 'object', + properties: { + 'terminalManager.treeViewLocation': { + 'type': 'string', + 'enum': ['left', 'right'], + 'description': nls.localize('theia/terminalManager/treeViewLocation', 'The location of the terminal manager\'s tree view'), + 'default': 'left', + }, + }, +}; + +export type TerminalManagerTreeViewLocation = 'left' | 'right'; + +export interface TerminalManagerConfiguration { + 'terminalManager.treeViewLocation': TerminalManagerTreeViewLocation; +} + +export const TerminalManagerPreferences = Symbol('TerminalManagerPreferences'); +export const TerminalManagerPreferenceContribution = Symbol('TerminalManagerPreferenceContribution'); +export type TerminalManagerPreferences = PreferenceProxy; + diff --git a/packages/terminal-manager/src/browser/terminal-manager-tree-model.ts b/packages/terminal-manager/src/browser/terminal-manager-tree-model.ts new file mode 100644 index 0000000000000..46432731cfd13 --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-tree-model.ts @@ -0,0 +1,349 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable, postConstruct } from '@theia/core/shared/inversify'; +import { TreeModelImpl, CompositeTreeNode, SelectableTreeNode, DepthFirstTreeIterator } from '@theia/core/lib/browser'; +import { Emitter } from '@theia/core'; +import { TerminalManagerTreeTypes } from './terminal-manager-types'; + +@injectable() +export class TerminalManagerTreeModel extends TreeModelImpl { + activePageNode: TerminalManagerTreeTypes.PageNode | undefined; + activeGroupNode: TerminalManagerTreeTypes.TerminalGroupNode | undefined; + activeTerminalNode: TerminalManagerTreeTypes.TerminalNode | undefined; + + protected onTreeSelectionChangedEmitter = new Emitter(); + readonly onTreeSelectionChanged = this.onTreeSelectionChangedEmitter.event; + + protected onPageAddedEmitter = new Emitter<{ pageId: TerminalManagerTreeTypes.PageId, terminalKey: TerminalManagerTreeTypes.TerminalKey }>(); + readonly onPageAdded = this.onPageAddedEmitter.event; + protected onPageDeletedEmitter = new Emitter(); + readonly onPageDeleted = this.onPageDeletedEmitter.event; + + protected onNodeRenamedEmitter = new Emitter(); + readonly onNodeRenamed = this.onNodeRenamedEmitter.event; + + protected onTerminalGroupAddedEmitter = new Emitter<{ + groupId: TerminalManagerTreeTypes.GroupId, + pageId: TerminalManagerTreeTypes.PageId, + terminalKey: TerminalManagerTreeTypes.TerminalKey, + }>(); + + readonly onTerminalGroupAdded = this.onTerminalGroupAddedEmitter.event; + protected onTerminalGroupDeletedEmitter = new Emitter(); + readonly onTerminalGroupDeleted = this.onTerminalGroupDeletedEmitter.event; + + protected onTerminalAddedToGroupEmitter = new Emitter<{ + terminalId: TerminalManagerTreeTypes.TerminalKey, + groupId: TerminalManagerTreeTypes.GroupId, + }>(); + + readonly onTerminalAddedToGroup = this.onTerminalAddedToGroupEmitter.event; + protected onTerminalDeletedFromGroupEmitter = new Emitter<{ + terminalId: TerminalManagerTreeTypes.TerminalKey, + groupId: TerminalManagerTreeTypes.GroupId, + }>(); + + readonly onTerminalDeletedFromGroup = this.onTerminalDeletedFromGroupEmitter.event; + + @postConstruct() + protected override init(): void { + super.init(); + this.toDispose.push(this.selectionService.onSelectionChanged(selectionEvent => { + const selectedNode = selectionEvent.find(node => node.selected); + if (selectedNode) { + this.handleSelectionChanged(selectedNode); + } + })); + this.root = { id: 'root', parent: undefined, children: [], visible: false } as CompositeTreeNode; + } + + protected getContext = () => this; + + addTerminalPage( + terminalKey: TerminalManagerTreeTypes.TerminalKey, + groupId: TerminalManagerTreeTypes.GroupId, + pageId: TerminalManagerTreeTypes.PageId, + ): void { + const pageNode = this.createPageNode(pageId); + const groupNode = this.createGroupNode(groupId, pageId); + const terminalNode = this.createTerminalNode(terminalKey, groupId); + if (this.root && CompositeTreeNode.is(this.root)) { + this.activePageNode = pageNode; + CompositeTreeNode.addChild(groupNode, terminalNode); + CompositeTreeNode.addChild(pageNode, groupNode); + this.root = CompositeTreeNode.addChild(this.root, pageNode); + this.onPageAddedEmitter.fire({ pageId: pageNode.id, terminalKey }); + setTimeout(() => { + this.selectionService.addSelection(terminalNode); + }); + } + } + + getName(): string { + const pageLabel = this.activePageNode?.label; + const groupLabel = this.activeGroupNode?.label; + const terminalLabel = this.activeTerminalNode?.label; + let name = ''; + if (pageLabel) { + name += pageLabel; + } + if (groupLabel) { + name += ` > ${groupLabel}`; + } + if (terminalLabel) { + name += ` > ${terminalLabel}`; + } + return name; + } + + protected createPageNode(pageId: TerminalManagerTreeTypes.PageId): TerminalManagerTreeTypes.PageNode { + const currentPageNumber = this.getNextPageCounter(); + return { + id: pageId, + label: `Page(${currentPageNumber})`, + parent: undefined, + selected: false, + children: [], + page: true, + isEditing: false, + expanded: true, + counter: currentPageNumber, + }; + } + + protected getNextPageCounter(): number { + return Math.max(0, ...Array.from(this.pages.values(), page => page.counter)) + 1; + } + + deleteTerminalPage(pageId: TerminalManagerTreeTypes.PageId): void { + const pageNode = this.getNode(pageId); + if (TerminalManagerTreeTypes.isPageNode(pageNode) && CompositeTreeNode.is(this.root)) { + while (pageNode.children.length > 0) { + const groupNode = pageNode.children[pageNode.children.length - 1]; + this.doDeleteTerminalGroup(groupNode, pageNode); + } + this.onPageDeletedEmitter.fire(pageNode.id); + CompositeTreeNode.removeChild(this.root, pageNode); + setTimeout(() => this.selectPrevNode()); + this.refresh(); + } + } + + addTerminalGroup( + terminalKey: TerminalManagerTreeTypes.TerminalKey, + groupId: TerminalManagerTreeTypes.GroupId, + pageId: TerminalManagerTreeTypes.PageId, + ): void { + const groupNode = this.createGroupNode(groupId, pageId); + const terminalNode = this.createTerminalNode(terminalKey, groupId); + const pageNode = this.getNode(pageId); + if (this.root && CompositeTreeNode.is(this.root) && TerminalManagerTreeTypes.isPageNode(pageNode)) { + this.onTerminalGroupAddedEmitter.fire({ groupId: groupNode.id, pageId, terminalKey }); + CompositeTreeNode.addChild(groupNode, terminalNode); + CompositeTreeNode.addChild(pageNode, groupNode); + this.refresh(); + setTimeout(() => { + this.selectionService.addSelection(terminalNode); + }); + } + } + + protected createGroupNode( + groupId: TerminalManagerTreeTypes.GroupId, + pageId: TerminalManagerTreeTypes.PageId, + ): TerminalManagerTreeTypes.TerminalGroupNode { + const currentGroupNum = this.getNextGroupCounterForPage(pageId); + return { + id: groupId, + label: `Group(${currentGroupNum})`, + parent: undefined, + selected: false, + children: [], + terminalGroup: true, + isEditing: false, + parentPageId: pageId, + expanded: true, + counter: currentGroupNum, + }; + } + + protected getNextGroupCounterForPage(pageId: TerminalManagerTreeTypes.PageId): number { + const page = this.pages.get(pageId); + if (page) { + return Math.max(0, ...page.children.map(group => group.counter)) + 1; + } + return 1; + } + + deleteTerminalGroup(groupId: TerminalManagerTreeTypes.GroupId): void { + const groupNode = this.tree.getNode(groupId); + const parentPageNode = groupNode?.parent; + if (TerminalManagerTreeTypes.isGroupNode(groupNode) && TerminalManagerTreeTypes.isPageNode(parentPageNode)) { + if (parentPageNode.children.length === 1) { + this.deleteTerminalPage(parentPageNode.id); + } else { + this.doDeleteTerminalGroup(groupNode, parentPageNode); + this.refresh(); + } + } + } + + protected doDeleteTerminalGroup(group: TerminalManagerTreeTypes.TerminalGroupNode, page: TerminalManagerTreeTypes.PageNode): void { + while (group.children.length > 0) { + const terminalNode = group.children[group.children.length - 1]; + this.doDeleteTerminalNode(terminalNode, group); + } + this.onTerminalGroupDeletedEmitter.fire(group.id); + CompositeTreeNode.removeChild(page, group); + } + + addTerminal(newTerminalId: TerminalManagerTreeTypes.TerminalKey, groupId: TerminalManagerTreeTypes.GroupId): void { + const groupNode = this.getNode(groupId); + if (groupNode && TerminalManagerTreeTypes.isGroupNode(groupNode)) { + const terminalNode = this.createTerminalNode(newTerminalId, groupId); + CompositeTreeNode.addChild(groupNode, terminalNode); + this.onTerminalAddedToGroupEmitter.fire({ terminalId: newTerminalId, groupId }); + this.refresh(); + setTimeout(() => { + if (SelectableTreeNode.is(terminalNode)) { + this.selectionService.addSelection(terminalNode); + } + }); + } + } + + createTerminalNode( + terminalId: TerminalManagerTreeTypes.TerminalKey, + groupId: TerminalManagerTreeTypes.GroupId, + ): TerminalManagerTreeTypes.TerminalNode { + return { + id: terminalId, + label: 'Terminal', + parent: undefined, + children: [], + selected: false, + terminal: true, + isEditing: false, + parentGroupId: groupId, + }; + } + + deleteTerminalNode(terminalId: TerminalManagerTreeTypes.TerminalKey): void { + const terminalNode = this.getNode(terminalId); + const parentGroupNode = terminalNode?.parent; + if (TerminalManagerTreeTypes.isTerminalNode(terminalNode) && TerminalManagerTreeTypes.isGroupNode(parentGroupNode)) { + if (parentGroupNode.children.length === 1) { + this.deleteTerminalGroup(parentGroupNode.id); + } else { + this.doDeleteTerminalNode(terminalNode, parentGroupNode); + this.refresh(); + } + } + } + + protected doDeleteTerminalNode(node: TerminalManagerTreeTypes.TerminalNode, parent: TerminalManagerTreeTypes.TerminalGroupNode): void { + if (TerminalManagerTreeTypes.isGroupNode(parent)) { + this.onTerminalDeletedFromGroupEmitter.fire({ + terminalId: node.id, + groupId: parent.id, + }); + CompositeTreeNode.removeChild(parent, node); + } + } + + toggleRenameTerminal(entityId: TerminalManagerTreeTypes.TerminalManagerValidId): void { + const node = this.getNode(entityId); + if (TerminalManagerTreeTypes.isTerminalManagerTreeNode(node)) { + node.isEditing = true; + this.fireChanged(); + } + } + + acceptRename(nodeId: string, newName: string): void { + const node = this.getNode(nodeId); + if (TerminalManagerTreeTypes.isTerminalManagerTreeNode(node)) { + const trimmedName = newName.trim(); + node.label = trimmedName === '' ? node.label : newName; + node.isEditing = false; + this.fireChanged(); + this.onNodeRenamedEmitter.fire(node); + } + } + + handleSelectionChanged(selectedNode: SelectableTreeNode): void { + let activeTerminal: TerminalManagerTreeTypes.TerminalNode | undefined = undefined; + let activeGroup: TerminalManagerTreeTypes.TerminalGroupNode | undefined = undefined; + let activePage: TerminalManagerTreeTypes.PageNode | undefined = undefined; + + if (TerminalManagerTreeTypes.isTerminalNode(selectedNode)) { + activeTerminal = selectedNode; + const { parent } = activeTerminal; + if (TerminalManagerTreeTypes.isGroupNode(parent)) { + activeGroup = parent; + const grandparent = activeGroup.parent; + if (TerminalManagerTreeTypes.isPageNode(grandparent)) { + activePage = grandparent; + } + } else if (TerminalManagerTreeTypes.isPageNode(parent)) { + activePage = parent; + } + } else if (TerminalManagerTreeTypes.isGroupNode(selectedNode)) { + const { parent } = selectedNode; + activeGroup = selectedNode; + if (TerminalManagerTreeTypes.isPageNode(parent)) { + activePage = parent; + } + } else if (TerminalManagerTreeTypes.isPageNode(selectedNode)) { + activePage = selectedNode; + } + + this.activeTerminalNode = activeTerminal; + this.activeGroupNode = activeGroup; + this.activePageNode = activePage; + this.onTreeSelectionChangedEmitter.fire({ + activePageId: activePage?.id, + activeTerminalId: activeTerminal?.id, + activeGroupId: activeGroup?.id, + }); + } + + get pages(): Map { + const pages = new Map(); + if (!this.root) { + return pages; + } + for (const node of new DepthFirstTreeIterator(this.root)) { + if (TerminalManagerTreeTypes.isPageNode(node)) { + pages.set(node.id, node); + } + } + return pages; + } + + getPageIdForTerminal(terminalKey: TerminalManagerTreeTypes.TerminalKey): TerminalManagerTreeTypes.PageId | undefined { + const terminalNode = this.getNode(terminalKey); + if (!TerminalManagerTreeTypes.isTerminalNode(terminalNode)) { + return undefined; + } + const { parentGroupId } = terminalNode; + const groupNode = this.getNode(parentGroupId); + if (!TerminalManagerTreeTypes.isGroupNode(groupNode)) { + return undefined; + } + return groupNode.parentPageId; + } +} diff --git a/packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx b/packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx new file mode 100644 index 0000000000000..a20bdf42a1a9c --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx @@ -0,0 +1,305 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as React from '@theia/core/shared/react'; +import { Container, inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; +import { + codicon, + CompositeTreeNode, + createTreeContainer, + Key, + Message, + NodeProps, + SelectableTreeNode, + TreeModel, + TreeNode, + TreeWidget, + TREE_NODE_INDENT_GUIDE_CLASS, +} from '@theia/core/lib/browser'; +import { CommandRegistry, CompoundMenuNode, Emitter, MenuAction, MenuModelRegistry } from '@theia/core'; +import { TerminalManagerTreeModel } from './terminal-manager-tree-model'; +import { ReactInteraction, TerminalManagerTreeTypes, TERMINAL_MANAGER_TREE_CONTEXT_MENU } from './terminal-manager-types'; + +/* eslint-disable no-param-reassign */ +@injectable() +export class TerminalManagerTreeWidget extends TreeWidget { + static ID = 'terminal-manager-tree-widget'; + + protected onDidChangeEmitter = new Emitter(); + readonly onDidChange = this.onDidChangeEmitter.event; + + @inject(MenuModelRegistry) protected menuRegistry: MenuModelRegistry; + @inject(TreeModel) override readonly model: TerminalManagerTreeModel; + @inject(CommandRegistry) protected commandRegistry: CommandRegistry; + + static createContainer(parent: interfaces.Container): Container { + const child = createTreeContainer( + parent, + { + props: { + leftPadding: 8, + contextMenuPath: TERMINAL_MANAGER_TREE_CONTEXT_MENU, + expandOnlyOnExpansionToggleClick: true, + }, + }, + ); + child.bind(TerminalManagerTreeModel).toSelf().inSingletonScope(); + child.rebind(TreeModel).to(TerminalManagerTreeModel); + child.bind(TerminalManagerTreeWidget).toSelf().inSingletonScope(); + return child; + } + + static createWidget(parent: interfaces.Container): TerminalManagerTreeWidget { + return TerminalManagerTreeWidget.createContainer(parent).get(TerminalManagerTreeWidget); + } + + @postConstruct() + protected override init(): void { + super.init(); + this.id = 'terminal-manager-tree-widget'; + this.addClass(TerminalManagerTreeWidget.ID); + this.toDispose.push(this.onDidChangeEmitter); + } + + protected override toContextMenuArgs(node: SelectableTreeNode): TerminalManagerTreeTypes.ContextMenuArgs | undefined { + if ( + TerminalManagerTreeTypes.isPageNode(node) + || TerminalManagerTreeTypes.isTerminalNode(node) + || TerminalManagerTreeTypes.isGroupNode(node) + ) { + return TerminalManagerTreeTypes.toContextMenuArgs(this, node); + } + return undefined; + } + + protected override renderCaption(node: TreeNode, props: NodeProps): React.ReactNode { + if (TerminalManagerTreeTypes.isTerminalManagerTreeNode(node) && !!node.isEditing) { + const label = this.toNodeName(node); + // eslint-disable-next-line @typescript-eslint/ban-types + const assignRef = (element: HTMLInputElement | null) => { + if (element) { + element.selectionStart = 0; + element.selectionEnd = label.length; + } + }; + return ( + + ); + } + return super.renderCaption(node, props); + } + + protected handleRenameOnBlur = (e: React.FocusEvent): void => this.doHandleRenameOnBlur(e); + protected doHandleRenameOnBlur(e: React.FocusEvent): void { + const { value } = e.currentTarget; + const id = e.currentTarget.getAttribute('data-id'); + if (id) { + this.model.acceptRename(id, value); + } + } + + protected override renderExpansionToggle(node: TreeNode, props: NodeProps): React.ReactNode { + return super.renderExpansionToggle(node, props); + } + + protected handleRenameOnKeyDown = (e: React.KeyboardEvent): void => this.doHandleRenameOnKeyDown(e); + protected doHandleRenameOnKeyDown(e: React.KeyboardEvent): void { + const { value, defaultValue } = e.currentTarget; + const id = e.currentTarget.getAttribute('data-id'); + e.stopPropagation(); + if (e.key === 'Escape') { + e.preventDefault(); + if (value && id) { + this.model.acceptRename(id, defaultValue); + } + } else if (e.key === 'Tab' || e.key === 'Enter') { + e.preventDefault(); + if (value && id) { + this.model.acceptRename(id, value.trim()); + } + } + } + + // @ts-expect-error 2416 cf. https://github.com/eclipse-theia/theia/issues/11640 + protected override handleLeft(event: KeyboardEvent): boolean | Promise { + if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + return super.handleLeft(event); + } + + // @ts-expect-error 2416 cf. https://github.com/eclipse-theia/theia/issues/11640 + protected override handleRight(event: KeyboardEvent): boolean | Promise { + if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + return super.handleRight(event); + } + + // cf. https://github.com/eclipse-theia/theia/issues/11640 + protected override handleEscape(event: KeyboardEvent): boolean | void { + if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + return super.handleEscape(event); + } + + // cf. https://github.com/eclipse-theia/theia/issues/11640 + protected override handleEnter(event: KeyboardEvent): boolean | void { + if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + return super.handleEnter(event); + } + + // cf. https://github.com/eclipse-theia/theia/issues/11640 + protected override handleSpace(event: KeyboardEvent): boolean | void { + if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + return super.handleSpace(event); + } + + protected override renderTailDecorations(node: TreeNode, _props: NodeProps): React.ReactNode { + if (TerminalManagerTreeTypes.isTerminalManagerTreeNode(node)) { + const inlineActionsForNode = this.resolveInlineActionForNode(node); + return ( +
+
+ {inlineActionsForNode.map(({ icon, commandId, label }) => ( + + ))} +
+
+ ); + } + return undefined; + } + + protected handleActionItemOnClick = (e: ReactInteraction): void => this.doHandleActionItemOnClick(e); + protected doHandleActionItemOnClick(e: ReactInteraction): void { + if ('key' in e && e.key !== Key.ENTER.code) { + return; + } + e.stopPropagation(); + const commandId = e.currentTarget.getAttribute('data-command-id'); + const nodeId = e.currentTarget.getAttribute('data-node-id'); + if (commandId && nodeId) { + const node = this.model.getNode(nodeId); + if (TerminalManagerTreeTypes.isTerminalManagerTreeNode(node)) { + const args = TerminalManagerTreeTypes.toContextMenuArgs(this, node); + this.commandRegistry.executeCommand(commandId, ...args); + } + } + } + + protected resolveInlineActionForNode(node: TerminalManagerTreeTypes.TerminalManagerTreeNode): MenuAction[] { + let menuNode: CompoundMenuNode | undefined = undefined; + const inlineActionProps: MenuAction[] = []; + if (TerminalManagerTreeTypes.isPageNode(node)) { + menuNode = this.menuRegistry.getMenu(TerminalManagerTreeTypes.PAGE_NODE_MENU); + } else if (TerminalManagerTreeTypes.isGroupNode(node)) { + menuNode = this.menuRegistry.getMenu(TerminalManagerTreeTypes.GROUP_NODE_MENU); + } else if (TerminalManagerTreeTypes.isTerminalNode(node)) { + menuNode = this.menuRegistry.getMenu(TerminalManagerTreeTypes.TERMINAL_NODE_MENU); + } + if (!menuNode) { + return []; + } + const menuItems = menuNode.children; + menuItems.forEach(item => { + const commandId = item.id; + const args = TerminalManagerTreeTypes.toContextMenuArgs(this, node); + const isVisible = this.commandRegistry.isVisible(commandId, ...args); + if (isVisible) { + const command = this.commandRegistry.getCommand(commandId); + const icon = command?.iconClass ? command.iconClass : ''; + const label = command?.label ? command.label : ''; + inlineActionProps.push({ icon, label, commandId }); + } + }); + return inlineActionProps; + } + + protected override renderIcon(node: TreeNode, _props: NodeProps): React.ReactNode { + if (TerminalManagerTreeTypes.isTerminalNode(node)) { + return ; + } else if (TerminalManagerTreeTypes.isPageNode(node)) { + return ; + } else if (TerminalManagerTreeTypes.isGroupNode(node)) { + return ; + } + return undefined; + } + + protected override toNodeName(node: TerminalManagerTreeTypes.TerminalManagerTreeNode): string { + return node.label ?? 'node.id'; + } + + protected override onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.onDidChangeEmitter.fire(undefined); + } + + protected override renderIndent(node: TreeNode, props: NodeProps): React.ReactNode { + const renderIndentGuides = this.corePreferences['workbench.tree.renderIndentGuides']; + if (renderIndentGuides === 'none') { + return undefined; + } + + const indentDivs: React.ReactNode[] = []; + let current: TreeNode | undefined = node; + let { depth } = props; + while (current && depth) { + const classNames: string[] = [TREE_NODE_INDENT_GUIDE_CLASS]; + if (this.needsActiveIndentGuideline(current)) { + classNames.push('active'); + } else { + classNames.push(renderIndentGuides === 'onHover' ? 'hover' : 'always'); + } + const paddingLeft = this.props.leftPadding * depth; + indentDivs.unshift(
); + current = current.parent; + depth -= 1; + } + return indentDivs; + } + + protected override getDepthForNode(node: TreeNode, depths: Map): number { + const parentDepth = depths.get(node.parent); + if (TerminalManagerTreeTypes.isTerminalNode(node) && parentDepth === undefined) { + return 1; + } + return super.getDepthForNode(node, depths); + } +} + diff --git a/packages/terminal-manager/src/browser/terminal-manager-types.ts b/packages/terminal-manager/src/browser/terminal-manager-types.ts new file mode 100644 index 0000000000000..d18586a21cdcc --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-types.ts @@ -0,0 +1,173 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Command, MenuPath } from '@theia/core'; +import { + SelectableTreeNode, + CompositeTreeNode, + SplitPanel, + codicon, + ExpandableTreeNode, + Widget, +} from '@theia/core/lib/browser'; +import { TerminalWidgetFactoryOptions, TerminalWidgetImpl } from '@theia/terminal/lib/browser/terminal-widget-impl'; + +export namespace TerminalManagerCommands { + export const MANAGER_NEW_TERMINAL_GROUP = Command.toDefaultLocalizedCommand({ + id: 'terminal:new-in-manager-toolbar', + category: 'Terminal Manager', + label: 'Create New Terminal Group', + iconClass: codicon('split-horizontal'), + }); + export const MANAGER_DELETE_TERMINAL = Command.toDefaultLocalizedCommand({ + id: 'terminal:delete-terminal', + category: 'Terminal Manager', + label: 'Delete Terminal', + iconClass: codicon('trash'), + }); + export const MANAGER_RENAME_TERMINAL = Command.toDefaultLocalizedCommand({ + id: 'terminal: rename-terminal', + category: 'Terminal Manager', + label: 'Rename...', + iconClass: codicon('edit'), + }); + export const MANAGER_NEW_PAGE_BOTTOM_TOOLBAR = Command.toDefaultLocalizedCommand({ + id: 'terminal:new-manager-page', + category: 'Terminal Manager', + label: 'Create New Terminal Page', + iconClass: codicon('new-file'), + }); + export const MANAGER_DELETE_PAGE = Command.toDefaultLocalizedCommand({ + id: 'terminal:delete-page', + category: 'Terminal Manager', + label: 'Delete Page', + iconClass: codicon('trash'), + }); + export const MANAGER_ADD_TERMINAL_TO_GROUP = Command.toDefaultLocalizedCommand({ + id: 'terminal:manager-split-horizontal', + category: 'Terminal Manager', + label: 'Add terminal to group', + iconClass: codicon('split-vertical'), + }); + export const MANAGER_DELETE_GROUP = Command.toDefaultLocalizedCommand({ + id: 'terminal:manager-delete-group', + category: 'Terminal Manager', + label: 'Delete Group...', + iconClass: codicon('trash'), + }); + export const MANAGER_SHOW_TREE_TOOLBAR = Command.toDefaultLocalizedCommand({ + id: 'terminal:manager-toggle-tree', + category: 'Terminal Manager', + label: 'Toggle Tree View', + iconClass: codicon('list-tree'), + }); + + export const MANAGER_MAXIMIZE_BOTTOM_PANEL_TOOLBAR = Command.toDefaultLocalizedCommand({ + id: 'terminal:manager-maximize-bottom-panel', + category: 'Terminal Manager', + label: 'Maximize Bottom Panel', + }); + export const MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR = Command.toDefaultLocalizedCommand({ + id: 'terminal:manager-minimize-bottom-panel', + category: 'Terminal Manager', + label: 'Minimize Bottom Panel', + }); + export const MANAGER_CLEAR_ALL = Command.toDefaultLocalizedCommand({ + id: 'terminal:manager-clear-all', + category: 'Terminal Manager', + label: 'Reset Terminal Manager layout', + iconClass: codicon('trash'), + }); + export const MANAGER_OPEN_VIEW: Command = { + id: 'terminal:open-manager', + category: 'View', + label: 'Open Terminal Manager', + }; + export const MANAGER_CLOSE_VIEW: Command = { + id: 'terminal:close-manager', + category: 'View', + label: 'Close Terminal Manager', + }; +} + +export const TERMINAL_MANAGER_TREE_CONTEXT_MENU = ['terminal-manager-tree-context-menu']; +export namespace TerminalManagerTreeTypes { + export type TerminalKey = `terminal-${string}`; + export const generateTerminalKey = (widget: TerminalWidgetImpl): TerminalKey => { + const { created } = widget.options as TerminalWidgetFactoryOptions; + return `terminal-${created}`; + }; + export const isTerminalKey = (obj: unknown): obj is TerminalKey => typeof obj === 'string' && obj.startsWith('terminal-'); + export interface TerminalNode extends SelectableTreeNode, CompositeTreeNode { + terminal: true; + isEditing: boolean; + label: string; + id: TerminalKey; + parentGroupId: GroupId; + } + + export type GroupId = `group-${string}`; + export const isGroupId = (obj: unknown): obj is GroupId => typeof obj === 'string' && obj.startsWith('group-'); + export interface GroupSplitPanel extends SplitPanel { + id: GroupId; + } + export interface TerminalGroupNode extends SelectableTreeNode, ExpandableTreeNode { + terminalGroup: true; + isEditing: boolean; + label: string; + id: GroupId; + parentPageId: PageId; + counter: number; + children: readonly TerminalNode[] + } + + export type PageId = `page-${string}`; + export const isPageId = (obj: unknown): obj is PageId => typeof obj === 'string' && obj.startsWith('page-'); + export interface PageSplitPanel extends SplitPanel { + id: PageId; + } + export interface PageNode extends SelectableTreeNode, ExpandableTreeNode { + page: true; + children: TerminalGroupNode[]; + isEditing: boolean; + label: string; + id: PageId; + counter: number; + } + + export type TerminalManagerTreeNode = PageNode | TerminalNode | TerminalGroupNode; + export type TerminalManagerValidId = PageId | TerminalKey | GroupId; + export const isPageNode = (obj: unknown): obj is PageNode => !!obj && typeof obj === 'object' && 'page' in obj; + export const isTerminalNode = (obj: unknown): obj is TerminalNode => !!obj && typeof obj === 'object' && 'terminal' in obj; + export const isGroupNode = (obj: unknown): obj is TerminalGroupNode => !!obj && typeof obj === 'object' && 'terminalGroup' in obj; + export const isTerminalManagerTreeNode = ( + obj: unknown, + ): obj is PageNode | TerminalNode => isPageNode(obj) || isTerminalNode(obj) || isGroupNode(obj); + export interface SelectionChangedEvent { + activePageId: PageId | undefined; + activeTerminalId: TerminalKey | undefined; + activeGroupId: GroupId | undefined; + } + + export type ContextMenuArgs = [Widget, TerminalManagerValidId]; + export const toContextMenuArgs = (widget: Widget, node: TerminalManagerTreeNode): ContextMenuArgs => [widget, node.id as TerminalManagerValidId]; + + export const PAGE_NODE_MENU: MenuPath = ['terminal-manager-page-node']; + export const GROUP_NODE_MENU: MenuPath = ['terminal-manager-group-node']; + export const TERMINAL_NODE_MENU: MenuPath = ['terminal-manager-terminal-node']; +} + +export type ReactInteraction = React.MouseEvent | React.KeyboardEvent; diff --git a/packages/terminal-manager/src/browser/terminal-manager-widget.ts b/packages/terminal-manager/src/browser/terminal-manager-widget.ts new file mode 100644 index 0000000000000..be37938ef4a1c --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager-widget.ts @@ -0,0 +1,629 @@ +// ***************************************************************************** +// Copyright (C) 2023 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; +import { + ApplicationShell, + BaseWidget, + codicon, + CompositeTreeNode, + Message, + Panel, + PanelLayout, + SplitLayout, + SplitPanel, + SplitPositionHandler, + StatefulWidget, + StorageService, + ViewContainerLayout, + Widget, + WidgetManager, +} from '@theia/core/lib/browser'; +import { CommandService, Emitter } from '@theia/core'; +import { UUID } from '@theia/core/shared/@phosphor/coreutils'; +import { TerminalWidget, TerminalWidgetOptions } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { TerminalWidgetImpl } from '@theia/terminal/lib/browser/terminal-widget-impl'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { TerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; +import { TerminalManagerPreferences } from './terminal-manager-preferences'; +import { TerminalManagerTreeTypes } from './terminal-manager-types'; +import { TerminalManagerTreeWidget } from './terminal-manager-tree-widget'; + +/* eslint-disable max-lines-per-function, @typescript-eslint/no-magic-numbers, @typescript-eslint/ban-types, max-lines, max-depth, max-len */ + +export namespace TerminalManagerWidgetState { + export interface BaseLayoutData { + id: ID, + childLayouts: unknown[]; + } + export interface TerminalWidgetLayoutData { + widget: TerminalWidget | undefined; + } + + export interface TerminalGroupLayoutData extends BaseLayoutData { + childLayouts: TerminalWidgetLayoutData[]; + widgetRelativeHeights: number[] | undefined; + } + + export interface PageLayoutData extends BaseLayoutData { + childLayouts: TerminalGroupLayoutData[]; + groupRelativeWidths: number[] | undefined; + } + export interface TerminalManagerLayoutData extends BaseLayoutData<'ParentPanel'> { + childLayouts: PageLayoutData[]; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export const isLayoutData = (obj: any): obj is LayoutData => typeof obj === 'object' && !!obj && 'type' in obj && obj.type === 'terminal-manager'; + export interface PanelRelativeSizes { + terminal: number; + tree: number; + } + export interface LayoutData { + items?: TerminalManagerLayoutData; + widget: TerminalManagerTreeWidget; + terminalAndTreeRelativeSizes: PanelRelativeSizes | undefined; + } + +} + +@injectable() +export class TerminalManagerWidget extends BaseWidget implements StatefulWidget, ApplicationShell.TrackableWidgetProvider { + static ID = 'terminal-manager-widget'; + static LABEL = 'Terminal Manager'; + + override layout: PanelLayout; + protected panel: SplitPanel; + + protected pageAndTreeLayout: SplitLayout | undefined; + protected stateIsSet = false; + + pagePanels = new Map(); + groupPanels = new Map(); + terminalWidgets = new Map(); + + protected readonly onDidChangeTrackableWidgetsEmitter = new Emitter(); + readonly onDidChangeTrackableWidgets = this.onDidChangeTrackableWidgetsEmitter.event; + + // serves as an empty container so that different view containers can be swapped out + protected terminalPanelWrapper = new Panel({ + layout: new PanelLayout(), + }); + + @inject(TerminalFrontendContribution) protected terminalFrontendContribution: TerminalFrontendContribution; + @inject(TerminalManagerTreeWidget) readonly treeWidget: TerminalManagerTreeWidget; + @inject(SplitPositionHandler) protected readonly splitPositionHandler: SplitPositionHandler; + + @inject(ApplicationShell) protected readonly shell: ApplicationShell; + @inject(CommandService) protected readonly commandService: CommandService; + @inject(TerminalManagerPreferences) protected readonly terminalManagerPreferences: TerminalManagerPreferences; + @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService; + @inject(WidgetManager) protected readonly widgetManager: WidgetManager; + @inject(StorageService) protected readonly storageService: StorageService; + + static createRestoreError = ( + nodeId: string, + ): Error => new Error(`Terminal manager widget state could not be restored, mismatch in restored data for ${nodeId}`); + + static createContainer(parent: interfaces.Container): interfaces.Container { + const child = parent.createChild(); + child.bind(TerminalManagerWidget).toSelf().inSingletonScope(); + return child; + } + + static createWidget(parent: interfaces.Container): TerminalManagerWidget { + return TerminalManagerWidget.createContainer(parent).get(TerminalManagerWidget); + } + + @postConstruct() + protected async init(): Promise { + this.title.iconClass = codicon('terminal-tmux'); + this.id = TerminalManagerWidget.ID; + this.title.closable = false; + this.title.label = TerminalManagerWidget.LABEL; + this.node.tabIndex = 0; + await this.terminalManagerPreferences.ready; + this.registerListeners(); + this.createPageAndTreeLayout(); + } + + async populateLayout(force?: boolean): Promise { + if (!this.stateIsSet || force) { + const terminalWidget = await this.createTerminalWidget(); + this.addTerminalPage(terminalWidget); + this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets()); + this.stateIsSet = true; + } + } + + async createTerminalWidget(): Promise { + const terminalWidget = await this.terminalFrontendContribution.newTerminal({ + // passing 'created' here as a millisecond value rather than the default `new Date().toString()` that Theia uses in + // its factory (resolves to something like 'Tue Aug 09 2022 13:21:26 GMT-0500 (Central Daylight Time)'). + // The state restoration system relies on identifying terminals by their unique options, using an ms value ensures we don't + // get a duplication since the original date method is only accurate to within 1s. + created: new Date().getTime().toString(), + } as TerminalWidgetOptions); + terminalWidget.start(); + return terminalWidget; + } + + protected registerListeners(): void { + this.toDispose.push(this.treeWidget); + this.toDispose.push(this.treeWidget.model.onTreeSelectionChanged(changeEvent => this.handleSelectionChange(changeEvent))); + + this.toDispose.push(this.treeWidget.model.onPageAdded(({ pageId }) => this.handlePageAdded(pageId))); + this.toDispose.push(this.treeWidget.model.onPageDeleted(pageId => this.handlePageDeleted(pageId))); + + this.toDispose.push(this.treeWidget.model.onTerminalGroupAdded(({ + groupId, pageId, + }) => this.handleTerminalGroupAdded(groupId, pageId))); + this.toDispose.push(this.treeWidget.model.onTerminalGroupDeleted(groupId => this.handleTerminalGroupDeleted(groupId))); + + this.toDispose.push(this.treeWidget.model.onTerminalAddedToGroup(({ + terminalId, groupId, + }) => this.handleWidgetAddedToTerminalGroup(terminalId, groupId))); + this.toDispose.push(this.treeWidget.model.onTerminalDeletedFromGroup(({ + terminalId, + }) => this.handleTerminalDeleted(terminalId))); + this.toDispose.push(this.treeWidget.model.onNodeRenamed(() => this.handlePageRenamed())); + + this.toDispose.push(this.shell.onDidChangeActiveWidget(({ newValue }) => this.handleOnDidChangeActiveWidget(newValue))); + + this.toDispose.push(this.terminalManagerPreferences.onPreferenceChanged(() => this.resolveMainLayout())); + } + + protected handlePageRenamed(): void { + this.title.label = this.treeWidget.model.getName(); + this.update(); + } + + setPanelSizes({ terminal, tree } = { terminal: .6, tree: .2 } as TerminalManagerWidgetState.PanelRelativeSizes): void { + const treeViewLocation = this.terminalManagerPreferences.get('terminalManager.treeViewLocation'); + const panelSizes = treeViewLocation === 'left' ? [tree, terminal] : [terminal, tree]; + requestAnimationFrame(() => this.pageAndTreeLayout?.setRelativeSizes(panelSizes)); + } + + getTrackableWidgets(): Widget[] { + return [this.treeWidget, ...this.terminalWidgets.values()]; + } + + toggleTreeVisibility(): void { + if (this.treeWidget.isHidden) { + this.treeWidget.show(); + this.setPanelSizes(); + } else { + this.treeWidget.hide(); + } + } + + protected createPageAndTreeLayout(relativeSizes?: TerminalManagerWidgetState.PanelRelativeSizes): void { + this.layout = new PanelLayout(); + this.pageAndTreeLayout = new SplitLayout({ + renderer: SplitPanel.defaultRenderer, + orientation: 'horizontal', + spacing: 2, + }); + this.panel ??= new SplitPanel({ + layout: this.pageAndTreeLayout, + }); + + this.layout.addWidget(this.panel); + this.resolveMainLayout(relativeSizes); + this.update(); + } + + protected resolveMainLayout(relativeSizes?: TerminalManagerWidgetState.PanelRelativeSizes): void { + if (!this.pageAndTreeLayout) { + return; + } + const treeViewLocation = this.terminalManagerPreferences.get('terminalManager.treeViewLocation'); + const widgetsInDesiredOrder = treeViewLocation === 'left' ? [this.treeWidget, this.terminalPanelWrapper] : [this.terminalPanelWrapper, this.treeWidget]; + widgetsInDesiredOrder.forEach((widget, index) => { + this.pageAndTreeLayout?.insertWidget(index, widget); + }); + this.setPanelSizes(relativeSizes); + } + + protected override onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + this.populateLayout(); + } + + addTerminalPage(widget: Widget): void { + if (widget instanceof TerminalWidgetImpl) { + const terminalKey = TerminalManagerTreeTypes.generateTerminalKey(widget); + this.terminalWidgets.set(terminalKey, widget); + this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets()); + const groupPanel = this.createTerminalGroupPanel(); + groupPanel.addWidget(widget); + const pagePanel = this.createPagePanel(); + pagePanel.addWidget(groupPanel); + this.treeWidget.model.addTerminalPage(terminalKey, groupPanel.id, pagePanel.id); + } + } + + protected createPagePanel(pageId?: TerminalManagerTreeTypes.PageId): TerminalManagerTreeTypes.PageSplitPanel { + const newPageLayout = new ViewContainerLayout({ + renderer: SplitPanel.defaultRenderer, + orientation: 'horizontal', + spacing: 2, + headerSize: 0, + animationDuration: 200, + }, this.splitPositionHandler); + const pagePanel = new SplitPanel({ + layout: newPageLayout, + }) as TerminalManagerTreeTypes.PageSplitPanel; + const idPrefix = 'page-'; + const uuid = this.generateUUIDAvoidDuplicatesFromStorage(idPrefix); + pagePanel.node.tabIndex = -1; + pagePanel.id = pageId ?? `${idPrefix}${uuid}`; + this.pagePanels.set(pagePanel.id, pagePanel); + + return pagePanel; + } + + protected generateUUIDAvoidDuplicatesFromStorage(idPrefix: 'group-' | 'page-'): string { + // highly unlikely there would ever be a duplicate, but just to be safe :) + let didNotGenerateValidId = true; + let uuid = ''; + while (didNotGenerateValidId) { + uuid = UUID.uuid4(); + if (idPrefix === 'group-') { + didNotGenerateValidId = this.groupPanels.has(`group-${uuid}`); + } else if (idPrefix === 'page-') { + didNotGenerateValidId = this.pagePanels.has(`page-${uuid}`); + } + } + return uuid; + } + + protected handlePageAdded(pageId: TerminalManagerTreeTypes.PageId): void { + const pagePanel = this.pagePanels.get(pageId); + if (pagePanel) { + this.terminalPanelWrapper.addWidget(pagePanel); + this.update(); + } + } + + protected handlePageDeleted(pagePanelId: TerminalManagerTreeTypes.PageId): void { + this.pagePanels.get(pagePanelId)?.dispose(); + this.pagePanels.delete(pagePanelId); + if (this.pagePanels.size === 0) { + this.populateLayout(true); + } + } + + addTerminalGroupToPage(widget: Widget, pageId: TerminalManagerTreeTypes.PageId): void { + if (!this.treeWidget) { + return; + } + if (widget instanceof TerminalWidgetImpl) { + const terminalId = TerminalManagerTreeTypes.generateTerminalKey(widget); + this.terminalWidgets.set(terminalId, widget); + this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets()); + const groupPanel = this.createTerminalGroupPanel(); + groupPanel.addWidget(widget); + this.treeWidget.model.addTerminalGroup(terminalId, groupPanel.id, pageId); + } + } + + protected createTerminalGroupPanel(groupId?: TerminalManagerTreeTypes.GroupId): TerminalManagerTreeTypes.GroupSplitPanel { + const terminalColumnLayout = new ViewContainerLayout({ + renderer: SplitPanel.defaultRenderer, + orientation: 'vertical', + spacing: 0, + headerSize: 0, + animationDuration: 200, + alignment: 'end', + }, this.splitPositionHandler); + const groupPanel = new SplitPanel({ + layout: terminalColumnLayout, + }) as TerminalManagerTreeTypes.GroupSplitPanel; + const idPrefix = 'group-'; + const uuid = this.generateUUIDAvoidDuplicatesFromStorage(idPrefix); + groupPanel.node.tabIndex = -1; + groupPanel.id = groupId ?? `${idPrefix}${uuid}`; + this.groupPanels.set(groupPanel.id, groupPanel); + return groupPanel; + } + + protected handleTerminalGroupAdded( + groupId: TerminalManagerTreeTypes.GroupId, + pageId: TerminalManagerTreeTypes.PageId, + ): void { + if (!this.treeWidget) { + return; + } + const groupPanel = this.groupPanels.get(groupId); + if (!groupPanel) { + return; + } + const activePage = this.pagePanels.get(pageId); + if (activePage) { + activePage.addWidget(groupPanel); + this.update(); + } + } + + protected async activateTerminalWidget(terminalKey: TerminalManagerTreeTypes.TerminalKey): Promise { + const terminalWidgetToActivate = this.terminalWidgets.get(terminalKey)?.id; + if (terminalWidgetToActivate) { + const activeWidgetFound = await this.shell.activateWidget(terminalWidgetToActivate); + return activeWidgetFound; + } + return undefined; + } + + activateWidget(id: string): Widget | undefined { + const widget = Array.from(this.terminalWidgets.values()).find(terminalWidget => terminalWidget.id === id); + if (widget instanceof TerminalWidgetImpl) { + widget.activate(); + } + return widget; + } + + protected handleTerminalGroupDeleted(groupPanelId: TerminalManagerTreeTypes.GroupId): void { + this.groupPanels.get(groupPanelId)?.dispose(); + this.groupPanels.delete(groupPanelId); + } + + addWidgetToTerminalGroup(widget: Widget, groupId: TerminalManagerTreeTypes.GroupId): void { + if (widget instanceof TerminalWidgetImpl) { + const newTerminalId = TerminalManagerTreeTypes.generateTerminalKey(widget); + this.terminalWidgets.set(newTerminalId, widget); + this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets()); + this.treeWidget.model.addTerminal(newTerminalId, groupId); + } + } + + protected override onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + const activeTerminalId = this.treeWidget.model.activeTerminalNode?.id; + if (activeTerminalId) { + const activeTerminalWidget = this.terminalWidgets.get(activeTerminalId); + if (activeTerminalWidget) { + return activeTerminalWidget.node.focus(); + } + } + return this.node.focus(); + } + + protected handleWidgetAddedToTerminalGroup(terminalKey: TerminalManagerTreeTypes.TerminalKey, groupId: TerminalManagerTreeTypes.GroupId): void { + const terminalWidget = this.terminalWidgets.get(terminalKey); + const group = this.groupPanels.get(groupId); + if (terminalWidget && group) { + const groupPanel = this.groupPanels.get(groupId); + groupPanel?.addWidget(terminalWidget); + this.update(); + } + } + + protected handleTerminalDeleted(terminalId: TerminalManagerTreeTypes.TerminalKey): void { + const terminalWidget = this.terminalWidgets.get(terminalId); + terminalWidget?.dispose(); + this.terminalWidgets.delete(terminalId); + } + + protected handleOnDidChangeActiveWidget(widget: Widget | null): void { + if (!(widget instanceof TerminalWidgetImpl)) { + return; + } + const terminalKey = TerminalManagerTreeTypes.generateTerminalKey(widget); + this.selectTerminalNode(terminalKey); + } + + protected selectTerminalNode(terminalKey: TerminalManagerTreeTypes.TerminalKey): void { + const node = this.treeWidget.model.getNode(terminalKey); + if (node && TerminalManagerTreeTypes.isTerminalNode(node)) { + this.treeWidget.model.selectNode(node); + } + } + + protected handleSelectionChange(changeEvent: TerminalManagerTreeTypes.SelectionChangedEvent): void { + const { activePageId, activeTerminalId } = changeEvent; + if (activePageId && activePageId) { + const pageNode = this.treeWidget.model.getNode(activePageId); + if (!TerminalManagerTreeTypes.isPageNode(pageNode)) { + return; + } + this.title.label = this.treeWidget.model.getName(); + this.updateViewPage(activePageId); + } + if (activeTerminalId && activeTerminalId) { + this.flashActiveTerminal(activeTerminalId); + } + this.update(); + } + + protected flashActiveTerminal(terminalId: TerminalManagerTreeTypes.TerminalKey): void { + const terminal = this.terminalWidgets.get(terminalId); + if (terminal) { + terminal.addClass('attention'); + if (this.shell.activeWidget !== terminal) { + terminal.activate(); + } + } + const FLASH_TIMEOUT = 250; + setTimeout(() => terminal?.removeClass('attention'), FLASH_TIMEOUT); + } + + protected updateViewPage(activePageId: TerminalManagerTreeTypes.PageId): void { + const activePagePanel = this.pagePanels.get(activePageId); + if (activePagePanel) { + this.terminalPanelWrapper.widgets + .forEach(widget => widget !== activePagePanel && widget.hide()); + activePagePanel.show(); + this.update(); + } + } + + deleteTerminal(terminalId: TerminalManagerTreeTypes.TerminalKey): void { + this.treeWidget.model.deleteTerminalNode(terminalId); + } + + deleteGroup(groupId: TerminalManagerTreeTypes.GroupId): void { + this.treeWidget.model.deleteTerminalGroup(groupId); + } + + deletePage(pageNode: TerminalManagerTreeTypes.PageId): void { + this.treeWidget.model.deleteTerminalPage(pageNode); + } + + toggleRenameTerminal(entityId: TerminalManagerTreeTypes.TerminalManagerValidId): void { + this.treeWidget.model.toggleRenameTerminal(entityId); + } + + storeState(): TerminalManagerWidgetState.LayoutData { + return this.getLayoutData(); + } + + restoreState(oldState: TerminalManagerWidgetState.LayoutData): void { + const { items, widget, terminalAndTreeRelativeSizes } = oldState; + if (widget && terminalAndTreeRelativeSizes && items) { + this.setPanelSizes(terminalAndTreeRelativeSizes); + try { + this.restoreLayoutData(items, widget); + } catch (e) { + console.error(e); + this.resetLayout(); + this.populateLayout(true); + } finally { + this.stateIsSet = true; + const { activeTerminalNode } = this.treeWidget.model; + setTimeout(() => { + this.selectTerminalNode(activeTerminalNode?.id ?? Array.from(this.terminalWidgets.keys())[0]); + }); + } + } + } + + protected resetLayout(): void { + this.pagePanels = new Map(); + this.groupPanels = new Map(); + this.terminalWidgets = new Map(); + } + + protected iterateAndRestoreLayoutTree(pageLayouts: TerminalManagerWidgetState.PageLayoutData[], treeWidget: TerminalManagerTreeWidget): void { + for (const pageLayout of pageLayouts) { + const pageId = pageLayout.id; + + const pagePanel = this.createPagePanel(pageId); + const pageNode = treeWidget.model.getNode(pageId); + if (!TerminalManagerTreeTypes.isPageNode(pageNode)) { + throw TerminalManagerWidget.createRestoreError(pageId); + } + this.pagePanels.set(pageId, pagePanel); + this.terminalPanelWrapper.addWidget(pagePanel); + const { childLayouts: groupLayouts } = pageLayout; + for (const groupLayout of groupLayouts) { + const groupId = groupLayout.id; + const groupPanel = this.createTerminalGroupPanel(groupId); + const groupNode = treeWidget.model.getNode(groupId); + if (!TerminalManagerTreeTypes.isGroupNode(groupNode)) { + throw TerminalManagerWidget.createRestoreError(groupId); + } + this.groupPanels.set(groupId, groupPanel); + pagePanel.insertWidget(0, groupPanel); + const { childLayouts: widgetLayouts } = groupLayout; + for (const widgetLayout of widgetLayouts) { + const { widget } = widgetLayout; + if (widget instanceof TerminalWidgetImpl) { + const widgetId = TerminalManagerTreeTypes.generateTerminalKey(widget); + const widgetNode = treeWidget.model.getNode(widgetId); + if (!TerminalManagerTreeTypes.isTerminalNode(widgetNode)) { + throw TerminalManagerWidget.createRestoreError(widgetId); + } + this.terminalWidgets.set(widgetId, widget); + this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets()); + groupPanel.addWidget(widget); + } + } + const { widgetRelativeHeights } = groupLayout; + if (widgetRelativeHeights) { + requestAnimationFrame(() => groupPanel.setRelativeSizes(widgetRelativeHeights)); + } + } + const { groupRelativeWidths } = pageLayout; + if (groupRelativeWidths) { + requestAnimationFrame(() => pagePanel.setRelativeSizes(groupRelativeWidths)); + } + } + } + + restoreLayoutData(items: TerminalManagerWidgetState.TerminalManagerLayoutData, treeWidget: TerminalManagerTreeWidget): void { + const { childLayouts: pageLayouts } = items; + Array.from(this.pagePanels.keys()).forEach(pageId => this.deletePage(pageId)); + this.iterateAndRestoreLayoutTree(pageLayouts, treeWidget); + this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets()); + this.update(); + } + + getLayoutData(): TerminalManagerWidgetState.LayoutData { + const pageItems: TerminalManagerWidgetState.TerminalManagerLayoutData = { childLayouts: [], id: 'ParentPanel' }; + const treeViewLocation = this.terminalManagerPreferences.get('terminalManager.treeViewLocation'); + let terminalAndTreeRelativeSizes: TerminalManagerWidgetState.PanelRelativeSizes | undefined = undefined; + const sizeArray = this.pageAndTreeLayout?.relativeSizes(); + if (sizeArray && treeViewLocation === 'right') { + terminalAndTreeRelativeSizes = { tree: sizeArray[1], terminal: sizeArray[0] }; + } else if (sizeArray && treeViewLocation === 'left') { + terminalAndTreeRelativeSizes = { tree: sizeArray[0], terminal: sizeArray[1] }; + } + const fullLayoutData: TerminalManagerWidgetState.LayoutData = { + widget: this.treeWidget, + items: pageItems, + terminalAndTreeRelativeSizes, + }; + const treeRoot = this.treeWidget.model.root; + if (treeRoot && CompositeTreeNode.is(treeRoot)) { + const pageNodes = treeRoot.children; + for (const pageNode of pageNodes) { + if (TerminalManagerTreeTypes.isPageNode(pageNode)) { + const groupNodes = pageNode.children; + const pagePanel = this.pagePanels.get(pageNode.id); + const pageLayoutData: TerminalManagerWidgetState.PageLayoutData = { + childLayouts: [], + id: pageNode.id, + groupRelativeWidths: pagePanel?.relativeSizes(), + }; + for (const groupNode of groupNodes) { + const groupPanel = this.groupPanels.get(groupNode.id); + if (TerminalManagerTreeTypes.isGroupNode(groupNode)) { + const groupLayoutData: TerminalManagerWidgetState.TerminalGroupLayoutData = { + id: groupNode.id, + childLayouts: [], + widgetRelativeHeights: groupPanel?.relativeSizes(), + }; + const widgetNodes = groupNode.children; + for (const widgetNode of widgetNodes) { + if (TerminalManagerTreeTypes.isTerminalNode(widgetNode)) { + const widget = this.terminalWidgets.get(widgetNode.id); + const terminalLayoutData: TerminalManagerWidgetState.TerminalWidgetLayoutData = { + widget, + }; + groupLayoutData.childLayouts.push(terminalLayoutData); + } + } + pageLayoutData.childLayouts.unshift(groupLayoutData); + } + } + pageItems.childLayouts.push(pageLayoutData); + } + } + } + return fullLayoutData; + } +} diff --git a/packages/terminal-manager/src/browser/terminal-manager.css b/packages/terminal-manager/src/browser/terminal-manager.css new file mode 100644 index 0000000000000..2d4589088ca98 --- /dev/null +++ b/packages/terminal-manager/src/browser/terminal-manager.css @@ -0,0 +1,133 @@ +/******************************************************************************** + * Copyright (C) 2022-2023 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 + ********************************************************************************/ + +#terminal-manager-widget .p-Widget.p-Panel.p-SplitPanel { + height: 100%; +} + +#terminal-manager-widget .p-SplitPanel[data-orientation='horizontal'] > .p-SplitPanel-handle { + width: 1px !important; + background-color: var(--theia-tab-unfocusedInactiveForeground); +} + +#terminal-manager-widget .p-SplitPanel[data-orientation='vertical'] > .p-SplitPanel-handle { + height: 1px !important; + background-color: var(--theia-tab-unfocusedInactiveForeground); +} + +#terminal-manager-widget .p-Widget { + -webkit-transition: -webkit-filter 0.25s ease; + transition: filter 0.25s ease; +} + +#terminal-manager-widget .p-Widget.attention { + -webkit-filter: brightness(150%); + filter: brightness(150%); +} + +.terminal-manager-tree-widget .rename-node-input { + width: 100%; +} + +.terminal-manager-tree-widget .theia-CompositeTreeNode .terminal-manager-inline-actions-container { + visibility: hidden; +} + +.terminal-manager-tree-widget .theia-CompositeTreeNode:hover .terminal-manager-inline-actions-container { + visibility: unset; +} + +.terminal-manager-tree-widget .codicon { + margin-right: 3px; +} + +.terminal-manager-inline-actions { + display: flex; + align-items: center; + cursor: pointer; +} + +.theia-alert-dialog { + --icon-size: 35px; +} + +.theia-alert-dialog .dialogBlock { + max-width: 500px; +} + +.theia-alert-dialog .theia-alert-dialog-message-wrapper { + display: grid; + grid-template-columns: var(--icon-size) 5fr; +} + +.theia-alert-dialog .dialogBlock .dialogControl { + display: none; +} + +.theia-alert-dialog-content-wrapper { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.theia-alert-dialog-message-wrapper .alert-icon { + font-size: var(--icon-size); +} + +.theia-alert-dialog-message-wrapper .message-container { + display: flex; + align-items: center; + justify-content: center; +} + +.theia-alert-dialog-message-wrapper .message { + padding-left: var(--theia-ui-padding); +} + +.theia-alert-dialog-buttons-wrapper { + width: 100%; + display: flex; + justify-content: center; +} + +/* Error Styles */ +.theia-alert-dialog.error .dialogTitle { + background-color: var(--theia-statusBar-errorForground); +} + +.theia-alert-dialog.error .alert-icon { + color: var(--theia-statusBar-errorForground); +} + +/* Info Styles */ +.theia-alert-dialog.info .dialogTitle { + background-color: var(--theia-statusBar-background); +} + +.theia-alert-dialog.info .alert-icon { + color: var(--theia-statusBar-background); +} + +/* Warning Styles */ +.theia-alert-dialog.warning .dialogTitle { + background-color: var(--theia-notificationsWarningIcon-foreground); +} + +.theia-alert-dialog.warning .alert-icon { + color: var(--theia-notificationsWarningIcon-foreground); +} diff --git a/packages/terminal-manager/tsconfig.json b/packages/terminal-manager/tsconfig.json new file mode 100644 index 0000000000000..bc676ce8dfe79 --- /dev/null +++ b/packages/terminal-manager/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "composite": true + }, + "include": [ + "src" + ], + "references": [] +} diff --git a/tsconfig.json b/tsconfig.json index b1a72fda40d8b..3c52e9c88c07b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -159,6 +159,9 @@ { "path": "packages/terminal" }, + { + "path": "packages/terminal-manager" + }, { "path": "packages/timeline" }, diff --git a/yarn.lock b/yarn.lock index b511dbdf93121..3390e0ebab58e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1687,11 +1687,117 @@ dependencies: defer-to-connect "^2.0.0" +"@theia/core@latest": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@theia/core/-/core-1.39.0.tgz#34bf07edce5e4e90ef0578acca46e51ce07450fc" + integrity sha512-EmvIpp3mKGSFO50iOm3hOa66WlyHigFOGyq1N4nVibzb82/T065LZDof8NTLXl50cMr6H3ZFhBC32TTGf4ircw== + dependencies: + "@babel/runtime" "^7.10.0" + "@phosphor/algorithm" "1" + "@phosphor/commands" "1" + "@phosphor/coreutils" "1" + "@phosphor/domutils" "1" + "@phosphor/dragdrop" "1" + "@phosphor/messaging" "1" + "@phosphor/properties" "1" + "@phosphor/signaling" "1" + "@phosphor/virtualdom" "1" + "@phosphor/widgets" "1" + "@theia/application-package" "1.39.0" + "@theia/request" "1.39.0" + "@types/body-parser" "^1.16.4" + "@types/cookie" "^0.3.3" + "@types/dompurify" "^2.2.2" + "@types/express" "^4.16.0" + "@types/fs-extra" "^4.0.2" + "@types/lodash.debounce" "4.0.3" + "@types/lodash.throttle" "^4.1.3" + "@types/markdown-it" "^12.2.3" + "@types/react" "^18.0.15" + "@types/react-dom" "^18.0.6" + "@types/route-parser" "^0.1.1" + "@types/safer-buffer" "^2.1.0" + "@types/ws" "^5.1.2" + "@types/yargs" "^15" + "@vscode/codicons" "*" + ajv "^6.5.3" + async-mutex "^0.4.0" + body-parser "^1.17.2" + cookie "^0.4.0" + dompurify "^2.2.9" + drivelist "^9.0.2" + es6-promise "^4.2.4" + express "^4.16.3" + fast-json-stable-stringify "^2.1.0" + file-icons-js "~1.0.3" + font-awesome "^4.7.0" + fs-extra "^4.0.2" + fuzzy "^0.1.3" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + iconv-lite "^0.6.0" + inversify "^6.0.1" + jschardet "^2.1.1" + keytar "7.2.0" + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + markdown-it "^12.3.2" + msgpackr "^1.6.1" + nsfw "^2.2.4" + p-debounce "^2.1.0" + perfect-scrollbar "^1.3.0" + react "^18.2.0" + react-dom "^18.2.0" + react-tooltip "^4.2.21" + react-virtuoso "^2.17.0" + reflect-metadata "^0.1.10" + route-parser "^0.0.5" + safer-buffer "^2.1.2" + socket.io "^4.5.3" + socket.io-client "^4.5.3" + uuid "^8.3.2" + vscode-languageserver-protocol "^3.17.2" + vscode-uri "^2.1.1" + ws "^7.1.2" + yargs "^15.3.1" + "@theia/monaco-editor-core@1.72.3": version "1.72.3" resolved "https://registry.yarnpkg.com/@theia/monaco-editor-core/-/monaco-editor-core-1.72.3.tgz#911d674c6e0c490442a355cfaa52beec919a025e" integrity sha512-2FK5m0G5oxiqCv0ZrjucMx5fVgQ9Jqv0CgxGvSzDc4wRrauBdeBoX90J99BEIOJ8Jp3W0++GoRBdh0yQNIGL2g== +"@theia/preferences@latest": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@theia/preferences/-/preferences-1.39.0.tgz#4c7228fc646b85c5b9f8ede448820d4f312cc3b5" + integrity sha512-abO0EwpmyOD0DBRk1ReXF7jLd7aDjx5Und9tLbggrZs4dDpNsIvED5axwkA+mRTlq3gdnnVRCbBqP5HZQ3LxFA== + dependencies: + "@theia/core" "1.39.0" + "@theia/editor" "1.39.0" + "@theia/filesystem" "1.39.0" + "@theia/monaco" "1.39.0" + "@theia/monaco-editor-core" "1.72.3" + "@theia/userstorage" "1.39.0" + "@theia/workspace" "1.39.0" + async-mutex "^0.3.1" + fast-deep-equal "^3.1.3" + jsonc-parser "^2.2.0" + p-debounce "^2.1.0" + +"@theia/terminal@latest": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@theia/terminal/-/terminal-1.39.0.tgz#5ed6cebe70bfa25be2a447923b31d1118f1bc541" + integrity sha512-a33ziDuLx+hF/3GvICvGTMBTdwQr96iUsiqBBDBNwIXoCIvG5Q33Pds0xNyH34Ik8XXRLSSWgyTiGK2eM6qreQ== + dependencies: + "@theia/core" "1.39.0" + "@theia/editor" "1.39.0" + "@theia/filesystem" "1.39.0" + "@theia/process" "1.39.0" + "@theia/variable-resolver" "1.39.0" + "@theia/workspace" "1.39.0" + xterm "^4.16.0" + xterm-addon-fit "^0.5.0" + xterm-addon-search "^0.8.2" + "@tootallnate/once@1", "@tootallnate/once@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" From 8e5bcc5b43d82e30f43fb0eb187ac93211d1751d Mon Sep 17 00:00:00 2001 From: eeakrnm Date: Tue, 18 Jul 2023 11:24:44 -0500 Subject: [PATCH 2/6] fix versions --- packages/terminal-manager/package.json | 6 +++--- packages/terminal-manager/tsconfig.json | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/terminal-manager/package.json b/packages/terminal-manager/package.json index ba743b0e98be1..8892cca514e62 100644 --- a/packages/terminal-manager/package.json +++ b/packages/terminal-manager/package.json @@ -25,9 +25,9 @@ "watch": "theiaext watch" }, "dependencies": { - "@theia/core": "latest", - "@theia/preferences": "latest", - "@theia/terminal": "latest" + "@theia/core": "1.39.0", + "@theia/preferences": "1.39.0", + "@theia/terminal": "1.39.0" }, "theiaExtensions": [ { diff --git a/packages/terminal-manager/tsconfig.json b/packages/terminal-manager/tsconfig.json index bc676ce8dfe79..4f3e2cf7fc636 100644 --- a/packages/terminal-manager/tsconfig.json +++ b/packages/terminal-manager/tsconfig.json @@ -8,5 +8,15 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../core" + }, + { + "path": "../preferences" + }, + { + "path": "../terminal" + } + ] } From 8462ec8a9052c9fa24d1cca9f2fa0405a2e0c4fd Mon Sep 17 00:00:00 2001 From: eeakrnm Date: Tue, 18 Jul 2023 11:58:39 -0500 Subject: [PATCH 3/6] yarn.lock --- yarn.lock | 106 ------------------------------------------------------ 1 file changed, 106 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3390e0ebab58e..b511dbdf93121 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1687,117 +1687,11 @@ dependencies: defer-to-connect "^2.0.0" -"@theia/core@latest": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@theia/core/-/core-1.39.0.tgz#34bf07edce5e4e90ef0578acca46e51ce07450fc" - integrity sha512-EmvIpp3mKGSFO50iOm3hOa66WlyHigFOGyq1N4nVibzb82/T065LZDof8NTLXl50cMr6H3ZFhBC32TTGf4ircw== - dependencies: - "@babel/runtime" "^7.10.0" - "@phosphor/algorithm" "1" - "@phosphor/commands" "1" - "@phosphor/coreutils" "1" - "@phosphor/domutils" "1" - "@phosphor/dragdrop" "1" - "@phosphor/messaging" "1" - "@phosphor/properties" "1" - "@phosphor/signaling" "1" - "@phosphor/virtualdom" "1" - "@phosphor/widgets" "1" - "@theia/application-package" "1.39.0" - "@theia/request" "1.39.0" - "@types/body-parser" "^1.16.4" - "@types/cookie" "^0.3.3" - "@types/dompurify" "^2.2.2" - "@types/express" "^4.16.0" - "@types/fs-extra" "^4.0.2" - "@types/lodash.debounce" "4.0.3" - "@types/lodash.throttle" "^4.1.3" - "@types/markdown-it" "^12.2.3" - "@types/react" "^18.0.15" - "@types/react-dom" "^18.0.6" - "@types/route-parser" "^0.1.1" - "@types/safer-buffer" "^2.1.0" - "@types/ws" "^5.1.2" - "@types/yargs" "^15" - "@vscode/codicons" "*" - ajv "^6.5.3" - async-mutex "^0.4.0" - body-parser "^1.17.2" - cookie "^0.4.0" - dompurify "^2.2.9" - drivelist "^9.0.2" - es6-promise "^4.2.4" - express "^4.16.3" - fast-json-stable-stringify "^2.1.0" - file-icons-js "~1.0.3" - font-awesome "^4.7.0" - fs-extra "^4.0.2" - fuzzy "^0.1.3" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - iconv-lite "^0.6.0" - inversify "^6.0.1" - jschardet "^2.1.1" - keytar "7.2.0" - lodash.debounce "^4.0.8" - lodash.throttle "^4.1.1" - markdown-it "^12.3.2" - msgpackr "^1.6.1" - nsfw "^2.2.4" - p-debounce "^2.1.0" - perfect-scrollbar "^1.3.0" - react "^18.2.0" - react-dom "^18.2.0" - react-tooltip "^4.2.21" - react-virtuoso "^2.17.0" - reflect-metadata "^0.1.10" - route-parser "^0.0.5" - safer-buffer "^2.1.2" - socket.io "^4.5.3" - socket.io-client "^4.5.3" - uuid "^8.3.2" - vscode-languageserver-protocol "^3.17.2" - vscode-uri "^2.1.1" - ws "^7.1.2" - yargs "^15.3.1" - "@theia/monaco-editor-core@1.72.3": version "1.72.3" resolved "https://registry.yarnpkg.com/@theia/monaco-editor-core/-/monaco-editor-core-1.72.3.tgz#911d674c6e0c490442a355cfaa52beec919a025e" integrity sha512-2FK5m0G5oxiqCv0ZrjucMx5fVgQ9Jqv0CgxGvSzDc4wRrauBdeBoX90J99BEIOJ8Jp3W0++GoRBdh0yQNIGL2g== -"@theia/preferences@latest": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@theia/preferences/-/preferences-1.39.0.tgz#4c7228fc646b85c5b9f8ede448820d4f312cc3b5" - integrity sha512-abO0EwpmyOD0DBRk1ReXF7jLd7aDjx5Und9tLbggrZs4dDpNsIvED5axwkA+mRTlq3gdnnVRCbBqP5HZQ3LxFA== - dependencies: - "@theia/core" "1.39.0" - "@theia/editor" "1.39.0" - "@theia/filesystem" "1.39.0" - "@theia/monaco" "1.39.0" - "@theia/monaco-editor-core" "1.72.3" - "@theia/userstorage" "1.39.0" - "@theia/workspace" "1.39.0" - async-mutex "^0.3.1" - fast-deep-equal "^3.1.3" - jsonc-parser "^2.2.0" - p-debounce "^2.1.0" - -"@theia/terminal@latest": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@theia/terminal/-/terminal-1.39.0.tgz#5ed6cebe70bfa25be2a447923b31d1118f1bc541" - integrity sha512-a33ziDuLx+hF/3GvICvGTMBTdwQr96iUsiqBBDBNwIXoCIvG5Q33Pds0xNyH34Ik8XXRLSSWgyTiGK2eM6qreQ== - dependencies: - "@theia/core" "1.39.0" - "@theia/editor" "1.39.0" - "@theia/filesystem" "1.39.0" - "@theia/process" "1.39.0" - "@theia/variable-resolver" "1.39.0" - "@theia/workspace" "1.39.0" - xterm "^4.16.0" - xterm-addon-fit "^0.5.0" - xterm-addon-search "^0.8.2" - "@tootallnate/once@1", "@tootallnate/once@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" From b75ba93fd006389893e9a70799b64ad5ea50fd4f Mon Sep 17 00:00:00 2001 From: eeakrnm Date: Tue, 18 Jul 2023 16:16:26 -0500 Subject: [PATCH 4/6] fix createWidget --- .../src/browser/terminal-manager-frontend-module.ts | 4 ++-- .../terminal-manager/src/browser/terminal-manager-widget.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts b/packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts index 9d42eee762271..b2c54b55b0da6 100644 --- a/packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts +++ b/packages/terminal-manager/src/browser/terminal-manager-frontend-module.ts @@ -43,8 +43,8 @@ export default new ContainerModule((bind: interfaces.Bind) => { id: TerminalManagerWidget.ID, createWidget: async () => { const child = container.createChild(); - const terminalManagerTreeWidget = await container.get(WidgetManager) - .getOrCreateWidget(TerminalManagerTreeWidget.ID); + const widgetManager = container.get(WidgetManager); + const terminalManagerTreeWidget = await widgetManager.getOrCreateWidget(TerminalManagerTreeWidget.ID); child.bind(TerminalManagerTreeWidget).toConstantValue(terminalManagerTreeWidget); return TerminalManagerWidget.createWidget(child); }, diff --git a/packages/terminal-manager/src/browser/terminal-manager-widget.ts b/packages/terminal-manager/src/browser/terminal-manager-widget.ts index be37938ef4a1c..46f8908aa7db9 100644 --- a/packages/terminal-manager/src/browser/terminal-manager-widget.ts +++ b/packages/terminal-manager/src/browser/terminal-manager-widget.ts @@ -124,8 +124,8 @@ export class TerminalManagerWidget extends BaseWidget implements StatefulWidget, return child; } - static createWidget(parent: interfaces.Container): TerminalManagerWidget { - return TerminalManagerWidget.createContainer(parent).get(TerminalManagerWidget); + static createWidget(parent: interfaces.Container): Promise { + return TerminalManagerWidget.createContainer(parent).getAsync(TerminalManagerWidget); } @postConstruct() From e17e07e187a4414cccd3b082b664eaed64dfb67d Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Tue, 18 Jul 2023 16:43:15 -0600 Subject: [PATCH 5/6] Semicolons --- .../src/browser/terminal-manager-tree-widget.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx b/packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx index a20bdf42a1a9c..e11e9324c1e63 100644 --- a/packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx +++ b/packages/terminal-manager/src/browser/terminal-manager-tree-widget.tsx @@ -145,31 +145,31 @@ export class TerminalManagerTreeWidget extends TreeWidget { // @ts-expect-error 2416 cf. https://github.com/eclipse-theia/theia/issues/11640 protected override handleLeft(event: KeyboardEvent): boolean | Promise { - if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + if ((event.target as HTMLElement).tagName === 'INPUT') { return false; }; return super.handleLeft(event); } // @ts-expect-error 2416 cf. https://github.com/eclipse-theia/theia/issues/11640 protected override handleRight(event: KeyboardEvent): boolean | Promise { - if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + if ((event.target as HTMLElement).tagName === 'INPUT') { return false; }; return super.handleRight(event); } // cf. https://github.com/eclipse-theia/theia/issues/11640 protected override handleEscape(event: KeyboardEvent): boolean | void { - if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + if ((event.target as HTMLElement).tagName === 'INPUT') { return false; }; return super.handleEscape(event); } // cf. https://github.com/eclipse-theia/theia/issues/11640 protected override handleEnter(event: KeyboardEvent): boolean | void { - if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + if ((event.target as HTMLElement).tagName === 'INPUT') { return false; }; return super.handleEnter(event); } // cf. https://github.com/eclipse-theia/theia/issues/11640 protected override handleSpace(event: KeyboardEvent): boolean | void { - if ((event.target as HTMLElement).tagName === 'INPUT') { return false }; + if ((event.target as HTMLElement).tagName === 'INPUT') { return false; }; return super.handleSpace(event); } From 991f5a2f456f3bb5283bc5c9fb3589983b65fe5b Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Tue, 18 Jul 2023 16:45:18 -0600 Subject: [PATCH 6/6] Test non-failure --- packages/terminal-manager/src/package.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/terminal-manager/src/package.spec.ts diff --git a/packages/terminal-manager/src/package.spec.ts b/packages/terminal-manager/src/package.spec.ts new file mode 100644 index 0000000000000..e6fb3518959cc --- /dev/null +++ b/packages/terminal-manager/src/package.spec.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// Copyright (C) 2017 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-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('terminal package', () => { + it('support code coverage statistics', () => true); +});