@theia/core 1.65.0-next.47 → 1.65.0-next.55

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 (61) hide show
  1. package/lib/browser/catalog.json +33 -5
  2. package/lib/browser/frontend-application-module.d.ts.map +1 -1
  3. package/lib/browser/frontend-application-module.js +5 -1
  4. package/lib/browser/frontend-application-module.js.map +1 -1
  5. package/lib/browser/secondary-window-handler.d.ts +21 -9
  6. package/lib/browser/secondary-window-handler.d.ts.map +1 -1
  7. package/lib/browser/secondary-window-handler.js +162 -21
  8. package/lib/browser/secondary-window-handler.js.map +1 -1
  9. package/lib/browser/shell/application-shell.d.ts +6 -1
  10. package/lib/browser/shell/application-shell.d.ts.map +1 -1
  11. package/lib/browser/shell/application-shell.js +20 -4
  12. package/lib/browser/shell/application-shell.js.map +1 -1
  13. package/lib/browser/shell/theia-dock-panel.d.ts +17 -1
  14. package/lib/browser/shell/theia-dock-panel.d.ts.map +1 -1
  15. package/lib/browser/shell/theia-dock-panel.js +76 -0
  16. package/lib/browser/shell/theia-dock-panel.js.map +1 -1
  17. package/lib/browser/widgets/extractable-widget.d.ts +3 -0
  18. package/lib/browser/widgets/extractable-widget.d.ts.map +1 -1
  19. package/lib/browser/widgets/extractable-widget.js.map +1 -1
  20. package/lib/browser/window/default-secondary-window-service.d.ts +9 -3
  21. package/lib/browser/window/default-secondary-window-service.d.ts.map +1 -1
  22. package/lib/browser/window/default-secondary-window-service.js +52 -11
  23. package/lib/browser/window/default-secondary-window-service.js.map +1 -1
  24. package/lib/browser/window/secondary-window-service.d.ts +15 -2
  25. package/lib/browser/window/secondary-window-service.d.ts.map +1 -1
  26. package/lib/browser/window/secondary-window-service.js +12 -1
  27. package/lib/browser/window/secondary-window-service.js.map +1 -1
  28. package/lib/electron-browser/preload.d.ts.map +1 -1
  29. package/lib/electron-browser/preload.js +3 -0
  30. package/lib/electron-browser/preload.js.map +1 -1
  31. package/lib/electron-browser/window/electron-secondary-window-service.d.ts.map +1 -1
  32. package/lib/electron-browser/window/electron-secondary-window-service.js +5 -4
  33. package/lib/electron-browser/window/electron-secondary-window-service.js.map +1 -1
  34. package/lib/electron-common/electron-api.d.ts +2 -0
  35. package/lib/electron-common/electron-api.d.ts.map +1 -1
  36. package/lib/electron-common/electron-api.js +2 -1
  37. package/lib/electron-common/electron-api.js.map +1 -1
  38. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  39. package/lib/electron-main/electron-api-main.js +9 -0
  40. package/lib/electron-main/electron-api-main.js.map +1 -1
  41. package/lib/node/logger-cli-contribution.d.ts +7 -1
  42. package/lib/node/logger-cli-contribution.d.ts.map +1 -1
  43. package/lib/node/logger-cli-contribution.js +22 -10
  44. package/lib/node/logger-cli-contribution.js.map +1 -1
  45. package/lib/node/logger-cli-contribution.spec.js +13 -2
  46. package/lib/node/logger-cli-contribution.spec.js.map +1 -1
  47. package/package.json +4 -4
  48. package/src/browser/frontend-application-module.ts +5 -1
  49. package/src/browser/secondary-window-handler.ts +189 -29
  50. package/src/browser/shell/application-shell.ts +29 -7
  51. package/src/browser/shell/theia-dock-panel.ts +93 -1
  52. package/src/browser/style/dockpanel.css +7 -0
  53. package/src/browser/widgets/extractable-widget.ts +3 -0
  54. package/src/browser/window/default-secondary-window-service.ts +53 -15
  55. package/src/browser/window/secondary-window-service.ts +23 -2
  56. package/src/electron-browser/preload.ts +4 -1
  57. package/src/electron-browser/window/electron-secondary-window-service.ts +7 -4
  58. package/src/electron-common/electron-api.ts +2 -0
  59. package/src/electron-main/electron-api-main.ts +11 -1
  60. package/src/node/logger-cli-contribution.spec.ts +17 -2
  61. package/src/node/logger-cli-contribution.ts +18 -3
