@theia/core 1.48.2 → 1.49.0

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 (90) hide show
  1. package/README.md +6 -6
  2. package/i18n/nls.cs.json +5 -2
  3. package/i18n/nls.de.json +5 -2
  4. package/i18n/nls.es.json +5 -2
  5. package/i18n/nls.fr.json +5 -2
  6. package/i18n/nls.hu.json +5 -2
  7. package/i18n/nls.it.json +5 -2
  8. package/i18n/nls.ja.json +5 -2
  9. package/i18n/nls.json +5 -2
  10. package/i18n/nls.pl.json +5 -2
  11. package/i18n/nls.pt-br.json +5 -2
  12. package/i18n/nls.pt-pt.json +5 -2
  13. package/i18n/nls.ru.json +5 -2
  14. package/i18n/nls.zh-cn.json +5 -2
  15. package/lib/browser/command-open-handler.js +1 -1
  16. package/lib/browser/command-open-handler.js.map +1 -1
  17. package/lib/browser/frontend-application-module.d.ts.map +1 -1
  18. package/lib/browser/frontend-application-module.js +2 -0
  19. package/lib/browser/frontend-application-module.js.map +1 -1
  20. package/lib/browser/index.d.ts +1 -0
  21. package/lib/browser/index.d.ts.map +1 -1
  22. package/lib/browser/index.js +1 -0
  23. package/lib/browser/index.js.map +1 -1
  24. package/lib/browser/keybinding.js +1 -1
  25. package/lib/browser/keybinding.js.map +1 -1
  26. package/lib/browser/open-with-service.d.ts +44 -0
  27. package/lib/browser/open-with-service.d.ts.map +1 -0
  28. package/lib/browser/open-with-service.js +66 -0
  29. package/lib/browser/open-with-service.js.map +1 -0
  30. package/lib/browser/shell/view-contribution.d.ts.map +1 -1
  31. package/lib/browser/shell/view-contribution.js +2 -1
  32. package/lib/browser/shell/view-contribution.js.map +1 -1
  33. package/lib/common/command.d.ts +4 -0
  34. package/lib/common/command.d.ts.map +1 -1
  35. package/lib/common/command.js.map +1 -1
  36. package/lib/common/event.d.ts +5 -0
  37. package/lib/common/event.d.ts.map +1 -1
  38. package/lib/common/event.js +14 -1
  39. package/lib/common/event.js.map +1 -1
  40. package/lib/electron-browser/preload.d.ts.map +1 -1
  41. package/lib/electron-browser/preload.js +3 -0
  42. package/lib/electron-browser/preload.js.map +1 -1
  43. package/lib/electron-browser/window/electron-window-service.js +1 -1
  44. package/lib/electron-browser/window/electron-window-service.js.map +1 -1
  45. package/lib/electron-common/electron-api.d.ts +3 -1
  46. package/lib/electron-common/electron-api.d.ts.map +1 -1
  47. package/lib/electron-common/electron-api.js +2 -1
  48. package/lib/electron-common/electron-api.js.map +1 -1
  49. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  50. package/lib/electron-main/electron-api-main.js +6 -1
  51. package/lib/electron-main/electron-api-main.js.map +1 -1
  52. package/lib/electron-main/electron-main-application.d.ts +17 -1
  53. package/lib/electron-main/electron-main-application.d.ts.map +1 -1
  54. package/lib/electron-main/electron-main-application.js +83 -8
  55. package/lib/electron-main/electron-main-application.js.map +1 -1
  56. package/lib/electron-main/theia-electron-window.d.ts +6 -0
  57. package/lib/electron-main/theia-electron-window.d.ts.map +1 -1
  58. package/lib/electron-main/theia-electron-window.js +3 -1
  59. package/lib/electron-main/theia-electron-window.js.map +1 -1
  60. package/lib/node/application-server.js +3 -3
  61. package/lib/node/application-server.js.map +1 -1
  62. package/lib/node/backend-application-module.d.ts.map +1 -1
  63. package/lib/node/backend-application-module.js +1 -4
  64. package/lib/node/backend-application-module.js.map +1 -1
  65. package/lib/node/backend-application.d.ts +6 -3
  66. package/lib/node/backend-application.d.ts.map +1 -1
  67. package/lib/node/backend-application.js +10 -18
  68. package/lib/node/backend-application.js.map +1 -1
  69. package/lib/node/env-variables/env-variables-server.d.ts.map +1 -1
  70. package/lib/node/env-variables/env-variables-server.js +2 -4
  71. package/lib/node/env-variables/env-variables-server.js.map +1 -1
  72. package/package.json +6 -6
  73. package/src/browser/command-open-handler.ts +1 -1
  74. package/src/browser/frontend-application-module.ts +3 -0
  75. package/src/browser/index.ts +1 -0
  76. package/src/browser/keybinding.ts +1 -1
  77. package/src/browser/open-with-service.ts +93 -0
  78. package/src/browser/shell/view-contribution.ts +3 -2
  79. package/src/common/command.ts +4 -0
  80. package/src/common/event.ts +18 -0
  81. package/src/electron-browser/preload.ts +5 -2
  82. package/src/electron-browser/window/electron-window-service.ts +1 -1
  83. package/src/electron-common/electron-api.ts +3 -1
  84. package/src/electron-main/electron-api-main.ts +9 -2
  85. package/src/electron-main/electron-main-application.ts +101 -11
  86. package/src/electron-main/theia-electron-window.ts +9 -1
  87. package/src/node/application-server.ts +3 -3
  88. package/src/node/backend-application-module.ts +2 -5
  89. package/src/node/backend-application.ts +10 -18
  90. package/src/node/env-variables/env-variables-server.ts +2 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theia/core",
