diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index 10a24e06f0598..be12463809da9 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -46,6 +46,11 @@ export function createNotebookEditorWidgetContainer(parent: interfaces.Container const NotebookEditorProps = Symbol('NotebookEditorProps'); +interface RenderMessage { + rendererId: string; + message: unknown; +} + export interface NotebookEditorProps { uri: URI, readonly notebookType: string, @@ -87,6 +92,18 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa protected readonly onDidChangeReadOnlyEmitter = new Emitter(); readonly onDidChangeReadOnly = this.onDidChangeReadOnlyEmitter.event; + protected readonly onPostKernelMessageEmitter = new Emitter(); + readonly onPostKernelMessage = this.onPostKernelMessageEmitter.event; + + protected readonly onDidPostKernelMessageEmitter = new Emitter(); + readonly onDidPostKernelMessage = this.onDidPostKernelMessageEmitter.event; + + protected readonly onPostRendererMessageEmitter = new Emitter(); + readonly onPostRendererMessage = this.onPostRendererMessageEmitter.event; + + protected readonly onDidReceiveKernelMessageEmitter = new Emitter(); + readonly onDidRecieveKernelMessage = this.onDidReceiveKernelMessageEmitter.event; + protected readonly renderers = new Map(); protected _model?: NotebookModel; protected _ready: Deferred = new Deferred(); @@ -190,4 +207,24 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa super.onAfterDetach(msg); this.notebookEditorService.removeNotebookEditor(this); } + + postKernelMessage(message: unknown): void { + this.onDidPostKernelMessageEmitter.fire(message); + } + + postRendererMessage(rendererId: string, message: unknown): void { + this.onPostRendererMessageEmitter.fire({ rendererId, message }); + } + + recieveKernelMessage(message: unknown): void { + this.onDidReceiveKernelMessageEmitter.fire(message); + } + + override dispose(): void { + this.onDidChangeModelEmitter.dispose(); + this.onDidPostKernelMessageEmitter.dispose(); + this.onDidReceiveKernelMessageEmitter.dispose(); + this.onPostRendererMessageEmitter.dispose(); + super.dispose(); + } } diff --git a/packages/notebook/src/browser/notebook-renderer-registry.ts b/packages/notebook/src/browser/notebook-renderer-registry.ts index cad1d9c6ba772..4a945c447dce6 100644 --- a/packages/notebook/src/browser/notebook-renderer-registry.ts +++ b/packages/notebook/src/browser/notebook-renderer-registry.ts @@ -30,6 +30,11 @@ export interface NotebookRendererInfo { readonly requiresMessaging: boolean; } +export interface NotebookPreloadInfo { + readonly type: string; + readonly entrypoint: string; +} + @injectable() export class NotebookRendererRegistry { @@ -39,6 +44,12 @@ export class NotebookRendererRegistry { return this._notebookRenderers; } + private readonly _staticNotebookPreloads: NotebookPreloadInfo[] = []; + + get staticNotebookPreloads(): readonly NotebookPreloadInfo[] { + return this._staticNotebookPreloads; + } + registerNotebookRenderer(type: NotebookRendererDescriptor, basePath: string): Disposable { let entrypoint; if (typeof type.entrypoint === 'string') { @@ -62,5 +73,13 @@ export class NotebookRendererRegistry { this._notebookRenderers.splice(this._notebookRenderers.findIndex(renderer => renderer.id === type.id), 1); }); } + + registerStaticNotebookPreload(type: string, entrypoint: string, basePath: string): Disposable { + const staticPreload = { type, entrypoint: new Path(basePath).join(entrypoint).toString() }; + this._staticNotebookPreloads.push(staticPreload); + return Disposable.create(() => { + this._staticNotebookPreloads.splice(this._staticNotebookPreloads.indexOf(staticPreload), 1); + }); + } } diff --git a/packages/notebook/src/browser/renderers/cell-output-webview.ts b/packages/notebook/src/browser/renderers/cell-output-webview.ts index 0466ef6b6c9e2..67c86fcc88437 100644 --- a/packages/notebook/src/browser/renderers/cell-output-webview.ts +++ b/packages/notebook/src/browser/renderers/cell-output-webview.ts @@ -16,10 +16,11 @@ import { Disposable } from '@theia/core'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { NotebookModel } from '../view-model/notebook-model'; export const CellOutputWebviewFactory = Symbol('outputWebviewFactory'); -export type CellOutputWebviewFactory = (cell: NotebookCellModel) => Promise; +export type CellOutputWebviewFactory = (cell: NotebookCellModel, notebook: NotebookModel) => Promise; export interface CellOutputWebview extends Disposable { diff --git a/packages/notebook/src/browser/service/notebook-kernel-service.ts b/packages/notebook/src/browser/service/notebook-kernel-service.ts index 85309f3ebae64..111365b8c973d 100644 --- a/packages/notebook/src/browser/service/notebook-kernel-service.ts +++ b/packages/notebook/src/browser/service/notebook-kernel-service.ts @@ -54,6 +54,11 @@ export interface NotebookKernel { // ID of the extension providing this kernel readonly extensionId: string; + readonly localResourceRoot: URI; + readonly preloadUris: URI[]; + readonly preloadProvides: string[]; + + readonly handle: number; label: string; description?: string; detail?: string; diff --git a/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts b/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts index db2072ba1f420..d70de22aae07f 100644 --- a/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts +++ b/packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts @@ -19,8 +19,9 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from '@theia/core'; -import { injectable } from '@theia/core/shared/inversify'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; +import { NotebookEditorWidgetService } from './notebook-editor-widget-service'; interface RendererMessage { editorId: string; @@ -50,6 +51,9 @@ export class NotebookRendererMessagingService implements Disposable { private readonly willActivateRendererEmitter = new Emitter(); readonly onWillActivateRenderer = this.willActivateRendererEmitter.event; + @inject(NotebookEditorWidgetService) + private readonly editorWidgetService: NotebookEditorWidgetService; + private readonly activations = new Map(); private readonly scopedMessaging = new Map(); @@ -86,6 +90,10 @@ export class NotebookRendererMessagingService implements Disposable { const messaging: RendererMessaging = { postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message), + receiveMessage: async (rendererId, message) => { + this.editorWidgetService.getNotebookEditor(editorId)?.postRendererMessage(rendererId, message); + return true; + }, dispose: () => this.scopedMessaging.delete(editorId), }; diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx index 4528b16d3a47d..6be50f8812666 100644 --- a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -61,7 +61,7 @@ export class NotebookCodeCellRenderer implements CellRenderer {
- this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, notebookModel, cell, cell.outputs[0])} />
@@ -166,6 +166,7 @@ export class NotebookCodeCellStatus extends React.Component React.ReactNode; } @@ -182,14 +183,14 @@ export class NotebookCodeCellOutputs extends React.Component { - const { cell, outputWebviewFactory } = this.props; + const { cell, notebook, outputWebviewFactory } = this.props; this.toDispose.push(cell.onDidChangeOutputs(async () => { if (!this.outputsWebviewPromise && cell.outputs.length > 0) { - this.outputsWebviewPromise = outputWebviewFactory(cell).then(webview => { + this.outputsWebviewPromise = outputWebviewFactory(cell, notebook).then(webview => { this.outputsWebview = webview; this.forceUpdate(); return webview; - }); + }); this.forceUpdate(); } else if (this.outputsWebviewPromise && cell.outputs.length === 0 && cell.internalMetadata.runEndTime) { (await this.outputsWebviewPromise).dispose(); @@ -199,7 +200,7 @@ export class NotebookCodeCellOutputs extends React.Component 0) { - this.outputsWebviewPromise = outputWebviewFactory(cell).then(webview => { + this.outputsWebviewPromise = outputWebviewFactory(cell, notebook).then(webview => { this.outputsWebview = webview; this.forceUpdate(); return webview; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 7539bd03dd869..d0ef6f8f54997 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -2288,7 +2288,7 @@ export const MAIN_RPC_CONTEXT = { NOTEBOOKS_EXT: createProxyIdentifier('NotebooksExt'), NOTEBOOK_DOCUMENTS_EXT: createProxyIdentifier('NotebookDocumentsExt'), NOTEBOOK_EDITORS_EXT: createProxyIdentifier('NotebookEditorsExt'), - NOTEBOOK_RENDERERS_EXT: createProxyIdentifier('NotebooksExt'), + NOTEBOOK_RENDERERS_EXT: createProxyIdentifier('NotebooksRenderersExt'), NOTEBOOK_KERNELS_EXT: createProxyIdentifier('NotebookKernelsExt'), TERMINAL_EXT: createProxyIdentifier('TerminalServiceExt'), OUTPUT_CHANNEL_REGISTRY_EXT: createProxyIdentifier('OutputChannelRegistryExt'), @@ -2488,7 +2488,7 @@ export interface NotebookKernelDto { id: string; notebookType: string; extensionId: string; - // extensionLocation: UriComponents; + extensionLocation: UriComponents; label: string; detail?: string; description?: string; diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 547074215bd60..d2ce75860d212 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -103,6 +103,7 @@ export interface PluginPackageContribution { terminal?: PluginPackageTerminal; notebooks?: PluginPackageNotebook[]; notebookRenderer?: PluginNotebookRendererContribution[]; + notebookPreload?: PluginPackageNotebookPreload[]; } export interface PluginPackageNotebook { @@ -120,6 +121,11 @@ export interface PluginNotebookRendererContribution { readonly requiresMessaging?: 'always' | 'optional' | 'never' } +export interface PluginPackageNotebookPreload { + type: string; + entrypoint: string; +} + export interface PluginPackageAuthenticationProvider { id: string; label: string; @@ -610,8 +616,8 @@ export interface PluginContribution { terminalProfiles?: TerminalProfile[]; notebooks?: NotebookContribution[]; notebookRenderer?: NotebookRendererContribution[]; + notebookPreload?: notebookPreloadContribution[]; } - export interface NotebookContribution { type: string; displayName: string; @@ -627,6 +633,11 @@ export interface NotebookRendererContribution { readonly requiresMessaging?: 'always' | 'optional' | 'never' } +export interface notebookPreloadContribution { + type: string; + entrypoint: string; +} + export interface AuthenticationProviderInformation { id: string; label: string; diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index af527a5e283b5..7c9661fc04db4 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -341,13 +341,19 @@ export class TheiaPluginScanner extends AbstractPluginScanner { try { contributions.notebooks = rawPlugin.contributes.notebooks; } catch (err) { - console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err); + console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.notebooks, err); } try { contributions.notebookRenderer = rawPlugin.contributes.notebookRenderer; } catch (err) { - console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err); + console.error(`Could not read '${rawPlugin.name}' contribution 'notebook-renderer'.`, rawPlugin.contributes.notebookRenderer, err); + } + + try { + contributions.notebookPreload = rawPlugin.contributes.notebookPreload; + } catch (err) { + console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks-preload'.`, rawPlugin.contributes.notebookPreload, err); } try { diff --git a/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts b/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts index d3ea21f53c753..a35aaf3b30453 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts @@ -23,7 +23,10 @@ import { UriComponents } from '@theia/core/lib/common/uri'; import { LanguageService } from '@theia/core/lib/browser/language-service'; import { CellExecuteUpdateDto, CellExecutionCompleteDto, MAIN_RPC_CONTEXT, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain } from '../../../common'; import { RPCProtocol } from '../../../common/rpc-protocol'; -import { CellExecution, NotebookExecutionStateService, NotebookKernelChangeEvent, NotebookKernelService, NotebookService } from '@theia/notebook/lib/browser'; +import { + CellExecution, NotebookEditorWidgetService, NotebookExecutionStateService, + NotebookKernelChangeEvent, NotebookKernelService, NotebookService +} from '@theia/notebook/lib/browser'; import { combinedDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle'; import { interfaces } from '@theia/core/shared/inversify'; import { NotebookKernelSourceAction } from '@theia/notebook/lib/common'; @@ -54,7 +57,7 @@ abstract class NotebookKernel { return this.preloads.map(p => p.provides).flat(); } - constructor(data: NotebookKernelDto, private languageService: LanguageService) { + constructor(public readonly handle: number, data: NotebookKernelDto, private languageService: LanguageService) { this.id = data.id; this.viewType = data.notebookType; this.extensionId = data.extensionId; @@ -65,6 +68,7 @@ abstract class NotebookKernel { this.detail = data.detail; this.supportedLanguages = (data.supportedLanguages && data.supportedLanguages.length > 0) ? data.supportedLanguages : languageService.languages.map(lang => lang.id); this.implementsExecutionOrder = data.supportsExecutionOrder ?? false; + this.localResourceRoot = URI.fromComponents(data.extensionLocation); this.preloads = data.preloads?.map(u => ({ uri: URI.fromComponents(u.uri), provides: u.provides })) ?? []; } @@ -125,6 +129,7 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { private notebookService: NotebookService; private languageService: LanguageService; private notebookExecutionStateService: NotebookExecutionStateService; + private notebookEditorWidgetService: NotebookEditorWidgetService; private readonly executions = new Map(); @@ -138,10 +143,46 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { this.notebookExecutionStateService = container.get(NotebookExecutionStateService); this.notebookService = container.get(NotebookService); this.languageService = container.get(LanguageService); + this.notebookEditorWidgetService = container.get(NotebookEditorWidgetService); + + this.notebookEditorWidgetService.onDidAddNotebookEditor(editor => { + editor.onDidRecieveKernelMessage(async message => { + const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(editor.model!); + if (kernel) { + this.proxy.$acceptKernelMessageFromRenderer(kernel.handle, editor.id, message); + } + }); + }); } - $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise { - throw new Error('Method not implemented.'); + async $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise { + const tuple = this.kernels.get(handle); + if (!tuple) { + throw new Error('kernel already disposed'); + } + const [kernel] = tuple; + let didSend = false; + for (const editor of this.notebookEditorWidgetService.getNotebookEditors()) { + if (!editor.model) { + continue; + } + if (this.notebookKernelService.getMatchingKernel(editor.model).selected !== kernel) { + // different kernel + continue; + } + if (editorId === undefined) { + // all editors + editor.postKernelMessage(message); + didSend = true; + } else if (editor.id === editorId) { + // selected editors + editor.postKernelMessage(message); + didSend = true; + break; + } + } + return didSend; + } async $addKernel(handle: number, data: NotebookKernelDto): Promise { @@ -153,7 +194,7 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain { async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise { await that.proxy.$cancelCells(handle, uri.toComponents(), handles); } - }(data, this.languageService); + }(handle, data, this.languageService); const listener = this.notebookKernelService.onDidChangeSelectedKernel(e => { if (e.oldKernel === kernel.id) { diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx index c0be337bfe6d1..a6d99ce2ecd7e 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx @@ -20,8 +20,11 @@ import * as React from '@theia/core/shared/react'; import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; -import { NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry, NotebookEditorWidgetService, NotebookCellOutputsSplice } from '@theia/notebook/lib/browser'; import { generateUuid } from '@theia/core/lib/common/uuid'; +import { + NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry, + NotebookEditorWidgetService, NotebookCellOutputsSplice, NOTEBOOK_EDITOR_ID_PREFIX, NotebookKernelService, NotebookEditorWidget +} from '@theia/notebook/lib/browser'; import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; import { WebviewWidget } from '../../webview/webview'; import { Message, WidgetManager } from '@theia/core/lib/browser'; @@ -31,12 +34,15 @@ import { ChangePreferredMimetypeMessage, FromWebviewMessage, OutputChangedMessag import { CellUri } from '@theia/notebook/lib/common'; import { Disposable, DisposableCollection, nls, QuickPickService } from '@theia/core'; import { NotebookCellOutputModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-output-model'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; const CellModel = Symbol('CellModel'); +const Notebook = Symbol('NotebookModel'); -export function createCellOutputWebviewContainer(ctx: interfaces.Container, cell: NotebookCellModel): interfaces.Container { +export function createCellOutputWebviewContainer(ctx: interfaces.Container, cell: NotebookCellModel, notebook: NotebookModel): interfaces.Container { const child = ctx.createChild(); child.bind(CellModel).toConstantValue(cell); + child.bind(Notebook).toConstantValue(notebook); child.bind(CellOutputWebviewImpl).toSelf().inSingletonScope(); return child; } @@ -50,6 +56,9 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { @inject(CellModel) protected readonly cell: NotebookCellModel; + @inject(Notebook) + protected readonly notebook: NotebookModel; + @inject(WidgetManager) protected readonly widgetManager: WidgetManager; @@ -62,11 +71,16 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { @inject(NotebookEditorWidgetService) protected readonly notebookEditorWidgetService: NotebookEditorWidgetService; + @inject(NotebookKernelService) + protected readonly notebookKernelService: NotebookKernelService; + @inject(QuickPickService) protected readonly quickPickService: QuickPickService; readonly id = generateUuid(); + protected editor: NotebookEditorWidget | undefined; + protected readonly elementRef = React.createRef(); protected outputPresentationListeners: DisposableCollection = new DisposableCollection(); @@ -76,11 +90,30 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { @postConstruct() protected async init(): Promise { + this.editor = this.notebookEditorWidgetService.getNotebookEditor(NOTEBOOK_EDITOR_ID_PREFIX + CellUri.parse(this.cell.uri)?.notebook); + this.toDispose.push(this.cell.onDidChangeOutputs(outputChange => this.updateOutput(outputChange))); this.toDispose.push(this.cell.onDidChangeOutputItems(output => { - this.updateOutput({start: this.cell.outputs.findIndex(o => o.outputId === output.outputId), deleteCount: 1, newOutputs: [output]}); + this.updateOutput({ start: this.cell.outputs.findIndex(o => o.outputId === output.outputId), deleteCount: 1, newOutputs: [output] }); })); + if (this.editor) { + this.toDispose.push(this.editor.onDidPostKernelMessage(message => { + this.webviewWidget.sendMessage({ + type: 'customKernelMessage', + message + }); + })); + + this.toDispose.push(this.editor.onPostRendererMessage(messageObj => { + this.webviewWidget.sendMessage({ + type: 'customRendererMessage', + ...messageObj + }); + })); + + } + this.webviewWidget = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: this.id }); this.webviewWidget.setContentOptions({ allowScripts: true }); this.webviewWidget.setHTML(await this.createWebviewContent()); @@ -134,8 +167,8 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { private async requestOutputPresentationUpdate(output: NotebookCellOutputModel): Promise { const selectedMime = await this.quickPickService.show( - output.outputs.map(item => ({label: item.mime})), - {description: nls.localizeByDefault('Select mimetype to render for current output' )}); + output.outputs.map(item => ({ label: item.mime })), + { description: nls.localizeByDefault('Select mimetype to render for current output') }); if (selectedMime) { this.webviewWidget.sendMessage({ type: 'changePreferredMimetype', @@ -146,35 +179,52 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { } private handleWebviewMessage(message: FromWebviewMessage): void { + if (!this.editor) { + throw new Error('No editor found for cell output webview'); + } + switch (message.type) { case 'initialized': - this.updateOutput({newOutputs: this.cell.outputs, start: 0, deleteCount: 0}); + this.updateOutput({ newOutputs: this.cell.outputs, start: 0, deleteCount: 0 }); break; case 'customRendererMessage': - this.messagingService.getScoped('').postMessage(message.rendererId, message.message); + this.messagingService.getScoped(this.editor.id).postMessage(message.rendererId, message.message); break; case 'didRenderOutput': this.webviewWidget.setIframeHeight(message.contentHeight + 5); break; case 'did-scroll-wheel': - this.notebookEditorWidgetService.getNotebookEditor(`notebook:${CellUri.parse(this.cell.uri)?.notebook}`)?.node.scrollBy(message.deltaX, message.deltaY); + this.editor.node.scrollBy(message.deltaX, message.deltaY); + break; + case 'customKernelMessage': + this.editor.recieveKernelMessage(message.message); break; } } + getPreloads(): string[] { + const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(this.notebook); + const kernelPreloads = kernel?.preloadUris.map(uri => uri.toString()) ?? []; + + const staticPreloads = this.notebookRendererRegistry.staticNotebookPreloads + .filter(preload => preload.type === this.notebook.viewType) + .map(preload => preload.entrypoint); + return kernelPreloads.concat(staticPreloads); + } + private async createWebviewContent(): Promise { const isWorkspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust(); const preloads = this.preloadsScriptString(isWorkspaceTrusted); const content = ` - - - - - - - - - `; + + + + + + + + + `; return content; } @@ -186,13 +236,14 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { lineLimit: 30, outputScrolling: false, outputWordWrap: false, - } + }, + staticPreloadsData: this.getPreloads() }; // TS will try compiling `import()` in webviewPreloads, so use a helper function instead // of using `import(...)` directly return ` const __import = (x) => import(x); - (${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`; + (${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`; } dispose(): void { diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts index 4d1f76c91dba4..5eb60f62ab5a0 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts @@ -58,6 +58,16 @@ export interface PreloadContext { readonly isWorkspaceTrusted: boolean; readonly rendererData: readonly webviewCommunication.RendererMetadata[]; readonly renderOptions: RenderOptions; + readonly staticPreloadsData: readonly string[]; +} + +interface KernelPreloadContext { + readonly onDidReceiveKernelMessage: Event; + postKernelMessage(data: unknown): void; +} + +interface KernelPreloadModule { + activate(ctx: KernelPreloadContext): Promise | void; } export async function outputWebviewPreload(ctx: PreloadContext): Promise { @@ -98,6 +108,36 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { const settingChange: EmitterLike = createEmitter(); + const onDidReceiveKernelMessage = createEmitter(); + + function createKernelContext(): KernelPreloadContext { + return Object.freeze({ + onDidReceiveKernelMessage: onDidReceiveKernelMessage.event, + postKernelMessage: (data: unknown) => { + theia.postMessage({ type: 'customKernelMessage', message: data }); + } + }); + } + + async function runKernelPreload(url: string): Promise { + try { + return activateModuleKernelPreload(url); + } catch (e) { + console.error(e); + throw e; + } + } + + async function activateModuleKernelPreload(url: string): Promise { + const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, ''); + const module: KernelPreloadModule = (await __import(`${baseUri}/${url}`)) as KernelPreloadModule; + if (!module.activate) { + console.error(`Notebook preload '${url}' was expected to be a module but it does not export an 'activate' function`); + return; + } + return module.activate(createKernelContext()); + } + class Output { readonly outputId: string; renderedItem?: rendererApi.OutputItem; @@ -160,6 +200,10 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { if (this.rendererApi) { return this.rendererApi; } + + // Preloads need to be loaded before loading renderers. + await kernelPreloads.waitForAllCurrent(); + const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, ''); const rendererModule = await __import(`${baseUri}/${this.data.entrypoint.uri}`) as { activate: rendererApi.ActivationFunction }; this.rendererApi = await rendererModule.activate(this.createRendererContext()); @@ -196,7 +240,9 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { if (this.data.requiresMessaging) { context.onDidReceiveMessage = this.onMessageEvent.event; - context.postMessage = message => theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message }); + context.postMessage = message => { + theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message }); + }; } return Object.freeze(context); @@ -385,6 +431,42 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { } }(); + const kernelPreloads = new class { + private readonly preloads = new Map>(); + + /** + * Returns a promise that resolves when the given preload is activated. + */ + public waitFor(uri: string): Promise { + return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`)); + } + + /** + * Loads a preload. + * @param uri URI to load from + * @param originalUri URI to show in an error message if the preload is invalid. + */ + public load(uri: string): Promise { + const promise = Promise.all([ + runKernelPreload(uri), + this.waitForAllCurrent(), + ]); + + this.preloads.set(uri, promise); + return promise; + } + + /** + * Returns a promise that waits for all currently-registered preloads to + * activate before resolving. + */ + public waitForAllCurrent(): Promise { + return Promise.all([...this.preloads.values()].map(p => p.catch(err => err))); + } + }; + + await Promise.all(ctx.staticPreloadsData.map(preload => kernelPreloads.load(preload))); + function clearOutput(output: Output): void { output.clear(); output.element.remove(); @@ -460,7 +542,6 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { window.addEventListener('message', async rawEvent => { const event = rawEvent as ({ data: webviewCommunication.ToWebviewMessage }); - switch (event.data.type) { case 'updateRenderers': renderers.updateRendererData(event.data.rendererData); @@ -478,6 +559,16 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise { clearOutput(outputs.splice(index, 1)[0]); renderers.render(outputs[index], event.data.mimeType, undefined, new AbortController().signal); break; + case 'customKernelMessage': + onDidReceiveKernelMessage.fire(event.data.message); + break; + case 'preload': { + const resources = event.data.resources; + for (const uri of resources) { + kernelPreloads.load(uri); + } + break; + } } }); window.addEventListener('wheel', handleWheel); diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts index b514145d854fa..97586cc17e774 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts @@ -49,7 +49,17 @@ export interface ChangePreferredMimetypeMessage { readonly mimeType: string; } -export type ToWebviewMessage = UpdateRenderersMessage | OutputChangedMessage | ChangePreferredMimetypeMessage | CustomRendererMessage; +export interface KernelMessage { + readonly type: 'customKernelMessage'; + readonly message: unknown; +} + +export interface PreloadMessage { + readonly type: 'preload'; + readonly resources: string[]; +} + +export type ToWebviewMessage = UpdateRenderersMessage | OutputChangedMessage | ChangePreferredMimetypeMessage | CustomRendererMessage | KernelMessage | PreloadMessage; export interface WebviewInitialized { readonly type: 'initialized'; @@ -66,7 +76,7 @@ export interface WheelMessage { readonly deltaX: number; } -export type FromWebviewMessage = WebviewInitialized | OnDidRenderOutput | WheelMessage | CustomRendererMessage; +export type FromWebviewMessage = WebviewInitialized | OnDidRenderOutput | WheelMessage | CustomRendererMessage | KernelMessage; export interface Output { id: string diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index 96d02bbec2f60..cf5aa974efb7c 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -447,6 +447,14 @@ export class PluginContributionHandler { } } + if (contributions.notebookPreload) { + for (const preload of contributions.notebookPreload) { + pushContribution(`notebookPreloads.${preload.type}:${preload.entrypoint}`, + () => this.notebookRendererRegistry.registerStaticNotebookPreload(preload.type, preload.entrypoint, PluginPackage.toPluginUrl(plugin.metadata.model, '')) + ); + } + } + return toDispose; } 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 e16d944c95926..af90986b78bab 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 @@ -88,6 +88,7 @@ import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar import { CellOutputWebviewFactory } from '@theia/notebook/lib/browser'; import { CellOutputWebviewImpl, createCellOutputWebviewContainer } from './notebooks/renderers/cell-output-webview'; import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model'; +import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -262,7 +263,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { return provider.createProxy(languagePackServicePath); }).inSingletonScope(); - bind(CellOutputWebviewFactory).toFactory(ctx => async (cell: NotebookCellModel) => - createCellOutputWebviewContainer(ctx.container, cell).getAsync(CellOutputWebviewImpl) + bind(CellOutputWebviewFactory).toFactory(ctx => async (cell: NotebookCellModel, notebook: NotebookModel) => + createCellOutputWebviewContainer(ctx.container, cell, notebook).getAsync(CellOutputWebviewImpl) ); }); diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts index c089d53abb571..363d9d8657d42 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts @@ -18,14 +18,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '@theia/plugin'; import { - CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, NotebookKernelSourceActionDto, NotebookOutputDto, PLUGIN_RPC_CONTEXT + CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, + NotebookKernelSourceActionDto, NotebookOutputDto, PluginModel, PluginPackage, PLUGIN_RPC_CONTEXT } from '../../common'; import { RPCProtocol } from '../../common/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; -import * as theia from '@theia/plugin'; -import { CancellationTokenSource, Disposable, DisposableCollection, Emitter } from '@theia/core'; +import { CancellationTokenSource, Disposable, DisposableCollection, Emitter, Path } from '@theia/core'; import { Cell } from './notebook-document'; import { NotebooksExtImpl } from './notebooks'; import { NotebookCellOutputConverter, NotebookCellOutputItem, NotebookKernelSourceAction } from '../type-converters'; @@ -33,6 +32,8 @@ import { timeout, Deferred } from '@theia/core/lib/common/promise-util'; import { CellExecutionUpdateType, NotebookCellExecutionState } from '@theia/notebook/lib/common'; import { CommandRegistryImpl } from '../command-registry'; import { NotebookCellOutput, NotebookRendererScript, URI } from '../types-impl'; +import { toUriComponents } from '../../main/browser/hierarchy/hierarchy-types-converters'; +import type * as theia from '@theia/plugin'; interface KernelData { extensionId: string; @@ -62,18 +63,18 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { constructor( rpc: RPCProtocol, private readonly notebooks: NotebooksExtImpl, - private readonly commands: CommandRegistryImpl + private readonly commands: CommandRegistryImpl, ) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN); } private currentHandle = 0; - createNotebookController(extensionId: string, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[], + createNotebookController(extension: PluginModel, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[]): theia.NotebookController { for (const kernelData of this.kernelData.values()) { - if (kernelData.controller.id === id && extensionId === kernelData.extensionId) { + if (kernelData.controller.id === id && extension.id === kernelData.extensionId) { throw new Error(`notebook controller with id '${id}' ALREADY exist`); } } @@ -81,9 +82,9 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { const handle = this.currentHandle++; const that = this; - console.debug(`NotebookController[${handle}], CREATED by ${extensionId}, ${id}`); + console.debug(`NotebookController[${handle}], CREATED by ${extension.id}, ${id}`); - const defaultExecuteHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extensionId}'`); + const defaultExecuteHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.id}'`); let isDisposed = false; const commandDisposables = new DisposableCollection(); @@ -92,10 +93,12 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { const onDidReceiveMessage = new Emitter<{ editor: theia.NotebookEditor; message: unknown }>(); const data: NotebookKernelDto = { - id: createKernelId(extensionId, id), + id: createKernelId(extension.id, id), notebookType: viewType, - extensionId: extensionId, - label: label || extensionId, + extensionId: extension.id, + extensionLocation: toUriComponents(extension.packageUri), + label: label || extension.id, + preloads: rendererScripts?.map(preload => ({ uri: toUriComponents(preload.uri.toString()), provides: preload.provides })) ?? [] }; // @@ -131,12 +134,11 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { get id(): string { return id; }, get notebookType(): string { return data.notebookType; }, onDidChangeSelectedNotebooks: onDidChangeSelection.event, - onDidReceiveMessage: onDidReceiveMessage.event, get label(): string { return data.label; }, set label(value) { - data.label = value ?? extensionId; + data.label = value ?? extension.id; update(); }, get detail(): string { @@ -168,11 +170,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { update(); }, get rendererScripts(): NotebookRendererScript[] { - return data.rendererScripts ?? []; - }, - set rendererScripts(value) { - data.rendererScripts = value; - update(); + return data.preloads?.map(preload => (new NotebookRendererScript(URI.from(preload.uri), preload.provides))) ?? []; }, get executeHandler(): (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable { return executeHandler; @@ -197,7 +195,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { Array.from(associatedNotebooks.keys()).map(u => u.toString())); throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); } - return that.createNotebookCellExecution(cell, createKernelId(extensionId, this.id)); + return that.createNotebookCellExecution(cell, createKernelId(extension.id, this.id)); }, dispose: () => { if (!isDisposed) { @@ -213,16 +211,18 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { updateNotebookAffinity(notebook, priority): void { that.proxy.$updateNotebookPriority(handle, notebook.uri, priority); }, + onDidReceiveMessage: onDidReceiveMessage.event, async postMessage(message: unknown, editor?: theia.NotebookEditor): Promise { - return Promise.resolve(true); // TODO needs implementation + return that.proxy.$postMessage(handle, 'notebook:' + editor?.notebook.uri.toString(), message); }, asWebviewUri(localResource: theia.Uri): theia.Uri { - throw new Error('Method not implemented.'); + const basePath = PluginPackage.toPluginUrl(extension, ''); + return URI.from({ path: new Path(basePath).join(localResource.path).toString(), scheme: 'https' }); } }; this.kernelData.set(handle, { - extensionId: extensionId, + extensionId: extension.id, controller, onDidReceiveMessage, onDidChangeSelection, @@ -376,7 +376,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt { // Proposed Api though seems needed by jupyter for telemetry } - async $provideKernelSourceActions(handle: number, token: CancellationToken): Promise { + async $provideKernelSourceActions(handle: number, token: theia.CancellationToken): Promise { const provider = this.kernelSourceActionProviders.get(handle); if (provider) { const disposables = new DisposableCollection(); @@ -496,7 +496,7 @@ class NotebookCellExecutionTask implements Disposable { asApiObject(): theia.NotebookCellExecution { const that = this; const result: theia.NotebookCellExecution = { - get token(): CancellationToken { return that.tokenSource.token; }, + get token(): theia.CancellationToken { return that.tokenSource.token; }, get cell(): theia.NotebookCell { return that.cell.apiCell; }, get executionOrder(): number | undefined { return that.executionOrder; }, set executionOrder(v: number | undefined) { diff --git a/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts index cbb66bb0e69f0..425a460036317 100644 --- a/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts +++ b/packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts @@ -43,7 +43,6 @@ export class NotebookRenderersExtImpl implements NotebookRenderersExt { const messaging: theia.NotebookRendererMessaging = { onDidReceiveMessage: (listener, thisArg, disposables) => this.getOrCreateEmitterFor(rendererId).event(listener, thisArg, disposables), postMessage: (message, editorOrAlias) => { - const extHostEditor = editorOrAlias && NotebookEditor.apiEditorsToExtHost.get(editorOrAlias); return this.proxy.$postMessage(extHostEditor?.id, rendererId, message); }, diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index a6c8abba3b432..cf50897c194fc 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -1183,7 +1183,7 @@ export function createAPIFactory( controller: theia.NotebookController) => void | Thenable, rendererScripts?: NotebookRendererScript[] ) { - return notebookKernels.createNotebookController(plugin.model.id, id, notebookType, label, handler, rendererScripts); + return notebookKernels.createNotebookController(plugin.model, id, notebookType, label, handler, rendererScripts); }, createRendererMessaging(rendererId) { return notebookRenderers.createRendererMessaging(rendererId); diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts index 5ad4356ad832b..d80fe9d873e2d 100644 --- a/packages/plugin-ext/src/plugin/webviews.ts +++ b/packages/plugin-ext/src/plugin/webviews.ts @@ -203,6 +203,10 @@ export class WebviewsExtImpl implements WebviewsExt { public getWebview(handle: string): WebviewImpl | undefined { return this.webviews.get(handle); } + + public getResourceRoot(): string | undefined { + return this.initData?.webviewResourceRoot; + } } export class WebviewImpl implements theia.Webview {