diff --git a/packages/core/src/browser/extension-open-handler.ts b/packages/core/src/browser/extension-open-handler.ts new file mode 100644 index 0000000000000..a541ee01b0189 --- /dev/null +++ b/packages/core/src/browser/extension-open-handler.ts @@ -0,0 +1,63 @@ +// ***************************************************************************** +// 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 { injectable } from 'inversify'; +import { MaybePromise, URI } from '../common'; +import { OpenHandler, OpenerOptions } from './opener-service'; + +export interface UriHandler { + canHandleURI(uri: URI): boolean; + handleUri(uri: URI): Promise; +} + +@injectable() +export class ExtensionOpenHandler implements OpenHandler { + + readonly id = 'extensionsURIHandlers'; + + private providers = new Map(); + + canHandle(uri: URI, options?: OpenerOptions | undefined): MaybePromise { + if (!uri.scheme.startsWith('theia')) { + return 0; + } + + const authority = uri.authority; + const handler = this.providers.get(authority); + if (handler?.canHandleURI(uri)) { + return 500; + } + return 0; + } + + open(uri: URI, options?: OpenerOptions | undefined): MaybePromise { + const authority = uri.authority; + const provider = this.providers.get(authority); + if (provider) { + return provider.handleUri(uri); + } + return Promise.reject(`Impossible to handle ${uri}.`); + } + + public registerHandler(extensionId: string, handler: UriHandler): void { + this.providers.set(extensionId, handler); + } + + public unregisterHandler(extensionId: string): void { + this.providers.delete(extensionId); + } + +} diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 3dd79dd12d489..6befa8fd57e6a 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -140,6 +140,7 @@ import { HoverService } from './hover-service'; import { AdditionalViewsMenuWidget, AdditionalViewsMenuWidgetFactory } from './shell/additional-views-menu-widget'; import { LanguageIconLabelProvider } from './language-icon-provider'; import { bindTreePreferences } from './tree'; +import { ExtensionOpenHandler } from './extension-open-handler'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -455,4 +456,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(FrontendApplicationContribution).toService(StylingService); bind(SecondaryWindowHandler).toSelf().inSingletonScope(); + + bind(ExtensionOpenHandler).toSelf().inSingletonScope(); + bind(OpenHandler).toService(ExtensionOpenHandler); }); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 32140ec1c0625..0eee329655f57 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -2180,6 +2180,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(handle: number, uri: UriComponents): Promise; +} + +export interface UriMain { + $registerUriHandler(handle: number, extensionId: string, extensionName: string): void; + $unregisterUriHandler(handle: number): void; +} + export interface TestControllerUpdate { label: string; canRefresh: boolean; @@ -2257,7 +2268,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 = { @@ -2299,7 +2311,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/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index a7728f3816015..b4c89062ac6b7 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -69,6 +69,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); @@ -211,4 +212,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/uri-main.ts b/packages/plugin-ext/src/main/browser/uri-main.ts new file mode 100644 index 0000000000000..17b3f9f73e27e --- /dev/null +++ b/packages/plugin-ext/src/main/browser/uri-main.ts @@ -0,0 +1,76 @@ +// ***************************************************************************** +// 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 { ExtensionOpenHandler, UriHandler} from '@theia/core/lib/browser/extension-open-handler'; +import { MAIN_RPC_CONTEXT, UriExt, UriMain } from '../../common'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { interfaces } from '@theia/core/shared/inversify'; + +export class UriMainImpl implements UriMain, Disposable { + + private readonly proxy: UriExt; + private readonly handlers = new Map(); + private readonly extensionOpenHandler: ExtensionOpenHandler; + + constructor(rpc: RPCProtocol, container: interfaces.Container) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.URI_EXT); + this.extensionOpenHandler = container.get(ExtensionOpenHandler); + } + + dispose(): void { + this.handlers.clear(); + } + + async $registerUriHandler(handle: number, extensionId: string, extensionDisplayName: string): Promise { + const extensionUrlHandler = new ExtensionUriHandler(this.proxy, handle, extensionId, extensionDisplayName); + this.extensionOpenHandler.registerHandler(extensionId, extensionUrlHandler); + this.handlers.set(handle, extensionId); + + return Promise.resolve(undefined); + } + + async $unregisterUriHandler(handle: number): Promise { + const extensionId = this.handlers.get(handle); + if (extensionId) { + this.handlers.delete(handle); + this.extensionOpenHandler.unregisterHandler(extensionId); + } + } + +} + +class ExtensionUriHandler implements UriHandler { + + constructor( + private proxy: UriExt, + private readonly handle: number, + readonly extensionId: string, + readonly extensionDisplayName: string + ) { } + + canHandleURI(uri: URI): boolean { + return uri.authority === this.extensionId; + } + + handleUri(uri: URI): Promise { + if (this.extensionId !== uri.authority) { + return Promise.resolve(false); + } + + return Promise.resolve(this.proxy.$handleExternalUri(this.handle, uri.toComponents())).then(() => true); + } +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index c90338531dd4e..34862bbcb65ec 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -250,6 +250,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, @@ -298,6 +299,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 { @@ -570,8 +572,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/uri-ext.ts b/packages/plugin-ext/src/plugin/uri-ext.ts new file mode 100644 index 0000000000000..80bcea55d5eef --- /dev/null +++ b/packages/plugin-ext/src/plugin/uri-ext.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 * as theia from '@theia/plugin'; +import { + UriExt, + PLUGIN_RPC_CONTEXT, PluginInfo, UriMain +} from '../common/plugin-api-rpc'; +import { RPCProtocol } from '../common/rpc-protocol'; +import { Disposable, URI } from './types-impl'; +import { UriComponents } from '../common/uri-components'; + +export class UriExtImpl implements UriExt { + + private static handle = 0; + private handles = new Set(); + private handlers = new Map(); + + 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 extensionId = plugin.id; + if (this.handles.has(extensionId)) { + throw new Error(`URI handler already registered for extension ${extensionId}`); + } + + const handle = UriExtImpl.handle++; + this.handles.add(extensionId); + this.handlers.set(handle, handler); + this.proxy.$registerUriHandler(handle, extensionId, plugin.displayName || plugin.name); + + return new Disposable(() => { + this.proxy.$unregisterUriHandler(handle); + this.handles.delete(extensionId); + this.handlers.delete(handle); + }); + } + + $handleExternalUri(handle: number, uri: UriComponents): Promise { + const handler = this.handlers.get(handle); + + if (!handler) { + return Promise.resolve(undefined); + } + try { + handler.handleUri(URI.revive(uri)); + } catch (err) { + console.log(`error while handling external uri: ${uri}`); + } + + return Promise.resolve(undefined); + } + +}