3
- "version": "1.48.2",
3
+ "version": "1.49.0",
4
4
  "description": "Theia is a cloud & desktop IDE framework implemented in TypeScript.",
5
5
  "main": "lib/common/index.js",
6
6
  "typings": "lib/common/index.d.ts",
@@ -16,8 +16,8 @@
16
16
  "@phosphor/signaling": "1",
17
17
  "@phosphor/virtualdom": "1",
18
18
  "@phosphor/widgets": "1",
19
- "@theia/application-package": "1.48.2",
20
- "@theia/request": "1.48.2",
19
+ "@theia/application-package": "1.49.0",
20
+ "@theia/request": "1.49.0",
21
21
  "@types/body-parser": "^1.16.4",
22
22
  "@types/cookie": "^0.3.3",
23
23
  "@types/dompurify": "^2.2.2",
@@ -206,12 +206,12 @@
206
206
  "watch": "theiaext watch"
207
207
  },
208
208
  "devDependencies": {
209
- "@theia/ext-scripts": "1.48.2",
210
- "@theia/re-exports": "1.48.2",
209
+ "@theia/ext-scripts": "1.49.0",
210
+ "@theia/re-exports": "1.49.0",
211
211
  "minimist": "^1.2.0"
212
212
  },
213
213
  "nyc": {
214
214
  "extends": "../../configs/nyc.json"
215
215
  },
216
- "gitHead": "0e9583d043dde303de7a0f86f68439659f8827b0"
216
+ "gitHead": "d413dadd87a70fce1c80df7eb22d9d2529cc129f"
217
217
  }
@@ -41,7 +41,7 @@ export class CommandOpenHandler implements OpenHandler {
41
41
  try {
42
42
  args = JSON.parse(uri.query);
43
43
  } catch {
44
- // ignore error
44
+ args = uri.query;
45
45
  }
46
46
  }
