deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/terminal-manager/.eslintrc.js
Normal file
10
packages/terminal-manager/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
31
packages/terminal-manager/README.md
Normal file
31
packages/terminal-manager/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - TERMINAL MANAGER EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/terminal-manager` extension contributes a terminal manager widget to use several terminal widgets within one view.
|
||||
The extension provides setting `terminal.grouping.mode` to toggle using this instead of separate terminal views.
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
39
packages/terminal-manager/package.json
Normal file
39
packages/terminal-manager/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@theia/terminal-manager",
|
||||
"version": "1.68.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"
|
||||
},
|
||||
"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": "1.68.0",
|
||||
"@theia/preferences": "1.68.0",
|
||||
"@theia/terminal": "1.68.0"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/terminal-manager-frontend-module"
|
||||
}
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource 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.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { CommandRegistry, PreferenceService, DisposableCollection } from '@theia/core/lib/common';
|
||||
import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
|
||||
import { TerminalFrontendContribution, TerminalCommands } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
import { ApplicationShell, WidgetManager, FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser';
|
||||
import { TerminalManagerWidget } from './terminal-manager-widget';
|
||||
import { TerminalManagerFrontendViewContribution } from './terminal-manager-frontend-view-contribution';
|
||||
import { TerminalManagerPreferences } from './terminal-manager-preferences';
|
||||
/**
|
||||
* Re-registers terminal commands (e.g. new terminal) to execute them via the terminal manager
|
||||
* instead of creating new, separate terminals.
|
||||
*/
|
||||
@injectable()
|
||||
export class TerminalManagerFrontendContribution implements FrontendApplicationContribution {
|
||||
@inject(TerminalFrontendContribution)
|
||||
protected readonly terminalFrontendContribution: TerminalFrontendContribution;
|
||||
|
||||
@inject(TerminalManagerFrontendViewContribution)
|
||||
protected readonly terminalManagerViewContribution: TerminalManagerFrontendViewContribution;
|
||||
|
||||
@inject(WidgetManager)
|
||||
protected readonly widgetManager: WidgetManager;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(TerminalManagerPreferences)
|
||||
protected readonly preferences: TerminalManagerPreferences;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
protected commandHandlerDisposables = new DisposableCollection();
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
this.preferenceService.ready.then(() => {
|
||||
this.preferenceService.onPreferenceChanged(change => {
|
||||
if (change.preferenceName === 'terminal.grouping.mode') {
|
||||
this.handleTabsDisplayChange(this.preferences['terminal.grouping.mode']);
|
||||
}
|
||||
});
|
||||
if (this.preferences.get('terminal.grouping.mode') !== 'tree') {
|
||||
console.debug('Terminal tab style is not tree. Use separate terminal views.');
|
||||
return;
|
||||
}
|
||||
console.debug('Terminal tab style is tree. Override command handlers accordingly.');
|
||||
this.registerHandlers();
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleTabsDisplayChange(newValue: string): Promise<void> {
|
||||
if (newValue === 'tree') {
|
||||
await this.migrateTerminalsToManager();
|
||||
this.registerHandlers();
|
||||
} else {
|
||||
this.unregisterHandlers();
|
||||
await this.migrateTerminalsToTabs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate terminals from tabs to terminal manager. Applies only to terminals currently in bottom panel.
|
||||
*/
|
||||
protected async migrateTerminalsToManager(): Promise<void> {
|
||||
const bottomTerminals = this.shell.getWidgets('bottom')
|
||||
.filter((w): w is TerminalWidget => w instanceof TerminalWidget);
|
||||
|
||||
if (bottomTerminals.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const managerWidget = await this.widgetManager.getOrCreateWidget<TerminalManagerWidget>(TerminalManagerWidget.ID);
|
||||
if (!(managerWidget instanceof TerminalManagerWidget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before terminal manager attachment to precede creation of default widget.
|
||||
for (const terminal of bottomTerminals) {
|
||||
managerWidget.addTerminalPage(terminal);
|
||||
terminal.show(); // Clear hidden flag that may have been set by dock panel on removal.
|
||||
}
|
||||
|
||||
await this.terminalManagerViewContribution.openView({ reveal: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate terminals from the terminal manager to the bottom panel as separate tabs.
|
||||
*/
|
||||
protected async migrateTerminalsToTabs(): Promise<void> {
|
||||
const managerWidget = this.terminalManagerViewContribution.tryGetWidget();
|
||||
if (!(managerWidget instanceof TerminalManagerWidget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let toActivate: TerminalWidget | undefined = undefined;
|
||||
for (const terminal of managerWidget.drainWidgets()) {
|
||||
if (!terminal.isDisposed) {
|
||||
toActivate ??= terminal;
|
||||
this.shell.addWidget(terminal, { area: 'bottom' });
|
||||
}
|
||||
}
|
||||
|
||||
if (toActivate) {
|
||||
this.shell.activateWidget(toActivate.id);
|
||||
}
|
||||
}
|
||||
|
||||
protected unregisterHandlers(): void {
|
||||
this.commandHandlerDisposables.dispose();
|
||||
this.commandHandlerDisposables = new DisposableCollection();
|
||||
}
|
||||
|
||||
protected registerHandlers(): void {
|
||||
this.unregisterHandlers();
|
||||
this.registerCommands();
|
||||
}
|
||||
|
||||
protected registerCommands(): void {
|
||||
this.commandHandlerDisposables.push(this.commandRegistry.registerHandler(TerminalCommands.NEW.id, {
|
||||
execute: async () => {
|
||||
// Only create a new terminal if the view was existing as opening it automatically create a terminal
|
||||
const existing = this.terminalManagerViewContribution.tryGetWidget();
|
||||
const managerWidget = await this.terminalManagerViewContribution.openView({ reveal: true });
|
||||
if (managerWidget instanceof TerminalManagerWidget && existing) {
|
||||
const terminalWidget = await managerWidget.createTerminalWidget();
|
||||
managerWidget.addTerminalPage(terminalWidget);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.commandHandlerDisposables.push(this.commandRegistry.registerHandler(TerminalCommands.NEW_ACTIVE_WORKSPACE.id, {
|
||||
execute: async () => {
|
||||
// Only create a new terminal if the view was existing as opening it automatically create a terminal
|
||||
const existing = this.terminalManagerViewContribution.tryGetWidget();
|
||||
const managerWidget = await this.terminalManagerViewContribution.openView({ reveal: true });
|
||||
if (managerWidget instanceof TerminalManagerWidget && existing) {
|
||||
const terminalWidget = await managerWidget.createTerminalWidget();
|
||||
managerWidget.addTerminalPage(terminalWidget);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.commandHandlerDisposables.push(this.commandRegistry.registerHandler(TerminalCommands.SPLIT.id, {
|
||||
execute: async () => {
|
||||
const managerWidget = await this.terminalManagerViewContribution.openView({ reveal: true });
|
||||
if (managerWidget instanceof TerminalManagerWidget) {
|
||||
const terminalWidget = await managerWidget.createTerminalWidget();
|
||||
const { model } = managerWidget.treeWidget;
|
||||
const activeGroupId = model.activeGroupNode?.id;
|
||||
const activePageId = model.activePageNode?.id;
|
||||
|
||||
if (activeGroupId) {
|
||||
managerWidget.addWidgetToTerminalGroup(terminalWidget, activeGroupId);
|
||||
} else if (activePageId) {
|
||||
managerWidget.addTerminalGroupToPage(terminalWidget, activePageId);
|
||||
} else {
|
||||
managerWidget.addTerminalPage(terminalWidget);
|
||||
}
|
||||
}
|
||||
},
|
||||
isEnabled: w => w instanceof TerminalWidget || w instanceof TerminalManagerWidget,
|
||||
isVisible: w => w instanceof TerminalWidget || w instanceof TerminalManagerWidget,
|
||||
}));
|
||||
|
||||
this.commandHandlerDisposables.push(this.commandRegistry.registerHandler(TerminalCommands.TOGGLE_TERMINAL.id, {
|
||||
execute: async () => {
|
||||
const existing = this.terminalManagerViewContribution.tryGetWidget();
|
||||
if (!existing || !(existing instanceof TerminalManagerWidget)) {
|
||||
const managerWidget = await this.terminalManagerViewContribution.openView({ activate: true });
|
||||
if (managerWidget instanceof TerminalManagerWidget && !this.shell.isExpanded('bottom')) {
|
||||
this.shell.expandPanel('bottom');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existing.isAttached) {
|
||||
await this.terminalManagerViewContribution.openView({ activate: true });
|
||||
if (!this.shell.isExpanded('bottom')) {
|
||||
this.shell.expandPanel('bottom');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.shell.isExpanded('bottom')) {
|
||||
this.shell.expandPanel('bottom');
|
||||
this.shell.bottomPanel.activateWidget(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
const active = this.shell.activeWidget;
|
||||
const isManagerOrChildActive = active === existing || Array.from(existing.terminalWidgets.values()).some(widget => widget === active);
|
||||
if (isManagerOrChildActive) {
|
||||
this.shell.collapsePanel('bottom');
|
||||
} else {
|
||||
this.shell.bottomPanel.activateWidget(existing);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// *****************************************************************************
|
||||
// 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,
|
||||
WidgetFactory,
|
||||
WidgetManager,
|
||||
FrontendApplicationContribution,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { TerminalManagerWidget } from './terminal-manager-widget';
|
||||
import { TerminalManagerFrontendViewContribution } from './terminal-manager-frontend-view-contribution';
|
||||
import { TerminalManagerFrontendContribution } from './terminal-manager-frontend-contribution';
|
||||
import { TerminalManagerPreferenceContribution, TerminalManagerPreferences, TerminalManagerPreferenceSchema } from './terminal-manager-preferences';
|
||||
import { TerminalManagerTreeWidget } from './terminal-manager-tree-widget';
|
||||
import '../../src/browser/terminal-manager.css';
|
||||
import { PreferenceContribution, PreferenceProxyFactory } from '@theia/core';
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind) => {
|
||||
bindViewContribution(bind, TerminalManagerFrontendViewContribution);
|
||||
bind(TabBarToolbarContribution).toService(TerminalManagerFrontendViewContribution);
|
||||
|
||||
// Bind the contribution for overridden terminal commands
|
||||
bind(TerminalManagerFrontendContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(TerminalManagerFrontendContribution);
|
||||
|
||||
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 widgetManager = container.get(WidgetManager);
|
||||
const terminalManagerTreeWidget = await widgetManager.getOrCreateWidget<TerminalManagerTreeWidget>(TerminalManagerTreeWidget.ID);
|
||||
child.bind(TerminalManagerTreeWidget).toConstantValue(terminalManagerTreeWidget);
|
||||
return TerminalManagerWidget.createWidget(child);
|
||||
},
|
||||
}));
|
||||
|
||||
bind(TerminalManagerPreferences).toDynamicValue(ctx => {
|
||||
const factory = ctx.container.get<PreferenceProxyFactory>(PreferenceProxyFactory);
|
||||
return factory(TerminalManagerPreferenceSchema);
|
||||
}).inSingletonScope();
|
||||
bind(TerminalManagerPreferenceContribution).toConstantValue({ schema: TerminalManagerPreferenceSchema });
|
||||
bind(PreferenceContribution).toService(TerminalManagerPreferenceContribution);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
// *****************************************************************************
|
||||
// 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, inject } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
AbstractViewContribution,
|
||||
codicon,
|
||||
KeybindingContribution,
|
||||
KeybindingRegistry,
|
||||
MAXIMIZED_CLASS,
|
||||
Widget,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { CommandRegistry, Disposable, Event, MenuModelRegistry, nls } from '@theia/core';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { BOTTOM_AREA_ID } 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 { ConfirmDialog, Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { TerminalManagerPreferences } from './terminal-manager-preferences';
|
||||
|
||||
@injectable()
|
||||
export class TerminalManagerFrontendViewContribution extends AbstractViewContribution<TerminalManagerWidget>
|
||||
implements TabBarToolbarContribution, KeybindingContribution {
|
||||
|
||||
@inject(TerminalManagerPreferences)
|
||||
protected readonly preferences: TerminalManagerPreferences;
|
||||
|
||||
protected quickViewDisposable: Disposable | undefined;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: TerminalManagerWidget.ID,
|
||||
widgetName: nls.localize('theia/terminal-manager', 'Terminal Manager'),
|
||||
defaultWidgetOptions: {
|
||||
area: 'bottom',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(commands: CommandRegistry): void {
|
||||
// Don't call super.registerCommands() - we manage quick view registration manually
|
||||
// based on the terminal.grouping.mode preference
|
||||
|
||||
this.preferences.ready.then(() => {
|
||||
this.updateQuickViewRegistration();
|
||||
this.preferences.onPreferenceChanged(change => {
|
||||
if (change.preferenceName === 'terminal.grouping.mode') {
|
||||
this.updateQuickViewRegistration();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
) => 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 = nls.localize('theia/terminal-manager/resetLayout', 'Reset Layout');
|
||||
const dialogResponse = await this.confirmUserAction({
|
||||
title: nls.localize('theia/terminal-manager/resetLayoutPrompt', 'Do you want to reset the terminal manager layout?'),
|
||||
message: nls.localize(
|
||||
'theia/terminal-manager/resetLayoutMessage',
|
||||
'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) {
|
||||
widget.resetView();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
commands.registerCommand(TerminalManagerCommands.MANAGER_OPEN_VIEW, {
|
||||
execute: () => this.openView({ activate: true }),
|
||||
isEnabled: () => this.isTreeMode(),
|
||||
isVisible: () => this.isTreeMode(),
|
||||
});
|
||||
commands.registerCommand(TerminalManagerCommands.MANAGER_CLOSE_VIEW, {
|
||||
isVisible: () => Boolean(this.tryGetWidget()),
|
||||
isEnabled: () => Boolean(this.tryGetWidget()),
|
||||
execute: () => this.closeView(),
|
||||
});
|
||||
}
|
||||
|
||||
protected isTreeMode(): boolean {
|
||||
return this.preferences.get('terminal.grouping.mode') === 'tree';
|
||||
}
|
||||
|
||||
protected updateQuickViewRegistration(): void {
|
||||
if (this.isTreeMode()) {
|
||||
if (!this.quickViewDisposable) {
|
||||
this.quickViewDisposable = this.quickView?.registerItem({
|
||||
label: this.viewLabel,
|
||||
open: () => this.openView({ activate: true })
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.quickViewDisposable?.dispose();
|
||||
this.quickViewDisposable = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected async confirmUserAction(options: { title: string, message: string, primaryButtonText: string }): Promise<string | undefined> {
|
||||
const dialog = new ConfirmDialog({
|
||||
title: options.title,
|
||||
msg: options.message,
|
||||
ok: options.primaryButtonText,
|
||||
cancel: Dialog.CANCEL,
|
||||
});
|
||||
const confirmed = await dialog.open();
|
||||
return confirmed ? options.primaryButtonText : undefined;
|
||||
}
|
||||
|
||||
protected maximizeBottomPanel(): void {
|
||||
this.shell.bottomPanel.toggleMaximized();
|
||||
}
|
||||
|
||||
protected async createNewTerminalPage(): Promise<void> {
|
||||
const terminalManagerWidget = await this.widget;
|
||||
const terminalWidget = await terminalManagerWidget.createTerminalWidget();
|
||||
terminalManagerWidget.addTerminalPage(terminalWidget);
|
||||
}
|
||||
|
||||
protected async createNewTerminalGroup(pageId: TerminalManagerTreeTypes.PageId): Promise<void> {
|
||||
const terminalManagerWidget = await this.widget;
|
||||
const terminalWidget = await terminalManagerWidget.createTerminalWidget();
|
||||
terminalManagerWidget.addTerminalGroupToPage(terminalWidget, pageId);
|
||||
}
|
||||
|
||||
protected async addTerminalToGroup(groupId: TerminalManagerTreeTypes.GroupId): Promise<void> {
|
||||
const terminalManagerWidget = await this.widget;
|
||||
const terminalWidget = await terminalManagerWidget.createTerminalWidget();
|
||||
terminalManagerWidget.addWidgetToTerminalGroup(terminalWidget, groupId);
|
||||
}
|
||||
|
||||
protected async handleToggleTree(): Promise<void> {
|
||||
const terminalManagerWidget = await this.widget;
|
||||
terminalManagerWidget.toggleTreeVisibility();
|
||||
}
|
||||
|
||||
protected async deleteTerminalFromManager(terminalId: TerminalManagerTreeTypes.TerminalKey): Promise<void> {
|
||||
const terminalManagerWidget = await this.widget;
|
||||
terminalManagerWidget?.deleteTerminal(terminalId);
|
||||
}
|
||||
|
||||
protected async deleteGroupFromManager(groupId: TerminalManagerTreeTypes.GroupId): Promise<void> {
|
||||
const widget = await this.widget;
|
||||
widget.deleteGroup(groupId);
|
||||
}
|
||||
|
||||
protected async deletePageFromManager(pageId: TerminalManagerTreeTypes.PageId): Promise<void> {
|
||||
const widget = await this.widget;
|
||||
widget.deletePage(pageId);
|
||||
}
|
||||
|
||||
protected async toggleRenameTerminalFromManager(entityId: TerminalManagerTreeTypes.TerminalManagerValidId): Promise<void> {
|
||||
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,
|
||||
});
|
||||
const bottomPanelMaximizationChanged = Event.map(Event.filter(this.shell.onDidToggleMaximized, widget => widget === this.shell.bottomPanel), () => undefined);
|
||||
toolbar.registerItem({
|
||||
id: TerminalManagerCommands.MANAGER_MAXIMIZE_BOTTOM_PANEL_TOOLBAR.id,
|
||||
command: TerminalManagerCommands.MANAGER_MAXIMIZE_BOTTOM_PANEL_TOOLBAR.id,
|
||||
icon: codicon('chevron-up'),
|
||||
onDidChange: bottomPanelMaximizationChanged,
|
||||
});
|
||||
toolbar.registerItem({
|
||||
id: TerminalManagerCommands.MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR.id,
|
||||
command: TerminalManagerCommands.MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR.id,
|
||||
icon: codicon('chevron-down'),
|
||||
onDidChange: bottomPanelMaximizationChanged,
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: TerminalManagerCommands.MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR.id,
|
||||
keybinding: 'alt+q',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// *****************************************************************************
|
||||
// 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, PreferenceProxy, PreferenceSchema, PreferenceScope } from '@theia/core';
|
||||
|
||||
export const TerminalManagerPreferenceSchema: PreferenceSchema = {
|
||||
properties: {
|
||||
'terminal.grouping.treeViewLocation': {
|
||||
'type': 'string',
|
||||
'enum': ['left', 'right'],
|
||||
'description': nls.localize('theia/terminal-manager/treeViewLocation', 'The location of the terminal manager\'s tree view.'
|
||||
+ ' Only applies when \'terminal.grouping.mode\' is set to \'tree\'.'),
|
||||
'default': 'left',
|
||||
'scope': PreferenceScope.Workspace,
|
||||
},
|
||||
'terminal.grouping.mode': {
|
||||
'type': 'string',
|
||||
'enum': ['tabbed', 'tree'],
|
||||
'description': nls.localize('theia/terminal-manager/tabsDisplay',
|
||||
'Controls how terminals are displayed. \'tree\' shows multiple terminals in a single view with a tree view for management,'
|
||||
+ '\'tabbed\' shows each terminal in its own view in a separate tab.'),
|
||||
'default': 'tabbed',
|
||||
'scope': PreferenceScope.Workspace,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type TerminalManagerTreeViewLocation = 'left' | 'right';
|
||||
export type TerminalGroupingMode = 'tabbed' | 'tree';
|
||||
|
||||
export interface TerminalManagerConfiguration {
|
||||
'terminal.grouping.treeViewLocation': TerminalManagerTreeViewLocation;
|
||||
'terminal.grouping.mode': TerminalGroupingMode;
|
||||
}
|
||||
|
||||
export const TerminalManagerPreferences = Symbol('TerminalManagerPreferences');
|
||||
export const TerminalManagerPreferenceContribution = Symbol('TerminalManagerPreferenceContribution');
|
||||
export type TerminalManagerPreferences = PreferenceProxy<TerminalManagerConfiguration>;
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
// *****************************************************************************
|
||||
// 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, TreeNode } from '@theia/core/lib/browser';
|
||||
import { Emitter, nls } 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 onDidChangeTreeSelectionEmitter = new Emitter<TerminalManagerTreeTypes.SelectionChangedEvent>();
|
||||
readonly onDidChangeTreeSelection = this.onDidChangeTreeSelectionEmitter.event;
|
||||
|
||||
protected onDidAddPageEmitter = new Emitter<{ pageId: TerminalManagerTreeTypes.PageId, terminalKey: TerminalManagerTreeTypes.TerminalKey }>();
|
||||
readonly onDidAddPage = this.onDidAddPageEmitter.event;
|
||||
protected onDidDeletePageEmitter = new Emitter<TerminalManagerTreeTypes.PageId>();
|
||||
readonly onDidDeletePage = this.onDidDeletePageEmitter.event;
|
||||
|
||||
protected onDidRenameNodeEmitter = new Emitter<TerminalManagerTreeTypes.TerminalManagerTreeNode>();
|
||||
readonly onDidRenameNode = this.onDidRenameNodeEmitter.event;
|
||||
|
||||
protected onDidAddTerminalGroupEmitter = new Emitter<{
|
||||
groupId: TerminalManagerTreeTypes.GroupId,
|
||||
pageId: TerminalManagerTreeTypes.PageId,
|
||||
terminalKey: TerminalManagerTreeTypes.TerminalKey,
|
||||
}>();
|
||||
readonly onDidAddTerminalGroup = this.onDidAddTerminalGroupEmitter.event;
|
||||
|
||||
protected onDidDeleteTerminalGroupEmitter = new Emitter<TerminalManagerTreeTypes.GroupId>();
|
||||
readonly onDidDeleteTerminalGroup = this.onDidDeleteTerminalGroupEmitter.event;
|
||||
|
||||
protected onDidAddTerminalToGroupEmitter = new Emitter<{
|
||||
terminalId: TerminalManagerTreeTypes.TerminalKey,
|
||||
groupId: TerminalManagerTreeTypes.GroupId,
|
||||
}>();
|
||||
readonly onDidAddTerminalToGroup = this.onDidAddTerminalToGroupEmitter.event;
|
||||
|
||||
protected onDidDeleteTerminalFromGroupEmitter = new Emitter<{
|
||||
terminalId: TerminalManagerTreeTypes.TerminalKey,
|
||||
groupId: TerminalManagerTreeTypes.GroupId,
|
||||
}>();
|
||||
readonly onDidDeleteTerminalFromGroup = this.onDidDeleteTerminalFromGroupEmitter.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;
|
||||
}
|
||||
|
||||
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.onDidAddPageEmitter.fire({ pageId: pageNode.id, terminalKey });
|
||||
setTimeout(() => {
|
||||
this.selectionService.addSelection(terminalNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected createPageNode(pageId: TerminalManagerTreeTypes.PageId): TerminalManagerTreeTypes.PageNode {
|
||||
const currentPageNumber = this.getNextPageCounter();
|
||||
return {
|
||||
id: pageId,
|
||||
label: `${nls.localize('theia/terminal-manager/page', '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)) {
|
||||
const isActive = this.activePageNode === pageNode;
|
||||
this.onDidDeletePageEmitter.fire(pageNode.id);
|
||||
CompositeTreeNode.removeChild(this.root, pageNode);
|
||||
this.refreshWithSelection(this.root, undefined, isActive ? pageNode : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
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.onDidAddTerminalGroupEmitter.fire({ groupId: groupNode.id, pageId, terminalKey });
|
||||
CompositeTreeNode.addChild(groupNode, terminalNode);
|
||||
CompositeTreeNode.addChild(pageNode, groupNode);
|
||||
this.refreshWithSelection(pageNode, terminalNode);
|
||||
}
|
||||
}
|
||||
|
||||
protected createGroupNode(
|
||||
groupId: TerminalManagerTreeTypes.GroupId,
|
||||
pageId: TerminalManagerTreeTypes.PageId,
|
||||
): TerminalManagerTreeTypes.TerminalGroupNode {
|
||||
const currentGroupNum = this.getNextGroupCounterForPage(pageId);
|
||||
return {
|
||||
id: groupId,
|
||||
label: `${nls.localize('theia/terminal-manager/group', '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 {
|
||||
const isActive = this.activeGroupNode === groupNode;
|
||||
this.doDeleteTerminalGroup(groupNode, parentPageNode);
|
||||
this.refreshWithSelection(parentPageNode, undefined, isActive ? groupNode : undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected doDeleteTerminalGroup(group: TerminalManagerTreeTypes.TerminalGroupNode, page: TerminalManagerTreeTypes.PageNode): void {
|
||||
this.onDidDeleteTerminalGroupEmitter.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.onDidAddTerminalToGroupEmitter.fire({ terminalId: newTerminalId, groupId });
|
||||
this.refreshWithSelection(undefined, terminalNode);
|
||||
}
|
||||
}
|
||||
|
||||
createTerminalNode(
|
||||
terminalId: TerminalManagerTreeTypes.TerminalKey,
|
||||
groupId: TerminalManagerTreeTypes.GroupId,
|
||||
): TerminalManagerTreeTypes.TerminalNode {
|
||||
return {
|
||||
id: terminalId,
|
||||
label: nls.localizeByDefault('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 {
|
||||
const isActive = this.activeTerminalNode === terminalNode;
|
||||
this.doDeleteTerminalNode(terminalNode, parentGroupNode);
|
||||
this.refreshWithSelection(parentGroupNode, undefined, isActive ? terminalNode : undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected doDeleteTerminalNode(node: TerminalManagerTreeTypes.TerminalNode, parent: TerminalManagerTreeTypes.TerminalGroupNode): void {
|
||||
this.onDidDeleteTerminalFromGroupEmitter.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.onDidRenameNodeEmitter.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.onDidChangeTreeSelectionEmitter.fire({
|
||||
activePageId: activePage?.id,
|
||||
activeTerminalId: activeTerminal?.id,
|
||||
activeGroupId: activeGroup?.id,
|
||||
});
|
||||
}
|
||||
|
||||
get pages(): Map<TerminalManagerTreeTypes.PageId, TerminalManagerTreeTypes.PageNode> {
|
||||
const pages = new Map<TerminalManagerTreeTypes.PageId, TerminalManagerTreeTypes.PageNode>();
|
||||
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;
|
||||
}
|
||||
|
||||
selectTerminalNode(terminalKey: TerminalManagerTreeTypes.TerminalKey): void {
|
||||
const node = this.getNode(terminalKey);
|
||||
if (node && TerminalManagerTreeTypes.isTerminalNode(node)) {
|
||||
this.selectNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
protected async refreshWithSelection(refreshTarget?: CompositeTreeNode, selectionTarget?: SelectableTreeNode, selectionReferent?: TreeNode): Promise<void> {
|
||||
await this.refresh(refreshTarget);
|
||||
if (selectionTarget) {
|
||||
return this.selectNode(selectionTarget);
|
||||
}
|
||||
if (selectionReferent) {
|
||||
const { previousSibling, nextSibling } = selectionReferent;
|
||||
const toSelect = this.findSelection(previousSibling) ?? this.findSelection(nextSibling);
|
||||
if (toSelect) {
|
||||
this.selectNode(toSelect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected findSelection(start?: TreeNode): SelectableTreeNode | undefined {
|
||||
if (!start) { return undefined; }
|
||||
if (TerminalManagerTreeTypes.isTerminalNode(start)) { return start; }
|
||||
if (TerminalManagerTreeTypes.isGroupNode(start)) { return start.children.at(0); }
|
||||
if (TerminalManagerTreeTypes.isPageNode(start)) { return start.children.at(0)?.children.at(0); }
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<input
|
||||
spellCheck={false}
|
||||
type='text'
|
||||
className='theia-input rename-node-input'
|
||||
defaultValue={label}
|
||||
onBlur={this.handleRenameOnBlur}
|
||||
data-id={node.id}
|
||||
onKeyDown={this.handleRenameOnKeyDown}
|
||||
autoFocus={true}
|
||||
ref={assignRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return super.renderCaption(node, props);
|
||||
}
|
||||
|
||||
protected handleRenameOnBlur = (e: React.FocusEvent<HTMLInputElement>): void => this.doHandleRenameOnBlur(e);
|
||||
protected doHandleRenameOnBlur(e: React.FocusEvent<HTMLInputElement>): 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<HTMLInputElement>): void => this.doHandleRenameOnKeyDown(e);
|
||||
protected doHandleRenameOnKeyDown(e: React.KeyboardEvent<HTMLInputElement>): 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<void> {
|
||||
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<void> {
|
||||
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 (
|
||||
<div className='terminal-manager-inline-actions-container'>
|
||||
<div className='terminal-manager-inline-actions'>
|
||||
{inlineActionsForNode.map(({ icon, commandId, label }) => (
|
||||
<span
|
||||
key={commandId}
|
||||
data-command-id={commandId}
|
||||
data-node-id={node.id}
|
||||
className={icon}
|
||||
onClick={this.handleActionItemOnClick}
|
||||
onKeyDown={this.handleActionItemOnClick}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
title={label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected handleActionItemOnClick = (e: ReactInteraction<HTMLSpanElement>): void => this.doHandleActionItemOnClick(e);
|
||||
protected doHandleActionItemOnClick(e: ReactInteraction<HTMLSpanElement>): 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 <span className={`${codicon('terminal')}`} />;
|
||||
} else if (TerminalManagerTreeTypes.isPageNode(node)) {
|
||||
return <span className={`${codicon('terminal-tmux')}`} />;
|
||||
} else if (TerminalManagerTreeTypes.isGroupNode(node)) {
|
||||
return <span className={`${codicon('split-horizontal')}`} />;
|
||||
}
|
||||
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(<div
|
||||
key={depth}
|
||||
className={classNames.join(' ')}
|
||||
style={{
|
||||
paddingLeft: `${paddingLeft}px`,
|
||||
}}
|
||||
/>);
|
||||
current = current.parent;
|
||||
depth -= 1;
|
||||
}
|
||||
return indentDivs;
|
||||
}
|
||||
|
||||
protected override getDepthForNode(node: TreeNode, depths: Map<CompositeTreeNode | undefined, number>): number {
|
||||
const parentDepth = depths.get(node.parent);
|
||||
if (TerminalManagerTreeTypes.isTerminalNode(node) && parentDepth === undefined) {
|
||||
return 1;
|
||||
}
|
||||
return super.getDepthForNode(node, depths);
|
||||
}
|
||||
}
|
||||
|
||||
174
packages/terminal-manager/src/browser/terminal-manager-types.ts
Normal file
174
packages/terminal-manager/src/browser/terminal-manager-types.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
// *****************************************************************************
|
||||
// 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.toLocalizedCommand({
|
||||
id: 'terminal:new-in-manager-toolbar',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Create New Terminal Group',
|
||||
iconClass: codicon('split-horizontal'),
|
||||
}, 'theia/terminal-manager/createNewTerminalGroup');
|
||||
export const MANAGER_DELETE_TERMINAL = Command.toLocalizedCommand({
|
||||
id: 'terminal:delete-terminal',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Delete Terminal',
|
||||
iconClass: codicon('trash'),
|
||||
}, 'theia/terminal-manager/deleteTerminal');
|
||||
export const MANAGER_RENAME_TERMINAL = Command.toLocalizedCommand({
|
||||
id: 'terminal: rename-terminal',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Rename',
|
||||
iconClass: codicon('edit'),
|
||||
}, 'theia/terminal-manager/rename');
|
||||
export const MANAGER_NEW_PAGE_BOTTOM_TOOLBAR = Command.toLocalizedCommand({
|
||||
id: 'terminal:new-manager-page',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Create New Terminal Page',
|
||||
iconClass: codicon('new-file'),
|
||||
}, 'theia/terminal-manager/createNewTerminalPage');
|
||||
export const MANAGER_DELETE_PAGE = Command.toLocalizedCommand({
|
||||
id: 'terminal:delete-page',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Delete Page',
|
||||
iconClass: codicon('trash'),
|
||||
}, 'theia/terminal-manager/deletePage');
|
||||
export const MANAGER_ADD_TERMINAL_TO_GROUP = Command.toLocalizedCommand({
|
||||
id: 'terminal:manager-split-horizontal',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Add terminal to group',
|
||||
iconClass: codicon('split-vertical'),
|
||||
}, 'theia/terminal-manager/addTerminalToGroup');
|
||||
export const MANAGER_DELETE_GROUP = Command.toLocalizedCommand({
|
||||
id: 'terminal:manager-delete-group',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Delete Group',
|
||||
iconClass: codicon('trash'),
|
||||
}, 'theia/terminal-manager/deleteGroup');
|
||||
export const MANAGER_SHOW_TREE_TOOLBAR = Command.toLocalizedCommand({
|
||||
id: 'terminal:manager-toggle-tree',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Toggle Tree View',
|
||||
iconClass: codicon('list-tree'),
|
||||
}, 'theia/terminal-manager/toggleTreeView');
|
||||
export const MANAGER_MAXIMIZE_BOTTOM_PANEL_TOOLBAR = Command.toLocalizedCommand({
|
||||
id: 'terminal:manager-maximize-bottom-panel',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Maximize Bottom Panel',
|
||||
}, 'theia/terminal-manager/maximizeBottomPanel');
|
||||
export const MANAGER_MINIMIZE_BOTTOM_PANEL_TOOLBAR = Command.toLocalizedCommand({
|
||||
id: 'terminal:manager-minimize-bottom-panel',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Minimize Bottom Panel',
|
||||
}, 'theia/terminal-manager/minimizeBottomPanel');
|
||||
export const MANAGER_CLEAR_ALL = Command.toLocalizedCommand({
|
||||
id: 'terminal:manager-clear-all',
|
||||
category: 'Terminal Manager',
|
||||
label: 'Reset Terminal Manager Layout',
|
||||
iconClass: codicon('trash'),
|
||||
}, 'theia/terminal-manager/resetTerminalManagerLayout');
|
||||
export const MANAGER_OPEN_VIEW = Command.toLocalizedCommand({
|
||||
id: 'terminal:open-manager',
|
||||
category: 'View',
|
||||
label: 'Open Terminal Manager',
|
||||
}, 'theia/terminal-manager/openTerminalManager');
|
||||
export const MANAGER_CLOSE_VIEW = Command.toLocalizedCommand({
|
||||
id: 'terminal:close-manager',
|
||||
category: 'View',
|
||||
label: 'Close Terminal Manager',
|
||||
}, 'theia/terminal-manager/closeTerminalManager');
|
||||
}
|
||||
|
||||
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;
|
||||
widgets: readonly TerminalWidgetImpl[];
|
||||
}
|
||||
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;
|
||||
widgets: readonly GroupSplitPanel[];
|
||||
}
|
||||
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<T = Element, U = MouseEvent> = React.MouseEvent<T, U> | React.KeyboardEvent<T>;
|
||||
732
packages/terminal-manager/src/browser/terminal-manager-widget.ts
Normal file
732
packages/terminal-manager/src/browser/terminal-manager-widget.ts
Normal file
@@ -0,0 +1,732 @@
|
||||
// *****************************************************************************
|
||||
// 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 { Emitter, nls } from '@theia/core';
|
||||
import { UUID } from '@theia/core/shared/@lumino/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';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
|
||||
export namespace TerminalManagerWidgetState {
|
||||
export interface BaseLayoutData<ID> {
|
||||
id: ID,
|
||||
}
|
||||
export interface TerminalWidgetLayoutData {
|
||||
widget: TerminalWidget | undefined;
|
||||
}
|
||||
|
||||
export interface TerminalGroupLayoutData extends BaseLayoutData<TerminalManagerTreeTypes.GroupId> {
|
||||
childLayouts: TerminalWidgetLayoutData[];
|
||||
widgetRelativeHeights: number[] | undefined;
|
||||
}
|
||||
|
||||
export interface PageLayoutData extends BaseLayoutData<TerminalManagerTreeTypes.PageId> {
|
||||
childLayouts: TerminalGroupLayoutData[];
|
||||
groupRelativeWidths: number[] | undefined;
|
||||
}
|
||||
export interface TerminalManagerLayoutData extends BaseLayoutData<'ParentPanel'> {
|
||||
childLayouts: PageLayoutData[];
|
||||
}
|
||||
|
||||
export const isLayoutData = (obj: unknown): 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 = nls.localize('theia/terminal-manager/label', 'Terminals');
|
||||
|
||||
protected panel: SplitPanel;
|
||||
protected pageAndTreeLayout: SplitLayout | undefined;
|
||||
protected stateIsSet = false;
|
||||
|
||||
pagePanels = new Map<TerminalManagerTreeTypes.PageId, TerminalManagerTreeTypes.PageSplitPanel>();
|
||||
groupPanels = new Map<TerminalManagerTreeTypes.GroupId, TerminalManagerTreeTypes.GroupSplitPanel>();
|
||||
/** By node ID: safer for state restoration. */
|
||||
terminalWidgets = new Map<TerminalManagerTreeTypes.TerminalKey, TerminalWidget>();
|
||||
/** By terminal ID to work from widget to internal metadata. */
|
||||
terminalWidgetIdsToNodeIds = new Map<string, TerminalManagerTreeTypes.TerminalKey>();
|
||||
|
||||
protected readonly onDidChangeTrackableWidgetsEmitter = new Emitter<Widget[]>();
|
||||
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(),
|
||||
});
|
||||
|
||||
protected interceptCloseRequest = true;
|
||||
|
||||
@inject(TerminalFrontendContribution) protected terminalFrontendContribution: TerminalFrontendContribution;
|
||||
@inject(TerminalManagerTreeWidget) readonly treeWidget: TerminalManagerTreeWidget;
|
||||
@inject(SplitPositionHandler) protected readonly splitPositionHandler: SplitPositionHandler;
|
||||
|
||||
@inject(ApplicationShell) protected readonly shell: ApplicationShell;
|
||||
@inject(TerminalManagerPreferences) protected readonly terminalManagerPreferences: TerminalManagerPreferences;
|
||||
@inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService;
|
||||
@inject(WidgetManager) protected readonly widgetManager: WidgetManager;
|
||||
@inject(StorageService) protected readonly storageService: StorageService;
|
||||
|
||||
protected readonly terminalsDeletingFromClose = new Set<TerminalManagerTreeTypes.TerminalKey>();
|
||||
|
||||
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): Promise<TerminalManagerWidget> {
|
||||
return TerminalManagerWidget.createContainer(parent).getAsync(TerminalManagerWidget);
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.title.iconClass = codicon('terminal-tmux');
|
||||
this.id = TerminalManagerWidget.ID;
|
||||
this.title.closable = true;
|
||||
this.title.label = TerminalManagerWidget.LABEL;
|
||||
this.title.caption = TerminalManagerWidget.LABEL;
|
||||
this.node.tabIndex = 0;
|
||||
this.registerListeners();
|
||||
this.createPageAndTreeLayout();
|
||||
}
|
||||
|
||||
/** Yields all terminal widgets owned by this widget and then closes this widget. */
|
||||
*drainWidgets(): IterableIterator<TerminalWidget> {
|
||||
for (const [key, widget] of this.terminalWidgets) {
|
||||
this.removeTerminalReferenceByNodeId(key);
|
||||
yield widget;
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
|
||||
async populateLayout(force?: boolean): Promise<void> {
|
||||
if ((!this.stateIsSet && this.terminalWidgets.size === 0) || force) {
|
||||
const terminalWidget = await this.createTerminalWidget();
|
||||
this.addTerminalPage(terminalWidget);
|
||||
this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
|
||||
this.stateIsSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
async createTerminalWidget(options: TerminalWidgetOptions = {}): Promise<TerminalWidget> {
|
||||
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(),
|
||||
...options,
|
||||
} as TerminalWidgetOptions);
|
||||
terminalWidget.start();
|
||||
return terminalWidget;
|
||||
}
|
||||
|
||||
protected registerListeners(): void {
|
||||
this.toDispose.push(this.treeWidget);
|
||||
this.toDispose.push(this.treeWidget.model.onDidChangeTreeSelection(changeEvent => this.handleSelectionChange(changeEvent)));
|
||||
|
||||
this.toDispose.push(this.treeWidget.model.onDidAddPage(({ pageId }) => this.handlePageAdded(pageId)));
|
||||
this.toDispose.push(this.treeWidget.model.onDidDeletePage(pageId => this.handlePageDeleted(pageId)));
|
||||
|
||||
this.toDispose.push(this.treeWidget.model.onDidAddTerminalGroup(({
|
||||
groupId, pageId,
|
||||
}) => this.handleTerminalGroupAdded(groupId, pageId)));
|
||||
this.toDispose.push(this.treeWidget.model.onDidDeleteTerminalGroup(groupId => this.handleTerminalGroupDeleted(groupId)));
|
||||
|
||||
this.toDispose.push(this.treeWidget.model.onDidAddTerminalToGroup(({
|
||||
terminalId, groupId,
|
||||
}) => this.handleWidgetAddedToTerminalGroup(terminalId, groupId)));
|
||||
this.toDispose.push(this.treeWidget.model.onDidDeleteTerminalFromGroup(({
|
||||
terminalId,
|
||||
}) => this.handleTerminalDeleted(terminalId)));
|
||||
this.toDispose.push(this.treeWidget.model.onDidRenameNode(() => this.handlePageRenamed()));
|
||||
|
||||
this.toDispose.push(this.shell.onDidChangeActiveWidget(({ newValue }) => this.handleOnDidChangeActiveWidget(newValue)));
|
||||
|
||||
this.toDispose.push(this.terminalManagerPreferences.onPreferenceChanged(() => this.resolveMainLayout()));
|
||||
}
|
||||
|
||||
protected handlePageRenamed(): void {
|
||||
this.update();
|
||||
}
|
||||
|
||||
setPanelSizes({ terminal, tree } = { terminal: .6, tree: .2 } as TerminalManagerWidgetState.PanelRelativeSizes): void {
|
||||
const treeViewLocation = this.terminalManagerPreferences.get('terminal.grouping.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 async createPageAndTreeLayout(relativeSizes?: TerminalManagerWidgetState.PanelRelativeSizes): Promise<void> {
|
||||
const layout = this.layout = new PanelLayout();
|
||||
this.pageAndTreeLayout = new SplitLayout({
|
||||
renderer: SplitPanel.defaultRenderer,
|
||||
orientation: 'horizontal',
|
||||
spacing: 2,
|
||||
});
|
||||
this.panel ??= new SplitPanel({
|
||||
layout: this.pageAndTreeLayout,
|
||||
});
|
||||
|
||||
layout.addWidget(this.panel);
|
||||
await this.resolveMainLayout(relativeSizes);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected async resolveMainLayout(relativeSizes?: TerminalManagerWidgetState.PanelRelativeSizes): Promise<void> {
|
||||
if (!this.pageAndTreeLayout) {
|
||||
return;
|
||||
}
|
||||
await this.terminalManagerPreferences.ready;
|
||||
const treeViewLocation = this.terminalManagerPreferences.get('terminal.grouping.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();
|
||||
}
|
||||
|
||||
protected override onCloseRequest(msg: Message): void {
|
||||
if (this.interceptCloseRequest && this.terminalWidgets.size > 0) {
|
||||
this.interceptCloseRequest = false;
|
||||
this.confirmClose()
|
||||
.then(confirmed => {
|
||||
if (confirmed) {
|
||||
super.onCloseRequest(msg);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.interceptCloseRequest = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
super.onCloseRequest(msg);
|
||||
}
|
||||
|
||||
protected async confirmClose(): Promise<boolean> {
|
||||
const CLOSE = nls.localizeByDefault('Close');
|
||||
const dialog = new ConfirmDialog({
|
||||
title: nls.localize('theia/terminal-manager/closeDialog/title', 'Do you want to close the terminal manager?'),
|
||||
msg: nls.localize(
|
||||
'theia/terminal-manager/closeDialog/message',
|
||||
'Once the Terminal Manager is closed, its layout cannot be restored. Are you sure you want to close the Terminal Manager?'
|
||||
),
|
||||
ok: CLOSE,
|
||||
cancel: nls.localizeByDefault('Cancel'),
|
||||
});
|
||||
const confirmed = await dialog.open();
|
||||
return confirmed === true;
|
||||
}
|
||||
|
||||
addTerminalPage(widget: Widget): void {
|
||||
this.doAddTerminalPage(widget);
|
||||
}
|
||||
|
||||
protected doAddTerminalPage(widget: Widget): TerminalManagerTreeTypes.PageSplitPanel | undefined {
|
||||
if (widget instanceof TerminalWidgetImpl) {
|
||||
const terminalKey = TerminalManagerTreeTypes.generateTerminalKey(widget);
|
||||
this.addTerminalReference(widget, terminalKey);
|
||||
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);
|
||||
return pagePanel;
|
||||
}
|
||||
}
|
||||
|
||||
protected addTerminalReference(widget: TerminalWidget, nodeId: TerminalManagerTreeTypes.TerminalKey): void {
|
||||
this.terminalWidgets.set(nodeId, widget);
|
||||
this.terminalWidgetIdsToNodeIds.set(widget.id, nodeId);
|
||||
}
|
||||
|
||||
protected removeTerminalReferenceByWidgetId(widgetId: string): boolean {
|
||||
const nodeId = this.terminalWidgetIdsToNodeIds.get(widgetId);
|
||||
if (nodeId === undefined) {return false; }
|
||||
return this.terminalWidgets.delete(nodeId);
|
||||
}
|
||||
|
||||
protected removeTerminalReferenceByNodeId(nodeId: TerminalManagerTreeTypes.TerminalKey): boolean {
|
||||
const widget = this.terminalWidgets.get(nodeId);
|
||||
if (!widget) {return false; }
|
||||
this.terminalWidgets.delete(nodeId);
|
||||
this.terminalWidgetIdsToNodeIds.delete(widget.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
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 {
|
||||
const panel = this.pagePanels.get(pagePanelId);
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const isLastPanel = this.pagePanels.size === 1;
|
||||
if (isLastPanel) {
|
||||
this.interceptCloseRequest = false;
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
this.clearGroupReferences(panel);
|
||||
panel.dispose();
|
||||
this.pagePanels.delete(pagePanelId);
|
||||
}
|
||||
|
||||
protected clearGroupReferences(panel: TerminalManagerTreeTypes.PageSplitPanel): void {
|
||||
for (const group of panel.widgets) {
|
||||
this.clearTerminalReferences(group);
|
||||
this.groupPanels.delete(group.id);
|
||||
}
|
||||
}
|
||||
|
||||
addTerminalGroupToPage(widget: Widget, pageId: TerminalManagerTreeTypes.PageId): void {
|
||||
if (!this.treeWidget) {
|
||||
return;
|
||||
}
|
||||
if (widget instanceof TerminalWidgetImpl) {
|
||||
const terminalId = TerminalManagerTreeTypes.generateTerminalKey(widget);
|
||||
this.addTerminalReference(widget, terminalId);
|
||||
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<Widget | undefined> {
|
||||
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);
|
||||
widget?.activate();
|
||||
return widget;
|
||||
}
|
||||
|
||||
protected handleTerminalGroupDeleted(groupPanelId: TerminalManagerTreeTypes.GroupId): void {
|
||||
const panel = this.groupPanels.get(groupPanelId);
|
||||
this.groupPanels.delete(groupPanelId);
|
||||
if (!panel) { return; }
|
||||
this.clearTerminalReferences(panel);
|
||||
panel.dispose();
|
||||
}
|
||||
|
||||
protected clearTerminalReferences(panel: TerminalManagerTreeTypes.GroupSplitPanel): void {
|
||||
for (const terminal of panel.widgets) {
|
||||
this.removeTerminalReferenceByWidgetId(terminal.id);
|
||||
}
|
||||
}
|
||||
|
||||
addWidgetToTerminalGroup(widget: Widget, groupId: TerminalManagerTreeTypes.GroupId): void {
|
||||
if (widget instanceof TerminalWidgetImpl) {
|
||||
const newTerminalId = TerminalManagerTreeTypes.generateTerminalKey(widget);
|
||||
this.addTerminalReference(widget, newTerminalId);
|
||||
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) {
|
||||
activeTerminalWidget.activate();
|
||||
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);
|
||||
if (!terminalWidget?.isDisposed) {
|
||||
terminalWidget?.dispose();
|
||||
}
|
||||
this.removeTerminalReferenceByNodeId(terminalId);
|
||||
}
|
||||
|
||||
protected handleOnDidChangeActiveWidget(widget: Widget | null): void {
|
||||
if (!(widget instanceof TerminalWidgetImpl)) {
|
||||
return;
|
||||
}
|
||||
const terminalKey = TerminalManagerTreeTypes.generateTerminalKey(widget);
|
||||
this.treeWidget.model.selectTerminalNode(terminalKey);
|
||||
}
|
||||
|
||||
protected handleSelectionChange(changeEvent: TerminalManagerTreeTypes.SelectionChangedEvent): void {
|
||||
const { activePageId } = changeEvent;
|
||||
if (activePageId && activePageId) {
|
||||
const pageNode = this.treeWidget.model.getNode(activePageId);
|
||||
if (!TerminalManagerTreeTypes.isPageNode(pageNode)) {
|
||||
return;
|
||||
}
|
||||
this.updateViewPage(activePageId);
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
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.treeWidget.model.selectTerminalNode(activeTerminalNode?.id ?? Array.from(this.terminalWidgets.keys())[0]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected resetLayout(): void {
|
||||
this.pagePanels = new Map();
|
||||
this.groupPanels = new Map();
|
||||
this.terminalWidgets = new Map();
|
||||
}
|
||||
|
||||
async resetView(): Promise<void> {
|
||||
const terminalWidget = await this.createTerminalWidget();
|
||||
const page = this.doAddTerminalPage(terminalWidget);
|
||||
for (const id of this.pagePanels.keys()) {
|
||||
if (id !== page?.id) {
|
||||
this.deletePage(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.addTerminalReference(widget, widgetId);
|
||||
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('terminal.grouping.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;
|
||||
}
|
||||
|
||||
protected activateNextAvailableTerminal(excludeTerminalKey: TerminalManagerTreeTypes.TerminalKey): void {
|
||||
const remainingTerminals = Array.from(this.terminalWidgets.entries()).filter(([key]) => key !== excludeTerminalKey);
|
||||
if (remainingTerminals.length > 0) {
|
||||
const activeTerminalId = this.treeWidget.model.activeTerminalNode?.id;
|
||||
let targetTerminal: TerminalWidget | undefined;
|
||||
if (activeTerminalId && activeTerminalId !== excludeTerminalKey && this.terminalWidgets.has(activeTerminalId)) {
|
||||
targetTerminal = this.terminalWidgets.get(activeTerminalId);
|
||||
} else {
|
||||
targetTerminal = remainingTerminals[0][1];
|
||||
}
|
||||
if (targetTerminal) {
|
||||
this.shell.activateWidget(targetTerminal.id);
|
||||
}
|
||||
} else {
|
||||
this.shell.activateWidget(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
super.dispose();
|
||||
this.terminalWidgets.clear();
|
||||
}
|
||||
}
|
||||
51
packages/terminal-manager/src/browser/terminal-manager.css
Normal file
51
packages/terminal-manager/src/browser/terminal-manager.css
Normal file
@@ -0,0 +1,51 @@
|
||||
/********************************************************************************
|
||||
* 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 .lm-Widget.lm-Panel.lm-SplitPanel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#terminal-manager-widget .lm-SplitPanel[data-orientation='horizontal']>.lm-SplitPanel-handle {
|
||||
width: 1px !important;
|
||||
background-color: var(--theia-tab-unfocusedInactiveForeground);
|
||||
}
|
||||
|
||||
#terminal-manager-widget .lm-SplitPanel[data-orientation='vertical']>.lm-SplitPanel-handle {
|
||||
height: 1px !important;
|
||||
background-color: var(--theia-tab-unfocusedInactiveForeground);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
27
packages/terminal-manager/src/package.spec.ts
Normal file
27
packages/terminal-manager/src/package.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
22
packages/terminal-manager/tsconfig.json
Normal file
22
packages/terminal-manager/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib",
|
||||
"composite": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../preferences"
|
||||
},
|
||||
{
|
||||
"path": "../terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user