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 146837611ee4b..c1089da9ed710 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -260,7 +260,11 @@ export class BackLayerWebView extends Themable { 'notebook-cell-renderer-not-found-error': nls.localize({ key: 'notebook.error.rendererNotFound', comment: ['$0 is a placeholder for the mime type'] - }, "No renderer found for '$0' a"), + }, "No renderer found for '$0'"), + 'notebook-cell-renderer-fallbacks-exhausted': nls.localize({ + key: 'notebook.error.rendererFallbacksExhausted', + comment: ['$0 is a placeholder for the mime type'] + }, "Could not render content for '$0'"), }; } 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 c70e4ed9086ff..485b6f262cc52 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -714,8 +714,13 @@ async function webviewPreloads(ctx: PreloadContext) { } }; - interface ExtendedOutputItem { - readonly _allOutputItems: ReadonlyArray<{ readonly mime: string; getItem(): Promise }>; + interface AdditionalOutputItemInfo { + readonly mime: string; + getItem(): Promise; + } + + interface ExtendedOutputItem extends rendererApi.OutputItem { + readonly _allOutputItems: ReadonlyArray; } let hasWarnedAboutAllOutputItemsProposal = false; @@ -726,15 +731,15 @@ async function webviewPreloads(ctx: PreloadContext) { metadata: unknown, valueBytes: Uint8Array, allOutputItemData: ReadonlyArray<{ readonly mime: string }> - ): rendererApi.OutputItem & ExtendedOutputItem { + ): ExtendedOutputItem { function create( id: string, mime: string, metadata: unknown, valueBytes: Uint8Array, - ): rendererApi.OutputItem & ExtendedOutputItem { - return Object.freeze({ + ): ExtendedOutputItem { + return Object.freeze({ id, mime, metadata, @@ -1268,6 +1273,8 @@ async function webviewPreloads(ctx: PreloadContext) { } }); + const renderFallbackErrorName = 'vscode.fallbackToNextRenderer'; + class Renderer { private _onMessageEvent = createEmitter(); @@ -1305,11 +1312,16 @@ async function webviewPreloads(ctx: PreloadContext) { this.postDebugMessage('Rendered output item', { id: item.id, duration: `${performance.now() - renderStart}ms` }); } catch (e) { - if (!signal.aborted) { - showRenderError(`Error rendering output item using '${this.data.id}'`, element, e instanceof Error ? [e] : []); + if (signal.aborted) { + return; + } - this.postDebugMessage('Rendering output item failed', { id: item.id, error: e + '' }); + if (e instanceof Error && e.name === renderFallbackErrorName) { + throw e; } + + showRenderError(`Error rendering output item using '${this.data.id}'`, element, e instanceof Error ? [e] : []); + this.postDebugMessage('Rendering output item failed', { id: item.id, error: e + '' }); } } @@ -1560,7 +1572,63 @@ async function webviewPreloads(ctx: PreloadContext) { this._renderers.get(rendererId)?.disposeOutputItem(outputId); } - public async render(info: rendererApi.OutputItem, preferredRendererId: string | undefined, element: HTMLElement, signal: AbortSignal): Promise { + public async render(item: ExtendedOutputItem, preferredRendererId: string | undefined, element: HTMLElement, signal: AbortSignal): Promise { + const primaryRenderer = this.findRenderer(preferredRendererId, item); + if (!primaryRenderer) { + const errorMessage = (document.documentElement.style.getPropertyValue('--notebook-cell-renderer-not-found-error') || '').replace('$0', () => item.mime); + this.showRenderError(item, element, errorMessage); + return; + } + + // Try primary renderer first + if (!(await this._doRender(item, element, primaryRenderer, signal)).continue) { + return; + } + + // Primary renderer failed in an expected way. Fallback to render the next mime types + for (const additionalItemData of item._allOutputItems) { + if (additionalItemData.mime === item.mime) { + continue; + } + + const additionalItem = await additionalItemData.getItem(); + if (signal.aborted) { + return; + } + + if (additionalItem) { + const renderer = this.findRenderer(undefined, additionalItem); + if (renderer) { + if (!(await this._doRender(additionalItem, element, renderer, signal)).continue) { + return; // We rendered successfully + } + } + } + } + + // All renderers have failed and there is nothing left to fallback to + const errorMessage = (document.documentElement.style.getPropertyValue('--notebook-cell-renderer-fallbacks-exhausted') || '').replace('$0', () => item.mime); + this.showRenderError(item, element, errorMessage); + } + + private async _doRender(item: rendererApi.OutputItem, element: HTMLElement, renderer: Renderer, signal: AbortSignal): Promise<{ continue: boolean }> { + try { + await renderer.renderOutputItem(item, element, signal); + return { continue: false }; // We rendered successfully + } catch (e) { + if (signal.aborted) { + return { continue: false }; + } + + if (e instanceof Error && e.name === renderFallbackErrorName) { + return { continue: true }; + } else { + throw e; // Bail and let callers handle unknown errors + } + } + } + + private findRenderer(preferredRendererId: string | undefined, info: rendererApi.OutputItem) { let renderer: Renderer | undefined; if (typeof preferredRendererId === 'string') { @@ -1578,26 +1646,24 @@ async function webviewPreloads(ctx: PreloadContext) { renderer = renderers[0]; } } + return renderer; + } - if (renderer) { - await renderer.renderOutputItem(info, element, signal); - } else { - const errorContainer = document.createElement('div'); + private showRenderError(info: rendererApi.OutputItem, element: HTMLElement, errorMessage: string) { + const errorContainer = document.createElement('div'); - const error = document.createElement('div'); - error.className = 'no-renderer-error'; - const errorText = (document.documentElement.style.getPropertyValue('--notebook-cell-renderer-not-found-error') || '').replace('$0', () => info.mime); - error.innerText = errorText; + const error = document.createElement('div'); + error.className = 'no-renderer-error'; + error.innerText = errorMessage; - const cellText = document.createElement('div'); - cellText.innerText = info.text(); + const cellText = document.createElement('div'); + cellText.innerText = info.text(); - errorContainer.appendChild(error); - errorContainer.appendChild(cellText); + errorContainer.appendChild(error); + errorContainer.appendChild(cellText); - element.innerText = ''; - element.appendChild(errorContainer); - } + element.innerText = ''; + element.appendChild(errorContainer); } }(); @@ -1819,7 +1885,7 @@ async function webviewPreloads(ctx: PreloadContext) { public readonly id: string; public readonly element: HTMLElement; - private readonly outputItem: rendererApi.OutputItem; + private readonly outputItem: ExtendedOutputItem; /// Internal field that holds text content private _content: { readonly value: string; readonly version: number; readonly metadata: NotebookCellMetadata }; @@ -1840,7 +1906,7 @@ async function webviewPreloads(ctx: PreloadContext) { }); let cachedData: { readonly version: number; readonly value: Uint8Array } | undefined; - this.outputItem = Object.freeze({ + this.outputItem = Object.freeze({ id, mime, @@ -1868,7 +1934,12 @@ async function webviewPreloads(ctx: PreloadContext) { blob(): Blob { return new Blob([this.data()], { type: this.mime }); - } + }, + + _allOutputItems: [{ + mime, + getItem: async () => this.outputItem, + }] }); const root = document.getElementById('container')!;