@@ -47,6 +47,8 @@ export class TheiaDockPanel extends DockPanel {
47
47
  readonly widgetRemoved = new Signal<this, Widget>(this);
48
48
 
49
49
  protected readonly onDidChangeCurrentEmitter = new Emitter<Title<Widget> | undefined>();
50
+ protected disableDND: boolean | undefined = false;
51
+ protected tabWithDNDDisabledStyling?: HTMLElement = undefined;
50
52
 
51
53
  get onDidChangeCurrent(): Event<Title<Widget> | undefined> {
52
54
  return this.onDidChangeCurrentEmitter.event;
@@ -57,6 +59,7 @@ export class TheiaDockPanel extends DockPanel {
57
59
  protected readonly maximizeCallback?: (area: TheiaDockPanel) => void
58
60
  ) {
59
61
  super(options);
62
+ this.disableDND = TheiaDockPanel.isTheiaDockPanelIOptions(options) && options.disableDragAndDrop;
60
63
  this['_onCurrentChanged'] = (sender: TabBar<Widget>, args: TabBar.ICurrentChangedArgs<Widget>) => {
61
64
  this.markAsCurrent(args.currentTitle || undefined);
62
65
  super['_onCurrentChanged'](sender, args);
@@ -68,12 +71,72 @@ export class TheiaDockPanel extends DockPanel {
68
71
  if (tabBar instanceof ToolbarAwareTabBar) {
69
72
  tabBar.setDockPanel(this);
70
73
  }
74
+ if (this.disableDND) {
75
+ tabBar['tabDetachRequested'].disconnect(this['_onTabDetachRequested'], this);
76
+ tabBar['tabDetachRequested'].connect(this.onTabDetachRequestedWithDisabledDND, this);
77
+
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-null/no-null
79
+ let dragDataValue: any = null;
80
+ Object.defineProperty(tabBar, '_dragData', {
81
+ get: () => dragDataValue,
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ set: (value: any) => {
84
+ dragDataValue = value;
85
+ // eslint-disable-next-line no-null/no-null
86
+ if (value === null) {
87
+ this.onNullTabDragDataWithDisabledDND();
88
+ }
89
+ },
90
+ configurable: true
91
+ });
92
+ }
71
93
  return tabBar;
72
94
  };
73
95
  this['_onTabActivateRequested'] = (sender: TabBar<Widget>, args: TabBar.ITabActivateRequestedArgs<Widget>) => {
74
96
  this.markAsCurrent(args.title);
75
97
  super['_onTabActivateRequested'](sender, args);
76
98
  };
99
+ this['_onTabCloseRequested'] = (sender: TabBar<Widget>, args: TabBar.ITabCloseRequestedArgs<Widget>) => {
100
+ if (TheiaDockPanel.isTheiaDockPanelIOptions(options) && options.closeHandler !== undefined) {
101
+ if (options.closeHandler(sender, args)) {
102
+ return;
103
+ }
104
+ }
105
+ super['_onTabCloseRequested'](sender, args);
106
+ };
107
+ }
108
+
109
+ protected onTabDetachRequestedWithDisabledDND(sender: TabBar<Widget>, args: TabBar.ITabDetachRequestedArgs<Widget>): void {
110
+ // don't process the detach request at all. We still want to support other drag starts, e.g. tab reorder
111
+ // provide visual feedback that DnD is disabled by adding not-allowed class
112
+ const tab = sender.contentNode.children[args.index] as HTMLElement;
113
+ if (tab) {
114
+ tab.classList.add('theia-drag-not-allowed');
115
+ this.tabWithDNDDisabledStyling = tab;
116
+ }
117
+ }
118
+
119
+ protected onNullTabDragDataWithDisabledDND(): void {
120
+ if (this.tabWithDNDDisabledStyling) {
121
+ this.tabWithDNDDisabledStyling.classList.remove('theia-drag-not-allowed');
122
+ this.tabWithDNDDisabledStyling = undefined;
123
+ }
124
+ }
125
+
126
+ override handleEvent(event: globalThis.Event): void {
127
+ if (this.disableDND) {
128
+ switch (event.type) {
129
+ case 'lm-dragenter':
130
+ case 'lm-dragleave':
131
+ case 'lm-dragover':
132
+ case 'lm-drop':
133
+ /* no-op */
134
+ break;
135
+ default:
136
+ super.handleEvent(event);
137
+ }
138
+ }
139
+ super.handleEvent(event);
77
140
  }
78
141
 
79
142
  toggleMaximized(): void {
@@ -182,7 +245,36 @@ export class TheiaDockPanel extends DockPanel {
182
245
  export namespace TheiaDockPanel {
183
246
  export const Factory = Symbol('TheiaDockPanel#Factory');
184
247
  export interface Factory {
185
- (options?: DockPanel.IOptions, maximizeCallback?: (area: TheiaDockPanel) => void): TheiaDockPanel;
248
+ (options?: DockPanel.IOptions | TheiaDockPanel.IOptions, maximizeCallback?: (area: TheiaDockPanel) => void): TheiaDockPanel;
249
+ }
250
+
251
+ export interface IOptions extends DockPanel.IOptions {
252
+ /** whether drag and drop for tabs should be disabled */
253
+ disableDragAndDrop?: boolean;
254
+
255
+ /**
256
+ * @param sender the tab bar
257
+ * @param args the widget (title)
258
+ * @returns true if the request was handled by this handler, false if the tabbar should handle the request
259
+ */
260
+ closeHandler?: (sender: TabBar<Widget>, args: TabBar.ITabCloseRequestedArgs<Widget>) => boolean;
261
+ }
262
+
263
+ export function isTheiaDockPanelIOptions(options: DockPanel.IOptions | undefined): options is IOptions {
264
+ if (options === undefined) {
265
+ return false;
266
+ }
267
+ if ('disableDragAndDrop' in options) {
268
+ if (options.disableDragAndDrop !== undefined && typeof options.disableDragAndDrop !== 'boolean') {
269
+ return false;
270
+ }
271
+ }
272
+ if ('closeHandler' in options) {
273
+ if (options.closeHandler !== undefined && typeof options.closeHandler !== 'function') {
274
+ return false;
275
+ }
276
+ }
277
+ return true;
186
278
  }
187
279
 
188
280
  export interface AddOptions extends DockPanel.IAddOptions {
@@ -84,3 +84,10 @@
84
84
  .lm-DockPanel-overlay.lm-mod-root-bottom {
85
85
  background: var(--theia-panel-dropBackground);
86
86
  }
87
+
88
+ .lm-TabBar-tab.theia-drag-not-allowed {
89
+ cursor: not-allowed !important;
90
+ background-color: var(--theia-errorBackground) !important;
91
+ opacity: 0.8;
92
+ transition: background-color 0.3s ease;
93
+ }
@@ -14,6 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
+ import { ApplicationShell } from '../shell';
17
18
  import { Widget } from './widget';
18
19
 
19
20
  /**
@@ -24,6 +25,8 @@ export interface ExtractableWidget extends Widget {
24
25
  isExtractable: boolean;
25
26
  /** The secondary window that the window was extracted to or `undefined` if it is not yet extracted. */
26
27
  secondaryWindow: Window | undefined;
28
+ /** Stores the area which contained the widget before being extracted. This is undefined if the widget wasn't extracted or if the area could not be determined */
29
+ previousArea?: ApplicationShell.Area;
27
30
  }
28
31
 
29
32
  export namespace ExtractableWidget {
@@ -14,13 +14,14 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
  import { inject, injectable, postConstruct } from 'inversify';
17
- import { SecondaryWindowService } from './secondary-window-service';
17
+ import { SecondaryWindow, SecondaryWindowService } from './secondary-window-service';
18
18
  import { WindowService } from './window-service';
19
- import { ExtractableWidget } from '../widgets';
19
+ import { ExtractableWidget, Widget } from '../widgets';
20
20
  import { ApplicationShell } from '../shell';
21
21
  import { Saveable } from '../saveable';
22
22
  import { Emitter, environment, Event, PreferenceService } from '../../common';
23
23
  import { SaveableService } from '../saveable-service';
24
+ import { getAllWidgetsFromSecondaryWindow, getDefaultRestoreArea } from '../secondary-window-handler';
24
25
 
25
26
  @injectable()
26
27
  export class DefaultSecondaryWindowService implements SecondaryWindowService {
@@ -28,6 +29,8 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
28
29
  readonly onWindowOpened: Event<Window> = this.onWindowOpenedEmitter.event;
29
30
  protected readonly onWindowClosedEmitter = new Emitter<Window>;
30
31
  readonly onWindowClosed: Event<Window> = this.onWindowClosedEmitter.event;
32
+ protected readonly beforeWidgetRestoreEmitter = new Emitter<[Widget, Window]>;
33
+ readonly beforeWidgetRestore: Event<[Widget, Window]> = this.beforeWidgetRestoreEmitter.event;
31
34
  // secondary-window.html is part of Theia's generated code. It is generated by dev-packages/application-manager/src/generator/frontend-generator.ts
32
35
  protected static SECONDARY_WINDOW_URL = 'secondary-window.html';
33
36
 
@@ -92,7 +95,7 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
92
95
  });
93
96
  }
94
97
 
95
- createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined {
98
+ createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | SecondaryWindow | undefined {
96
99
  const [height, width, left, top] = this.findSecondaryWindowCoordinates(widget);
97
100
  let options = `popup=1,width=${width},height=${height},left=${left},top=${top}`;
98
101
  if (this.preferenceService.get('window.secondaryWindowAlwaysOnTop')) {
@@ -104,21 +107,19 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
104
107
  this.onWindowOpenedEmitter.fire(newWindow);
105
108
  newWindow.addEventListener('DOMContentLoaded', () => {
106
109
  newWindow.addEventListener('beforeunload', evt => {
107
- const saveable = Saveable.get(widget);
108
- const wouldLoseState = !!saveable && saveable.dirty && this.saveResourceService.autoSave === 'off';
109
- if (wouldLoseState) {
110
- evt.returnValue = '';
111
- evt.preventDefault();
112
- return 'non-empty';
110
+ const widgets = getAllWidgetsFromSecondaryWindow(newWindow) ?? [widget];
111
+ for (const w of widgets) {
112
+ const saveable = Saveable.get(w);
113
+ const wouldLoseState = !!saveable && saveable.dirty && this.saveResourceService.autoSave === 'off';
114
+ if (wouldLoseState) {
115
+ evt.returnValue = '';
116
+ evt.preventDefault();
117
+ return 'non-empty';
118
+ }
113
119
  }
114
120
  }, { capture: true });
115
121
 
116
122
  newWindow.addEventListener('unload', () => {
117
- const saveable = Saveable.get(widget);
118
- shell.closeWidget(widget.id, {
119
- save: !!saveable && saveable.dirty && this.saveResourceService.autoSave !== 'off'
120
- });
121
-
122
123
  const extIndex = this.secondaryWindows.indexOf(newWindow);
123
124
  if (extIndex > -1) {
124
125
  this.onWindowClosedEmitter.fire(newWindow);
@@ -128,12 +129,13 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
128
129
  this.windowCreated(newWindow, widget, shell);
129
130
  });
130
131
  }
132
+ (newWindow as SecondaryWindow).rootWidget = undefined;
131
133
  return newWindow;
132
134
  }
133
135
 
134
136
  protected windowCreated(newWindow: Window, widget: ExtractableWidget, shell: ApplicationShell): void {
135
137
  newWindow.addEventListener('unload', () => {
136
- shell.closeWidget(widget.id);
138
+ this.restoreWidgets(newWindow, widget, shell);
137
139
  });
138
140
  }
139
141
 
@@ -199,4 +201,40 @@ export class DefaultSecondaryWindowService implements SecondaryWindowService {
199
201
  protected nextWindowId(): string {
200
202
  return `${this.prefix}-secondaryWindow-${this.nextId++}`;
201
203
  }
204
+
205
+ /**
206
+ * Restore the widgets back to the main window. SecondaryWindowHandler needs to get informated about this.
207
+ */
208
+ protected async restoreWidgets(newWindow: Window, extractableWidget: ExtractableWidget, shell: ApplicationShell): Promise<boolean> {
209
+ const widgets = getAllWidgetsFromSecondaryWindow(newWindow) ?? new Set([extractableWidget]);
210
+ const defaultRestoreArea = getDefaultRestoreArea(newWindow);
211
+
212
+ let allMovedOrDisposed = true;
213
+ for (const widget of widgets) {
214
+ if (widget.isDisposed) {
215
+ continue;
216
+ }
217
+ try {
218
+ const preferredRestoreArea = ExtractableWidget.is(widget) ? widget.previousArea : defaultRestoreArea;
219
+ const area = (preferredRestoreArea === undefined || preferredRestoreArea === 'top' || preferredRestoreArea === 'secondaryWindow') ? 'main' : preferredRestoreArea;
220
+ // fire removed event before adding it to shell
221
+ this.beforeWidgetRestoreEmitter.fire([widget, newWindow]);
222
+ // reset ExtractableWidget properties before moving back so that handler evaluation is correct immediately
223
+ if (ExtractableWidget.is(widget)) {
224
+ widget.secondaryWindow = undefined;
225
+ widget.previousArea = undefined;
226
+ }
227
+ await shell.addWidget(widget, { area });
228
+ await shell.activateWidget(widget.id);
229
+ } catch (e) {
230
+ // we can't move back, close instead
231
+ // otherwise the window will just stay open with no way to close it
232
+ await shell.closeWidget(widget.id);
233
+ if (!widget.isDisposed) {
234
+ allMovedOrDisposed = false;
235
+ }
236
+ }
237
+ }
238
+ return allMovedOrDisposed;
239
+ }
202
240
  }
@@ -16,7 +16,27 @@
16
16
 
17
17
  import { Event } from '../../common';
18
18
  import { ApplicationShell } from '../shell';
19
- import { ExtractableWidget } from '../widgets';
19
+ import { TheiaDockPanel } from '../shell/theia-dock-panel';
20
+ import { ExtractableWidget, TabBar, Widget } from '../widgets';
21
+
22
+ export abstract class SecondaryWindowRootWidget extends Widget {
23
+ secondaryWindow: Window | SecondaryWindow;
24
+ defaultRestoreArea?: ApplicationShell.Area;
25
+ abstract widgets: ReadonlyArray<Widget>;
26
+ abstract addWidget(widget: Widget, disposeCallback: () => void, options?: TheiaDockPanel.AddOptions): void;
27
+ getTabBar?(widget: Widget): TabBar<Widget> | undefined;
28
+ }
29
+
30
+ export interface SecondaryWindow extends Window {
31
+ rootWidget: SecondaryWindowRootWidget | undefined;
32
+ }
33
+
34
+ export function isSecondaryWindow(window: unknown): window is SecondaryWindow {
35
+ if (!window) {
36
+ return false;
37
+ }
38
+ return typeof window === 'object' && 'rootWidget' in window;
39
+ }
20
40
 
21
41
  export const SecondaryWindowService = Symbol('SecondaryWindowService');
22
42
 
@@ -33,9 +53,10 @@ export interface SecondaryWindowService {
33
53
  * @param onClose optional callback that is invoked when the secondary window is closed
34
54
  * @returns the created window or `undefined` if it could not be created
35
55
  */
36
- createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): Window | undefined;
56
+ createSecondaryWindow(widget: ExtractableWidget, shell: ApplicationShell): SecondaryWindow | Window | undefined;
37
57
  readonly onWindowOpened: Event<Window>;
38
58
  readonly onWindowClosed: Event<Window>;
59
+ readonly beforeWidgetRestore: Event<[Widget, Window]>;
39
60
 
40
61
  /** Handles focussing the given secondary window in the browser and on Electron. */
41
62
  focus(win: Window): void;
@@ -27,7 +27,7 @@ import {
27
27
  CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD,
28
28
  CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE, CHANNEL_SET_BACKGROUND_COLOR,
29
29
  CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP,
30
- CHANNEL_OPEN_URL, CHANNEL_SET_THEME
30
+ CHANNEL_OPEN_URL, CHANNEL_SET_THEME, CHANNEL_OPEN_DEVTOOLS_FOR_WINDOW
31
31
  } from '../electron-common/electron-api';
32
32
 
33
33
  // eslint-disable-next-line import/no-extraneous-dependencies
@@ -200,6 +200,9 @@ const api: TheiaCoreAPI = {
200
200
  toggleDevTools: function (): void {
201
201
  ipcRenderer.send(CHANNEL_TOGGLE_DEVTOOLS);
202
202
  },
203
+ openDevToolsForWindow: function (windowName: string): void {
204
+ ipcRenderer.send(CHANNEL_OPEN_DEVTOOLS_FOR_WINDOW, windowName);
205
+ },
203
206
  getZoomLevel: function (): Promise<number> {
204
207
  return ipcRenderer.invoke(CHANNEL_GET_ZOOM_LEVEL);
205
208
  },
@@ -48,10 +48,13 @@ export class ElectronSecondaryWindowService extends DefaultSecondaryWindowServic
48
48
 
49
49
  protected override windowCreated(newWindow: Window, widget: ExtractableWidget, shell: ApplicationShell): void {
50
50
  window.electronTheiaCore.setMenuBarVisible(false, newWindow.name);
51
- window.electronTheiaCore.setSecondaryWindowCloseRequestHandler(newWindow.name, () => this.canClose(widget, shell));
51
+ window.electronTheiaCore.setSecondaryWindowCloseRequestHandler(newWindow.name, () => this.canClose(widget, shell, newWindow));
52
+
53
+ // Below code may be used to debug contents of secondary window
54
+ // window.electronTheiaCore.openDevToolsForWindow(newWindow.name);
52
55
  }
53
- private async canClose(widget: ExtractableWidget, shell: ApplicationShell): Promise<boolean> {
54
- await shell.closeWidget(widget.id);
55
- return widget.isDisposed;
56
+ private async canClose(extractableWidget: ExtractableWidget, shell: ApplicationShell, newWindow: Window): Promise<boolean> {
57
+ return this.restoreWidgets(newWindow, extractableWidget, shell);
56
58
  }
59
+
57
60
  }
@@ -83,6 +83,7 @@ export interface TheiaCoreAPI {
83
83
  setSecondaryWindowCloseRequestHandler(windowName: string, handler: () => Promise<boolean>): void;
84
84
 
85
85
  toggleDevTools(): void;
86
+ openDevToolsForWindow(windowName: string): void;
86
87
  getZoomLevel(): Promise<number>;
87
88
  setZoomLevel(desired: number): void;
88
89
 
@@ -141,6 +142,7 @@ export const CHANNEL_OPEN_URL = 'OpenUrl';
141
142
  export const CHANNEL_UNMAXIMIZE = 'UnMaximize';
142
143
  export const CHANNEL_ON_WINDOW_EVENT = 'OnWindowEvent';
143
144
  export const CHANNEL_TOGGLE_DEVTOOLS = 'ToggleDevtools';
145
+ export const CHANNEL_OPEN_DEVTOOLS_FOR_WINDOW = 'OpenDevtoolsForWindow';
144
146
  export const CHANNEL_GET_ZOOM_LEVEL = 'GetZoomLevel';
145
147
  export const CHANNEL_SET_ZOOM_LEVEL = 'SetZoomLevel';
146
148
  export const CHANNEL_IS_FULL_SCREENABLE = 'IsFullScreenable';
@@ -56,7 +56,8 @@ import {
56
56
  CHANNEL_ABOUT_TO_CLOSE,
57
57
  CHANNEL_OPEN_WITH_SYSTEM_APP,
58
58
  CHANNEL_OPEN_URL,
59
- CHANNEL_SET_THEME
59
+ CHANNEL_SET_THEME,
60
+ CHANNEL_OPEN_DEVTOOLS_FOR_WINDOW
60
61
  } from '../electron-common/electron-api';
61
62
  import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application';
62
63
  import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common';
@@ -207,6 +208,15 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {
207
208
  event.sender.toggleDevTools();
208
209
  });
209
210
 
211
+ ipcMain.on(CHANNEL_OPEN_DEVTOOLS_FOR_WINDOW, (event, windowName: string) => {
212
+ const electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName);
213
+ if (electronWindow) {
214
+ electronWindow.webContents.openDevTools();
215
+ } else {
216
+ console.warn(`There is no known window '${windowName}'. Thus, the devtools could not be opened.`);
217
+ }
218
+ });
219
+
210
220
  ipcMain.on(CHANNEL_SET_ZOOM_LEVEL, (event, zoomLevel: number) => {
211
221
  event.sender.setZoomLevel(zoomLevel);
212
222
  });
@@ -22,17 +22,30 @@ import { ContainerModule, Container } from 'inversify';
22
22
  import { LogLevel } from '../common/logger';
23
23
  import { LogLevelCliContribution } from './logger-cli-contribution';
24
24
  import * as sinon from 'sinon';
25
+ import { Disposable, DisposableCollection } from '../common';
25
26
 
26
27
  // Allow creating temporary files, but remove them when we are done.
27
28
  const track = temp.track();
28
29
 
29
30
  let cli: LogLevelCliContribution;
30
31
  let consoleErrorSpy: sinon.SinonSpy;
32
+ let container: Container;
33
+ let toDisposeAfter: DisposableCollection;
31
34
 
32
35
  describe('log-level-cli-contribution', () => {
33
36
 
37
+ before(() => {
38
+ toDisposeAfter = new DisposableCollection(
39
+ Disposable.create(() => track.cleanupSync())
40
+ );
41
+ });
42
+
43
+ after(() => {
44
+ toDisposeAfter.dispose();
45
+ });
46
+
34
47
  beforeEach(() => {
35
- const container = new Container();
48
+ container = new Container();
36
49
 
37
50
  const module = new ContainerModule(bind => {
38
51
  bind(LogLevelCliContribution).toSelf().inSingletonScope();
@@ -47,8 +60,10 @@ describe('log-level-cli-contribution', () => {
47
60
  consoleErrorSpy = sinon.spy(console, 'error');
48
61
  });
49
62
 
50
- afterEach(() => {
63
+ afterEach(async () => {
51
64
  consoleErrorSpy.restore();
65
+ await cli.dispose();
66
+ container.unload();
52
67
  });
53
68
 
54
69
  it('should use --log-level flag', async () => {
@@ -19,9 +19,10 @@ import { injectable } from 'inversify';
19
19
  import { LogLevel } from '../common/logger';
20
20
  import { CliContribution } from './cli';
21
21
  import * as fs from 'fs-extra';
22
- import { subscribe } from '@parcel/watcher';
22
+ import { AsyncSubscription, subscribe } from '@parcel/watcher';
23
23
  import { Event, Emitter } from '../common/event';
24
24
  import * as path from 'path';
25
+ import { Disposable, DisposableCollection } from '../common';
25
26
 
26
27
  /** Maps logger names to log levels. */
27
28
  export interface LogLevels {
@@ -34,9 +35,11 @@ export interface LogLevels {
34
35
  * what the log level per logger should be.
35
36
  */
36
37
  @injectable()
37
- export class LogLevelCliContribution implements CliContribution {
38
+ export class LogLevelCliContribution implements CliContribution, Disposable {
38
39
 
39
40
  protected _logLevels: LogLevels = {};
41
+ protected asyncSubscriptions: AsyncSubscription[] = [];
42
+ protected toDispose = new DisposableCollection();
40
43
 
41
44
  /**
42
45
  * Log level to use for loggers not specified in `logLevels`.
@@ -59,6 +62,10 @@ export class LogLevelCliContribution implements CliContribution {
59
62
  return this._logFile;
60
63
  }
61
64
 
65
+ constructor() {
66
+ this.toDispose.push(this.logConfigChangedEvent);
67
+ }
68
+
62
69
  configure(conf: yargs.Argv): void {
63
70
  conf.option('log-level', {
64
71
  description: 'Sets the default log level',
@@ -129,7 +136,7 @@ export class LogLevelCliContribution implements CliContribution {
129
136
 
130
137
  protected async watchLogConfigFile(filename: string): Promise<void> {
131
138
  const dir = path.dirname(filename);
132
- await subscribe(dir, async (err, events) => {
139
+ const subscription = await subscribe(dir, async (err, events) => {
133
140
  if (err) {
134
141
  console.log(`Error during log file watching ${filename}: ${err}`);
135
142
  return;
@@ -150,6 +157,14 @@ export class LogLevelCliContribution implements CliContribution {
150
157
  console.error(`Error reading log config file ${filename}: ${e}`);
151
158
  }
152
159
  });
160
+ this.asyncSubscriptions.push(subscription);
161
+ }
162
+
163
+ async dispose(): Promise<void> {
164
+ for (const sub of this.asyncSubscriptions) {
165
+ sub.unsubscribe();
166
+ }
167
+ this.toDispose.dispose();
153
168
  }
154
169
 
155
170
  protected async slurpLogConfigFile(filename: string): Promise<void> {