47
47
  if (!Array.isArray(args)) {
@@ -140,6 +140,7 @@ import { HoverService } from './hover-service';
140
140
  import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget';
141
141
  import { LanguageIconLabelProvider } from './language-icon-provider';
142
142
  import { bindTreePreferences } from './tree';
143
+ import { OpenWithService } from './open-with-service';
143
144
 
144
145
  export { bindResourceProvider, bindMessageService, bindPreferenceService };
145
146
 
@@ -224,6 +225,8 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is
224
225
  bind(CommandOpenHandler).toSelf().inSingletonScope();
225
226
  bind(OpenHandler).toService(CommandOpenHandler);
226
227
 
228
+ bind(OpenWithService).toSelf().inSingletonScope();
229
+
227
230
  bind(TooltipServiceImpl).toSelf().inSingletonScope();
228
231
  bind(TooltipService).toService(TooltipServiceImpl);
229
232
 
@@ -19,6 +19,7 @@ export * from './frontend-application';
19
19
  export * from './frontend-application-contribution';
20
20
  export * from './keyboard';
21
21
  export * from './opener-service';
22
+ export * from './open-with-service';
22
23
  export * from './browser';
23
24
  export * from './context-menu-renderer';
24
25
  export * from './widgets';
@@ -497,7 +497,7 @@ export class KeybindingRegistry {
497
497
 
498
498
  isEnabledInScope(binding: common.Keybinding, target: HTMLElement | undefined): boolean {
499
499
  const context = binding.context && this.contexts[binding.context];
500
- if (binding.command && !this.commandRegistry.isEnabled(binding.command, binding.args)) {
500
+ if (binding.command && (!this.isPseudoCommand(binding.command) && !this.commandRegistry.isEnabled(binding.command, binding.args))) {
501
501
  return false;
502
502
  }
503
503
  if (context && !context.isEnabled(binding)) {
@@ -0,0 +1,93 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 TypeFox 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 } from 'inversify';
18
+ import { Disposable } from '../common/disposable';
19
+ import { nls } from '../common/nls';
20
+ import { MaybePromise } from '../common/types';
21
+ import { URI } from '../common/uri';
22
+ import { QuickInputService } from './quick-input';
23
+
24
+ export interface OpenWithHandler {
25
+ /**
26
+ * A unique id of this handler.
27
+ */
28
+ readonly id: string;
29
+ /**
30
+ * A human-readable name of this handler.
31
+ */
32
+ readonly label?: string;
33
+ /**
34
+ * A human-readable provider name of this handler.
35
+ */
36
+ readonly providerName?: string;
37
+ /**
38
+ * A css icon class of this handler.
39
+ */
40
+ readonly iconClass?: string;
41
+ /**
42
+ * Test whether this handler can open the given URI for given options.
43
+ * Return a nonzero number if this handler can open; otherwise it cannot.
44
+ * Never reject.
45
+ *
46
+ * A returned value indicating a priority of this handler.
47
+ */
48
+ canHandle(uri: URI): number;
49
+ /**
50
+ * Open a widget for the given URI and options.
51
+ * Resolve to an opened widget or undefined, e.g. if a page is opened.
52
+ * Never reject if `canHandle` return a positive number; otherwise should reject.
53
+ */
54
+ open(uri: URI): MaybePromise<object | undefined>;
55
+ }
56
+
57
+ @injectable()
58
+ export class OpenWithService {
59
+
60
+ @inject(QuickInputService)
61
+ protected readonly quickInputService: QuickInputService;
62
+
63
+ protected readonly handlers: OpenWithHandler[] = [];
64
+
65
+ registerHandler(handler: OpenWithHandler): Disposable {
66
+ this.handlers.push(handler);
67
+ return Disposable.create(() => {
68
+ const index = this.handlers.indexOf(handler);
69
+ if (index !== -1) {
70
+ this.handlers.splice(index, 1);
71
+ }
72
+ });
73
+ }
74
+
75
+ async openWith(uri: URI): Promise<object | undefined> {
76
+ const handlers = this.getHandlers(uri);
77
+ const result = await this.quickInputService.pick(handlers.map(handler => ({
78
+ handler: handler,
79
+ label: handler.label ?? handler.id,
80
+ detail: handler.providerName
81
+ })), {
82
+ placeHolder: nls.localizeByDefault("Select editor for '{0}'", uri.path.base)
83
+ });
84
+ if (result) {
85
+ return result.handler.open(uri);
86
+ }
87
+ }
88
+
89
+ getHandlers(uri: URI): OpenWithHandler[] {
90
+ const map = new Map<OpenWithHandler, number>(this.handlers.map(handler => [handler, handler.canHandle(uri)]));
91
+ return this.handlers.filter(handler => map.get(handler)! > 0).sort((a, b) => map.get(b)! - map.get(a)!);
92
+ }
93
+ }
@@ -18,7 +18,7 @@ import { injectable, inject, interfaces, optional } from 'inversify';
18
18
  import { Widget } from '@phosphor/widgets';
19
19
  import {
20
20
  MenuModelRegistry, Command, CommandContribution,
21
- MenuContribution, CommandRegistry
21
+ MenuContribution, CommandRegistry, nls
22
22
  } from '../../common';
23
23
  import { KeybindingContribution, KeybindingRegistry } from '../keybinding';
24
24
  import { WidgetManager } from '../widget-manager';
@@ -69,7 +69,8 @@ export abstract class AbstractViewContribution<T extends Widget> implements Comm
69
69
  if (options.toggleCommandId) {
70
70
  this.toggleCommand = {
71
71
  id: options.toggleCommandId,
72
- label: 'Toggle ' + this.viewLabel + ' View'
72
+ category: nls.localizeByDefault('View'),
73
+ label: nls.localizeByDefault('Toggle {0}', this.viewLabel)
73
74
  };
74
75
  }
75
76
  }
@@ -41,6 +41,10 @@ export interface Command {
41
41
  * An icon class of this command.
42
42
  */
43
43
  iconClass?: string;
44
+ /**
45
+ * A short title used for display in menus.
46
+ */
47
+ shortTitle?: string;
44
48
  /**
45
49
  * A category of this command.
46
50
  */
@@ -467,3 +467,21 @@ export class AsyncEmitter<T extends WaitUntilEvent> extends Emitter<T> {
467
467
  }
468
468
 
469
469
  }
470
+
471
+ export class QueueableEmitter<T> extends Emitter<T[]> {
472
+
473
+ currentQueue?: T[];
474
+
475
+ queue(...arg: T[]): void {
476
+ if (!this.currentQueue) {
477
+ this.currentQueue = [];
478
+ }
479
+ this.currentQueue.push(...arg);
480
+ }
481
+
482
+ override fire(): void {
483
+ super.fire(this.currentQueue || []);
484
+ this.currentQueue = undefined;
485
+ }
486
+
487
+ }
@@ -26,7 +26,7 @@ import {
26
26
  CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART,
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
- CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE
29
+ CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP
30
30
  } from '../electron-common/electron-api';
31
31
 
32
32
  // eslint-disable-next-line import/no-extraneous-dependencies
@@ -75,10 +75,13 @@ const api: TheiaCoreAPI = {
75
75
  ipcRenderer.send(CHANNEL_SET_MENU, mainMenuId, convertMenu(menu, handlers));
76
76
  },
77
77
  getSecurityToken: () => ipcRenderer.sendSync(CHANNEL_GET_SECURITY_TOKEN),
78
- focusWindow: (name: string) => ipcRenderer.send(CHANNEL_FOCUS_WINDOW, name),
78
+ focusWindow: (name?: string) => ipcRenderer.send(CHANNEL_FOCUS_WINDOW, name),
79
79
  showItemInFolder: fsPath => {
80
80
  ipcRenderer.send(CHANNEL_SHOW_ITEM_IN_FOLDER, fsPath);
81
81
  },
82
+ openWithSystemApp: fsPath => {
83
+ ipcRenderer.send(CHANNEL_OPEN_WITH_SYSTEM_APP, fsPath);
84
+ },
82
85
  attachSecurityToken: (endpoint: string) => ipcRenderer.invoke(CHANNEL_ATTACH_SECURITY_TOKEN, endpoint),
83
86
 
84
87
  popup: async function (menu: MenuDto[], x: number, y: number, onClosed: () => void, windowName?: string): Promise<number> {
@@ -58,7 +58,7 @@ export class ElectronWindowService extends DefaultWindowService {
58
58
  }
59
59
 
60
60
  override focus(): void {
61
- window.electronTheiaCore.focusWindow(window.name);
61
+ window.electronTheiaCore.focusWindow();
62
62
  }
63
63
  @postConstruct()
64
64
  protected init(): void {
@@ -53,9 +53,10 @@ export interface TheiaCoreAPI {
53
53
  popup(menu: MenuDto[], x: number, y: number, onClosed: () => void, windowName?: string): Promise<number>;
54
54
  closePopup(handle: number): void;
55
55
 
56
- focusWindow(name: string): void;
56
+ focusWindow(name?: string): void;
57
57
 
58
58
  showItemInFolder(fsPath: string): void;
59
+ openWithSystemApp(fsPath: string): void;
59
60
 
60
61
  getTitleBarStyleAtStartup(): Promise<string>;
61
62
  setTitleBarStyle(style: string): void;
@@ -112,6 +113,7 @@ export const CHANNEL_FOCUS_WINDOW = 'FocusWindow';
112
113
  export const CHANNEL_SHOW_OPEN = 'ShowOpenDialog';
113
114
  export const CHANNEL_SHOW_SAVE = 'ShowSaveDialog';
114
115
  export const CHANNEL_SHOW_ITEM_IN_FOLDER = 'ShowItemInFolder';
116
+ export const CHANNEL_OPEN_WITH_SYSTEM_APP = 'OpenWithSystemApp';
115
117
  export const CHANNEL_ATTACH_SECURITY_TOKEN = 'AttachSecurityToken';
116
118
 
117
119
  export const CHANNEL_GET_TITLE_STYLE_AT_STARTUP = 'GetTitleStyleAtStartup';
@@ -53,7 +53,8 @@ import {
53
53
  CHANNEL_REQUEST_SECONDARY_CLOSE,
54
54
  CHANNEL_SET_BACKGROUND_COLOR,
55
55
  CHANNEL_WC_METADATA,
56
- CHANNEL_ABOUT_TO_CLOSE
56
+ CHANNEL_ABOUT_TO_CLOSE,
57
+ CHANNEL_OPEN_WITH_SYSTEM_APP
57
58
  } from '../electron-common/electron-api';
58
59
  import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application';
59
60
  import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common';
@@ -147,7 +148,9 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {
147
148
 
148
149
  // focus windows for secondary window support
149
150
  ipcMain.on(CHANNEL_FOCUS_WINDOW, (event, windowName) => {
150
- const electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName);
151
+ const electronWindow = windowName
152
+ ? BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName)
153
+ : BrowserWindow.fromWebContents(event.sender);
151
154
  if (electronWindow) {
152
155
  if (electronWindow.isMinimized()) {
153
156
  electronWindow.restore();
@@ -162,6 +165,10 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {
162
165
  shell.showItemInFolder(fsPath);
163
166
  });
164
167
 
168
+ ipcMain.on(CHANNEL_OPEN_WITH_SYSTEM_APP, (event, fsPath) => {
169
+ shell.openPath(fsPath);
170
+ });
171
+
165
172
  ipcMain.handle(CHANNEL_GET_TITLE_STYLE_AT_STARTUP, event => application.getTitleBarStyleAtStartup(event.sender));
166
173
 
167
174
  ipcMain.on(CHANNEL_SET_TITLE_STYLE, (event, style) => application.setTitleBarStyle(event.sender, style));
@@ -22,16 +22,16 @@ import { AddressInfo } from 'net';
22
22
  import { promises as fs } from 'fs';
23
23
  import { existsSync, mkdirSync } from 'fs-extra';
24
24
  import { fork, ForkOptions } from 'child_process';
25
- import { DefaultTheme, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
25
+ import { DefaultTheme, ElectronFrontendApplicationConfig, FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
26
26
  import URI from '../common/uri';
27
27
  import { FileUri } from '../common/file-uri';
28
- import { Deferred } from '../common/promise-util';
28
+ import { Deferred, timeout } from '../common/promise-util';
29
29
  import { MaybePromise } from '../common/types';
30
30
  import { ContributionProvider } from '../common/contribution-provider';
31
31
  import { ElectronSecurityTokenService } from './electron-security-token-service';
32
32
  import { ElectronSecurityToken } from '../electron-common/electron-token';
33
33
  import Storage = require('electron-store');
34
- import { Disposable, DisposableCollection, isOSX, isWindows } from '../common';
34
+ import { CancellationTokenSource, Disposable, DisposableCollection, isOSX, isWindows } from '../common';
35
35
  import { DEFAULT_WINDOW_HASH, WindowSearchParams } from '../common/window';
36
36
  import { TheiaBrowserWindowOptions, TheiaElectronWindow, TheiaElectronWindowFactory } from './theia-electron-window';
37
37
  import { ElectronMainApplicationGlobals } from './electron-main-constants';
@@ -182,6 +182,7 @@ export class ElectronMainApplication {
182
182
  protected windows = new Map<number, TheiaElectronWindow>();
183
183
  protected restarting = false;
184
184
 
185
+ /** Used to temporarily store the reference to an early created main window */
185
186
  protected initialWindow?: BrowserWindow;
186
187
 
187
188
  get config(): FrontendApplicationConfig {
@@ -287,18 +288,110 @@ export class ElectronMainApplication {
287
288
  return this.didUseNativeWindowFrameOnStart.get(webContents.id) ? 'native' : 'custom';
288
289
  }
289
290
 
291
+ protected async determineSplashScreenBounds(initialWindowBounds: { x: number, y: number, width: number, height: number }):
292
+ Promise<{ x: number, y: number, width: number, height: number }> {
293
+ const splashScreenOptions = this.getSplashScreenOptions();
294
+ const width = splashScreenOptions?.width ?? 640;
295
+ const height = splashScreenOptions?.height ?? 480;
296
+
297
+ // determine the screen on which to show the splash screen via the center of the window to show
298
+ const windowCenterPoint = { x: initialWindowBounds.x + (initialWindowBounds.width / 2), y: initialWindowBounds.y + (initialWindowBounds.height / 2) };
299
+ const { bounds } = screen.getDisplayNearestPoint(windowCenterPoint);
300
+
301
+ // place splash screen center of screen
302
+ const screenCenterPoint = { x: bounds.x + (bounds.width / 2), y: bounds.y + (bounds.height / 2) };
303
+ const x = screenCenterPoint.x - (width / 2);
304
+ const y = screenCenterPoint.y - (height / 2);
305
+
306
+ return {
307
+ x, y, width, height
308
+ };
309
+ }
310
+
311
+ protected isShowWindowEarly(): boolean {
312
+ return !!this.config.electron.showWindowEarly &&
313
+ !('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1');
314
+ }
315
+
290
316
  protected showInitialWindow(): void {
291
- if (this.config.electron.showWindowEarly &&
292
- !('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1')) {
293
- console.log('Showing main window early');
317
+ if (this.isShowWindowEarly() || this.isShowSplashScreen()) {
294
318
  app.whenReady().then(async () => {
295
319
  const options = await this.getLastWindowOptions();
320
+ // If we want to show a splash screen, don't auto open the main window
321
+ if (this.isShowSplashScreen()) {
322
+ options.preventAutomaticShow = true;
323
+ }
296
324
  this.initialWindow = await this.createWindow({ ...options });
297
- this.initialWindow.show();
325
+
326
+ if (this.isShowSplashScreen()) {
327
+ console.log('Showing splash screen');
328
+ this.configureAndShowSplashScreen(this.initialWindow);
329
+ }
330
+
331
+ // Show main window early if windows shall be shown early and splash screen is not configured
332
+ if (this.isShowWindowEarly() && !this.isShowSplashScreen()) {
333
+ console.log('Showing main window early');
334
+ this.initialWindow.show();
335
+ }
298
336
  });
299
337
  }
300
338
  }
301
339
 
340
+ protected async configureAndShowSplashScreen(mainWindow: BrowserWindow): Promise<BrowserWindow> {
341
+ const splashScreenOptions = this.getSplashScreenOptions()!;
342
+ console.debug('SplashScreen options', splashScreenOptions);
343
+
344
+ const splashScreenBounds = await this.determineSplashScreenBounds(mainWindow.getBounds());
345
+ const splashScreenWindow = new BrowserWindow({
346
+ ...splashScreenBounds,
347
+ frame: false,
348
+ alwaysOnTop: true,
349
+ show: false
350
+ });
351
+
352
+ if (this.isShowWindowEarly()) {
353
+ console.log('Showing splash screen early');
354
+ splashScreenWindow.show();
355
+ } else {
356
+ splashScreenWindow.on('ready-to-show', () => {
357
+ splashScreenWindow.show();
358
+ });
359
+ }
360
+
361
+ splashScreenWindow.loadFile(path.resolve(this.globals.THEIA_APP_PROJECT_PATH, splashScreenOptions.content!).toString());
362
+
363
+ // close splash screen and show main window once frontend is ready or a timeout is hit
364
+ const cancelTokenSource = new CancellationTokenSource();
365
+ const minTime = timeout(splashScreenOptions.minDuration ?? 0, cancelTokenSource.token);
366
+ const maxTime = timeout(splashScreenOptions.maxDuration ?? 30000, cancelTokenSource.token);
367
+
368
+ const showWindowAndCloseSplashScreen = () => {
369
+ cancelTokenSource.cancel();
370
+ if (!mainWindow.isVisible()) {
371
+ mainWindow.show();
372
+ }
373
+ splashScreenWindow.close();
374
+ };
375
+ TheiaRendererAPI.onApplicationStateChanged(mainWindow.webContents, state => {
376
+ if (state === 'ready') {
377
+ minTime.then(() => showWindowAndCloseSplashScreen());
378
+ }
379
+ });
380
+ maxTime.then(() => showWindowAndCloseSplashScreen());
381
+ return splashScreenWindow;
382
+ }
383
+
384
+ protected isShowSplashScreen(): boolean {
385
+ return typeof this.config.electron.splashScreenOptions === 'object' && !!this.config.electron.splashScreenOptions.content;
386
+ }
387
+
388
+ protected getSplashScreenOptions(): ElectronFrontendApplicationConfig.SplashScreenOptions | undefined {
389
+ if (this.isShowSplashScreen()) {
390
+ return this.config.electron.splashScreenOptions;
391
+ }
392
+ return undefined;
393
+ }
394
+
302
395
  /**
303
396
  * Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it.
304
397
  *
@@ -316,6 +409,7 @@ export class ElectronMainApplication {
316
409
  electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus'));
317
410
  this.attachSaveWindowState(electronWindow.window);
318
411
  this.configureNativeSecondaryWindowCreation(electronWindow.window);
412
+
319
413
  return electronWindow.window;
320
414
  }
321
415
 
@@ -548,10 +642,6 @@ export class ElectronMainApplication {
548
642
  protected async startBackend(): Promise<number> {
549
643
  // Check if we should run everything as one process.
550
644
  const noBackendFork = process.argv.indexOf('--no-cluster') !== -1;
551
- // We cannot use the `process.cwd()` as the application project path (the location of the `package.json` in other words)
552
- // in a bundled electron application because it depends on the way we start it. For instance, on OS X, these are a differences:
553
- // https://github.com/eclipse-theia/theia/issues/3297#issuecomment-439172274
554
- process.env.THEIA_APP_PROJECT_PATH = this.globals.THEIA_APP_PROJECT_PATH;
555
645
  // Set the electron version for both the dev and the production mode. (https://github.com/eclipse-theia/theia/issues/3254)
556
646
  // Otherwise, the forked backend processes will not know that they're serving the electron frontend.
557
647
  process.env.THEIA_ELECTRON_VERSION = process.versions.electron;
@@ -37,6 +37,12 @@ export interface TheiaBrowserWindowOptions extends BrowserWindowConstructorOptio
37
37
  * in which case we want to invalidate the stored options and use the default options instead.
38
38
  */
39
39
  screenLayout?: string;
40
+ /**
41
+ * By default, the window will be shown as soon as the content is ready to render.
42
+ * This can be prevented by handing over preventAutomaticShow: `true`.
43
+ * Use this for fine-grained control over when to show the window, e.g. to coordinate with a splash screen.
44
+ */
45
+ preventAutomaticShow?: boolean;
40
46
  }
41
47
 
42
48
  export const TheiaBrowserWindowOptions = Symbol('TheiaBrowserWindowOptions');
@@ -76,7 +82,9 @@ export class TheiaElectronWindow {
76
82
  protected init(): void {
77
83
  this._window = new BrowserWindow(this.options);
78
84
  this._window.setMenuBarVisibility(false);
79
- this.attachReadyToShow();
85
+ if (!this.options.preventAutomaticShow) {
86
+ this.attachReadyToShow();
87
+ }
80
88
  this.restoreMaximizedState();
81
89
  this.attachCloseListeners();
82
90
  this.trackApplicationState();
@@ -26,9 +26,9 @@ export class ApplicationServerImpl implements ApplicationServer {
26
26
  protected readonly applicationPackage: ApplicationPackage;
27
27
 
28
28
  getExtensionsInfos(): Promise<ExtensionInfo[]> {
29
- const extensions = this.applicationPackage.extensionPackages;
30
- const infos: ExtensionInfo[] = extensions.map(extension => ({ name: extension.name, version: extension.version }));
31
- return Promise.resolve(infos);
29
+ // @ts-expect-error
30
+ const appInfo: ExtensionInfo[] = globalThis.extensionInfo;
31
+ return Promise.resolve(appInfo);
32
32
  }
33
33
 
34
34
  getApplicationInfo(): Promise<ApplicationInfo | undefined> {
@@ -21,7 +21,7 @@ import {
21
21
  bindContributionProvider, MessageService, MessageClient, ConnectionHandler, RpcConnectionHandler,
22
22
  CommandService, commandServicePath, messageServicePath, OSBackendProvider, OSBackendProviderPath
23
23
  } from '../common';
24
- import { BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution, BackendApplicationServer } from './backend-application';
24
+ import { BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution, BackendApplicationServer, BackendApplicationPath } from './backend-application';
25
25
  import { CliManager, CliContribution } from './cli';
26
26
  import { IPCConnectionProvider } from './messaging';
27
27
  import { ApplicationServerImpl } from './application-server';
@@ -101,10 +101,7 @@ export const backendApplicationModule = new ContainerModule(bind => {
101
101
  })
102
102
  ).inSingletonScope();
103
103
 
104
- bind(ApplicationPackage).toDynamicValue(({ container }) => {
105
- const { projectPath } = container.get(BackendApplicationCliContribution);
106
- return new ApplicationPackage({ projectPath });
107
- }).inSingletonScope();
104
+ bind(ApplicationPackage).toConstantValue(new ApplicationPackage({ projectPath: BackendApplicationPath }));
108
105
 
109
106
  bind(WsRequestValidator).toSelf().inSingletonScope();
110
107
  bindContributionProvider(bind, WsRequestValidatorContribution);
@@ -27,9 +27,14 @@ import { CliContribution } from './cli';
27
27
  import { Deferred } from '../common/promise-util';
28
28
  import { environment } from '../common/index';
29
29
  import { AddressInfo } from 'net';
30
- import { ApplicationPackage } from '@theia/application-package';
31
30
  import { ProcessUtils } from './process-utils';
32
31
 
32
+ /**
33
+ * The path to the application project directory. This is the directory where the application code is located.
34
+ * Mostly contains the `package.json` file and the `lib` directory.
35
+ */
36
+ export const BackendApplicationPath = process.env.THEIA_APP_PROJECT_PATH || process.cwd();
37
+
33
38
  export type DnsResultOrder = 'ipv4first' | 'verbatim' | 'nodeDefault';
34
39
 
35
40
  const APP_PROJECT_PATH = 'app-project-path';
@@ -115,7 +120,8 @@ export class BackendApplicationCliContribution implements CliContribution {
115
120
  ssl: boolean | undefined;
116
121
  cert: string | undefined;
117
122
  certkey: string | undefined;
118
- projectPath: string;
123
+ /** @deprecated Use the `BackendApplicationPath` constant or `process.env.THEIA_APP_PROJECT_PATH` environment variable instead */
124
+ projectPath = BackendApplicationPath;
119
125
 
120
126
  configure(conf: yargs.Argv): void {
121
127
  conf.option('port', { alias: 'p', description: 'The port the backend server listens on.', type: 'number', default: DEFAULT_PORT });
@@ -123,7 +129,7 @@ export class BackendApplicationCliContribution implements CliContribution {
123
129
  conf.option('ssl', { description: 'Use SSL (HTTPS), cert and certkey must also be set', type: 'boolean', default: DEFAULT_SSL });
124
130
  conf.option('cert', { description: 'Path to SSL certificate.', type: 'string' });
125
131
  conf.option('certkey', { description: 'Path to SSL certificate key.', type: 'string' });
126
- conf.option(APP_PROJECT_PATH, { description: 'Sets the application project directory', default: this.appProjectPath() });
132
+ conf.option(APP_PROJECT_PATH, { description: 'Sets the application project directory', deprecated: true });
127
133
  conf.option('dnsDefaultResultOrder', {
128
134
  type: 'string',
129
135
  description: 'Configure Node\'s DNS resolver default behavior, see https://nodejs.org/docs/latest-v18.x/api/dns.html#dnssetdefaultresultorderorder',
@@ -138,19 +144,8 @@ export class BackendApplicationCliContribution implements CliContribution {
138
144
  this.ssl = args.ssl as boolean;
139
145
  this.cert = args.cert as string;
140
146
  this.certkey = args.certkey as string;
141
- this.projectPath = args[APP_PROJECT_PATH] as string;
142
147
  this.dnsDefaultResultOrder = args.dnsDefaultResultOrder as DnsResultOrder;
143
148
  }
144
-
145
- protected appProjectPath(): string {
146
- if (environment.electron.is()) {
147
- if (process.env.THEIA_APP_PROJECT_PATH) {
148
- return process.env.THEIA_APP_PROJECT_PATH;
149
- }
150
- throw new Error('The \'THEIA_APP_PROJECT_PATH\' environment variable must be set when running in electron.');
151
- }
152
- return process.cwd();
153
- }
154
149
  }
155
150
 
156
151
  /**
@@ -161,9 +156,6 @@ export class BackendApplication {
161
156
 
162
157
  protected readonly app: express.Application = express();
163
158
 
164
- @inject(ApplicationPackage)
165
- protected readonly applicationPackage: ApplicationPackage;
166
-
167
159
  @inject(ProcessUtils)
168
160
  protected readonly processUtils: ProcessUtils;
169
161
 
@@ -352,7 +344,7 @@ export class BackendApplication {
352
344
  const acceptedEncodings = req.acceptsEncodings();
353
345
 
354
346
  const gzUrl = `${req.url}.gz`;
355
- const gzPath = path.join(this.applicationPackage.projectPath, 'lib', 'frontend', gzUrl);
347
+ const gzPath = path.join(BackendApplicationPath, 'lib', 'frontend', gzUrl);
356
348
  if (acceptedEncodings.indexOf('gzip') === -1 || !(await fs.pathExists(gzPath))) {
357
349
  next();
358
350
  return;
@@ -22,6 +22,7 @@ import { pathExists, mkdir } from 'fs-extra';
22
22
  import { EnvVariable, EnvVariablesServer } from '../../common/env-variables';
23
23
  import { isWindows } from '../../common/os';
24
24
  import { FileUri } from '../../common/file-uri';
25
+ import { BackendApplicationPath } from '../backend-application';
25
26
 
26
27
  @injectable()
27
28
  export class EnvVariablesServerImpl implements EnvVariablesServer {
@@ -45,10 +46,7 @@ export class EnvVariablesServerImpl implements EnvVariablesServer {
45
46
  }
46
47
 
47
48
  protected async createConfigDirUri(): Promise<string> {
48
- let dataFolderPath: string = '';
49
- if (process.env.THEIA_APP_PROJECT_PATH) {
50
- dataFolderPath = join(process.env.THEIA_APP_PROJECT_PATH, 'data');
51
- }
49
+ const dataFolderPath = join(BackendApplicationPath, 'data');
52
50
  const userDataPath = join(dataFolderPath, 'user-data');
53
51
  const dataFolderExists = this.pathExistenceCache[dataFolderPath] ??= await pathExists(dataFolderPath);
54
52
  if (dataFolderExists) {