@theia/terminal-manager 1.67.0-next.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +31 -0
  2. package/lib/browser/terminal-manager-frontend-contribution.d.ts +25 -0
  3. package/lib/browser/terminal-manager-frontend-contribution.d.ts.map +1 -0
  4. package/lib/browser/terminal-manager-frontend-contribution.js +173 -0
  5. package/lib/browser/terminal-manager-frontend-contribution.js.map +1 -0
  6. package/lib/browser/terminal-manager-frontend-module.d.ts +5 -0
  7. package/lib/browser/terminal-manager-frontend-module.d.ts.map +1 -0
  8. package/lib/browser/terminal-manager-frontend-module.js +55 -0
  9. package/lib/browser/terminal-manager-frontend-module.js.map +1 -0
  10. package/lib/browser/terminal-manager-frontend-view-contribution.d.ts +27 -0
  11. package/lib/browser/terminal-manager-frontend-view-contribution.d.ts.map +1 -0
  12. package/lib/browser/terminal-manager-frontend-view-contribution.js +278 -0
  13. package/lib/browser/terminal-manager-frontend-view-contribution.js.map +1 -0
  14. package/lib/browser/terminal-manager-preferences.d.ts +12 -0
  15. package/lib/browser/terminal-manager-preferences.d.ts.map +1 -0
  16. package/lib/browser/terminal-manager-preferences.js +42 -0
  17. package/lib/browser/terminal-manager-preferences.js.map +1 -0
  18. package/lib/browser/terminal-manager-tree-model.d.ts +71 -0
  19. package/lib/browser/terminal-manager-tree-model.d.ts.map +1 -0
  20. package/lib/browser/terminal-manager-tree-model.js +299 -0
  21. package/lib/browser/terminal-manager-tree-model.js.map +1 -0
  22. package/lib/browser/terminal-manager-tree-widget.d.ts +40 -0
  23. package/lib/browser/terminal-manager-tree-widget.d.ts.map +1 -0
  24. package/lib/browser/terminal-manager-tree-widget.js +276 -0
  25. package/lib/browser/terminal-manager-tree-widget.js.map +1 -0
  26. package/lib/browser/terminal-manager-types.d.ts +77 -0
  27. package/lib/browser/terminal-manager-types.d.ts.map +1 -0
  28. package/lib/browser/terminal-manager-types.js +117 -0
  29. package/lib/browser/terminal-manager-types.js.map +1 -0
  30. package/lib/browser/terminal-manager-widget.d.ts +108 -0
  31. package/lib/browser/terminal-manager-widget.d.ts.map +1 -0
  32. package/lib/browser/terminal-manager-widget.js +616 -0
  33. package/lib/browser/terminal-manager-widget.js.map +1 -0
  34. package/lib/package.spec.d.ts +1 -0
  35. package/lib/package.spec.d.ts.map +1 -0
  36. package/lib/package.spec.js +26 -0
  37. package/lib/package.spec.js.map +1 -0
  38. package/package.json +40 -0
  39. package/src/browser/terminal-manager-frontend-contribution.ts +164 -0
  40. package/src/browser/terminal-manager-frontend-module.ts +64 -0
  41. package/src/browser/terminal-manager-frontend-view-contribution.ts +309 -0
  42. package/src/browser/terminal-manager-preferences.ts +52 -0
  43. package/src/browser/terminal-manager-tree-model.ts +336 -0
  44. package/src/browser/terminal-manager-tree-widget.tsx +305 -0
  45. package/src/browser/terminal-manager-types.ts +173 -0
  46. package/src/browser/terminal-manager-widget.ts +686 -0
  47. package/src/browser/terminal-manager.css +51 -0
  48. package/src/package.spec.ts +27 -0
