From 66fa5c41b3afd35f1cc28dbf9aab017c422b5b0f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 12 May 2021 19:32:57 -0700 Subject: [PATCH 1/2] First cut at unifying notebook renderers apis This implements the api described in #123540. Major points: - Instead of having the `markdown-it` renderer pull it its dependencies, instead the dependencies can call `getRenderer` to import the object returned by the `markdown-it` renderer - We try to detect if a renderer is using the old or new api. Old renderers are still run as globals while new ones are loaded with `import` - I have only hooked up the new API for markdown renderers so far --- .../notebook/index.ts | 24 +- .../notebook/emoji.ts | 10 +- .../notebook/katex.ts | 37 +-- .../view/renderers/backLayerWebView.ts | 37 +-- .../browser/view/renderers/webviewPreloads.ts | 229 ++++++++++++------ 5 files changed, 206 insertions(+), 131 deletions(-) diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index b7c2df8581746..7b44fba65838c 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -5,35 +5,23 @@ const MarkdownIt = require('markdown-it'); -export async function activate(ctx: { - dependencies: ReadonlyArray<{ entrypoint: string }> -}) { +export function activate() { let markdownIt = new MarkdownIt({ html: true }); - // Should we load the deps before this point? - // Also could we await inside `renderMarkup`? - await Promise.all(ctx.dependencies.map(async (dep) => { - try { - const api = await import(dep.entrypoint); - if (api?.extendMarkdownIt) { - markdownIt = api.extendMarkdownIt(markdownIt); - } - } catch (e) { - console.error('Could not load markdown entryPoint', e); - } - })); - return { - renderMarkup: (context: { element: HTMLElement, content: string }) => { - const rendered = markdownIt.render(context.content); + renderCell: (_id: string, context: { element: HTMLElement, value: string }) => { + const rendered = markdownIt.render(context.value); context.element.innerHTML = rendered; // Insert styles into markdown preview shadow dom so that they are applied for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) { context.element.insertAdjacentElement('beforebegin', markdownStyleNode.cloneNode(true) as Element); } + }, + extendMarkdownIt: (f: (md: typeof markdownIt) => void) => { + f(markdownIt); } }; } diff --git a/extensions/notebook-markdown-extensions/notebook/emoji.ts b/extensions/notebook-markdown-extensions/notebook/emoji.ts index bf82f98ba0f71..b842750a03cd5 100644 --- a/extensions/notebook-markdown-extensions/notebook/emoji.ts +++ b/extensions/notebook-markdown-extensions/notebook/emoji.ts @@ -6,6 +6,12 @@ import type * as markdownIt from 'markdown-it'; const emoji = require('markdown-it-emoji'); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(emoji); +export function activate(ctx: { + getRenderer: (id: string) => any +}) { + const markdownItRenderer = ctx.getRenderer('markdownItRenderer'); + + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(emoji); + }); } diff --git a/extensions/notebook-markdown-extensions/notebook/katex.ts b/extensions/notebook-markdown-extensions/notebook/katex.ts index 910036babf246..ccb1256905369 100644 --- a/extensions/notebook-markdown-extensions/notebook/katex.ts +++ b/extensions/notebook-markdown-extensions/notebook/katex.ts @@ -6,23 +6,28 @@ import type * as markdownIt from 'markdown-it'; const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css'); -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.classList.add('markdown-style'); -link.href = styleHref; -document.head.append(link); +export function activate(ctx: { + getRenderer: (id: string) => any +}) { + const markdownItRenderer = ctx.getRenderer('markdownItRenderer'); -const style = document.createElement('style'); -style.classList.add('markdown-style'); -style.textContent = ` - .katex-error { - color: var(--vscode-editorError-foreground); - } -`; -document.head.append(style); + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.classList.add('markdown-style'); + link.href = styleHref; + document.head.append(link); -const katex = require('@iktakahiro/markdown-it-katex'); + const style = document.createElement('style'); + style.classList.add('markdown-style'); + style.textContent = ` + .katex-error { + color: var(--vscode-editorError-foreground); + } + `; + document.head.append(style); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(katex); + const katex = require('@iktakahiro/markdown-it-katex'); + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(katex); + }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 7fc1c3f8bf3d8..bfa244d863df2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -26,10 +26,10 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { preloadsScriptStr, WebviewPreloadRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; +import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; -import { INotebookKernel, INotebookRendererInfo, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewContentPurpose, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -453,7 +453,7 @@ export class BackLayerWebView extends Disposable { this.element.style.position = 'absolute'; } private generateContent(coreDependencies: string, baseUrl: string) { - const markupRenderer = this.getMarkdownRenderer(); + const renderersData = this.getRendererData(); const outputWidth = `calc(100% - ${this.options.leftMargin + this.options.rightMargin + this.options.runGutter}px)`; const outputMarginLeft = `${this.options.leftMargin + this.options.runGutter}px`; return html` @@ -707,36 +707,19 @@ export class BackLayerWebView extends Disposable { ${coreDependencies}
- + `; } - private getMarkdownRenderer(): WebviewPreloadRenderer[] { - const markdownMimeType = 'text/markdown'; - const allRenderers = this.notebookService.getRenderers() - .filter(renderer => renderer.matchesWithoutKernel(markdownMimeType) !== NotebookRendererMatch.Never); - - const topLevelMarkdownRenderers = allRenderers - .filter(renderer => renderer.dependencies.length === 0); - - const subRenderers = new Map>(); - for (const renderer of allRenderers) { - for (const dep of renderer.dependencies) { - if (!subRenderers.has(dep)) { - subRenderers.set(dep, []); - } - const entryPoint = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation); - subRenderers.get(dep)!.push({ entrypoint: entryPoint.toString(true) }); - } - } - - return topLevelMarkdownRenderers.map((renderer): WebviewPreloadRenderer => { - const src = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation); + private getRendererData(): RendererMetadata[] { + return this.notebookService.getRenderers().map((renderer): RendererMetadata => { + const entrypoint = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation).toString(); return { - entrypoint: src.toString(), + id: renderer.id, + entrypoint, mimeTypes: renderer.mimeTypes, - dependencies: subRenderers.get(renderer.id) || [], + dependencies: Array.from(renderer.dependencies.values()) }; }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 54e0ed72b83c3..9f0e468f5c621 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -41,7 +41,7 @@ interface PreloadStyles { declare function __import(path: string): Promise; -async function webviewPreloads(style: PreloadStyles, rendererData: readonly WebviewPreloadRenderer[]) { +async function webviewPreloads(style: PreloadStyles, rendererData: readonly RendererMetadata[]) { const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); delete (globalThis as any).acquireVsCodeApi; @@ -111,32 +111,68 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } }; - const runScript = async (url: string, originalUri: string, globals: { [name: string]: unknown } = {}): Promise<() => (PreloadResult)> => { - let text: string; - try { - const res = await fetch(url); - text = await res.text(); - if (!res.ok) { - throw new Error(`Unexpected ${res.status} requesting ${originalUri}: ${text || res.statusText}`); - } - - globals.scriptUrl = url; - } catch (e) { - return () => ({ state: PreloadState.Error, error: e.message }); + async function loadScriptSource(url: string, originalUri = url): Promise { + const res = await fetch(url); + const text = await res.text(); + if (!res.ok) { + throw new Error(`Unexpected ${res.status} requesting ${originalUri}: ${text || res.statusText}`); } + return text; + } + + interface RendererContext { + getState(): T | undefined; + setState(newState: T): void; + + getRenderer(id: string): any | undefined; + } + + function createRendererContext(rendererId: string): RendererContext { + const api = acquireNotebookRendererApi(rendererId); + return { + getState: api.getState.bind(api), + setState: api.setState.bind(api), + getRenderer: (id: string) => renderers.getRenderer(id), + }; + } + + interface ScriptModule { + activate: (ctx?: RendererContext) => any; + } + + const invokeSourceWithGlobals = (functionSrc: string, globals: { [name: string]: unknown }) => { const args = Object.entries(globals); - return () => { - try { - new Function(...args.map(([k]) => k), text)(...args.map(([, v]) => v)); - return { state: PreloadState.Ok }; - } catch (e) { - console.error(e); - return { state: PreloadState.Error, error: e.message }; + return new Function(...args.map(([k]) => k), functionSrc)(...args.map(([, v]) => v)); + }; + + const runPreload = async (url: string, originalUri: string): Promise => { + const text = await loadScriptSource(url, originalUri); + return { + activate: () => { + return invokeSourceWithGlobals(text, kernelPreloadGlobals); } }; }; + const runRenderScript = async (url: string, rendererId: string): Promise => { + const text = await loadScriptSource(url); + // TODO: Support both the new module based renderers and the old style global renderers + const isModule = /\bexport\b.*\bactivate\b/.test(text); + if (isModule) { + return __import(url); + } else { + return { + activate: () => { + const globals = { + acquireNotebookRendererApi: () => acquireNotebookRendererApi(rendererId) + }; + return invokeSourceWithGlobals(text, globals); + } + }; + } + }; + const dimensionUpdater = new class { private readonly pending = new Map(); @@ -389,7 +425,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv // the dontEmit symbol to skip emission. function mapEmitter(emitter: EmitterLike, mapFn: (data: T) => R | typeof dontEmit) { let listener: IDisposable; - const mapped = createEmitter(listeners => { + const mapped = createEmitter(listeners => { if (listeners.size && !listener) { listener = emitter.event(data => { const v = mapFn(data); @@ -407,7 +443,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv interface ICreateCellInfo { element: HTMLElement; - outputId: string; + outputId?: string; mime: string; value: unknown; @@ -422,7 +458,14 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv const onDidCreateOutput = createEmitter<{ rendererId: string, info: ICreateCellInfo }>(); const onDidReceiveKernelMessage = createEmitter(); - const acquireNotebookRendererApi = (id: string) => ({ + interface GlobalNotebookRendererApi { + setState: (newState: T) => void; + getState(): T | undefined; + readonly onWillDestroyOutput: Event; + readonly onDidCreateOutput: Event; + } + + const acquireNotebookRendererApi = (id: string): GlobalNotebookRendererApi => ({ setState(newState: T) { vscode.setState({ ...vscode.getState(), [id]: newState }); }, @@ -632,6 +675,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv cellOutputContainer.appendChild(outputContainer); outputContainer.appendChild(outputNode); } else { + // TODO: this should go through renderers instead onDidCreateOutput.fire({ rendererId: data.rendererId!, info: { @@ -754,21 +798,32 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv const resources = event.data.resources; let queue: Promise = Promise.resolve({ state: PreloadState.Ok }); for (const { uri, originalUri, source } of resources) { - const globals = source === 'kernel' - ? kernelPreloadGlobals - : { acquireNotebookRendererApi: () => acquireNotebookRendererApi(source.rendererId) }; - // create the promise so that the scripts download in parallel, but // only invoke them in series within the queue - const promise = runScript(uri, originalUri, globals); - queue = queue.then(() => promise.then(fn => { - const result = fn(); - if (result.state === PreloadState.Error) { - console.error(result.error); - } - return result; - })); + if (source === 'kernel') { + const promise = runPreload(uri, originalUri); + queue = queue.then(() => promise.then(async module => { + try { + await module.activate(); + return { state: PreloadState.Ok }; + } catch (error) { + console.error(error); + return { state: PreloadState.Error, error: error.toString() }; + } + })); + } else { + queue = queue.then(async () => { + try { + await renderers.load(source.rendererId); + return { state: PreloadState.Ok }; + } catch (error) { + console.error(error); + return { state: PreloadState.Error, error: error.toString() }; + } + }); + } + preloadPromises.set(uri, queue); } break; @@ -789,51 +844,88 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } }); - interface MarkupRenderer { - renderMarkup: (context: { element: HTMLElement, content: string }) => void; + interface RendererApi { + renderCell: (id: string, context: ICreateCellInfo) => void; + } + + class Renderer { + constructor( + public readonly data: RendererMetadata, + private readonly loadDependency: (id: string) => Promise, + ) { } + + private _loadPromise: Promise | undefined; + private _api: RendererApi | undefined; + + public get api() { return this._api; } + + public load(): Promise { + if (!this._loadPromise) { + this._loadPromise = Promise.all(this.data.dependencies.map(dependencyId => this.loadDependency(dependencyId))) + .then(() => runRenderScript(this.data.entrypoint, this.data.id)) + .then(module => { + if (module) { + const api = module.activate(createRendererContext(this.data.id)); + this._api = api; + return api; + } + return undefined; + }); + } + return this._loadPromise; + } } - const markupRenderers = new class { + const renderers = new class { - private readonly mimeTypesToRenderers = new Map Promise; - }>(); + private readonly _renderers = new Map(); constructor() { for (const renderer of rendererData) { - let loadPromise: Promise | undefined; + this._renderers.set(renderer.id, new Renderer(renderer, async (dependencyId) => { + const parent = this._renderers.get(dependencyId); + if (!parent) { + throw new Error(`Could not find renderer dependency: ${dependencyId}`); + } + await parent.load(); + })); + } + } - const entry = { - load: () => { - if (!loadPromise) { - loadPromise = __import(renderer.entrypoint).then(module => { - return module.activate({ dependencies: renderer.dependencies }); - }); - } - return loadPromise; - }, - renderer: undefined, - }; + public getRenderer(id: string): RendererApi | undefined { + return this._renderers.get(id)?.api; + } - for (const mime of renderer.mimeTypes || []) { - if (!this.mimeTypesToRenderers.has(mime)) { - this.mimeTypesToRenderers.set(mime, entry); - } - } + public load(id: string) { + const renderer = this._renderers.get(id); + if (!renderer) { + throw new Error('Could not find renderer'); } + + return renderer.load(); } - async renderMarkdown(element: HTMLElement, content: string): Promise { - const entry = this.mimeTypesToRenderers.get('text/markdown'); - if (!entry) { + public async renderMarkdown(id: string, element: HTMLElement, content: string): Promise { + const markdownRenderers = Array.from(this._renderers.values()) + .filter(renderer => renderer.data.mimeTypes.includes('text/markdown')); + + if (!markdownRenderers.length) { throw new Error('Could not find renderer'); } - const renderer = await entry.load(); - renderer.renderMarkup({ element, content }); + + await Promise.all(markdownRenderers.map(x => x.load())); + + const renderer = Array.from(this._renderers.values()).find(x => x.data.mimeTypes.includes('text/markdown')); + renderer?.api?.renderCell(id, { + element, + value: content, + mime: 'text/markdown', + metadata: undefined, + outputId: undefined, + }); } }(); - vscode.postMessage({ __vscode_notebook_message: true, type: 'initialized' @@ -961,7 +1053,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv previewNode.innerText = ''; } else { previewContainerNode.classList.remove('emptyMarkdownCell'); - await markupRenderers.renderMarkdown(previewNode, content); + await renderers.renderMarkdown(cellId, previewNode, content); if (!hasPostedRenderedMathTelemetry) { const hasRenderedMath = previewNode.querySelector('.katex'); @@ -1060,13 +1152,14 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv }(); } -export interface WebviewPreloadRenderer { +export interface RendererMetadata { + readonly id: string; readonly entrypoint: string; + readonly dependencies: readonly string[] readonly mimeTypes: readonly string[]; - readonly dependencies: ReadonlyArray<{ entrypoint: string }>; } -export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly WebviewPreloadRenderer[]) { +export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) { // TS will try compiling `import()` in webviePreloads, so use an helper function instead // of using `import(...)` directly return ` From 616e0fd99270e5a59a0762849a5085268743de4c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 18 May 2021 16:59:09 -0700 Subject: [PATCH 2/2] refactor: polish - Fix mis-used dependencies. Add the roughly proposed 'extends' model for notebook renderers. Keep it out of contribution schema for now until we can work it more. - Made new loading work with JS modules. - Consolidated the 'old style' code in the renderer so that we can just delete it when the time comes. - Removed duplicated code and sharp edges from the 'queue' mechaism. --- .../notebook-markdown-extensions/package.json | 22 +- .../notebook/browser/extensionPoint.ts | 25 +- .../view/renderers/backLayerWebView.ts | 58 +-- .../browser/view/renderers/webviewPreloads.ts | 378 ++++++++++-------- .../contrib/notebook/common/notebookCommon.ts | 3 + .../notebook/common/notebookOutputRenderer.ts | 18 +- 6 files changed, 264 insertions(+), 240 deletions(-) diff --git a/extensions/notebook-markdown-extensions/package.json b/extensions/notebook-markdown-extensions/package.json index a68a8b071144d..ef3911f2eb9f4 100644 --- a/extensions/notebook-markdown-extensions/package.json +++ b/extensions/notebook-markdown-extensions/package.json @@ -25,24 +25,18 @@ { "id": "markdownItRenderer-katex", "displayName": "Markdown it katex renderer", - "entrypoint": "./notebook-out/katex.js", - "mimeTypes": [ - "text/markdown" - ], - "dependencies": [ - "markdownItRenderer" - ] + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/katex.js" + } }, { "id": "markdownItRenderer-emoji", "displayName": "Markdown it emoji renderer", - "entrypoint": "./notebook-out/emoji.js", - "mimeTypes": [ - "text/markdown" - ], - "dependencies": [ - "markdownItRenderer" - ] + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/emoji.js" + } } ] }, diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index de640c7926e15..959e70bf58619 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -6,7 +6,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorPriority, NotebookRendererEntrypoint } from 'vs/workbench/contrib/notebook/common/notebookCommon'; namespace NotebookEditorContribution { export const viewType = 'viewType'; @@ -37,7 +37,7 @@ export interface INotebookRendererContribution { readonly [NotebookRendererContribution.viewType]?: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; - readonly [NotebookRendererContribution.entrypoint]: string; + readonly [NotebookRendererContribution.entrypoint]: NotebookRendererEntrypoint; readonly [NotebookRendererContribution.hardDependencies]: readonly string[]; readonly [NotebookRendererContribution.optionalDependencies]: readonly string[]; } @@ -130,8 +130,27 @@ const notebookRendererContribution: IJSONSchema = { } }, [NotebookRendererContribution.entrypoint]: { - type: 'string', description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + oneOf: [ + { + type: 'string', + }, + // todo@connor4312 + @mjbvz: uncomment this once it's ready for external adoption + // { + // type: 'object', + // required: ['extends', 'path'], + // properties: { + // extends: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'), + // }, + // path: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + // }, + // } + // } + ] }, [NotebookRendererContribution.hardDependencies]: { type: 'array', diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 8826726a08121..a48496cf9e47c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -203,7 +203,7 @@ export interface ICreationRequestMessage { cellTop: number; outputOffset: number; left: number; - requiredPreloads: ReadonlyArray; + requiredPreloads: ReadonlyArray; readonly initiallyHidden?: boolean; rendererId?: string | undefined; } @@ -263,17 +263,15 @@ export interface IAckOutputHeightMessage { height: number; } -export type PreloadSource = 'kernel' | { rendererId: string }; -export interface IPreloadResource { +export interface IControllerPreload { originalUri: string; uri: string; - source: PreloadSource; } -export interface IUpdatePreloadResourceMessage { +export interface IUpdateControllerPreloadsMessage { type: 'preload'; - resources: IPreloadResource[]; + resources: IControllerPreload[]; } export interface IUpdateDecorationsMessage { @@ -376,7 +374,7 @@ export type ToWebviewMessage = | IClearOutputRequestMessage | IHideOutputMessage | IShowOutputMessage - | IUpdatePreloadResourceMessage + | IUpdateControllerPreloadsMessage | IUpdateDecorationsMessage | ICustomKernelMessage | ICreateMarkdownMessage @@ -767,7 +765,7 @@ export class BackLayerWebView extends Disposable { id: renderer.id, entrypoint, mimeTypes: renderer.mimeTypes, - dependencies: Array.from(renderer.dependencies.values()) + extends: renderer.extends, }; }); } @@ -1194,7 +1192,6 @@ var requirejs = (function() { if (this._currentKernel) { this._updatePreloadsFromKernel(this._currentKernel); } - this.updateRendererPreloads(renderers); for (const [output, inset] of this.insetMapping.entries()) { this._sendMessageToWebview({ ...inset.cachedCreation, initiallyHidden: this.hiddenInsetMapping.has(output) }); @@ -1469,7 +1466,6 @@ var requirejs = (function() { ...messageBase, outputId: output.outputId, rendererId: content.renderer.id, - requiredPreloads: await this.updateRendererPreloads([content.renderer]), content: { type: RenderOutputType.Extension, outputId: output.outputId, @@ -1600,13 +1596,13 @@ var requirejs = (function() { } private _updatePreloadsFromKernel(kernel: INotebookKernel) { - const resources: IPreloadResource[] = []; + const resources: IControllerPreload[] = []; for (const preload of kernel.preloadUris) { const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https') ? preload : this.asWebviewUri(preload, undefined); if (!this._preloadsCache.has(uri.toString())) { - resources.push({ uri: uri.toString(), originalUri: preload.toString(), source: 'kernel' }); + resources.push({ uri: uri.toString(), originalUri: preload.toString() }); this._preloadsCache.add(uri.toString()); } } @@ -1618,43 +1614,7 @@ var requirejs = (function() { this._updatePreloads(resources); } - async updateRendererPreloads(renderers: Iterable) { - if (this._disposed) { - return []; - } - - const requiredPreloads: IPreloadResource[] = []; - const resources: IPreloadResource[] = []; - const extensionLocations: URI[] = []; - for (const rendererInfo of renderers) { - extensionLocations.push(rendererInfo.extensionLocation); - for (const preload of [rendererInfo.entrypoint, ...rendererInfo.preloads]) { - const uri = this.asWebviewUri(preload, rendererInfo.extensionLocation); - const resource: IPreloadResource = { - uri: uri.toString(), - originalUri: preload.toString(), - source: { rendererId: rendererInfo.id }, - }; - - requiredPreloads.push(resource); - - if (!this._preloadsCache.has(uri.toString())) { - resources.push(resource); - this._preloadsCache.add(uri.toString()); - } - } - } - - if (!resources.length) { - return requiredPreloads; - } - - this.rendererRootsCache = extensionLocations; - this._updatePreloads(resources); - return requiredPreloads; - } - - private _updatePreloads(resources: IPreloadResource[]) { + private _updatePreloads(resources: IControllerPreload[]) { if (!this.webview) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 48ce5768e1d3d..e1df7bf048558 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -129,10 +129,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } function createRendererContext(rendererId: string): RendererContext { - const api = acquireNotebookRendererApi(rendererId); return { - getState: api.getState.bind(api), - setState: api.setState.bind(api), + setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }), + getState: () => { + const state = vscode.getState(); + return typeof state === 'object' && state ? state[rendererId] as T : undefined; + }, getRenderer: (id: string) => renderers.getRenderer(id), }; } @@ -162,16 +164,39 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend if (isModule) { return __import(url); } else { + return createBackCompatModule(rendererId, text); + } + }; + + const createBackCompatModule = (rendererId: string, scriptText: string): ScriptModule => ({ + activate: (): RendererApi => { + const onDidCreateOutput = createEmitter(); + const onWillDestroyOutput = createEmitter(); + + const globals = { + acquireNotebookRendererApi: (): GlobalNotebookRendererApi => ({ + onDidCreateOutput: onDidCreateOutput.event, + onWillDestroyOutput: onWillDestroyOutput.event, + setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }), + getState: () => { + const state = vscode.getState(); + return typeof state === 'object' && state ? state[rendererId] as T : undefined; + }, + }), + }; + + invokeSourceWithGlobals(scriptText, globals); + return { - activate: () => { - const globals = { - acquireNotebookRendererApi: () => acquireNotebookRendererApi(rendererId) - }; - return invokeSourceWithGlobals(text, globals); + renderCell(id, context) { + onDidCreateOutput.fire({ ...context, outputId: id }); + }, + destroyCell(id) { + onWillDestroyOutput.fire(id ? { outputId: id } : undefined); } }; } - }; + }); const dimensionUpdater = new class { private readonly pending = new Map(); @@ -388,8 +413,6 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend focusTrackers.set(outputId, new FocusTracker(element, outputId)); } - const dontEmit = Symbol('dontEmit'); - function createEmitter(listenerChange: (listeners: Set>) => void = () => undefined): EmitterLike { const listeners = new Set>(); return { @@ -421,24 +444,16 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend }; } - // Maps the events in the given emitter, invoking mapFn on each one. mapFn can return - // the dontEmit symbol to skip emission. - function mapEmitter(emitter: EmitterLike, mapFn: (data: T) => R | typeof dontEmit) { - let listener: IDisposable; - const mapped = createEmitter(listeners => { - if (listeners.size && !listener) { - listener = emitter.event(data => { - const v = mapFn(data); - if (v !== dontEmit) { - mapped.fire(v); - } - }); - } else if (listener && !listeners.size) { - listener.dispose(); - } - }); - - return mapped.event; + function showPreloadErrors(outputNode: HTMLElement, ...errors: readonly Error[]) { + outputNode.innerText = `Error loading preloads:`; + const errList = document.createElement('ul'); + for (const result of errors) { + console.error(result); + const item = document.createElement('li'); + item.innerText = result.message; + errList.appendChild(item); + } + outputNode.appendChild(errList); } interface ICreateCellInfo { @@ -454,10 +469,9 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend outputId: string; } - const onWillDestroyOutput = createEmitter<'all' | { rendererId: string, info: IDestroyCellInfo }>(); - const onDidCreateOutput = createEmitter<{ rendererId: string, info: ICreateCellInfo }>(); const onDidReceiveKernelMessage = createEmitter(); + /** @deprecated */ interface GlobalNotebookRendererApi { setState: (newState: T) => void; getState(): T | undefined; @@ -465,65 +479,12 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend readonly onDidCreateOutput: Event; } - const acquireNotebookRendererApi = (id: string): GlobalNotebookRendererApi => ({ - setState(newState: T) { - vscode.setState({ ...vscode.getState(), [id]: newState }); - }, - getState(): T | undefined { - const state = vscode.getState(); - return typeof state === 'object' && state ? state[id] as T : undefined; - }, - onWillDestroyOutput: mapEmitter(onWillDestroyOutput, (evt) => { - if (evt === 'all') { - return undefined; - } - return evt.rendererId === id ? evt.info : dontEmit; - }), - onDidCreateOutput: mapEmitter(onDidCreateOutput, ({ rendererId, info }) => rendererId === id ? info : dontEmit), - }); - const kernelPreloadGlobals = { acquireVsCodeApi, onDidReceiveKernelMessage: onDidReceiveKernelMessage.event, postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }), }; - const enum PreloadState { - Ok, - Error - } - - type PreloadResult = { state: PreloadState.Ok } | { state: PreloadState.Error, error: string }; - - /** - * Map of preload resource URIs to promises that resolve one the resource - * loads or errors. - */ - const preloadPromises = new Map>(); - const queuedOuputActions = new Map>(); - - /** - * Enqueues an action that affects a output. This blocks behind renderer load - * requests that affect the same output. This should be called whenever you - * do something that affects output to ensure it runs in - * the correct order. - */ - const enqueueOutputAction = (event: T, fn: (event: T) => Promise | void) => { - const queued = queuedOuputActions.get(event.outputId); - const maybePromise = queued ? queued.then(() => fn(event)) : fn(event); - if (typeof maybePromise === 'undefined') { - return; // a synchonrously-called function, we're done - } - - const promise = maybePromise.then(() => { - if (queuedOuputActions.get(event.outputId) === promise) { - queuedOuputActions.delete(event.outputId); - } - }); - - queuedOuputActions.set(event.outputId, promise); - }; - const ttPolicy = window.trustedTypes?.createPolicy('notebookOutputRenderer', { createHTML: value => value, createScript: value => value, @@ -605,10 +566,15 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } } break; - case 'html': - enqueueOutputAction(event.data, async data => { - const preloadResults = await Promise.all(data.requiredPreloads.map(p => preloadPromises.get(p.uri))); - if (!queuedOuputActions.has(data.outputId)) { // output was cleared while loading + case 'html': { + const data = event.data; + outputs.enqueue(event.data.outputId, async (state) => { + const preloadsAndErrors = await Promise.all([ + data.rendererId ? renderers.load(data.rendererId) : undefined, + ...data.requiredPreloads.map(p => kernelPreloads.waitFor(p.uri)), + ].map(p => p?.catch(err => err))); + + if (state.cancelled) { return; } @@ -658,38 +624,26 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend if (content.type === RenderOutputType.Html) { const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; outputNode.innerHTML = trustedHtml as string; - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); domEval(outputNode); - } else if (preloadResults.some(e => e?.state === PreloadState.Error)) { - outputNode.innerText = `Error loading preloads:`; - const errList = document.createElement('ul'); - for (const result of preloadResults) { - if (result?.state === PreloadState.Error) { - const item = document.createElement('li'); - item.innerText = result.error; - errList.appendChild(item); - } - } - outputNode.appendChild(errList); - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); + } else if (preloadsAndErrors.some(e => e instanceof Error)) { + const errors = preloadsAndErrors.filter((e): e is Error => e instanceof Error); + showPreloadErrors(outputNode, ...errors); } else { - // TODO: this should go through renderers instead - onDidCreateOutput.fire({ - rendererId: data.rendererId!, - info: { + const rendererApi = preloadsAndErrors[0] as RendererApi; + try { + rendererApi.renderCell(outputId, { element: outputNode, - outputId, mime: content.mimeType, value: content.value, metadata: content.metadata, - } - }); - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); + }); + } catch (e) { + showPreloadErrors(outputNode, e); + } } + cellOutputContainer.appendChild(outputContainer); + outputContainer.appendChild(outputNode); resizeObserver.observe(outputNode, outputId, true); const clientHeight = outputNode.clientHeight; @@ -714,6 +668,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend cellOutputContainer.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; }); break; + } case 'view-scroll': { // const date = new Date(); @@ -740,8 +695,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend break; } case 'clear': - queuedOuputActions.clear(); // stop all loading outputs - onWillDestroyOutput.fire('all'); + renderers.clearAll(); document.getElementById('container')!.innerText = ''; focusTrackers.forEach(ft => { @@ -753,26 +707,29 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend const output = document.getElementById(event.data.outputId); const { rendererId, outputId } = event.data; - queuedOuputActions.delete(outputId); // stop any in-progress rendering + outputs.cancelOutput(outputId); if (output && output.parentNode) { if (rendererId) { - onWillDestroyOutput.fire({ rendererId, info: { outputId } }); + renderers.clearOutput(rendererId, outputId); } output.parentNode.removeChild(output); } break; } - case 'hideOutput': - enqueueOutputAction(event.data, ({ outputId }) => { + case 'hideOutput': { + const { outputId } = event.data; + outputs.enqueue(event.data.outputId, () => { const container = document.getElementById(outputId)?.parentElement?.parentElement; if (container) { container.style.visibility = 'hidden'; } }); break; - case 'showOutput': - enqueueOutputAction(event.data, ({ outputId, cellTop: top, }) => { + } + case 'showOutput': { + const { outputId, cellTop: top } = event.data; + outputs.enqueue(event.data.outputId, () => { const output = document.getElementById(outputId); if (output) { output.parentElement!.parentElement!.style.visibility = 'visible'; @@ -784,6 +741,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } }); break; + } case 'ack-dimension': { const { outputId, height } = event.data; @@ -796,35 +754,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend } case 'preload': const resources = event.data.resources; - let queue: Promise = Promise.resolve({ state: PreloadState.Ok }); - for (const { uri, originalUri, source } of resources) { - // create the promise so that the scripts download in parallel, but - // only invoke them in series within the queue - - if (source === 'kernel') { - const promise = runPreload(uri, originalUri); - queue = queue.then(() => promise.then(async module => { - try { - await module.activate(); - return { state: PreloadState.Ok }; - } catch (error) { - console.error(error); - return { state: PreloadState.Error, error: error.toString() }; - } - })); - } else { - queue = queue.then(async () => { - try { - await renderers.load(source.rendererId); - return { state: PreloadState.Ok }; - } catch (error) { - console.error(error); - return { state: PreloadState.Error, error: error.toString() }; - } - }); - } - - preloadPromises.set(uri, queue); + for (const { uri, originalUri } of resources) { + kernelPreloads.load(uri, originalUri); } break; case 'focus-output': @@ -863,12 +794,13 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend interface RendererApi { renderCell: (id: string, context: ICreateCellInfo) => void; + destroyCell?: (id?: string) => void; } class Renderer { constructor( public readonly data: RendererMetadata, - private readonly loadDependency: (id: string) => Promise, + private readonly loadExtension: (id: string) => Promise, ) { } private _loadPromise: Promise | undefined; @@ -878,33 +810,116 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend public load(): Promise { if (!this._loadPromise) { - this._loadPromise = Promise.all(this.data.dependencies.map(dependencyId => this.loadDependency(dependencyId))) - .then(() => runRenderScript(this.data.entrypoint, this.data.id)) - .then(module => { - if (module) { - const api = module.activate(createRendererContext(this.data.id)); - this._api = api; - return api; - } - return undefined; - }); + this._loadPromise = this._load(); } + return this._loadPromise; } + + /** Inner function cached in the _loadPromise(). */ + private async _load() { + const module = await runRenderScript(this.data.entrypoint, this.data.id); + if (!module) { + return; + } + + const api = module.activate(createRendererContext(this.data.id)); + this._api = api; + + // Squash any errors extends errors. They won't prevent the renderer + // itself from working, so just log them. + await Promise.all(rendererData + .filter(d => d.extends === this.data.id) + .map(d => this.loadExtension(d.id).catch(console.error)), + ); + + return api; + } } - const renderers = new class { + const kernelPreloads = new class { + private readonly preloads = new Map>(); + + /** + * Returns a promise that resolves when the given preload is activated. + */ + public waitFor(uri: string) { + 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, originalUri: string) { + const promise = Promise.all([ + runPreload(uri, originalUri), + this.waitForAllCurrent(), + ]).then(([module]) => module.activate()); + + this.preloads.set(uri, promise); + return promise; + } + + /** + * Returns a promise that waits for all currently-registered preloads to + * activate before resolving. + */ + private waitForAllCurrent() { + return Promise.all([...this.preloads.values()].map(p => p.catch(err => err))); + } + }; + + const outputs = new class { + private outputs = new Map }>(); + /** + * Pushes the action onto the list of actions for the given output ID, + * ensuring that it's run in-order. + */ + public enqueue(outputId: string, action: (record: { cancelled: boolean }) => unknown) { + const record = this.outputs.get(outputId); + if (!record) { + this.outputs.set(outputId, { cancelled: false, queue: new Promise(r => r(action({ cancelled: false }))) }); + } else { + record.queue = record.queue.then(r => !record.cancelled && action(record)); + } + } + + /** + * Cancells the rendering of all outputs. + */ + public cancelAll() { + for (const record of this.outputs.values()) { + record.cancelled = true; + } + this.outputs.clear(); + } + /** + * Cancels any ongoing rendering out an output. + */ + public cancelOutput(outputId: string) { + const output = this.outputs.get(outputId); + if (output) { + output.cancelled = true; + this.outputs.delete(outputId); + } + } + }; + + const renderers = new class { private readonly _renderers = new Map(); constructor() { for (const renderer of rendererData) { - this._renderers.set(renderer.id, new Renderer(renderer, async (dependencyId) => { - const parent = this._renderers.get(dependencyId); - if (!parent) { - throw new Error(`Could not find renderer dependency: ${dependencyId}`); + this._renderers.set(renderer.id, new Renderer(renderer, async (extensionId) => { + const ext = this._renderers.get(extensionId); + if (!ext) { + throw new Error(`Could not find extending renderer: ${extensionId}`); } - await parent.load(); + + await ext.load(); })); } } @@ -913,7 +928,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend return this._renderers.get(id)?.api; } - public load(id: string) { + public async load(id: string) { const renderer = this._renderers.get(id); if (!renderer) { throw new Error('Could not find renderer'); @@ -922,9 +937,31 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend return renderer.load(); } + + public clearAll() { + outputs.cancelAll(); + for (const renderer of this._renderers.values()) { + renderer.api?.destroyCell?.(); + } + } + + public clearOutput(rendererId: string, outputId: string) { + outputs.cancelOutput(outputId); + this._renderers.get(rendererId)?.api?.destroyCell?.(outputId); + } + + public async renderCustom(rendererId: string, outputId: string, info: ICreateCellInfo) { + const api = await this.load(rendererId); + if (!api) { + throw new Error(`renderer ${rendererId} did not return an API`); + } + + api.renderCell(outputId, info); + } + public async renderMarkdown(id: string, element: HTMLElement, content: string): Promise { const markdownRenderers = Array.from(this._renderers.values()) - .filter(renderer => renderer.data.mimeTypes.includes('text/markdown')); + .filter(renderer => renderer.data.mimeTypes.includes('text/markdown') && !renderer.data.extends); if (!markdownRenderers.length) { throw new Error('Could not find renderer'); @@ -932,8 +969,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend await Promise.all(markdownRenderers.map(x => x.load())); - const renderer = Array.from(this._renderers.values()).find(x => x.data.mimeTypes.includes('text/markdown')); - renderer?.api?.renderCell(id, { + markdownRenderers[0].api?.renderCell(id, { element, value: content, mime: 'text/markdown', @@ -1172,8 +1208,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Rend export interface RendererMetadata { readonly id: string; readonly entrypoint: string; - readonly dependencies: readonly string[] readonly mimeTypes: readonly string[]; + readonly extends: string | undefined; } export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 816f3dbaf209c..354e926d95b79 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -53,6 +53,8 @@ export const ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER = [ export const BUILTIN_RENDERER_ID = '_builtin'; export const RENDERER_NOT_AVAILABLE = '_notAvailable'; +export type NotebookRendererEntrypoint = string | { extends: string; path: string }; + export enum NotebookRunState { Running = 1, Idle = 2 @@ -132,6 +134,7 @@ export const enum NotebookRendererMatch { export interface INotebookRendererInfo { id: string; displayName: string; + extends?: string; entrypoint: URI; preloads: ReadonlyArray; extensionLocation: URI; diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index b698cd5b3ee61..5025db38aebfd 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -8,7 +8,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { INotebookRendererInfo, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookRendererInfo, NotebookRendererEntrypoint, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; class DependencyList { private readonly value: ReadonlySet; @@ -34,6 +34,7 @@ class DependencyList { export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly id: string; + readonly extends?: string; readonly entrypoint: URI; readonly displayName: string; readonly extensionLocation: URI; @@ -49,7 +50,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { constructor(descriptor: { readonly id: string; readonly displayName: string; - readonly entrypoint: string; + readonly entrypoint: NotebookRendererEntrypoint; readonly mimeTypes: readonly string[]; readonly extension: IExtensionDescription; readonly dependencies: readonly string[] | undefined; @@ -58,7 +59,14 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { this.id = descriptor.id; this.extensionId = descriptor.extension.identifier; this.extensionLocation = descriptor.extension.extensionLocation; - this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); + + if (typeof descriptor.entrypoint === 'string') { + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); + } else { + this.extends = descriptor.entrypoint.extends; + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint.path); + } + this.displayName = descriptor.displayName; this.mimeTypes = descriptor.mimeTypes; this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); @@ -103,6 +111,10 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { } private matchesMimeTypeOnly(mimeType: string) { + if (this.extends !== undefined) { + return false; + } + return this.mimeTypeGlobs.some(pattern => pattern(mimeType)) || this.mimeTypes.some(pattern => pattern === mimeType); } }