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"