@@ -0,0 +1,686 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2023 Ericsson and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
18
+ import {
19
+ ApplicationShell,
20
+ BaseWidget,
21
+ codicon,
22
+ CompositeTreeNode,
23
+ Message,
24
+ Panel,
25
+ PanelLayout,
26
+ SplitLayout,
27
+ SplitPanel,
28
+ SplitPositionHandler,
29
+ StatefulWidget,
30
+ StorageService,
31
+ ViewContainerLayout,
32
+ Widget,
33
+ WidgetManager,
34
+ } from '@theia/core/lib/browser';
35
+ import { Emitter, nls } from '@theia/core';
36
+ import { UUID } from '@theia/core/shared/@lumino/coreutils';
37
+ import { TerminalWidget, TerminalWidgetOptions } from '@theia/terminal/lib/browser/base/terminal-widget';
38
+ import { TerminalWidgetImpl } from '@theia/terminal/lib/browser/terminal-widget-impl';
39
+ import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
40
+ import { TerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
41
+ import { TerminalManagerPreferences } from './terminal-manager-preferences';
42
+ import { TerminalManagerTreeTypes } from './terminal-manager-types';
43
+ import { TerminalManagerTreeWidget } from './terminal-manager-tree-widget';
44
+ import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
45
+
46
+ export namespace TerminalManagerWidgetState {
47
+ export interface BaseLayoutData<ID> {
48
+ id: ID,
49
+ }
50
+ export interface TerminalWidgetLayoutData {
51
+ widget: TerminalWidget | undefined;
52
+ }
53
+
54
+ export interface TerminalGroupLayoutData extends BaseLayoutData<TerminalManagerTreeTypes.GroupId> {
55
+ childLayouts: TerminalWidgetLayoutData[];
56
+ widgetRelativeHeights: number[] | undefined;
57
+ }
58
+
59
+ export interface PageLayoutData extends BaseLayoutData<TerminalManagerTreeTypes.PageId> {
60
+ childLayouts: TerminalGroupLayoutData[];
61
+ groupRelativeWidths: number[] | undefined;
62
+ }
63
+ export interface TerminalManagerLayoutData extends BaseLayoutData<'ParentPanel'> {
64
+ childLayouts: PageLayoutData[];
65
+ }
66
+
67
+ export const isLayoutData = (obj: unknown): obj is LayoutData => typeof obj === 'object' && !!obj && 'type' in obj && obj.type === 'terminal-manager';
68
+ export interface PanelRelativeSizes {
69
+ terminal: number;
70
+ tree: number;
71
+ }
72
+ export interface LayoutData {
73
+ items?: TerminalManagerLayoutData;
74
+ widget: TerminalManagerTreeWidget;
75
+ terminalAndTreeRelativeSizes: PanelRelativeSizes | undefined;
76
+ }
77
+
78
+ }
79
+
80
+ @injectable()
81
+ export class TerminalManagerWidget extends BaseWidget implements StatefulWidget, ApplicationShell.TrackableWidgetProvider {
82
+ static ID = 'terminal-manager-widget';
83
+ static LABEL = nls.localize('theia/terminal-manager/label', 'Terminals');
84
+
85
+ protected panel: SplitPanel;
86
+
87
+ protected pageAndTreeLayout: SplitLayout | undefined;
88
+ protected stateIsSet = false;
89
+
90
+ pagePanels = new Map<TerminalManagerTreeTypes.PageId, TerminalManagerTreeTypes.PageSplitPanel>();
91
+ groupPanels = new Map<TerminalManagerTreeTypes.GroupId, TerminalManagerTreeTypes.GroupSplitPanel>();
92
+ terminalWidgets = new Map<TerminalManagerTreeTypes.TerminalKey, TerminalWidget>();
93
+
94
+ protected readonly onDidChangeTrackableWidgetsEmitter = new Emitter<Widget[]>();
95
+ readonly onDidChangeTrackableWidgets = this.onDidChangeTrackableWidgetsEmitter.event;
96
+
97
+ // serves as an empty container so that different view containers can be swapped out
98
+ protected terminalPanelWrapper = new Panel({
99
+ layout: new PanelLayout(),
100
+ });
101
+
102
+ protected interceptCloseRequest = true;
103
+
104
+ @inject(TerminalFrontendContribution) protected terminalFrontendContribution: TerminalFrontendContribution;
105
+ @inject(TerminalManagerTreeWidget) readonly treeWidget: TerminalManagerTreeWidget;
106
+ @inject(SplitPositionHandler) protected readonly splitPositionHandler: SplitPositionHandler;
107
+
108
+ @inject(ApplicationShell) protected readonly shell: ApplicationShell;
109
+ @inject(TerminalManagerPreferences) protected readonly terminalManagerPreferences: TerminalManagerPreferences;
110
+ @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService;
111
+ @inject(WidgetManager) protected readonly widgetManager: WidgetManager;
112
+ @inject(StorageService) protected readonly storageService: StorageService;
113
+
114
+ protected readonly terminalsDeletingFromClose = new Set<TerminalManagerTreeTypes.TerminalKey>();
115
+
116
+ static createRestoreError = (
117
+ nodeId: string,
118
+ ): Error => new Error(`Terminal manager widget state could not be restored, mismatch in restored data for ${nodeId}`);
119
+
120
+ static createContainer(parent: interfaces.Container): interfaces.Container {
121
+ const child = parent.createChild();
122
+ child.bind(TerminalManagerWidget).toSelf().inSingletonScope();
123
+ return child;
124
+ }
125
+
126
+ static createWidget(parent: interfaces.Container): Promise<TerminalManagerWidget> {
127
+ return TerminalManagerWidget.createContainer(parent).getAsync(TerminalManagerWidget);
128
+ }
129
+
130
+ @postConstruct()
131
+ protected init(): void {
132
+ this.title.iconClass = codicon('terminal-tmux');
133
+ this.id = TerminalManagerWidget.ID;
134
+ this.title.closable = true;
135
+ this.title.label = TerminalManagerWidget.LABEL;
136
+ this.node.tabIndex = 0;
137
+ this.registerListeners();
138
+ this.createPageAndTreeLayout();
139
+ }
140
+
141
+ async populateLayout(force?: boolean): Promise<void> {
142
+ if (!this.stateIsSet || force) {
143
+ const terminalWidget = await this.createTerminalWidget();
144
+ this.addTerminalPage(terminalWidget);
145
+ this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
146
+ this.stateIsSet = true;
147
+ }
148
+ }
149
+
150
+ async createTerminalWidget(options: TerminalWidgetOptions = {}): Promise<TerminalWidget> {
151
+ const terminalWidget = await this.terminalFrontendContribution.newTerminal({
152
+ // passing 'created' here as a millisecond value rather than the default `new Date().toString()` that Theia uses in
153
+ // its factory (resolves to something like 'Tue Aug 09 2022 13:21:26 GMT-0500 (Central Daylight Time)').
154
+ // The state restoration system relies on identifying terminals by their unique options, using an ms value ensures we don't
155
+ // get a duplication since the original date method is only accurate to within 1s.
156
+ created: new Date().getTime().toString(),
157
+ ...options,
158
+ } as TerminalWidgetOptions);
159
+ terminalWidget.start();
160
+ return terminalWidget;
161
+ }
162
+
163
+ protected registerListeners(): void {
164
+ this.toDispose.push(this.treeWidget);
165
+ this.toDispose.push(this.treeWidget.model.onDidChangeTreeSelection(changeEvent => this.handleSelectionChange(changeEvent)));
166
+
167
+ this.toDispose.push(this.treeWidget.model.onDidAddPage(({ pageId }) => this.handlePageAdded(pageId)));
168
+ this.toDispose.push(this.treeWidget.model.onDidDeletePage(pageId => this.handlePageDeleted(pageId)));
169
+
170
+ this.toDispose.push(this.treeWidget.model.onDidAddTerminalGroup(({
171
+ groupId, pageId,
172
+ }) => this.handleTerminalGroupAdded(groupId, pageId)));
173
+ this.toDispose.push(this.treeWidget.model.onDidDeleteTerminalGroup(groupId => this.handleTerminalGroupDeleted(groupId)));
174
+
175
+ this.toDispose.push(this.treeWidget.model.onDidAddTerminalToGroup(({
176
+ terminalId, groupId,
177
+ }) => this.handleWidgetAddedToTerminalGroup(terminalId, groupId)));
178
+ this.toDispose.push(this.treeWidget.model.onDidDeleteTerminalFromGroup(({
179
+ terminalId,
180
+ }) => this.handleTerminalDeleted(terminalId)));
181
+ this.toDispose.push(this.treeWidget.model.onDidRenameNode(() => this.handlePageRenamed()));
182
+
183
+ this.toDispose.push(this.shell.onDidChangeActiveWidget(({ newValue }) => this.handleOnDidChangeActiveWidget(newValue)));
184
+
185
+ this.toDispose.push(this.terminalManagerPreferences.onPreferenceChanged(() => this.resolveMainLayout()));
186
+ }
187
+
188
+ protected handlePageRenamed(): void {
189
+ this.update();
190
+ }
191
+
192
+ setPanelSizes({ terminal, tree } = { terminal: .6, tree: .2 } as TerminalManagerWidgetState.PanelRelativeSizes): void {
193
+ const treeViewLocation = this.terminalManagerPreferences.get('terminal.grouping.treeViewLocation');
194
+ const panelSizes = treeViewLocation === 'left' ? [tree, terminal] : [terminal, tree];
195
+ requestAnimationFrame(() => this.pageAndTreeLayout?.setRelativeSizes(panelSizes));
196
+ }
197
+
198
+ getTrackableWidgets(): Widget[] {
199
+ return [this.treeWidget, ...this.terminalWidgets.values()];
200
+ }
201
+
202
+ toggleTreeVisibility(): void {
203
+ if (this.treeWidget.isHidden) {
204
+ this.treeWidget.show();
205
+ this.setPanelSizes();
206
+ } else {
207
+ this.treeWidget.hide();
208
+ }
209
+ }
210
+
211
+ protected async createPageAndTreeLayout(relativeSizes?: TerminalManagerWidgetState.PanelRelativeSizes): Promise<void> {
212
+ const layout = this.layout = new PanelLayout();
213
+ this.pageAndTreeLayout = new SplitLayout({
214
+ renderer: SplitPanel.defaultRenderer,
215
+ orientation: 'horizontal',
216
+ spacing: 2,
217
+ });
218
+ this.panel ??= new SplitPanel({
219
+ layout: this.pageAndTreeLayout,
220
+ });
221
+
222
+ layout.addWidget(this.panel);
223
+ await this.resolveMainLayout(relativeSizes);
224
+ this.update();
225
+ }
226
+
227
+ protected async resolveMainLayout(relativeSizes?: TerminalManagerWidgetState.PanelRelativeSizes): Promise<void> {
228
+ if (!this.pageAndTreeLayout) {
229
+ return;
230
+ }
231
+ await this.terminalManagerPreferences.ready;
232
+ const treeViewLocation = this.terminalManagerPreferences.get('terminal.grouping.treeViewLocation');
233
+ const widgetsInDesiredOrder = treeViewLocation === 'left' ? [this.treeWidget, this.terminalPanelWrapper] : [this.terminalPanelWrapper, this.treeWidget];
234
+ widgetsInDesiredOrder.forEach((widget, index) => {
235
+ this.pageAndTreeLayout?.insertWidget(index, widget);
236
+ });
237
+ this.setPanelSizes(relativeSizes);
238
+ }
239
+
240
+ protected override onAfterAttach(msg: Message): void {
241
+ super.onAfterAttach(msg);
242
+ this.populateLayout();
243
+ }
244
+
245
+ protected override onCloseRequest(msg: Message): void {
246
+ if (this.interceptCloseRequest) {
247
+ this.interceptCloseRequest = false;
248
+ this.confirmClose()
249
+ .then(confirmed => {
250
+ if (confirmed) {
251
+ super.onCloseRequest(msg);
252
+ }
253
+ })
254
+ .finally(() => {
255
+ this.interceptCloseRequest = true;
256
+ });
257
+ return;
258
+ }
259
+ super.onCloseRequest(msg);
260
+ }
261
+
262
+ protected async confirmClose(): Promise<boolean> {
263
+ const CLOSE = nls.localizeByDefault('Close');
264
+ const dialog = new ConfirmDialog({
265
+ title: nls.localize('theia/terminal-manager/closeDialog/title', 'Do you want to close the terminal manager?'),
266
+ msg: nls.localize(
267
+ 'theia/terminal-manager/closeDialog/message',
268
+ 'Once the Terminal Manager is closed, its layout cannot be restored. Are you sure you want to close the Terminal Manager?'
269
+ ),
270
+ ok: CLOSE,
271
+ cancel: nls.localizeByDefault('Cancel'),
272
+ });
273
+ const confirmed = await dialog.open();
274
+ return confirmed === true;
275
+ }
276
+
277
+ addTerminalPage(widget: Widget): void {
278
+ if (widget instanceof TerminalWidgetImpl) {
279
+ const terminalKey = TerminalManagerTreeTypes.generateTerminalKey(widget);
280
+ this.registerTerminalCloseListener(widget, terminalKey);
281
+ this.terminalWidgets.set(terminalKey, widget);
282
+ this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
283
+ const groupPanel = this.createTerminalGroupPanel();
284
+ groupPanel.addWidget(widget);
285
+ const pagePanel = this.createPagePanel();
286
+ pagePanel.addWidget(groupPanel);
287
+ this.treeWidget.model.addTerminalPage(terminalKey, groupPanel.id, pagePanel.id);
288
+ }
289
+ }
290
+
291
+ protected createPagePanel(pageId?: TerminalManagerTreeTypes.PageId): TerminalManagerTreeTypes.PageSplitPanel {
292
+ const newPageLayout = new ViewContainerLayout({
293
+ renderer: SplitPanel.defaultRenderer,
294
+ orientation: 'horizontal',
295
+ spacing: 2,
296
+ headerSize: 0,
297
+ animationDuration: 200,
298
+ }, this.splitPositionHandler);
299
+ const pagePanel = new SplitPanel({
300
+ layout: newPageLayout,
301
+ }) as TerminalManagerTreeTypes.PageSplitPanel;
302
+ const idPrefix = 'page-';
303
+ const uuid = this.generateUUIDAvoidDuplicatesFromStorage(idPrefix);
304
+ pagePanel.node.tabIndex = -1;
305
+ pagePanel.id = pageId ?? `${idPrefix}${uuid}`;
306
+ this.pagePanels.set(pagePanel.id, pagePanel);
307
+
308
+ return pagePanel;
309
+ }
310
+
311
+ protected generateUUIDAvoidDuplicatesFromStorage(idPrefix: 'group-' | 'page-'): string {
312
+ // highly unlikely there would ever be a duplicate, but just to be safe :)
313
+ let didNotGenerateValidId = true;
314
+ let uuid = '';
315
+ while (didNotGenerateValidId) {
316
+ uuid = UUID.uuid4();
317
+ if (idPrefix === 'group-') {
318
+ didNotGenerateValidId = this.groupPanels.has(`group-${uuid}`);
319
+ } else if (idPrefix === 'page-') {
320
+ didNotGenerateValidId = this.pagePanels.has(`page-${uuid}`);
321
+ }
322
+ }
323
+ return uuid;
324
+ }
325
+
326
+ protected handlePageAdded(pageId: TerminalManagerTreeTypes.PageId): void {
327
+ const pagePanel = this.pagePanels.get(pageId);
328
+ if (pagePanel) {
329
+ this.terminalPanelWrapper.addWidget(pagePanel);
330
+ this.update();
331
+ }
332
+ }
333
+
334
+ protected handlePageDeleted(pagePanelId: TerminalManagerTreeTypes.PageId): void {
335
+ this.pagePanels.get(pagePanelId)?.dispose();
336
+ this.pagePanels.delete(pagePanelId);
337
+ if (this.pagePanels.size === 0) {
338
+ this.populateLayout(true);
339
+ }
340
+ }
341
+
342
+ addTerminalGroupToPage(widget: Widget, pageId: TerminalManagerTreeTypes.PageId): void {
343
+ if (!this.treeWidget) {
344
+ return;
345
+ }
346
+ if (widget instanceof TerminalWidgetImpl) {
347
+ const terminalId = TerminalManagerTreeTypes.generateTerminalKey(widget);
348
+ this.registerTerminalCloseListener(widget, terminalId);
349
+ this.terminalWidgets.set(terminalId, widget);
350
+ this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
351
+ const groupPanel = this.createTerminalGroupPanel();
352
+ groupPanel.addWidget(widget);
353
+ this.treeWidget.model.addTerminalGroup(terminalId, groupPanel.id, pageId);
354
+ }
355
+ }
356
+
357
+ protected createTerminalGroupPanel(groupId?: TerminalManagerTreeTypes.GroupId): TerminalManagerTreeTypes.GroupSplitPanel {
358
+ const terminalColumnLayout = new ViewContainerLayout({
359
+ renderer: SplitPanel.defaultRenderer,
360
+ orientation: 'vertical',
361
+ spacing: 0,
362
+ headerSize: 0,
363
+ animationDuration: 200,
364
+ alignment: 'end',
365
+ }, this.splitPositionHandler);
366
+ const groupPanel = new SplitPanel({
367
+ layout: terminalColumnLayout,
368
+ }) as TerminalManagerTreeTypes.GroupSplitPanel;
369
+ const idPrefix = 'group-';
370
+ const uuid = this.generateUUIDAvoidDuplicatesFromStorage(idPrefix);
371
+ groupPanel.node.tabIndex = -1;
372
+ groupPanel.id = groupId ?? `${idPrefix}${uuid}`;
373
+ this.groupPanels.set(groupPanel.id, groupPanel);
374
+ return groupPanel;
375
+ }
376
+
377
+ protected handleTerminalGroupAdded(
378
+ groupId: TerminalManagerTreeTypes.GroupId,
379
+ pageId: TerminalManagerTreeTypes.PageId,
380
+ ): void {
381
+ if (!this.treeWidget) {
382
+ return;
383
+ }
384
+ const groupPanel = this.groupPanels.get(groupId);
385
+ if (!groupPanel) {
386
+ return;
387
+ }
388
+ const activePage = this.pagePanels.get(pageId);
389
+ if (activePage) {
390
+ activePage.addWidget(groupPanel);
391
+ this.update();
392
+ }
393
+ }
394
+
395
+ protected async activateTerminalWidget(terminalKey: TerminalManagerTreeTypes.TerminalKey): Promise<Widget | undefined> {
396
+ const terminalWidgetToActivate = this.terminalWidgets.get(terminalKey)?.id;
397
+ if (terminalWidgetToActivate) {
398
+ const activeWidgetFound = await this.shell.activateWidget(terminalWidgetToActivate);
399
+ return activeWidgetFound;
400
+ }
401
+ return undefined;
402
+ }
403
+
404
+ activateWidget(id: string): Widget | undefined {
405
+ const widget = Array.from(this.terminalWidgets.values()).find(terminalWidget => terminalWidget.id === id);
406
+ if (widget instanceof TerminalWidgetImpl) {
407
+ widget.activate();
408
+ }
409
+ return widget;
410
+ }
411
+
412
+ protected handleTerminalGroupDeleted(groupPanelId: TerminalManagerTreeTypes.GroupId): void {
413
+ this.groupPanels.get(groupPanelId)?.dispose();
414
+ this.groupPanels.delete(groupPanelId);
415
+ }
416
+
417
+ addWidgetToTerminalGroup(widget: Widget, groupId: TerminalManagerTreeTypes.GroupId): void {
418
+ if (widget instanceof TerminalWidgetImpl) {
419
+ const newTerminalId = TerminalManagerTreeTypes.generateTerminalKey(widget);
420
+ this.registerTerminalCloseListener(widget, newTerminalId);
421
+ this.terminalWidgets.set(newTerminalId, widget);
422
+ this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
423
+ this.treeWidget.model.addTerminal(newTerminalId, groupId);
424
+ }
425
+ }
426
+
427
+ protected override onActivateRequest(msg: Message): void {
428
+ super.onActivateRequest(msg);
429
+ const activeTerminalId = this.treeWidget.model.activeTerminalNode?.id;
430
+ if (activeTerminalId) {
431
+ const activeTerminalWidget = this.terminalWidgets.get(activeTerminalId);
432
+ if (activeTerminalWidget) {
433
+ activeTerminalWidget.activate();
434
+ return;
435
+ }
436
+ }
437
+ this.node.focus();
438
+ }
439
+
440
+ protected handleWidgetAddedToTerminalGroup(terminalKey: TerminalManagerTreeTypes.TerminalKey, groupId: TerminalManagerTreeTypes.GroupId): void {
441
+ const terminalWidget = this.terminalWidgets.get(terminalKey);
442
+ const group = this.groupPanels.get(groupId);
443
+ if (terminalWidget && group) {
444
+ const groupPanel = this.groupPanels.get(groupId);
445
+ groupPanel?.addWidget(terminalWidget);
446
+ this.update();
447
+ }
448
+ }
449
+
450
+ protected handleTerminalDeleted(terminalId: TerminalManagerTreeTypes.TerminalKey): void {
451
+ const terminalWidget = this.terminalWidgets.get(terminalId);
452
+ const wasActiveWidget = this.shell.activeWidget === terminalWidget;
453
+ if (!this.terminalsDeletingFromClose.has(terminalId)) {
454
+ terminalWidget?.dispose();
455
+ }
456
+ this.terminalWidgets.delete(terminalId);
457
+ if (wasActiveWidget) {
458
+ this.activateNextAvailableTerminal(terminalId);
459
+ }
460
+ }
461
+
462
+ protected handleOnDidChangeActiveWidget(widget: Widget | null): void {
463
+ if (!(widget instanceof TerminalWidgetImpl)) {
464
+ return;
465
+ }
466
+ const terminalKey = TerminalManagerTreeTypes.generateTerminalKey(widget);
467
+ this.treeWidget.model.selectTerminalNode(terminalKey);
468
+ }
469
+
470
+ protected handleSelectionChange(changeEvent: TerminalManagerTreeTypes.SelectionChangedEvent): void {
471
+ const { activePageId } = changeEvent;
472
+ if (activePageId && activePageId) {
473
+ const pageNode = this.treeWidget.model.getNode(activePageId);
474
+ if (!TerminalManagerTreeTypes.isPageNode(pageNode)) {
475
+ return;
476
+ }
477
+ this.updateViewPage(activePageId);
478
+ }
479
+ this.update();
480
+ }
481
+
482
+ protected updateViewPage(activePageId: TerminalManagerTreeTypes.PageId): void {
483
+ const activePagePanel = this.pagePanels.get(activePageId);
484
+ if (activePagePanel) {
485
+ this.terminalPanelWrapper.widgets
486
+ .forEach(widget => widget !== activePagePanel && widget.hide());
487
+ activePagePanel.show();
488
+ this.update();
489
+ }
490
+ }
491
+
492
+ deleteTerminal(terminalId: TerminalManagerTreeTypes.TerminalKey): void {
493
+ this.treeWidget.model.deleteTerminalNode(terminalId);
494
+ }
495
+
496
+ deleteGroup(groupId: TerminalManagerTreeTypes.GroupId): void {
497
+ this.treeWidget.model.deleteTerminalGroup(groupId);
498
+ }
499
+
500
+ deletePage(pageNode: TerminalManagerTreeTypes.PageId): void {
501
+ this.treeWidget.model.deleteTerminalPage(pageNode);
502
+ }
503
+
504
+ toggleRenameTerminal(entityId: TerminalManagerTreeTypes.TerminalManagerValidId): void {
505
+ this.treeWidget.model.toggleRenameTerminal(entityId);
506
+ }
507
+
508
+ storeState(): TerminalManagerWidgetState.LayoutData {
509
+ return this.getLayoutData();
510
+ }
511
+
512
+ restoreState(oldState: TerminalManagerWidgetState.LayoutData): void {
513
+ const { items, widget, terminalAndTreeRelativeSizes } = oldState;
514
+ if (widget && terminalAndTreeRelativeSizes && items) {
515
+ this.setPanelSizes(terminalAndTreeRelativeSizes);
516
+ try {
517
+ this.restoreLayoutData(items, widget);
518
+ } catch (e) {
519
+ console.error(e);
520
+ this.resetLayout();
521
+ this.populateLayout(true);
522
+ } finally {
523
+ this.stateIsSet = true;
524
+ const { activeTerminalNode } = this.treeWidget.model;
525
+ setTimeout(() => {
526
+ this.treeWidget.model.selectTerminalNode(activeTerminalNode?.id ?? Array.from(this.terminalWidgets.keys())[0]);
527
+ });
528
+ }
529
+ }
530
+ }
531
+
532
+ protected resetLayout(): void {
533
+ this.pagePanels = new Map();
534
+ this.groupPanels = new Map();
535
+ this.terminalWidgets = new Map();
536
+ }
537
+
538
+ protected iterateAndRestoreLayoutTree(pageLayouts: TerminalManagerWidgetState.PageLayoutData[], treeWidget: TerminalManagerTreeWidget): void {
539
+ for (const pageLayout of pageLayouts) {
540
+ const pageId = pageLayout.id;
541
+
542
+ const pagePanel = this.createPagePanel(pageId);
543
+ const pageNode = treeWidget.model.getNode(pageId);
544
+ if (!TerminalManagerTreeTypes.isPageNode(pageNode)) {
545
+ throw TerminalManagerWidget.createRestoreError(pageId);
546
+ }
547
+ this.pagePanels.set(pageId, pagePanel);
548
+ this.terminalPanelWrapper.addWidget(pagePanel);
549
+ const { childLayouts: groupLayouts } = pageLayout;
550
+ for (const groupLayout of groupLayouts) {
551
+ const groupId = groupLayout.id;
552
+ const groupPanel = this.createTerminalGroupPanel(groupId);
553
+ const groupNode = treeWidget.model.getNode(groupId);
554
+ if (!TerminalManagerTreeTypes.isGroupNode(groupNode)) {
555
+ throw TerminalManagerWidget.createRestoreError(groupId);
556
+ }
557
+ this.groupPanels.set(groupId, groupPanel);
558
+ pagePanel.insertWidget(0, groupPanel);
559
+ const { childLayouts: widgetLayouts } = groupLayout;
560
+ for (const widgetLayout of widgetLayouts) {
561
+ const { widget } = widgetLayout;
562
+ if (widget instanceof TerminalWidgetImpl) {
563
+ const widgetId = TerminalManagerTreeTypes.generateTerminalKey(widget);
564
+ const widgetNode = treeWidget.model.getNode(widgetId);
565
+ if (!TerminalManagerTreeTypes.isTerminalNode(widgetNode)) {
566
+ throw TerminalManagerWidget.createRestoreError(widgetId);
567
+ }
568
+ this.terminalWidgets.set(widgetId, widget);
569
+ this.registerTerminalCloseListener(widget, widgetId);
570
+ this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
571
+ groupPanel.addWidget(widget);
572
+ }
573
+ }
574
+ const { widgetRelativeHeights } = groupLayout;
575
+ if (widgetRelativeHeights) {
576
+ requestAnimationFrame(() => groupPanel.setRelativeSizes(widgetRelativeHeights));
577
+ }
578
+ }
579
+ const { groupRelativeWidths } = pageLayout;
580
+ if (groupRelativeWidths) {
581
+ requestAnimationFrame(() => pagePanel.setRelativeSizes(groupRelativeWidths));
582
+ }
583
+ }
584
+ }
585
+
586
+ restoreLayoutData(items: TerminalManagerWidgetState.TerminalManagerLayoutData, treeWidget: TerminalManagerTreeWidget): void {
587
+ const { childLayouts: pageLayouts } = items;
588
+ Array.from(this.pagePanels.keys()).forEach(pageId => this.deletePage(pageId));
589
+ this.iterateAndRestoreLayoutTree(pageLayouts, treeWidget);
590
+ this.onDidChangeTrackableWidgetsEmitter.fire(this.getTrackableWidgets());
591
+ this.update();
592
+ }
593
+
594
+ getLayoutData(): TerminalManagerWidgetState.LayoutData {
595
+ const pageItems: TerminalManagerWidgetState.TerminalManagerLayoutData = { childLayouts: [], id: 'ParentPanel' };
596
+ const treeViewLocation = this.terminalManagerPreferences.get('terminal.grouping.treeViewLocation');
597
+ let terminalAndTreeRelativeSizes: TerminalManagerWidgetState.PanelRelativeSizes | undefined = undefined;
598
+ const sizeArray = this.pageAndTreeLayout?.relativeSizes();
599
+ if (sizeArray && treeViewLocation === 'right') {
600
+ terminalAndTreeRelativeSizes = { tree: sizeArray[1], terminal: sizeArray[0] };
601
+ } else if (sizeArray && treeViewLocation === 'left') {
602
+ terminalAndTreeRelativeSizes = { tree: sizeArray[0], terminal: sizeArray[1] };
603
+ }
604
+ const fullLayoutData: TerminalManagerWidgetState.LayoutData = {
605
+ widget: this.treeWidget,
606
+ items: pageItems,
607
+ terminalAndTreeRelativeSizes,
608
+ };
609
+ const treeRoot = this.treeWidget.model.root;
610
+ if (treeRoot && CompositeTreeNode.is(treeRoot)) {
611
+ const pageNodes = treeRoot.children;
612
+ for (const pageNode of pageNodes) {
613
+ if (TerminalManagerTreeTypes.isPageNode(pageNode)) {
614
+ const groupNodes = pageNode.children;
615
+ const pagePanel = this.pagePanels.get(pageNode.id);
616
+ const pageLayoutData: TerminalManagerWidgetState.PageLayoutData = {
617
+ childLayouts: [],
618
+ id: pageNode.id,
619
+ groupRelativeWidths: pagePanel?.relativeSizes(),
620
+ };
621
+ for (const groupNode of groupNodes) {
622
+ const groupPanel = this.groupPanels.get(groupNode.id);
623
+ if (TerminalManagerTreeTypes.isGroupNode(groupNode)) {
624
+ const groupLayoutData: TerminalManagerWidgetState.TerminalGroupLayoutData = {
625
+ id: groupNode.id,
626
+ childLayouts: [],
627
+ widgetRelativeHeights: groupPanel?.relativeSizes(),
628
+ };
629
+ const widgetNodes = groupNode.children;
630
+ for (const widgetNode of widgetNodes) {
631
+ if (TerminalManagerTreeTypes.isTerminalNode(widgetNode)) {
632
+ const widget = this.terminalWidgets.get(widgetNode.id);
633
+ const terminalLayoutData: TerminalManagerWidgetState.TerminalWidgetLayoutData = {
634
+ widget,
635
+ };
636
+ groupLayoutData.childLayouts.push(terminalLayoutData);
637
+ }
638
+ }
639
+ pageLayoutData.childLayouts.unshift(groupLayoutData);
640
+ }
641
+ }
642
+ pageItems.childLayouts.push(pageLayoutData);
643
+ }
644
+ }
645
+ }
646
+ return fullLayoutData;
647
+ }
648
+
649
+ protected registerTerminalCloseListener(widget: TerminalWidget, terminalKey: TerminalManagerTreeTypes.TerminalKey): void {
650
+ const originalOnCloseRequest = widget['onCloseRequest'].bind(widget);
651
+ widget['onCloseRequest'] = (msg: Message) => {
652
+ const wasActiveWidget = this.shell.activeWidget === widget;
653
+ if (wasActiveWidget) {
654
+ this.activateNextAvailableTerminal(terminalKey);
655
+ }
656
+ originalOnCloseRequest(msg);
657
+ };
658
+ const disposable = widget.onTerminalDidClose(() => {
659
+ this.terminalsDeletingFromClose.add(terminalKey);
660
+ try {
661
+ this.treeWidget.model.deleteTerminalNode(terminalKey);
662
+ } finally {
663
+ this.terminalsDeletingFromClose.delete(terminalKey);
664
+ }
665
+ });
666
+ this.toDispose.push(disposable);
667
+ }
668
+
669
+ protected activateNextAvailableTerminal(excludeTerminalKey: TerminalManagerTreeTypes.TerminalKey): void {
670
+ const remainingTerminals = Array.from(this.terminalWidgets.entries()).filter(([key]) => key !== excludeTerminalKey);
671
+ if (remainingTerminals.length > 0) {
672
+ const activeTerminalId = this.treeWidget.model.activeTerminalNode?.id;
673
+ let targetTerminal: TerminalWidget | undefined;
674
+ if (activeTerminalId && activeTerminalId !== excludeTerminalKey && this.terminalWidgets.has(activeTerminalId)) {
675
+ targetTerminal = this.terminalWidgets.get(activeTerminalId);
676
+ } else {
677
+ targetTerminal = remainingTerminals[0][1];
678
+ }
679
+ if (targetTerminal) {
680
+ this.shell.activateWidget(targetTerminal.id);
681
+ }
682
+ } else {
683
+ this.shell.activateWidget(this.id);
684
+ }
685
+ }
686
+ }