diff --git a/CHANGELOG.md b/CHANGELOG.md index 697912237e877..950e4dc037a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,15 @@ - [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/) - ## Unreleased +- [plugin] implement stubbed API window registerUriHandler() [#13306](https://github.com/eclipse-theia/theia/pull/13306) - contributed on behalf of STMicroelectronics - [core] Download json schema catalog at build-time - [#14065](https://github.com/eclipse-theia/theia/pull/14065/) - Contributed on behalf of STMicroelectronics [Breaking Changes:](#breaking_changes_1.53.0) - [dependencies] Updated electron to version 30.1.2 - [#14041](https://github.com/eclipse-theia/theia/pull/14041) - Contributed on behalf of STMicroelectronics - [dependencies] increased minimum node version to 18. [#14027](https://github.com/eclipse-theia/theia/pull/14027) - Contributed on behalf of STMicroelectronics + ## 1.52.0 - 07/25/2024 - [application-package] bumped the default supported API from `1.90.2` to `1.91.1` [#13955](https://github.com/eclipse-theia/theia/pull/13955) - Contributed on behalf of STMicroelectronics diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts index cf9320e7120fd..ca843a7e6729e 100644 --- a/dev-packages/application-package/src/application-props.ts +++ b/dev-packages/application-package/src/application-props.ts @@ -34,7 +34,8 @@ export namespace ElectronFrontendApplicationConfig { export const DEFAULT: ElectronFrontendApplicationConfig = { windowOptions: {}, showWindowEarly: true, - splashScreenOptions: {} + splashScreenOptions: {}, + uriScheme: 'theia' }; export interface SplashScreenOptions { /** @@ -85,6 +86,11 @@ export namespace ElectronFrontendApplicationConfig { * Defaults to `{}` which results in no splash screen being displayed. */ readonly splashScreenOptions?: SplashScreenOptions; + + /** + * The custom uri scheme the application registers to in the operating system. + */ + readonly uriScheme: string; } } @@ -122,7 +128,8 @@ export namespace FrontendApplicationConfig { electron: ElectronFrontendApplicationConfig.DEFAULT, defaultLocale: '', validatePreferencesSchema: true, - reloadOnReconnect: false + reloadOnReconnect: false, + uriScheme: 'theia' }; export interface Partial extends ApplicationConfig { diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 3ef33e6fe8238..51131f9b1ce93 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -465,7 +465,6 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(FrontendApplicationContribution).toService(StylingService); bind(SecondaryWindowHandler).toSelf().inSingletonScope(); - bind(ViewColumnService).toSelf().inSingletonScope(); bind(UndoRedoHandlerService).toSelf().inSingletonScope(); diff --git a/packages/core/src/browser/opener-service.ts b/packages/core/src/browser/opener-service.ts index e15418e3e6b29..d4f58f44a6fac 100644 --- a/packages/core/src/browser/opener-service.ts +++ b/packages/core/src/browser/opener-service.ts @@ -79,6 +79,12 @@ export interface OpenerService { * Add open handler i.e. for custom editors */ addHandler?(openHandler: OpenHandler): Disposable; + + /** + * Remove open handler + */ + removeHandler?(openHandler: OpenHandler): void; + /** * Event that fires when a new opener is added or removed. */ @@ -108,11 +114,15 @@ export class DefaultOpenerService implements OpenerService { this.onDidChangeOpenersEmitter.fire(); return Disposable.create(() => { - this.customEditorOpenHandlers.splice(this.customEditorOpenHandlers.indexOf(openHandler), 1); - this.onDidChangeOpenersEmitter.fire(); + this.removeHandler(openHandler); }); } + removeHandler(openHandler: OpenHandler): void { + this.customEditorOpenHandlers.splice(this.customEditorOpenHandlers.indexOf(openHandler), 1); + this.onDidChangeOpenersEmitter.fire(); + } + async getOpener(uri: URI, options?: OpenerOptions): Promise { const handlers = await this.prioritize(uri, options); if (handlers.length >= 1) { diff --git a/packages/core/src/electron-browser/electron-uri-handler.ts b/packages/core/src/electron-browser/electron-uri-handler.ts new file mode 100644 index 0000000000000..c3fdd3a993dae --- /dev/null +++ b/packages/core/src/electron-browser/electron-uri-handler.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// 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 { FrontendApplicationContribution, OpenerService } from '../browser'; + +import { injectable, inject } from 'inversify'; +import { URI } from '../common'; + +@injectable() +export class ElectronUriHandlerContribution implements FrontendApplicationContribution { + @inject(OpenerService) + protected readonly openenerService: OpenerService; + + initialize(): void { + window.electronTheiaCore.setOpenUrlHandler(async url => { + const uri = new URI(url); + try { + const handler = await this.openenerService.getOpener(uri); + if (handler) { + await handler.open(uri); + return true; + } + } catch (e) { + // no handler + } + return false; + }); + } +} diff --git a/packages/core/src/electron-browser/preload.ts b/packages/core/src/electron-browser/preload.ts index 43eabd8271cca..fb87355fc8dda 100644 --- a/packages/core/src/electron-browser/preload.ts +++ b/packages/core/src/electron-browser/preload.ts @@ -26,7 +26,8 @@ import { CHANNEL_IS_FULL_SCREEN, CHANNEL_SET_MENU_BAR_VISIBLE, CHANNEL_REQUEST_CLOSE, CHANNEL_SET_TITLE_STYLE, CHANNEL_RESTART, CHANNEL_REQUEST_RELOAD, CHANNEL_APP_STATE_CHANGED, CHANNEL_SHOW_ITEM_IN_FOLDER, CHANNEL_READ_CLIPBOARD, CHANNEL_WRITE_CLIPBOARD, CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE, CHANNEL_SET_BACKGROUND_COLOR, - CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP + CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP, + CHANNEL_OPEN_URL } from '../electron-common/electron-api'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -38,6 +39,16 @@ let nextHandlerId = 0; const mainMenuId = 0; let nextMenuId = mainMenuId + 1; +let openUrlHandler: ((url: string) => Promise) | undefined; + +ipcRenderer.on(CHANNEL_OPEN_URL, async (event: Electron.IpcRendererEvent, url: string, replyChannel: string) => { + if (openUrlHandler) { + event.sender.send(replyChannel, await openUrlHandler(url)); + } else { + event.sender.send(replyChannel, false); + } +}); + function convertMenu(menu: MenuDto[] | undefined, handlerMap: Map void>): InternalMenuDto[] | undefined { if (!menu) { return undefined; @@ -135,6 +146,10 @@ const api: TheiaCoreAPI = { return Disposable.create(() => ipcRenderer.off(CHANNEL_ABOUT_TO_CLOSE, h)); }, + setOpenUrlHandler(handler: (url: string) => Promise): void { + openUrlHandler = handler; + }, + onWindowEvent: function (event: WindowEvent, handler: () => void): Disposable { const h = (_event: unknown, evt: WindowEvent) => { if (event === evt) { diff --git a/packages/core/src/electron-browser/window/electron-window-module.ts b/packages/core/src/electron-browser/window/electron-window-module.ts index 78f490c1c98db..b4edcee810a10 100644 --- a/packages/core/src/electron-browser/window/electron-window-module.ts +++ b/packages/core/src/electron-browser/window/electron-window-module.ts @@ -29,6 +29,7 @@ import { ElectronSecondaryWindowService } from './electron-secondary-window-serv import { bindWindowPreferences } from './electron-window-preferences'; import { ElectronWindowService } from './electron-window-service'; import { ExternalAppOpenHandler } from './external-app-open-handler'; +import { ElectronUriHandlerContribution } from '../electron-uri-handler'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMainWindowService).toDynamicValue(context => @@ -37,6 +38,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindWindowPreferences(bind); bind(WindowService).to(ElectronWindowService).inSingletonScope(); bind(FrontendApplicationContribution).toService(WindowService); + bind(ElectronUriHandlerContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(ElectronUriHandlerContribution); bind(ClipboardService).to(ElectronClipboardService).inSingletonScope(); rebind(FrontendApplicationStateService).to(ElectronFrontendApplicationStateService).inSingletonScope(); bind(SecondaryWindowService).to(ElectronSecondaryWindowService).inSingletonScope(); diff --git a/packages/core/src/electron-browser/window/external-app-open-handler.ts b/packages/core/src/electron-browser/window/external-app-open-handler.ts index d74c3a2378e1d..26f86c64be266 100644 --- a/packages/core/src/electron-browser/window/external-app-open-handler.ts +++ b/packages/core/src/electron-browser/window/external-app-open-handler.ts @@ -36,7 +36,7 @@ export class ExternalAppOpenHandler implements OpenHandler { async open(uri: URI): Promise { // For files 'file:' scheme, system accepts only the path. // For other protocols e.g. 'vscode:' we use the full URI to propagate target app information. - window.electronTheiaCore.openWithSystemApp(uri.scheme === 'file' ? uri.path.fsPath() : uri.toString(true)); + window.electronTheiaCore.openWithSystemApp(uri.toString(true)); return undefined; } } diff --git a/packages/core/src/electron-common/electron-api.ts b/packages/core/src/electron-common/electron-api.ts index 4edab9cb6f7b0..79035b3e00d37 100644 --- a/packages/core/src/electron-common/electron-api.ts +++ b/packages/core/src/electron-common/electron-api.ts @@ -74,6 +74,8 @@ export interface TheiaCoreAPI { onAboutToClose(handler: () => void): Disposable; setCloseRequestHandler(handler: (reason: StopReason) => Promise): void; + setOpenUrlHandler(handler: (url: string) => Promise): void; + setSecondaryWindowCloseRequestHandler(windowName: string, handler: () => Promise): void; toggleDevTools(): void; @@ -129,6 +131,7 @@ export const CHANNEL_MAXIMIZE = 'Maximize'; export const CHANNEL_IS_MAXIMIZED = 'IsMaximized'; export const CHANNEL_ABOUT_TO_CLOSE = 'AboutToClose'; +export const CHANNEL_OPEN_URL = 'OpenUrl'; export const CHANNEL_UNMAXIMIZE = 'UnMaximize'; export const CHANNEL_ON_WINDOW_EVENT = 'OnWindowEvent'; diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts index d5966536dd470..ab58fb28cb2c9 100644 --- a/packages/core/src/electron-main/electron-api-main.ts +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -54,7 +54,8 @@ import { CHANNEL_SET_BACKGROUND_COLOR, CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, - CHANNEL_OPEN_WITH_SYSTEM_APP + CHANNEL_OPEN_WITH_SYSTEM_APP, + CHANNEL_OPEN_URL } from '../electron-common/electron-api'; import { ElectronMainApplication, ElectronMainApplicationContribution } from './electron-main-application'; import { Disposable, DisposableCollection, isOSX, MaybePromise } from '../common'; @@ -165,8 +166,8 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { shell.showItemInFolder(fsPath); }); - ipcMain.on(CHANNEL_OPEN_WITH_SYSTEM_APP, (event, fsPath) => { - shell.openPath(fsPath); + ipcMain.on(CHANNEL_OPEN_WITH_SYSTEM_APP, (event, uri) => { + shell.openExternal(uri); }); ipcMain.handle(CHANNEL_GET_TITLE_STYLE_AT_STARTUP, event => application.getTitleBarStyleAtStartup(event.sender)); @@ -274,6 +275,20 @@ export namespace TheiaRendererAPI { wc.send(CHANNEL_ON_WINDOW_EVENT, event); } + export function openUrl(wc: WebContents, url: string): Promise { + return new Promise(resolve => { + const channelNr = nextReplyChannel++; + const replyChannel = `openUrl${channelNr}`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const l = createDisposableListener(ipcMain, replyChannel, (e, args: any[]) => { + l.dispose(); + resolve(args[0]); + }); + + wc.send(CHANNEL_OPEN_URL, url, replyChannel); + }); + } + export function sendAboutToClose(wc: WebContents): Promise { return new Promise(resolve => { const channelNr = nextReplyChannel++; diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 5a895f7ca2221..b464c916a84d9 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -117,13 +117,13 @@ export class ElectronMainProcessArgv { return 1; } - protected get isBundledElectronApp(): boolean { + get isBundledElectronApp(): boolean { // process.defaultApp is either set by electron in an electron unbundled app, or undefined // see https://github.com/electron/electron/blob/master/docs/api/process.md#processdefaultapp-readonly return this.isElectronApp && !(process as ElectronMainProcessArgv.ElectronMainProcess).defaultApp; } - protected get isElectronApp(): boolean { + get isElectronApp(): boolean { // process.versions.electron is either set by electron, or undefined // see https://github.com/electron/electron/blob/master/docs/api/process.md#processversionselectron-readonly return !!(process as ElectronMainProcessArgv.ElectronMainProcess).versions.electron; @@ -183,6 +183,7 @@ export class ElectronMainApplication { protected customBackgroundColor?: string; protected didUseNativeWindowFrameOnStart = new Map(); protected windows = new Map(); + protected activeWindowStack: number[] = []; protected restarting = false; /** Used to temporarily store the reference to an early created main window */ @@ -229,7 +230,7 @@ export class ElectronMainApplication { this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native'; this._config = config; this.hookApplicationEvents(); - this.showInitialWindow(); + this.showInitialWindow(argv.includes('--open-url') ? argv[argv.length - 1] : undefined); const port = await this.startBackend(); this._backendPort.resolve(port); await app.whenReady(); @@ -317,7 +318,7 @@ export class ElectronMainApplication { !('THEIA_ELECTRON_NO_EARLY_WINDOW' in process.env && process.env.THEIA_ELECTRON_NO_EARLY_WINDOW === '1'); } - protected showInitialWindow(): void { + protected showInitialWindow(urlToOpen: string | undefined): void { if (this.isShowWindowEarly() || this.isShowSplashScreen()) { app.whenReady().then(async () => { const options = await this.getLastWindowOptions(); @@ -326,7 +327,11 @@ export class ElectronMainApplication { options.preventAutomaticShow = true; } this.initialWindow = await this.createWindow({ ...options }); - + TheiaRendererAPI.onApplicationStateChanged(this.initialWindow.webContents, state => { + if (state === 'ready' && urlToOpen) { + this.openUrl(urlToOpen); + } + }); if (this.isShowSplashScreen()) { console.log('Showing splash screen'); this.configureAndShowSplashScreen(this.initialWindow); @@ -410,11 +415,25 @@ export class ElectronMainApplication { options = this.avoidOverlap(options); const electronWindow = this.windowFactory(options, this.config); const id = electronWindow.window.webContents.id; + this.activeWindowStack.push(id); this.windows.set(id, electronWindow); - electronWindow.onDidClose(() => this.windows.delete(id)); + electronWindow.onDidClose(() => { + const stackIndex = this.activeWindowStack.indexOf(id); + if (stackIndex >= 0) { + this.activeWindowStack.splice(stackIndex, 1); + } + this.windows.delete(id); + }); electronWindow.window.on('maximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'maximize')); electronWindow.window.on('unmaximize', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'unmaximize')); - electronWindow.window.on('focus', () => TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus')); + electronWindow.window.on('focus', () => { + const stackIndex = this.activeWindowStack.indexOf(id); + if (stackIndex >= 0) { + this.activeWindowStack.splice(stackIndex, 1); + } + this.activeWindowStack.unshift(id); + TheiaRendererAPI.sendWindowEvent(electronWindow.window.webContents, 'focus'); + }); this.attachSaveWindowState(electronWindow.window); return electronWindow.window; @@ -521,6 +540,15 @@ export class ElectronMainApplication { } } + async openUrl(url: string): Promise { + for (const id of this.activeWindowStack) { + const window = this.windows.get(id); + if (window && await window.openUrl(url)) { + break; + } + } + } + protected async createWindowUri(params: WindowSearchParams = {}): Promise { if (!('port' in params)) { params.port = (await this.backendPort).toString(); @@ -696,6 +724,16 @@ export class ElectronMainApplication { app.on('second-instance', this.onSecondInstance.bind(this)); app.on('window-all-closed', this.onWindowAllClosed.bind(this)); app.on('web-contents-created', this.onWebContentsCreated.bind(this)); + + if (isWindows) { + const args = this.processArgv.isBundledElectronApp ? [] : [app.getAppPath()]; + args.push('--open-url'); + app.setAsDefaultProtocolClient(this.config.electron.uriScheme, process.execPath, args); + } else { + app.on('open-url', (evt, url) => { + this.openUrl(url); + }); + } } protected onWillQuit(event: ElectronEvent): void { @@ -703,19 +741,23 @@ export class ElectronMainApplication { } protected async onSecondInstance(event: ElectronEvent, argv: string[], cwd: string): Promise { - createYargs(this.processArgv.getProcessArgvWithoutBin(argv), process.cwd()) - .help(false) - .command('$0 [file]', false, - cmd => cmd - .positional('file', { type: 'string' }), - async args => { - this.handleMainCommand({ - file: args.file, - cwd: process.cwd(), - secondInstance: true - }); - }, - ).parse(); + if (argv.includes('--open-url')) { + this.openUrl(argv[argv.length - 1]); + } else { + createYargs(this.processArgv.getProcessArgvWithoutBin(argv), process.cwd()) + .help(false) + .command('$0 [file]', false, + cmd => cmd + .positional('file', { type: 'string' }), + async args => { + await this.handleMainCommand({ + file: args.file, + cwd: process.cwd(), + secondInstance: true + }); + }, + ).parse(); + } } protected onWebContentsCreated(event: ElectronEvent, webContents: WebContents): void { diff --git a/packages/core/src/electron-main/theia-electron-window.ts b/packages/core/src/electron-main/theia-electron-window.ts index 29569d4970593..720865f8f59e1 100644 --- a/packages/core/src/electron-main/theia-electron-window.ts +++ b/packages/core/src/electron-main/theia-electron-window.ts @@ -58,6 +58,7 @@ enum ClosingState { @injectable() export class TheiaElectronWindow { + @inject(TheiaBrowserWindowOptions) protected readonly options: TheiaBrowserWindowOptions; @inject(WindowApplicationConfig) protected readonly config: WindowApplicationConfig; @inject(ElectronMainApplicationGlobals) protected readonly globals: ElectronMainApplicationGlobals; @@ -202,6 +203,10 @@ export class TheiaElectronWindow { this.toDispose.push(TheiaRendererAPI.onRequestReload(this.window.webContents, (newUrl?: string) => this.reload(newUrl))); } + openUrl(url: string): Promise { + return TheiaRendererAPI.openUrl(this.window.webContents, url); + } + dispose(): void { this.toDispose.dispose(); } diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 0e6c0570041ea..ed117bb1eac19 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -181,6 +181,7 @@ export interface EnvInit { appName: string; appHost: string; appRoot: string; + appUriScheme: string; } export interface PluginAPI { @@ -2241,6 +2242,17 @@ export interface TestingExt { $onResolveChildren(controllerId: string, path: string[]): void; } +// based from https://github.com/microsoft/vscode/blob/1.85.1/src/vs/workbench/api/common/extHostUrls.ts +export interface UriExt { + registerUriHandler(handler: theia.UriHandler, plugin: PluginInfo): theia.Disposable; + $handleExternalUri(uri: UriComponents): Promise; +} + +export interface UriMain { + $registerUriHandler(extensionId: string, extensionName: string): void; + $unregisterUriHandler(extensionId: string): void; +} + export interface TestControllerUpdate { label: string; canRefresh: boolean; @@ -2318,7 +2330,8 @@ export const PLUGIN_RPC_CONTEXT = { TABS_MAIN: >createProxyIdentifier('TabsMain'), TELEMETRY_MAIN: >createProxyIdentifier('TelemetryMain'), LOCALIZATION_MAIN: >createProxyIdentifier('LocalizationMain'), - TESTING_MAIN: createProxyIdentifier('TestingMain') + TESTING_MAIN: createProxyIdentifier('TestingMain'), + URI_MAIN: createProxyIdentifier('UriMain') }; export const MAIN_RPC_CONTEXT = { @@ -2360,7 +2373,8 @@ export const MAIN_RPC_CONTEXT = { COMMENTS_EXT: createProxyIdentifier('CommentsExt'), TABS_EXT: createProxyIdentifier('TabsExt'), TELEMETRY_EXT: createProxyIdentifier('TelemetryExt)'), - TESTING_EXT: createProxyIdentifier('TestingExt') + TESTING_EXT: createProxyIdentifier('TestingExt'), + URI_EXT: createProxyIdentifier('UriExt') }; export interface TasksExt { diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 43fadf44f7e3a..fb4c21585fcf5 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -328,7 +328,8 @@ export class HostedPluginSupport extends AbstractHostedPluginSupport { + await this.activateByEvent(`onUri:${scheme}://${authority}`); + } + async activateByCommand(commandId: string): Promise { await this.activateByEvent(`onCommand:${commandId}`); } diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 262926efc80d9..6467bf99c2b35 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -64,6 +64,7 @@ import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main'; import { NotebookKernelsMainImpl } from './notebooks/notebook-kernels-main'; import { NotebooksAndEditorsMain } from './notebooks/notebook-documents-and-editors-main'; import { TestingMainImpl } from './test-main'; +import { UriMainImpl } from './uri-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const authenticationMain = new AuthenticationMainImpl(rpc, container); @@ -203,4 +204,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const localizationMain = new LocalizationMainImpl(container); rpc.set(PLUGIN_RPC_CONTEXT.LOCALIZATION_MAIN, localizationMain); + + const uriMain = new UriMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.URI_MAIN, uriMain); } 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 68874ede43d97..a8510268870f3 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 @@ -286,6 +286,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(CellOutputWebviewFactory).toFactory(ctx => async (cell: NotebookCellModel, notebook: NotebookModel) => createCellOutputWebviewContainer(ctx.container, cell, notebook).getAsync(CellOutputWebviewImpl) ); - bindContributionProvider(bind, ArgumentProcessorContribution); + }); diff --git a/packages/plugin-ext/src/main/browser/uri-main.ts b/packages/plugin-ext/src/main/browser/uri-main.ts new file mode 100644 index 0000000000000..6e18cdea5241c --- /dev/null +++ b/packages/plugin-ext/src/main/browser/uri-main.ts @@ -0,0 +1,72 @@ +// ***************************************************************************** +// Copyright (C) 2024 STMicroelectronics. +// +// 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 { Disposable, URI } from '@theia/core'; +import { MAIN_RPC_CONTEXT, UriExt, UriMain } from '../../common'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { interfaces } from '@theia/core/shared/inversify'; +import { OpenHandler, OpenerOptions, OpenerService } from '@theia/core/lib/browser'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; + +export class UriMainImpl implements UriMain, Disposable { + private readonly proxy: UriExt; + private handlers = new Set(); + private readonly openerService: OpenerService; + private readonly pluginSupport: HostedPluginSupport; + private readonly openHandler: OpenHandler; + + constructor(rpc: RPCProtocol, container: interfaces.Container) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.URI_EXT); + this.openerService = container.get(OpenerService); + this.pluginSupport = container.get(HostedPluginSupport); + + this.openHandler = { + id: 'theia-plugin-open-handler', + canHandle: async (uri: URI, options?: OpenerOptions | undefined): Promise => { + if (uri.scheme !== FrontendApplicationConfigProvider.get().electron.uriScheme) { + return 0; + } + await this.pluginSupport.activateByUri(uri.scheme, uri.authority); + if (this.handlers.has(uri.authority)) { + return 500; + } + return 0; + }, + open: async (uri: URI, options?: OpenerOptions | undefined): Promise => { + if (!this.handlers.has(uri.authority)) { + throw new Error(`No plugin to handle this uri: : '${uri}'`); + } + this.proxy.$handleExternalUri(uri.toComponents()); + } + }; + + this.openerService.addHandler?.(this.openHandler); + } + + dispose(): void { + this.openerService.removeHandler?.(this.openHandler); + this.handlers.clear(); + } + + async $registerUriHandler(pluginId: string, extensionDisplayName: string): Promise { + this.handlers.add(pluginId); + } + + async $unregisterUriHandler(pluginId: string): Promise { + this.handlers.delete(pluginId); + } +} diff --git a/packages/plugin-ext/src/plugin/env.ts b/packages/plugin-ext/src/plugin/env.ts index 14d2bda0b9878..bb9c405bb6a55 100644 --- a/packages/plugin-ext/src/plugin/env.ts +++ b/packages/plugin-ext/src/plugin/env.ts @@ -35,6 +35,7 @@ export abstract class EnvExtImpl { private envSessionId: string; private host: string; private applicationRoot: string; + private appUriScheme: string; private _remoteName: string | undefined; constructor() { @@ -89,6 +90,10 @@ export abstract class EnvExtImpl { this.applicationRoot = appRoot; } + setAppUriScheme(uriScheme: string): void { + this.appUriScheme = uriScheme; + } + getClientOperatingSystem(): Promise { return this.proxy.$getClientOperatingSystem(); } @@ -121,7 +126,7 @@ export abstract class EnvExtImpl { return this.envSessionId; } get uriScheme(): string { - return 'theia'; + return this.appUriScheme; } get uiKind(): theia.UIKind { return this.ui; diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index edac3d7b2b7dc..8a36492244edc 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -276,6 +276,7 @@ import { NotebookKernelsExtImpl } from './notebook/notebook-kernels'; import { NotebookDocumentsExtImpl } from './notebook/notebook-documents'; import { NotebookEditorsExtImpl } from './notebook/notebook-editors'; import { TestingExtImpl } from './tests'; +import { UriExtImpl } from './uri-ext'; export function createAPIFactory( rpc: RPCProtocol, @@ -324,6 +325,7 @@ export function createAPIFactory( const webviewViewsExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEW_VIEWS_EXT, new WebviewViewsExtImpl(rpc, webviewExt)); const telemetryExt = rpc.set(MAIN_RPC_CONTEXT.TELEMETRY_EXT, new TelemetryExtImpl()); const testingExt = rpc.set(MAIN_RPC_CONTEXT.TESTING_EXT, new TestingExtImpl(rpc, commandRegistry)); + const uriExt = rpc.set(MAIN_RPC_CONTEXT.URI_EXT, new UriExtImpl(rpc)); rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt); return function (plugin: InternalPlugin): typeof theia { @@ -596,8 +598,7 @@ export function createAPIFactory( return decorationsExt.registerFileDecorationProvider(provider, pluginToPluginInfo(plugin)); }, registerUriHandler(handler: theia.UriHandler): theia.Disposable { - // TODO ? - return new Disposable(() => { }); + return uriExt.registerUriHandler(handler, pluginToPluginInfo(plugin)); }, createInputBox(): theia.InputBox { return quickOpenExt.createInputBox(plugin); diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts index 47ced24b00a76..b0fa44833e77e 100644 --- a/packages/plugin-ext/src/plugin/plugin-manager.ts +++ b/packages/plugin-ext/src/plugin/plugin-manager.ts @@ -252,7 +252,7 @@ export abstract class AbstractPluginManagerExtImpl

} for (let activationEvent of activationEvents) { if (activationEvent === 'onUri') { - activationEvent = `onUri:theia://${plugin.model.id}`; + activationEvent = `onUri:${this.envExt.uriScheme}://${plugin.model.id}`; } this.setActivation(activationEvent, activation); } @@ -481,6 +481,7 @@ export class PluginManagerExtImpl extends AbstractPluginManagerExtImpl(); + + private readonly proxy: UriMain; + + constructor(readonly rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.URI_MAIN); + console.log(this.proxy); + } + + registerUriHandler(handler: theia.UriHandler, plugin: PluginInfo): theia.Disposable { + const pluginId = plugin.id; + if (this.handlers.has(pluginId)) { + throw new Error(`URI handler already registered for plugin ${pluginId}`); + } + + this.handlers.set(pluginId, handler); + this.proxy.$registerUriHandler(pluginId, plugin.displayName || plugin.name); + + return new Disposable(() => { + this.proxy.$unregisterUriHandler(pluginId); + this.handlers.delete(pluginId); + }); + } + + $handleExternalUri(uri: UriComponents): Promise { + const handler = this.handlers.get(uri.authority); + if (!handler) { + return Promise.resolve(); + } + handler.handleUri(URI.revive(uri)); + return Promise.resolve(); + } +}