diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21929257f8b64..5aac54a08a816 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,8 +10,8 @@
- [plugin] Extend TextEditorLineNumbersStyle with Interval [#13458](https://github.com/eclipse-theia/theia/pull/13458) - contributed on behalf of STMicroelectronics
[Breaking Changes:](#breaking_changes_not_yet_released)
+- [core] Add secondary windows support for text editors. [#13493](https://github.com/eclipse-theia/theia/pull/13493 ). The changes in require more extensive patches for our dependencies than before. For this purpose, we are using the `patch-package` library. However, this change requires adopters to add the line `"postinstall": "theia-patch"` to the `package.json` at the root of their monorepo (where the `node_modules` folder is located). - contributed on behalf of STMicroelectronics
-- [component] add here
## v1.47.0 - 02/29/2024
diff --git a/dev-packages/application-manager/package.json b/dev-packages/application-manager/package.json
index 7b9e46b97e031..f08b8a849f11c 100644
--- a/dev-packages/application-manager/package.json
+++ b/dev-packages/application-manager/package.json
@@ -57,7 +57,6 @@
"source-map": "^0.6.1",
"source-map-loader": "^2.0.1",
"source-map-support": "^0.5.19",
- "string-replace-loader": "^3.1.0",
"style-loader": "^2.0.0",
"tslib": "^2.6.2",
"umd-compat-loader": "^2.1.2",
diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts
index ee304fec409a7..bf586df8db3e5 100644
--- a/dev-packages/application-manager/src/generator/frontend-generator.ts
+++ b/dev-packages/application-manager/src/generator/frontend-generator.ts
@@ -188,15 +188,6 @@ ${Array.from(frontendModules.values(), jsModulePath => `\
}
-
diff --git a/dev-packages/application-manager/src/generator/webpack-generator.ts b/dev-packages/application-manager/src/generator/webpack-generator.ts
index 9fe4f05e7e143..c02fae28273e5 100644
--- a/dev-packages/application-manager/src/generator/webpack-generator.ts
+++ b/dev-packages/application-manager/src/generator/webpack-generator.ts
@@ -128,24 +128,6 @@ module.exports = [{
cache: staticCompression,
module: {
rules: [
- {
- // Removes the host check in PhosphorJS to enable moving widgets to secondary windows.
- test: /widget\\.js$/,
- loader: 'string-replace-loader',
- include: /node_modules[\\\\/]@phosphor[\\\\/]widgets[\\\\/]lib/,
- options: {
- multiple: [
- {
- search: /document\\.body\\.contains\\(widget.node\\)/gm,
- replace: 'widget.node.ownerDocument.body.contains(widget.node)'
- },
- {
- search: /\\!document\\.body\\.contains\\(host\\)/gm,
- replace: ' !host.ownerDocument.body.contains(host)'
- }
- ]
- }
- },
{
test: /\\.css$/,
exclude: /materialcolors\\.css$|\\.useable\\.css$/,
@@ -280,6 +262,10 @@ module.exports = [{
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"]
+ },
+ {
+ test: /\.wasm$/,
+ type: 'asset/resource'
}
]
},
diff --git a/dev-packages/cli/bin/theia-patch.js b/dev-packages/cli/bin/theia-patch.js
new file mode 100755
index 0000000000000..5c9e7ad2ad828
--- /dev/null
+++ b/dev-packages/cli/bin/theia-patch.js
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+// *****************************************************************************
+// Copyright (C) 2024 STMicroelectronics and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+// @ts-check
+const path = require('path');
+const cp = require('child_process');
+
+const patchPackage= require.resolve('patch-package');
+console.log(`patch-package = ${patchPackage}`);
+
+const patchesDir = path.join('.', 'node_modules', '@theia', 'cli', 'patches');
+
+console.log(`patchesdir = ${patchesDir}`);
+
+const env = Object.assign({}, process.env);
+
+const scriptProcess = cp.exec(`node ${patchPackage} --patch-dir "${patchesDir}"`, {
+ cwd: process.cwd(),
+ env,
+
+});
+
+scriptProcess.stdout.pipe(process.stdout);
+scriptProcess.stderr.pipe(process.stderr);
diff --git a/dev-packages/cli/package.json b/dev-packages/cli/package.json
index b882060db672d..b8137f9dfa1b0 100644
--- a/dev-packages/cli/package.json
+++ b/dev-packages/cli/package.json
@@ -20,7 +20,8 @@
"src"
],
"bin": {
- "theia": "./bin/theia"
+ "theia": "./bin/theia",
+ "theia-patch": "./bin/theia-patch.js"
},
"scripts": {
"prepare": "tsc -b",
@@ -30,6 +31,7 @@
"clean": "theiaext clean"
},
"dependencies": {
+ "patch-package": "^8.0.0",
"@theia/application-manager": "1.47.0",
"@theia/application-package": "1.47.0",
"@theia/ffmpeg": "1.47.0",
diff --git a/dev-packages/cli/patches/@phosphor+widgets+1.9.3.patch b/dev-packages/cli/patches/@phosphor+widgets+1.9.3.patch
new file mode 100644
index 0000000000000..40f582ab14dcf
--- /dev/null
+++ b/dev-packages/cli/patches/@phosphor+widgets+1.9.3.patch
@@ -0,0 +1,157 @@
+diff --git a/node_modules/@phosphor/widgets/lib/menu.d.ts b/node_modules/@phosphor/widgets/lib/menu.d.ts
+index 5d5053c..7802167 100644
+--- a/node_modules/@phosphor/widgets/lib/menu.d.ts
++++ b/node_modules/@phosphor/widgets/lib/menu.d.ts
+@@ -195,7 +195,7 @@ export declare class Menu extends Widget {
+ *
+ * This is a no-op if the menu is already attached to the DOM.
+ */
+- open(x: number, y: number, options?: Menu.IOpenOptions): void;
++ open(x: number, y: number, options?: Menu.IOpenOptions, anchor?: HTMLElement): void;
+ /**
+ * Handle the DOM events for the menu.
+ *
+diff --git a/node_modules/@phosphor/widgets/lib/menu.js b/node_modules/@phosphor/widgets/lib/menu.js
+index de23022..a8b15b1 100644
+--- a/node_modules/@phosphor/widgets/lib/menu.js
++++ b/node_modules/@phosphor/widgets/lib/menu.js
+@@ -13,7 +13,7 @@ var __extends = (this && this.__extends) || (function () {
+ };
+ })();
+ var __assign = (this && this.__assign) || function () {
+- __assign = Object.assign || function(t) {
++ __assign = Object.assign || function (t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
+@@ -424,7 +424,7 @@ var Menu = /** @class */ (function (_super) {
+ *
+ * This is a no-op if the menu is already attached to the DOM.
+ */
+- Menu.prototype.open = function (x, y, options) {
++ Menu.prototype.open = function (x, y, options, node) {
+ if (options === void 0) { options = {}; }
+ // Bail early if the menu is already attached.
+ if (this.isAttached) {
+@@ -434,7 +434,7 @@ var Menu = /** @class */ (function (_super) {
+ var forceX = options.forceX || false;
+ var forceY = options.forceY || false;
+ // Open the menu as a root menu.
+- Private.openRootMenu(this, x, y, forceX, forceY);
++ Private.openRootMenu(this, x, y, forceX, forceY, node);
+ // Activate the menu to accept keyboard input.
+ this.activate();
+ };
+@@ -484,8 +484,16 @@ var Menu = /** @class */ (function (_super) {
+ this.node.addEventListener('mouseenter', this);
+ this.node.addEventListener('mouseleave', this);
+ this.node.addEventListener('contextmenu', this);
+- document.addEventListener('mousedown', this, true);
+ };
++
++ Menu.prototype.onAfterAttach = function (msg) {
++ this.node.ownerDocument.addEventListener('mousedown', this, true);
++ }
++
++ Menu.prototype.onBeforeDetach = function (msg) {
++ this.node.ownerDocument.removeEventListener('mousedown', this, true);
++ }
++
+ /**
+ * A message handler invoked on an `'after-detach'` message.
+ */
+@@ -496,7 +504,6 @@ var Menu = /** @class */ (function (_super) {
+ this.node.removeEventListener('mouseenter', this);
+ this.node.removeEventListener('mouseleave', this);
+ this.node.removeEventListener('contextmenu', this);
+- document.removeEventListener('mousedown', this, true);
+ };
+ /**
+ * A message handler invoked on an `'activate-request'` message.
+@@ -1124,14 +1131,15 @@ var Private;
+ /**
+ * Open a menu as a root menu at the target location.
+ */
+- function openRootMenu(menu, x, y, forceX, forceY) {
++ function openRootMenu(menu, x, y, forceX, forceY, element) {
+ // Ensure the menu is updated before attaching and measuring.
+ messaging_1.MessageLoop.sendMessage(menu, widget_1.Widget.Msg.UpdateRequest);
+ // Get the current position and size of the main viewport.
++ var doc = element ? element.ownerDocument : document;
+ var px = window.pageXOffset;
+ var py = window.pageYOffset;
+- var cw = document.documentElement.clientWidth;
+- var ch = document.documentElement.clientHeight;
++ var cw = doc.documentElement.clientWidth;
++ var ch = doc.documentElement.clientHeight;
+ // Compute the maximum allowed height for the menu.
+ var maxHeight = ch - (forceY ? y : 0);
+ // Fetch common variables.
+@@ -1145,7 +1153,7 @@ var Private;
+ style.visibility = 'hidden';
+ style.maxHeight = maxHeight + "px";
+ // Attach the menu to the document.
+- widget_1.Widget.attach(menu, document.body);
++ widget_1.Widget.attach(menu, doc.body);
+ // Measure the size of the menu.
+ var _a = node.getBoundingClientRect(), width = _a.width, height = _a.height;
+ // Adjust the X position of the menu to fit on-screen.
+@@ -1177,8 +1185,8 @@ var Private;
+ // Get the current position and size of the main viewport.
+ var px = window.pageXOffset;
+ var py = window.pageYOffset;
+- var cw = document.documentElement.clientWidth;
+- var ch = document.documentElement.clientHeight;
++ var cw = itemNode.ownerDocument.documentElement.clientWidth;
++ var ch = itemNode.ownerDocument.documentElement.clientHeight;
+ // Compute the maximum allowed height for the menu.
+ var maxHeight = ch;
+ // Fetch common variables.
+@@ -1192,7 +1200,7 @@ var Private;
+ style.visibility = 'hidden';
+ style.maxHeight = maxHeight + "px";
+ // Attach the menu to the document.
+- widget_1.Widget.attach(submenu, document.body);
++ widget_1.Widget.attach(submenu, itemNode.ownerDocument.body);
+ // Measure the size of the menu.
+ var _a = node.getBoundingClientRect(), width = _a.width, height = _a.height;
+ // Compute the box sizing for the menu.
+diff --git a/node_modules/@phosphor/widgets/lib/menubar.js b/node_modules/@phosphor/widgets/lib/menubar.js
+index a8e10f4..da2ee82 100644
+--- a/node_modules/@phosphor/widgets/lib/menubar.js
++++ b/node_modules/@phosphor/widgets/lib/menubar.js
+@@ -521,7 +521,7 @@ var MenuBar = /** @class */ (function (_super) {
+ // Get the positioning data for the new menu.
+ var _a = itemNode.getBoundingClientRect(), left = _a.left, bottom = _a.bottom;
+ // Open the new menu at the computed location.
+- newMenu.open(left, bottom, { forceX: true, forceY: true });
++ newMenu.open(left, bottom, { forceX: true, forceY: true }, this.node);
+ };
+ /**
+ * Close the child menu immediately.
+diff --git a/node_modules/@phosphor/widgets/lib/widget.js b/node_modules/@phosphor/widgets/lib/widget.js
+index 01241fa..62da27c 100644
+--- a/node_modules/@phosphor/widgets/lib/widget.js
++++ b/node_modules/@phosphor/widgets/lib/widget.js
+@@ -906,10 +906,10 @@ exports.Widget = Widget;
+ if (widget.parent) {
+ throw new Error('Cannot attach a child widget.');
+ }
+- if (widget.isAttached || document.body.contains(widget.node)) {
++ if (widget.isAttached || widget.node.ownerDocument.body.contains(widget.node)) {
+ throw new Error('Widget is already attached.');
+ }
+- if (!document.body.contains(host)) {
++ if (!host.ownerDocument.body.contains(host)) {
+ throw new Error('Host is not attached.');
+ }
+ messaging_1.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
+@@ -930,7 +930,7 @@ exports.Widget = Widget;
+ if (widget.parent) {
+ throw new Error('Cannot detach a child widget.');
+ }
+- if (!widget.isAttached || !document.body.contains(widget.node)) {
++ if (!widget.isAttached || !widget.node.ownerDocument.body.contains(widget.node)) {
+ throw new Error('Widget is not attached.');
+ }
+ messaging_1.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
diff --git a/dev-packages/cli/patches/@theia+monaco-editor-core+1.83.101.patch b/dev-packages/cli/patches/@theia+monaco-editor-core+1.83.101.patch
new file mode 100644
index 0000000000000..da1e72d06ac90
--- /dev/null
+++ b/dev-packages/cli/patches/@theia+monaco-editor-core+1.83.101.patch
@@ -0,0 +1,32 @@
+diff --git a/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js b/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js
+index 111dec4..b196066 100644
+--- a/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js
++++ b/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js
+@@ -47,14 +47,15 @@ function setGlobalHoverDelay(size) {
+ }
+ exports.setGlobalHoverDelay = setGlobalHoverDelay;
+ class MouseEventFactory {
+- constructor() {
++ constructor(el) {
++ this.el = el;
+ this.disposables = new lifecycle_1.DisposableStore();
+ }
+ get onPointerMove() {
+- return this.disposables.add(new event_1.DomEmitter(window, 'mousemove')).event;
++ return this.disposables.add(new event_1.DomEmitter(this.el.ownerDocument.defaultView, 'mousemove')).event;
+ }
+ get onPointerUp() {
+- return this.disposables.add(new event_1.DomEmitter(window, 'mouseup')).event;
++ return this.disposables.add(new event_1.DomEmitter(this.el.ownerDocument.defaultView, 'mouseup')).event;
+ }
+ dispose() {
+ this.disposables.dispose();
+@@ -243,7 +244,7 @@ class Sash extends lifecycle_1.Disposable {
+ this.el.classList.add('mac');
+ }
+ const onMouseDown = this._register(new event_1.DomEmitter(this.el, 'mousedown')).event;
+- this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory()), this));
++ this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory(this.el)), this));
+ const onMouseDoubleClick = this._register(new event_1.DomEmitter(this.el, 'dblclick')).event;
+ this._register(onMouseDoubleClick(this.onPointerDoublePress, this));
+ const onMouseEnter = this._register(new event_1.DomEmitter(this.el, 'mouseenter')).event;
diff --git a/package.json b/package.json
index a7df25c10403e..d82581281ddd4 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
"lint:clean": "rimraf .eslintcache",
"lint:oneshot": "node --max-old-space-size=4096 node_modules/eslint/bin/eslint.js --cache=true \"{dev-packages,packages,examples}/**/*.{ts,tsx}\"",
"preinstall": "node-gyp install",
+ "postinstall": "theia-patch",
"prepare": "yarn -s compile:references && lerna run prepare && yarn -s compile",
"publish:latest": "lerna publish --exact --yes --no-push",
"publish:next": "lerna publish preminor --exact --canary --preid next --dist-tag next --no-git-reset --no-git-tag-version --no-push --yes && yarn -s publish:check",
diff --git a/packages/core/package.json b/packages/core/package.json
index 8b63571a3d75a..a001804f9bbd0 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -213,4 +213,4 @@
"nyc": {
"extends": "../../configs/nyc.json"
}
-}
+}
\ No newline at end of file
diff --git a/packages/core/src/browser/color-application-contribution.ts b/packages/core/src/browser/color-application-contribution.ts
index 4ad35b3de44b5..91cb42774fbdd 100644
--- a/packages/core/src/browser/color-application-contribution.ts
+++ b/packages/core/src/browser/color-application-contribution.ts
@@ -22,6 +22,7 @@ import { FrontendApplicationContribution } from './frontend-application-contribu
import { ContributionProvider } from '../common/contribution-provider';
import { Disposable, DisposableCollection } from '../common/disposable';
import { DEFAULT_BACKGROUND_COLOR_STORAGE_KEY } from './frontend-application-config-provider';
+import { SecondaryWindowHandler } from './secondary-window-handler';
export const ColorContribution = Symbol('ColorContribution');
export interface ColorContribution {
@@ -43,6 +44,9 @@ export class ColorApplicationContribution implements FrontendApplicationContribu
@inject(ThemeService) protected readonly themeService: ThemeService;
+ @inject(SecondaryWindowHandler)
+ protected readonly secondaryWindowHandler: SecondaryWindowHandler;
+
onStart(): void {
for (const contribution of this.colorContributions.getContributions()) {
contribution.registerColors(this.colors);
@@ -55,13 +59,18 @@ export class ColorApplicationContribution implements FrontendApplicationContribu
this.colors.onDidChange(() => this.update());
this.registerWindow(window);
+ this.secondaryWindowHandler.onWillAddWidget(([widget, window]) => {
+ this.registerWindow(window);
+ });
+ this.secondaryWindowHandler.onWillRemoveWidget(([widget, window]) => {
+ this.windows.delete(window);
+ });
}
- registerWindow(win: Window): Disposable {
+ registerWindow(win: Window): void {
this.windows.add(win);
this.updateWindow(win);
this.onDidChangeEmitter.fire();
- return Disposable.create(() => this.windows.delete(win));
}
protected readonly toUpdate = new DisposableCollection();
diff --git a/packages/core/src/browser/menu/browser-context-menu-renderer.ts b/packages/core/src/browser/menu/browser-context-menu-renderer.ts
index 775efaec0e28f..1ae5f1f826878 100644
--- a/packages/core/src/browser/menu/browser-context-menu-renderer.ts
+++ b/packages/core/src/browser/menu/browser-context-menu-renderer.ts
@@ -40,7 +40,8 @@ export class BrowserContextMenuRenderer extends ContextMenuRenderer {
if (onHide) {
contextMenu.aboutToClose.connect(() => onHide!());
}
- contextMenu.open(x, y);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ contextMenu.open(x, y, undefined, context);
return new BrowserContextMenuAccess(contextMenu);
}
diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts
index 8145d9d58e84f..931b0719d931e 100644
--- a/packages/core/src/browser/menu/browser-menu-plugin.ts
+++ b/packages/core/src/browser/menu/browser-menu-plugin.ts
@@ -267,14 +267,14 @@ export class DynamicMenuWidget extends MenuWidget {
});
}
- public override open(x: number, y: number, options?: MenuWidget.IOpenOptions): void {
+ public override open(x: number, y: number, options?: MenuWidget.IOpenOptions, anchor?: HTMLElement): void {
const cb = () => {
this.restoreFocusedElement();
this.aboutToClose.disconnect(cb);
};
this.aboutToClose.connect(cb);
this.preserveFocusedElement();
- super.open(x, y, options);
+ super.open(x, y, options, anchor);
}
protected updateSubMenus(parent: MenuWidget, menu: CompoundMenuNode, commands: MenuCommandRegistry): void {
diff --git a/packages/core/src/browser/secondary-window-handler.ts b/packages/core/src/browser/secondary-window-handler.ts
index d5aaef2c30a17..e967da357b1e5 100644
--- a/packages/core/src/browser/secondary-window-handler.ts
+++ b/packages/core/src/browser/secondary-window-handler.ts
@@ -22,8 +22,6 @@ import { ApplicationShell } from './shell/application-shell';
import { Emitter } from '../common/event';
import { SecondaryWindowService } from './window/secondary-window-service';
import { KeybindingRegistry } from './keybinding';
-import { ColorApplicationContribution } from './color-application-contribution';
-import { StylingService } from './styling-service';
/** Widget to be contained directly in a secondary window. */
class SecondaryWindowRootWidget extends Widget {
@@ -47,7 +45,6 @@ class SecondaryWindowRootWidget extends Widget {
* This handler manages the opened secondary windows and sets up messaging between them and the Theia main window.
* In addition, it provides access to the extracted widgets and provides notifications when widgets are added to or removed from this handler.
*
- * @experimental The functionality provided by this handler is experimental and has known issues in Electron apps.
*/
@injectable()
export class SecondaryWindowHandler {
@@ -59,17 +56,19 @@ export class SecondaryWindowHandler {
@inject(KeybindingRegistry)
protected keybindings: KeybindingRegistry;
- @inject(ColorApplicationContribution)
- protected colorAppContribution: ColorApplicationContribution;
-
- @inject(StylingService)
- protected stylingService: StylingService;
+ protected readonly onWillAddWidgetEmitter = new Emitter<[Widget, Window]>();
+ /** Subscribe to get notified when a widget is added to this handler, i.e. the widget was moved to an secondary window . */
+ readonly onWillAddWidget = this.onWillAddWidgetEmitter.event;
- protected readonly onDidAddWidgetEmitter = new Emitter();
+ protected readonly onDidAddWidgetEmitter = new Emitter<[Widget, Window]>();
/** Subscribe to get notified when a widget is added to this handler, i.e. the widget was moved to an secondary window . */
readonly onDidAddWidget = this.onDidAddWidgetEmitter.event;
- protected readonly onDidRemoveWidgetEmitter = new Emitter();
+ protected readonly onWillRemoveWidgetEmitter = new Emitter<[Widget, Window]>();
+ /** Subscribe to get notified when a widget is removed from this handler, i.e. the widget's window was closed or the widget was disposed. */
+ readonly onWillRemoveWidget = this.onWillRemoveWidgetEmitter.event;
+
+ protected readonly onDidRemoveWidgetEmitter = new Emitter<[Widget, Window]>();
/** Subscribe to get notified when a widget is removed from this handler, i.e. the widget's window was closed or the widget was disposed. */
readonly onDidRemoveWidget = this.onDidRemoveWidgetEmitter.event;
@@ -122,7 +121,8 @@ export class SecondaryWindowHandler {
}
const mainWindowTitle = document.title;
- newWindow.onload = () => {
+
+ newWindow.addEventListener('load', () => {
this.keybindings.registerEventListeners(newWindow);
// Use the widget's title as the window title
// Even if the widget's label were malicious, this should be safe against XSS because the HTML standard defines this is inserted via a text node.
@@ -134,8 +134,8 @@ export class SecondaryWindowHandler {
console.error('Could not find dom element to attach to in secondary window');
return;
}
- const unregisterWithColorContribution = this.colorAppContribution.registerWindow(newWindow);
- const unregisterWithStylingService = this.stylingService.registerWindow(newWindow);
+
+ this.onWillAddWidgetEmitter.fire([widget, newWindow]);
widget.secondaryWindow = newWindow;
const rootWidget = new SecondaryWindowRootWidget();
@@ -145,13 +145,12 @@ export class SecondaryWindowHandler {
widget.show();
widget.update();
- this.addWidget(widget);
+ this.addWidget(widget, newWindow);
// Close the window if the widget is disposed, e.g. by a command closing all widgets.
widget.disposed.connect(() => {
- unregisterWithColorContribution.dispose();
- unregisterWithStylingService.dispose();
- this.removeWidget(widget);
+ this.onWillRemoveWidgetEmitter.fire([widget, newWindow]);
+ this.removeWidget(widget, newWindow);
if (!newWindow.closed) {
newWindow.close();
}
@@ -165,7 +164,7 @@ export class SecondaryWindowHandler {
updateWidget();
});
widget.activate();
- };
+ });
}
/**
@@ -195,18 +194,18 @@ export class SecondaryWindowHandler {
return undefined;
}
- protected addWidget(widget: ExtractableWidget): void {
+ protected addWidget(widget: ExtractableWidget, win: Window): void {
if (!this._widgets.includes(widget)) {
this._widgets.push(widget);
- this.onDidAddWidgetEmitter.fire(widget);
+ this.onDidAddWidgetEmitter.fire([widget, win]);
}
}
- protected removeWidget(widget: ExtractableWidget): void {
+ protected removeWidget(widget: ExtractableWidget, win: Window): void {
const index = this._widgets.indexOf(widget);
if (index > -1) {
this._widgets.splice(index, 1);
- this.onDidRemoveWidgetEmitter.fire(widget);
+ this.onDidRemoveWidgetEmitter.fire([widget, win]);
}
}
}
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index da6e9e99f256a..424c0bf43dcb5 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -44,6 +44,7 @@ import { SecondaryWindowHandler } from '../secondary-window-handler';
import URI from '../../common/uri';
import { OpenerService } from '../opener-service';
import { PreviewableWidget } from '../widgets/previewable-widget';
+import { WindowService } from '../window/window-service';
/** The class name added to ApplicationShell instances. */
const APPLICATION_SHELL_CLASS = 'theia-ApplicationShell';
@@ -273,6 +274,7 @@ export class ApplicationShell extends Widget {
@inject(CorePreferences) protected readonly corePreferences: CorePreferences,
@inject(SaveResourceService) protected readonly saveResourceService: SaveResourceService,
@inject(SecondaryWindowHandler) protected readonly secondaryWindowHandler: SecondaryWindowHandler,
+ @inject(WindowService) protected readonly windowService: WindowService
) {
super(options as Widget.IOptions);
@@ -338,8 +340,8 @@ export class ApplicationShell extends Widget {
this.rightPanelHandler.dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget));
this.secondaryWindowHandler.init(this);
- this.secondaryWindowHandler.onDidAddWidget(widget => this.fireDidAddWidget(widget));
- this.secondaryWindowHandler.onDidRemoveWidget(widget => this.fireDidRemoveWidget(widget));
+ this.secondaryWindowHandler.onDidAddWidget(([widget, window]) => this.fireDidAddWidget(widget));
+ this.secondaryWindowHandler.onDidRemoveWidget(([widget, window]) => this.fireDidRemoveWidget(widget));
this.layout = this.createLayout();
@@ -1323,20 +1325,23 @@ export class ApplicationShell extends Widget {
let widget = find(this.mainPanel.widgets(), w => w.id === id);
if (widget) {
this.mainPanel.activateWidget(widget);
- return widget;
}
- widget = find(this.bottomPanel.widgets(), w => w.id === id);
- if (widget) {
- this.expandBottomPanel();
- this.bottomPanel.activateWidget(widget);
- return widget;
+ if (!widget) {
+ widget = find(this.bottomPanel.widgets(), w => w.id === id);
+ if (widget) {
+ this.expandBottomPanel();
+ this.bottomPanel.activateWidget(widget);
+ }
}
- widget = this.leftPanelHandler.activate(id);
- if (widget) {
- return widget;
+ if (!widget) {
+ widget = this.leftPanelHandler.activate(id);
+ }
+
+ if (!widget) {
+ widget = this.rightPanelHandler.activate(id);
}
- widget = this.rightPanelHandler.activate(id);
if (widget) {
+ this.windowService.focus();
return widget;
}
return this.secondaryWindowHandler.activateWidget(id);
@@ -1433,17 +1438,19 @@ export class ApplicationShell extends Widget {
if (tabBar) {
tabBar.currentTitle = widget.title;
}
- return widget;
}
- widget = this.leftPanelHandler.expand(id);
- if (widget) {
- return widget;
+ if (!widget) {
+ widget = this.leftPanelHandler.expand(id);
+ }
+ if (!widget) {
+ widget = this.rightPanelHandler.expand(id);
}
- widget = this.rightPanelHandler.expand(id);
if (widget) {
+ this.windowService.focus();
return widget;
+ } else {
+ return this.secondaryWindowHandler.revealWidget(id);
}
- return this.secondaryWindowHandler.revealWidget(id);
}
/**
diff --git a/packages/core/src/browser/styling-service.ts b/packages/core/src/browser/styling-service.ts
index 8acac1b1372be..221577711a09d 100644
--- a/packages/core/src/browser/styling-service.ts
+++ b/packages/core/src/browser/styling-service.ts
@@ -21,7 +21,7 @@ import { ColorRegistry } from './color-registry';
import { DecorationStyle } from './decoration-style';
import { FrontendApplicationContribution } from './frontend-application-contribution';
import { ThemeService } from './theming';
-import { Disposable } from '../common';
+import { SecondaryWindowHandler } from './secondary-window-handler';
export const StylingParticipant = Symbol('StylingParticipant');
@@ -52,16 +52,25 @@ export class StylingService implements FrontendApplicationContribution {
@inject(ContributionProvider) @named(StylingParticipant)
protected readonly themingParticipants: ContributionProvider;
+ @inject(SecondaryWindowHandler)
+ protected readonly secondaryWindowHandler: SecondaryWindowHandler;
+
onStart(): void {
this.registerWindow(window);
+ this.secondaryWindowHandler.onWillAddWidget(([widget, window]) => {
+ this.registerWindow(window);
+ });
+ this.secondaryWindowHandler.onWillRemoveWidget(([widget, window]) => {
+ this.cssElements.delete(window);
+ });
+
this.themeService.onDidColorThemeChange(e => this.applyStylingToWindows(e.newTheme));
}
- registerWindow(win: Window): Disposable {
+ registerWindow(win: Window): void {
const cssElement = DecorationStyle.createStyleElement('contributedColorTheme', win.document.head);
this.cssElements.set(win, cssElement);
this.applyStyling(this.themeService.getCurrentTheme(), cssElement);
- return Disposable.create(() => this.cssElements.delete(win));
}
protected applyStylingToWindows(theme: Theme): void {
diff --git a/packages/core/src/browser/window/default-window-service.ts b/packages/core/src/browser/window/default-window-service.ts
index bee19fa4a0408..f2d83ca5858fb 100644
--- a/packages/core/src/browser/window/default-window-service.ts
+++ b/packages/core/src/browser/window/default-window-service.ts
@@ -57,6 +57,10 @@ export class DefaultWindowService implements WindowService, FrontendApplicationC
this.openNewWindow(`#${DEFAULT_WINDOW_HASH}`);
}
+ focus(): void {
+ window.focus();
+ }
+
/**
* Returns a list of actions that {@link FrontendApplicationContribution}s would like to take before shutdown
* It is expected that this will succeed - i.e. return an empty array - at most once per session. If no vetoes are received
diff --git a/packages/core/src/browser/window/test/mock-window-service.ts b/packages/core/src/browser/window/test/mock-window-service.ts
index 3d924337b04a5..245c0c691acfd 100644
--- a/packages/core/src/browser/window/test/mock-window-service.ts
+++ b/packages/core/src/browser/window/test/mock-window-service.ts
@@ -21,6 +21,7 @@ import { WindowService } from '../window-service';
export class MockWindowService implements WindowService {
openNewWindow(): undefined { return undefined; }
openNewDefaultWindow(): void { }
+ focus(): void { }
reload(): void { }
isSafeToShutDown(): Promise { return Promise.resolve(true); }
setSafeToShutDown(): void { }
diff --git a/packages/core/src/browser/window/window-service.ts b/packages/core/src/browser/window/window-service.ts
index 694046fe910a2..6f1a0fc7bb40e 100644
--- a/packages/core/src/browser/window/window-service.ts
+++ b/packages/core/src/browser/window/window-service.ts
@@ -42,6 +42,11 @@ export interface WindowService {
*/
openNewDefaultWindow(params?: WindowReloadOptions): void;
+ /**
+ * Reveal and focuses the current window
+ */
+ focus(): void;
+
/**
* Fires when the `window` unloads. The unload event is inevitable. On this event, the frontend application can save its state and release resource.
* Saving the state and releasing any resources must be a synchronous call. Any asynchronous calls invoked after emitting this event might be ignored.
diff --git a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
index 8ed8b1cebd1a8..7633930144c03 100644
--- a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
+++ b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
@@ -104,11 +104,13 @@ export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer {
const menu = this.electronMenuFactory.createElectronContextMenu(menuPath, args, context, contextKeyService, skipSingleRootNode);
const { x, y } = coordinateFromAnchor(anchor);
+ const windowName = options.context?.ownerDocument.defaultView?.Window.name;
+
const menuHandle = window.electronTheiaCore.popup(menu, x, y, () => {
if (onHide) {
onHide();
}
- });
+ }, windowName);
// native context menu stops the event loop, so there is no keyboard events
this.context.resetAltPressed();
return new ElectronContextMenuAccess(menuHandle);
diff --git a/packages/core/src/electron-browser/preload.ts b/packages/core/src/electron-browser/preload.ts
index c78043cb1fc9c..4018bdb0936c0 100644
--- a/packages/core/src/electron-browser/preload.ts
+++ b/packages/core/src/electron-browser/preload.ts
@@ -81,11 +81,11 @@ const api: TheiaCoreAPI = {
},
attachSecurityToken: (endpoint: string) => ipcRenderer.invoke(CHANNEL_ATTACH_SECURITY_TOKEN, endpoint),
- popup: async function (menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise {
+ popup: async function (menu: MenuDto[], x: number, y: number, onClosed: () => void, windowName?: string): Promise {
const menuId = nextMenuId++;
const handlers = new Map void>();
commandHandlers.set(menuId, handlers);
- const handle = await ipcRenderer.invoke(CHANNEL_OPEN_POPUP, menuId, convertMenu(menu, handlers), x, y);
+ const handle = await ipcRenderer.invoke(CHANNEL_OPEN_POPUP, menuId, convertMenu(menu, handlers), x, y, windowName);
const closeListener = () => {
ipcRenderer.removeListener(CHANNEL_ON_CLOSE_POPUP, closeListener);
commandHandlers.delete(menuId);
diff --git a/packages/core/src/electron-browser/window/electron-window-service.ts b/packages/core/src/electron-browser/window/electron-window-service.ts
index f2ec5d6ae2edc..f9248cb54806e 100644
--- a/packages/core/src/electron-browser/window/electron-window-service.ts
+++ b/packages/core/src/electron-browser/window/electron-window-service.ts
@@ -57,6 +57,9 @@ export class ElectronWindowService extends DefaultWindowService {
this.delegate.openNewDefaultWindow(params);
}
+ override focus(): void {
+ window.electronTheiaCore.focusWindow(window.name);
+ }
@postConstruct()
protected init(): void {
// Update the default zoom level on startup when the preferences event is fired.
diff --git a/packages/core/src/electron-common/electron-api.ts b/packages/core/src/electron-common/electron-api.ts
index 5bff4341c68ab..77ffa0d5f3704 100644
--- a/packages/core/src/electron-common/electron-api.ts
+++ b/packages/core/src/electron-common/electron-api.ts
@@ -50,7 +50,7 @@ export interface TheiaCoreAPI {
setMenuBarVisible(visible: boolean, windowName?: string): void;
setMenu(menu: MenuDto[] | undefined): void;
- popup(menu: MenuDto[], x: number, y: number, onClosed: () => void): Promise;
+ popup(menu: MenuDto[], x: number, y: number, onClosed: () => void, windowName?: string): Promise;
closePopup(handle: number): void;
focusWindow(name: string): void;
diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts
index add9606bbdad1..6b3cb9d8fd46b 100644
--- a/packages/core/src/electron-main/electron-api-main.ts
+++ b/packages/core/src/electron-main/electron-api-main.ts
@@ -115,7 +115,7 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {
});
// popup menu
- ipcMain.handle(CHANNEL_OPEN_POPUP, (event, menuId, menu, x, y) => {
+ ipcMain.handle(CHANNEL_OPEN_POPUP, (event, menuId, menu, x, y, windowName?: string) => {
const zoom = event.sender.getZoomFactor();
// TODO: Remove the offset once Electron fixes https://github.com/electron/electron/issues/31641
const offset = process.platform === 'win32' ? 0 : 2;
@@ -124,7 +124,14 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {
y = Math.round(y * zoom) + offset;
const popup = Menu.buildFromTemplate(this.fromMenuDto(event.sender, menuId, menu));
this.openPopups.set(menuId, popup);
+ let electronWindow: BrowserWindow | undefined;
+ if (windowName) {
+ electronWindow = BrowserWindow.getAllWindows().find(win => win.webContents.mainFrame.name === windowName);
+ } else {
+ electronWindow = BrowserWindow.fromWebContents(event.sender) || undefined;
+ }
popup.popup({
+ window: electronWindow,
callback: () => {
this.openPopups.delete(menuId);
event.sender.send(CHANNEL_ON_CLOSE_POPUP, menuId);
diff --git a/packages/debug/package.json b/packages/debug/package.json
index ff5aa34b473bc..d23b9564552b9 100644
--- a/packages/debug/package.json
+++ b/packages/debug/package.json
@@ -28,6 +28,7 @@
"theiaExtensions": [
{
"frontend": "lib/browser/debug-frontend-module",
+ "secondaryWindow": "lib/browser/debug-frontend-module",
"backend": "lib/node/debug-backend-module"
}
],
@@ -62,4 +63,4 @@
"nyc": {
"extends": "../../configs/nyc.json"
}
-}
\ No newline at end of file
+}
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 0a23dbe92e228..6521ff71a9601 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -12,7 +12,8 @@
},
"theiaExtensions": [
{
- "frontend": "lib/browser/editor-frontend-module"
+ "frontend": "lib/browser/editor-frontend-module",
+ "secondaryWindow": "lib/browser/editor-frontend-module"
}
],
"keywords": [
diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts
index d31e089b09843..bba055996c5a4 100644
--- a/packages/editor/src/browser/editor-widget.ts
+++ b/packages/editor/src/browser/editor-widget.ts
@@ -15,12 +15,12 @@
// *****************************************************************************
import { Disposable, SelectionService, Event, UNTITLED_SCHEME, DisposableCollection } from '@theia/core/lib/common';
-import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel, unlock } from '@theia/core/lib/browser';
+import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel, unlock, ExtractableWidget } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { find } from '@theia/core/shared/@phosphor/algorithm';
import { TextEditor } from './editor';
-export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget {
+export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget, ExtractableWidget {
protected toDisposeOnTabbarChange = new DisposableCollection();
protected currentTabbar: TabBar | undefined;
@@ -51,6 +51,8 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
}
}));
}
+ isExtractable: boolean = true;
+ secondaryWindow: Window | undefined;
setSelection(): void {
if (this.editor.isFocused() && this.selectionService.selection !== this.editor) {
diff --git a/packages/monaco/package.json b/packages/monaco/package.json
index e9e5ef83c38a4..c728b911ff6a3 100644
--- a/packages/monaco/package.json
+++ b/packages/monaco/package.json
@@ -22,7 +22,8 @@
},
"theiaExtensions": [
{
- "frontend": "lib/browser/monaco-frontend-module"
+ "frontend": "lib/browser/monaco-frontend-module",
+ "secondaryWindow": "lib/browser/monaco-frontend-module"
}
],
"keywords": [
diff --git a/packages/monaco/src/browser/monaco-command.ts b/packages/monaco/src/browser/monaco-command.ts
index 2b8e13fe188e1..c6687702c07f2 100644
--- a/packages/monaco/src/browser/monaco-command.ts
+++ b/packages/monaco/src/browser/monaco-command.ts
@@ -38,16 +38,16 @@ export namespace MonacoCommands {
['redo', CommonCommands.REDO.id],
['editor.action.selectAll', CommonCommands.SELECT_ALL.id],
['actions.find', CommonCommands.FIND.id],
- ['editor.action.startFindReplaceAction', CommonCommands.REPLACE.id]
+ ['editor.action.startFindReplaceAction', CommonCommands.REPLACE.id],
+ ['editor.action.clipboardCutAction', CommonCommands.CUT.id],
+ ['editor.action.clipboardCopyAction', CommonCommands.COPY.id],
+ ['editor.action.clipboardPasteAction', CommonCommands.PASTE.id]
]);
export const GO_TO_DEFINITION = 'editor.action.revealDefinition';
export const EXCLUDE_ACTIONS = new Set([
- 'editor.action.quickCommand',
- 'editor.action.clipboardCutAction',
- 'editor.action.clipboardCopyAction',
- 'editor.action.clipboardPasteAction'
+ 'editor.action.quickCommand'
]);
}
diff --git a/packages/monaco/src/browser/monaco-context-menu.ts b/packages/monaco/src/browser/monaco-context-menu.ts
index 681909b0a4a0b..ad4f7239de0d2 100644
--- a/packages/monaco/src/browser/monaco-context-menu.ts
+++ b/packages/monaco/src/browser/monaco-context-menu.ts
@@ -51,9 +51,20 @@ export class MonacoContextMenuService implements IContextMenuService {
}
}
+ private getContext(delegate: IContextMenuDelegate): HTMLElement | undefined {
+ const anchor = delegate.getAnchor();
+ if (anchor instanceof HTMLElement) {
+ return anchor;
+ } else if (anchor instanceof StandardMouseEvent) {
+ return anchor.target;
+ } else {
+ return undefined;
+ }
+ }
showContextMenu(delegate: IContextMenuDelegate): void {
const anchor = this.toAnchor(delegate.getAnchor());
const actions = delegate.getActions();
+ const context = this.getContext(delegate);
const onHide = () => {
delegate.onHide?.(false);
this.onDidHideContextMenuEmitter.fire();
@@ -63,6 +74,7 @@ export class MonacoContextMenuService implements IContextMenuService {
// In case of 'Quick Fix' actions come as 'CodeActionAction' items
if (actions.length > 0 && actions[0] instanceof MenuItemAction) {
this.contextMenuRenderer.render({
+ context: context,
menuPath: this.menuPath(),
anchor,
onHide
diff --git a/packages/monaco/src/browser/monaco-editor-service.ts b/packages/monaco/src/browser/monaco-editor-service.ts
index 85c57658b2e1b..0138443dabcf3 100644
--- a/packages/monaco/src/browser/monaco-editor-service.ts
+++ b/packages/monaco/src/browser/monaco-editor-service.ts
@@ -143,6 +143,9 @@ export class MonacoEditorService extends StandaloneCodeEditorService {
}
const area = (ref && this.shell.getAreaFor(ref)) || 'main';
const mode = ref && sideBySide ? 'split-right' : undefined;
+ if (area === 'secondaryWindow') {
+ return { area: 'main', mode };
+ }
return { area, mode, ref };
}
diff --git a/packages/monaco/src/browser/monaco-frontend-application-contribution.ts b/packages/monaco/src/browser/monaco-frontend-application-contribution.ts
index 6d9b4251d5b41..e93e9700b8295 100644
--- a/packages/monaco/src/browser/monaco-frontend-application-contribution.ts
+++ b/packages/monaco/src/browser/monaco-frontend-application-contribution.ts
@@ -27,6 +27,12 @@ import { editorOptionsRegistry, IEditorOption } from '@theia/monaco-editor-core/
import { MAX_SAFE_INTEGER } from '@theia/core';
import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/browser/editor-generated-preference-schema';
import { WorkspaceFileService } from '@theia/workspace/lib/common/workspace-file-service';
+import { SecondaryWindowHandler } from '@theia/core/lib/browser/secondary-window-handler';
+import { EditorWidget } from '@theia/editor/lib/browser';
+import { MonacoEditor } from './monaco-editor';
+import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
+import { StandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneThemeService';
+import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
@injectable()
export class MonacoFrontendApplicationContribution implements FrontendApplicationContribution, StylingParticipant {
@@ -47,6 +53,9 @@ export class MonacoFrontendApplicationContribution implements FrontendApplicatio
@inject(WorkspaceFileService) protected readonly workspaceFileService: WorkspaceFileService;
+ @inject(SecondaryWindowHandler)
+ protected readonly secondaryWindowHandler: SecondaryWindowHandler;
+
@postConstruct()
protected init(): void {
this.addAdditionalPreferenceValidations();
@@ -82,6 +91,14 @@ export class MonacoFrontendApplicationContribution implements FrontendApplicatio
'extensions': workspaceExtensions.map(ext => `.${ext}`)
});
}
+ onStart(): void {
+ this.secondaryWindowHandler.onDidAddWidget(([widget, window]) => {
+ if (widget instanceof EditorWidget && widget.editor instanceof MonacoEditor) {
+ const themeService = StandaloneServices.get(IStandaloneThemeService) as StandaloneThemeService;
+ themeService.registerEditorContainer(widget.node);
+ }
+ });
+ }
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
if (isHighContrast(theme.type)) {
diff --git a/packages/monaco/src/browser/monaco-init.ts b/packages/monaco/src/browser/monaco-init.ts
index 1fa6c367b0f40..80ec1fd2d8338 100644
--- a/packages/monaco/src/browser/monaco-init.ts
+++ b/packages/monaco/src/browser/monaco-init.ts
@@ -46,6 +46,8 @@ import { IBulkEditService } from '@theia/monaco-editor-core/esm/vs/editor/browse
import { ICommandService } from '@theia/monaco-editor-core/esm/vs/platform/commands/common/commands';
import { MonacoQuickInputImplementation } from './monaco-quick-input-service';
import { IQuickInputService } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput';
+import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
+import { MonacoStandaloneThemeService } from './monaco-standalone-theme-service';
class MonacoEditorServiceConstructor {
/**
@@ -109,6 +111,7 @@ export namespace MonacoInit {
[IBulkEditService.toString()]: new SyncDescriptor(MonacoBulkEditServiceConstructor, [container]),
[ICommandService.toString()]: new SyncDescriptor(MonacoCommandServiceConstructor, [container]),
[IQuickInputService.toString()]: new SyncDescriptor(MonacoQuickInputImplementationConstructor, [container]),
+ [IStandaloneThemeService.toString()]: new MonacoStandaloneThemeService()
});
}
}
diff --git a/packages/monaco/src/browser/monaco-standalone-theme-service.ts b/packages/monaco/src/browser/monaco-standalone-theme-service.ts
new file mode 100644
index 0000000000000..f36d7083d6b48
--- /dev/null
+++ b/packages/monaco/src/browser/monaco-standalone-theme-service.ts
@@ -0,0 +1,50 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// *****************************************************************************
+// Copyright (C) 2024 STMicroelectronics and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { IDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
+import { StandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneThemeService';
+
+export class MonacoStandaloneThemeService extends StandaloneThemeService {
+ protected get styleElements(): HTMLStyleElement[] {
+ // access private style element array
+ return (this as any)._styleElements;
+ }
+
+ protected get allCSS(): string {
+ return (this as any)._allCSS;
+ }
+
+ override registerEditorContainer(domNode: HTMLElement): IDisposable {
+ const style = domNode.ownerDocument.createElement('style');
+ style.type = 'text/css';
+ style.media = 'screen';
+ style.className = 'monaco-colors';
+ style.textContent = this.allCSS;
+ domNode.ownerDocument.head.appendChild(style);
+ this.styleElements.push(style);
+ return {
+ dispose: () => {
+ for (let i = 0; i < this.styleElements.length; i++) {
+ if (this.styleElements[i] === style) {
+ this.styleElements.splice(i, 1);
+ return;
+ }
+ }
+ }
+ };
+ }
+}
diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
index 18168ac9d4d1e..10c4a6f29b79c 100644
--- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
+++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
@@ -90,6 +90,7 @@ import { CellOutputWebviewImpl, createCellOutputWebviewContainer } from './noteb
import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model';
import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
import { ArgumentProcessorContribution } from './command-registry-main';
+import { WebviewSecondaryWindowSupport } from './webview/webview-secondary-window-support';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
@@ -187,6 +188,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(WebviewWidgetFactory).toDynamicValue(ctx => new WebviewWidgetFactory(ctx.container)).inSingletonScope();
bind(WidgetFactory).toService(WebviewWidgetFactory);
bind(WebviewContextKeys).toSelf().inSingletonScope();
+ bind(WebviewSecondaryWindowSupport).toSelf().inSingletonScope();
+ bind(FrontendApplicationContribution).toService(WebviewSecondaryWindowSupport);
bind(FrontendApplicationContribution).toService(WebviewContextKeys);
bind(CustomEditorContribution).toSelf().inSingletonScope();
diff --git a/packages/plugin-ext/src/main/browser/webview/webview-secondary-window-support.ts b/packages/plugin-ext/src/main/browser/webview/webview-secondary-window-support.ts
new file mode 100644
index 0000000000000..3a756e871fd60
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/webview-secondary-window-support.ts
@@ -0,0 +1,47 @@
+// *****************************************************************************
+// Copyright (C) 2024 STMicroelectronics and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { MaybePromise } from '@theia/core/lib/common';
+import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { SecondaryWindowHandler } from '@theia/core/lib/browser/secondary-window-handler';
+import { WebviewWidget } from './webview';
+
+@injectable()
+export class WebviewSecondaryWindowSupport implements FrontendApplicationContribution {
+ @inject(SecondaryWindowHandler)
+ protected readonly secondaryWindowHandler: SecondaryWindowHandler;
+
+ onStart(app: FrontendApplication): MaybePromise {
+ this.secondaryWindowHandler.onDidAddWidget(([widget, win]) => {
+ if (widget instanceof WebviewWidget) {
+ const script = win.document.createElement('script');
+ script.text = `
+ window.addEventListener('message', e => {
+ // Only process messages from Theia main window
+ if (e.source === window.opener) {
+ // Delegate message to iframe
+ const frame = window.document.getElementsByTagName('iframe').item(0);
+ if (frame) {
+ frame.contentWindow?.postMessage({ ...e.data }, '*');
+ }
+ }
+ }); `;
+ win.document.head.append(script);
+ }
+ });
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 7ffee9d042681..489221288bada 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3900,7 +3900,7 @@ chromium-bidi@0.4.4:
dependencies:
mitt "3.0.0"
-ci-info@^3.2.0, ci-info@^3.6.1:
+ci-info@^3.2.0, ci-info@^3.6.1, ci-info@^3.7.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
@@ -5784,6 +5784,13 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
+find-yarn-workspace-root@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
+ integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
+ dependencies:
+ micromatch "^4.0.2"
+
fix-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fix-path/-/fix-path-3.0.0.tgz#c6b82fd5f5928e520b392a63565ebfef0ddf037e"
@@ -5913,7 +5920,7 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
-fs-extra@^9.0.8:
+fs-extra@^9.0.0, fs-extra@^9.0.8:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
@@ -7069,7 +7076,7 @@ is-windows@^1.0.2:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-is-wsl@^2.2.0:
+is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
@@ -7322,6 +7329,16 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
+json-stable-stringify@^1.0.2:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454"
+ integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==
+ dependencies:
+ call-bind "^1.0.5"
+ isarray "^2.0.5"
+ jsonify "^0.0.1"
+ object-keys "^1.1.1"
+
json-stringify-safe@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -7370,6 +7387,11 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
+jsonify@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978"
+ integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==
+
jsonparse@^1.2.0, jsonparse@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@@ -7423,6 +7445,13 @@ kind-of@^6.0.2, kind-of@^6.0.3:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+klaw-sync@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
+ integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+
lazystream@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638"
@@ -8001,7 +8030,7 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
-micromatch@^4.0.4:
+micromatch@^4.0.2, micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
@@ -8991,6 +9020,14 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
+open@^7.4.2:
+ version "7.4.2"
+ resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
+ integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
+ dependencies:
+ is-docker "^2.0.0"
+ is-wsl "^2.1.1"
+
open@^8.4.0:
version "8.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
@@ -9282,6 +9319,27 @@ parseurl@~1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+patch-package@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61"
+ integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==
+ dependencies:
+ "@yarnpkg/lockfile" "^1.1.0"
+ chalk "^4.1.2"
+ ci-info "^3.7.0"
+ cross-spawn "^7.0.3"
+ find-yarn-workspace-root "^2.0.0"
+ fs-extra "^9.0.0"
+ json-stable-stringify "^1.0.2"
+ klaw-sync "^6.0.0"
+ minimist "^1.2.6"
+ open "^7.4.2"
+ rimraf "^2.6.3"
+ semver "^7.5.3"
+ slash "^2.0.0"
+ tmp "^0.0.33"
+ yaml "^2.2.2"
+
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
@@ -10205,7 +10263,7 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-rimraf@2, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@2, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -10634,6 +10692,11 @@ slash@^1.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+ integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
@@ -10896,14 +10959,6 @@ string-argv@^0.1.1:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738"
integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==
-string-replace-loader@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-3.1.0.tgz#11ac6ee76bab80316a86af358ab773193dd57a4f"
- integrity sha512-5AOMUZeX5HE/ylKDnEa/KKBqvlnFmRZudSOjVJHxhoJg9QYTwl1rECx7SLR8BBH7tfxb4Rp7EM2XVfQFxIhsbQ==
- dependencies:
- loader-utils "^2.0.0"
- schema-utils "^3.0.0"
-
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -12377,6 +12432,11 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+yaml@^2.2.2:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed"
+ integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==
+
yargs-parser@20.2.4:
version "20.2.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"