diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 1a7103d51fe..b5da7449c5f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1227,6 +1227,9 @@ declare module 'vscode' { with(change: { start?: number, end?: number }): NotebookRange; } + // todo@API document which mime types are supported out of the box and + // which are considered secure + // code specific mime types // application/x.notebook.error-traceback // application/x.notebook.stdout @@ -1234,23 +1237,48 @@ declare module 'vscode' { // application/x.notebook.stream export class NotebookCellOutputItem { - // todo@API - // add factory functions for common mime types - // static textplain(value:string): NotebookCellOutputItem; - // static errortrace(value:any): NotebookCellOutputItem; + static error(err: Error): NotebookCellOutputItem; + + static stdout(value: string): NotebookCellOutputItem; + + static stderr(value: string): NotebookCellOutputItem; /** - * Creates `application/x.notebook.error` + * Factory function to create a `NotebookCellOutputItem` from + * a JSON object. * - * @param err An error for which an output item is wanted + * *Note* that this function is not expecting "stringified JSON" but + * an object that can be stringified. This function will throw an error + * when the passed value cannot be JSON-stringified. + * + * @param value A JSON-stringifyable value. + * @param mime Optional MIME type, defaults to `application/json` */ - static error(err: Error): NotebookCellOutputItem; + static json(value: any, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` from a string. + * + * *Note* that an UTF-8 encoder is used to create bytes for the string. + * + * @param value A string/ + * @param mime Optional MIME type, defaults to `text/plain`. + */ + static text(value: string, mime?: string): NotebookCellOutputItem; + + /** + * + * @param value + * @param mime Optional MIME type, defaults to `application/octet-stream`. + */ + //todo@API bytes, raw, buffer? + static bytes(value: Uint8Array, mime?: string): NotebookCellOutputItem; mime: string; //todo@API string or Unit8Array? // value: string | Uint8Array | unknown; - value: unknown; + value: Uint8Array | unknown; metadata?: { [key: string]: any }; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 1d2e22cd8ec..9461b28329b 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -301,9 +301,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } if (editorId) { - throw new Error(`Could NOT open editor for "${notebookOrUri.toString()}" because another editor opened in the meantime.`); + throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}" because another editor opened in the meantime.`); } else { - throw new Error(`Could NOT open editor for "${notebookOrUri.toString()}".`); + throw new Error(`Could NOT open editor for "${notebookOrUri.uri.toString()}".`); } } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 9aaec755c6b..94bfff83773 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; +import { VSBuffer } from 'vs/base/common/buffer'; import * as htmlContent from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; import * as marked from 'vs/base/common/marked/marked'; @@ -1533,15 +1534,32 @@ export namespace NotebookCellData { export namespace NotebookCellOutputItem { export function from(item: types.NotebookCellOutputItem): notebooks.IOutputItemDto { + let value: unknown; + let valueBytes: number[] | undefined; + if (item.value instanceof Uint8Array) { + //todo@jrieken this HACKY and SLOW... hoist VSBuffer instead + valueBytes = Array.from(item.value); + } else { + value = item.value; + } return { + metadata: item.metadata, mime: item.mime, - value: item.value, - metadata: item.metadata + value, + valueBytes, }; } export function to(item: notebooks.IOutputItemDto): types.NotebookCellOutputItem { - return new types.NotebookCellOutputItem(item.mime, item.value, item.metadata); + + let value: Uint8Array | unknown; + if (item.value instanceof VSBuffer) { + value = item.value.buffer; + } else { + value = item.value; + } + + return new types.NotebookCellOutputItem(item.mime, value, item.metadata); } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0cd309dbf10..a566a4875f0 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3088,20 +3088,45 @@ export class NotebookCellOutputItem { } static error(err: Error): NotebookCellOutputItem { - return new NotebookCellOutputItem( - 'application/x.notebook.error', - JSON.stringify({ name: err.name, message: err.message, stack: err.stack }) - ); + const obj = { + name: err.name, + message: err.message, + stack: err.stack + }; + return NotebookCellOutputItem.json(obj, 'application/x.notebook.error'); + } + + static stdout(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/x.notebook.stdout'); + } + + static stderr(value: string): NotebookCellOutputItem { + return NotebookCellOutputItem.text(value, 'application/x.notebook.stderr'); + } + + static json(value: any, mime: string = 'application/json'): NotebookCellOutputItem { + const rawStr = JSON.stringify(value, undefined, '\t'); + return NotebookCellOutputItem.text(rawStr, mime); + } + + static text(value: string, mime: string = 'text/plain'): NotebookCellOutputItem { + const bytes = new TextEncoder().encode(String(value)); + return new NotebookCellOutputItem(mime, bytes); + } + + static bytes(value: Uint8Array, mime: string = 'application/octet-stream'): NotebookCellOutputItem { + return new NotebookCellOutputItem(mime, value); } constructor( public mime: string, - public value: unknown, // JSON'able + public value: Uint8Array | unknown, // JSON'able public metadata?: Record ) { if (isFalsyOrWhitespace(this.mime)) { throw new Error('INVALID mime type, must not be empty or falsy'); } + // todo@joh stringify and check metadata and throw when not JSONable } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index 60a32f043c7..8b766eadc4e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { dirname } from 'vs/base/common/resources'; import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -25,8 +25,14 @@ import { truncatedArrayOfString } from 'vs/workbench/contrib/notebook/browser/vi import { IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -function getStringValue(data: unknown): string { - return isArray(data) ? data.join('') : String(data); +function getStringValue(item: IOutputItemDto): string { + if (Array.isArray(item.valueBytes)) { + // todo@jrieken NOT proper, should be VSBuffer + return new TextDecoder().decode(new Uint8Array(item.valueBytes)); + } else { + // "old" world + return Array.isArray(item.value) ? item.value.join('') : String(item.value); + } } class JSONRendererContrib extends Disposable implements IOutputRendererContribution { @@ -47,8 +53,16 @@ class JSONRendererContrib extends Disposable implements IOutputRendererContribut super(); } - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - const str = items.map(item => JSON.stringify(item.value, null, '\t')).join(''); + render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement): IRenderOutput { + const str = items.map(item => { + if (isArray(item.valueBytes)) { + return getStringValue(item); + } else { + return JSON.stringify(item.value, null, '\t'); + } + }).join(''); + + // todo@jrieken LEAKING https://github.com/microsoft/vscode/issues/123667 const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { ...getOutputSimpleEditorOptions(), @@ -99,8 +113,7 @@ class JavaScriptRendererContrib extends Disposable implements IOutputRendererCon render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { let scriptVal = ''; items.forEach(item => { - const data = item.value; - const str = isArray(data) ? data.join('') : data; + const str = getStringValue(item); scriptVal += ``; }); @@ -121,7 +134,7 @@ class CodeRendererContrib extends Disposable implements IOutputRendererContribut return ['text/x-javascript']; } - private readonly _cellDisposables = new Map(); + // private readonly _cellDisposables = new Map(); constructor( public notebookEditor: ICommonNotebookEditor, @@ -133,19 +146,20 @@ class CodeRendererContrib extends Disposable implements IOutputRendererContribut } override dispose(): void { - dispose(this._cellDisposables.values()); - this._cellDisposables.clear(); + // dispose(this._cellDisposables.values()); + // this._cellDisposables.clear(); super.dispose(); } render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - let cellDisposables = this._cellDisposables.get(output.cellViewModel.handle); - cellDisposables?.dispose(); - cellDisposables = new DisposableStore(); - this._cellDisposables.set(output.cellViewModel.handle, cellDisposables); + // let cellDisposables = this._cellDisposables.get(output.cellViewModel.handle); + // cellDisposables?.dispose(); // still LEAKING, we need the IRenderOutput to be disposable so that the caller can issue the cleanup + // cellDisposables = new DisposableStore(); + // this._cellDisposables.set(output.cellViewModel.handle, cellDisposables); + // todo@jrieken LEAKING https://github.com/microsoft/vscode/issues/123667 - const str = items.map(item => getStringValue(item.value)).join(''); + const str = items.map(getStringValue).join(''); const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { ...getOutputSimpleEditorOptions(), dimension: { @@ -170,8 +184,8 @@ class CodeRendererContrib extends Disposable implements IOutputRendererContribut width }); - cellDisposables.add(editor); - cellDisposables.add(textModel); + this._register(editor); + this._register(textModel); container.style.height = `${height + 8}px`; @@ -202,7 +216,7 @@ class StreamRendererContrib extends Disposable implements IOutputRendererContrib const linkDetector = this.instantiationService.createInstance(LinkDetector); items.forEach(item => { - const text = getStringValue(item.value); + const text = getStringValue(item); const contentNode = DOM.$('span.output-stream'); truncatedArrayOfString(notebookUri!, output.cellViewModel, contentNode, [text], linkDetector, this.openerService, this.textFileService, this.themeService); container.appendChild(contentNode); @@ -228,6 +242,7 @@ class StderrRendererContrib extends StreamRendererContrib { } } +/** @deprecated */ class ErrorRendererContrib extends Disposable implements IOutputRendererContribution { getType() { return RenderOutputType.Mainframe; @@ -303,15 +318,9 @@ class JSErrorRendererContrib implements IOutputRendererContribution { const linkDetector = this._instantiationService.createInstance(LinkDetector); for (let item of items) { - - if (typeof item.value !== 'string') { - this._logService.warn('INVALID output item (not a string)', item.value); - continue; - } - let err: Error; try { - err = JSON.parse(item.value); + err = JSON.parse(getStringValue(item)); } catch (e) { this._logService.warn('INVALID output item (failed to parse)', e); continue; @@ -358,7 +367,7 @@ class PlainTextRendererContrib extends Disposable implements IOutputRendererCont render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { const linkDetector = this.instantiationService.createInstance(LinkDetector); - const str = items.map(item => getStringValue(item.value)); + const str = items.map(getStringValue); const contentNode = DOM.$('.output-plaintext'); truncatedArrayOfString(notebookUri!, output.cellViewModel, contentNode, str, linkDetector, this.openerService, this.textFileService, this.themeService); container.appendChild(contentNode); @@ -383,9 +392,7 @@ class HTMLRendererContrib extends Disposable implements IOutputRendererContribut } render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - const data = items.map(item => getStringValue(item.value)).join(''); - - const str = (isArray(data) ? data.join('') : data) as string; + const str = items.map(getStringValue).join(''); return { type: RenderOutputType.Html, source: output, @@ -410,7 +417,7 @@ class SVGRendererContrib extends Disposable implements IOutputRendererContributi } render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - const str = items.map(item => getStringValue(item.value)).join(''); + const str = items.map(getStringValue).join(''); return { type: RenderOutputType.Html, source: output, @@ -437,8 +444,7 @@ class MdRendererContrib extends Disposable implements IOutputRendererContributio render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { items.forEach(item => { - const data = item.value; - const str = (isArray(data) ? data.join('') : data) as string; + const str = getStringValue(item); const mdOutput = document.createElement('div'); const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(notebookUri) }); mdOutput.appendChild(mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }, undefined, { gfm: true }).element); @@ -449,13 +455,13 @@ class MdRendererContrib extends Disposable implements IOutputRendererContributio } } -class PNGRendererContrib extends Disposable implements IOutputRendererContribution { +class ImgRendererContrib extends Disposable implements IOutputRendererContribution { getType() { return RenderOutputType.Mainframe; } getMimetypes() { - return ['image/png']; + return ['image/png', 'image/jpeg']; } constructor( @@ -467,8 +473,24 @@ class PNGRendererContrib extends Disposable implements IOutputRendererContributi render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { items.forEach(item => { const image = document.createElement('img'); - const imagedata = item.value; - image.src = `data:image/png;base64,${imagedata}`; + + let src: string; + + if (Array.isArray(item.valueBytes)) { + const bytes = new Uint8Array(item.valueBytes); + const blob = new Blob([bytes], { type: item.mime }); + src = URL.createObjectURL(blob); + + // todo@jrieken + // we need to release the object URL again + // https://github.com/microsoft/vscode/issues/123667 + } else { + // OLD + const imagedata = item.value; + src = `data:${item.mime};base64,${imagedata}`; + } + + image.src = src; const display = document.createElement('div'); display.classList.add('display'); display.appendChild(image); @@ -478,43 +500,12 @@ class PNGRendererContrib extends Disposable implements IOutputRendererContributi } } -class JPEGRendererContrib extends Disposable implements IOutputRendererContribution { - getType() { - return RenderOutputType.Mainframe; - } - - getMimetypes() { - return ['image/jpeg']; - } - - constructor( - public notebookEditor: ICommonNotebookEditor, - ) { - super(); - } - - render(output: ICellOutputViewModel, items: IOutputItemDto[], container: HTMLElement, notebookUri: URI): IRenderOutput { - items.forEach(item => { - const image = document.createElement('img'); - const imagedata = item.value; - image.src = `data:image/jpeg;base64,${imagedata}`; - const display = document.createElement('div'); - display.classList.add('display'); - display.appendChild(image); - container.appendChild(display); - }); - - return { type: RenderOutputType.Mainframe }; - } -} - NotebookRegistry.registerOutputTransform('json', JSONRendererContrib); NotebookRegistry.registerOutputTransform('javascript', JavaScriptRendererContrib); NotebookRegistry.registerOutputTransform('html', HTMLRendererContrib); NotebookRegistry.registerOutputTransform('svg', SVGRendererContrib); NotebookRegistry.registerOutputTransform('markdown', MdRendererContrib); -NotebookRegistry.registerOutputTransform('png', PNGRendererContrib); -NotebookRegistry.registerOutputTransform('jpeg', JPEGRendererContrib); +NotebookRegistry.registerOutputTransform('img', ImgRendererContrib); NotebookRegistry.registerOutputTransform('plain', PlainTextRendererContrib); NotebookRegistry.registerOutputTransform('code', CodeRendererContrib); NotebookRegistry.registerOutputTransform('error-trace', ErrorRendererContrib); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 560db334bc9..43dc3c637b6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -168,6 +168,7 @@ export interface IOrderedMimeType { export interface IOutputItemDto { readonly mime: string; readonly value: unknown; + readonly valueBytes?: number[]; readonly metadata?: Record; } @@ -586,11 +587,11 @@ const _mimeTypeInfo = new Map([ ['image/svg+xml', { supportedByCore: true }], ['image/jpeg', { supportedByCore: true }], ['text/x-javascript', { alwaysSecure: true, supportedByCore: true }], // secure because rendered as text, not executed - ['application/x.notebook.error-traceback', { alwaysSecure: true, supportedByCore: true }], ['application/x.notebook.error', { alwaysSecure: true, supportedByCore: true }], - ['application/x.notebook.stream', { alwaysSecure: true, supportedByCore: true, mergeable: true }], ['application/x.notebook.stdout', { alwaysSecure: true, supportedByCore: true, mergeable: true }], ['application/x.notebook.stderr', { alwaysSecure: true, supportedByCore: true, mergeable: true }], + ['application/x.notebook.stream', { alwaysSecure: true, supportedByCore: true, mergeable: true }], // deprecated + ['application/x.notebook.error-traceback', { alwaysSecure: true, supportedByCore: true }], // deprecated ]); export function mimeTypeIsAlwaysSecure(mimeType: string): boolean {